diff --git a/locales/en.json b/locales/en.json index 856cbc418..4a8f2c1b2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -310,6 +310,7 @@ "domain_config_auth_key": "Authentication key", "domain_config_auth_secret": "Authentication secret", "domain_config_auth_token": "Authentication token", + "domain_config_default_app": "Default app", "domain_config_features_disclaimer": "So far, enabling/disabling mail or XMPP features only impact the recommended and automatic DNS configuration, not system configurations!", "domain_config_mail_in": "Incoming emails", "domain_config_mail_out": "Outgoing emails", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 9eee48716..ce395942f 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -896,6 +896,10 @@ app: -d: full: --domain help: Specific domain to put app on (the app domain by default) + -u: + full: --undo + help: Undo redirection + action: store_true ### app_ssowatconf() ssowatconf: diff --git a/share/config_domain.toml b/share/config_domain.toml index 93551458b..b0131f1c1 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -11,6 +11,11 @@ i18n = "domain_config" # [feature] + [feature.app] + [feature.app.default_app] + type = "app" + filters = ["is_webapp"] + default = "_none" [feature.mail] #services = ['postfix', 'dovecot'] diff --git a/src/app.py b/src/app.py index e03d94317..22946eabc 100644 --- a/src/app.py +++ b/src/app.py @@ -117,6 +117,7 @@ def app_info(app, full=False): Get info for a specific app """ from yunohost.permission import user_permission_list + from yunohost.domain import domain_config_get _assert_is_installed(app) @@ -153,6 +154,9 @@ def app_info(app, full=False): ret["is_webapp"] = "domain" in settings and "path" in settings + if ret["is_webapp"]: + ret["is_default"] = domain_config_get(settings["domain"], "feature.app.default_app") == app + ret["supports_change_url"] = os.path.exists( os.path.join(setting_path, "scripts", "change_url") ) @@ -989,6 +993,7 @@ def app_remove(operation_logger, app, purge=False): permission_delete, permission_sync_to_user, ) + from yunohost.domain import domain_list, domain_config_get, domain_config_set if not _is_installed(app): raise YunohostValidationError( @@ -1048,12 +1053,16 @@ def app_remove(operation_logger, app, purge=False): hook_remove(app) + for domain in domain_list()["domains"]: + if (domain_config_get(domain, "feature.app.default_app") == app): + domain_config_set(domain, "feature.app.default_app", "_none") + permission_sync_to_user() _assert_system_is_sane_for_app(manifest, "post") @is_unit_operation() -def app_makedefault(operation_logger, app, domain=None): +def app_makedefault(operation_logger, app, domain=None, undo=False): """ Redirect domain root to an app @@ -1062,11 +1071,10 @@ def app_makedefault(operation_logger, app, domain=None): domain """ - from yunohost.domain import _assert_domain_exists + from yunohost.domain import _assert_domain_exists, domain_config_set app_settings = _get_app_settings(app) app_domain = app_settings["domain"] - app_path = app_settings["path"] if domain is None: domain = app_domain @@ -1075,36 +1083,12 @@ def app_makedefault(operation_logger, app, domain=None): operation_logger.related_to.append(("domain", domain)) - if "/" in app_map(raw=True)[domain]: - raise YunohostValidationError( - "app_make_default_location_already_used", - app=app, - domain=app_domain, - other_app=app_map(raw=True)[domain]["/"]["id"], - ) - operation_logger.start() - # TODO / FIXME : current trick is to add this to conf.json.persisten - # This is really not robust and should be improved - # e.g. have a flag in /etc/yunohost/apps/$app/ to say that this is the - # default app or idk... - if not os.path.exists("/etc/ssowat/conf.json.persistent"): - ssowat_conf = {} + if undo: + domain_config_set(domain, 'feature.app.default_app', "_none") else: - ssowat_conf = read_json("/etc/ssowat/conf.json.persistent") - - if "redirected_urls" not in ssowat_conf: - ssowat_conf["redirected_urls"] = {} - - ssowat_conf["redirected_urls"][domain + "/"] = app_domain + app_path - - write_to_json( - "/etc/ssowat/conf.json.persistent", ssowat_conf, sort_keys=True, indent=4 - ) - chmod("/etc/ssowat/conf.json.persistent", 0o644) - - logger.success(m18n.n("ssowat_conf_updated")) + domain_config_set(domain, 'feature.app.default_app', app) def app_setting(app, key, value=None, delete=False): @@ -1303,7 +1287,7 @@ def app_ssowatconf(): """ - from yunohost.domain import domain_list, _get_maindomain + from yunohost.domain import domain_list, _get_maindomain, domain_config_get from yunohost.permission import user_permission_list main_domain = _get_maindomain() @@ -1341,6 +1325,21 @@ def app_ssowatconf(): redirected_urls.update(app_settings.get("redirected_urls", {})) redirected_regex.update(app_settings.get("redirected_regex", {})) + from .utils.legacy import translate_legacy_default_app_in_ssowant_conf_json_persistent + + translate_legacy_default_app_in_ssowant_conf_json_persistent() + + for domain in domains: + default_app = domain_config_get(domain, "feature.app.default_app") + if default_app != "_none" and _is_installed(default_app): + app_settings = _get_app_settings(default_app) + app_domain = app_settings["domain"] + app_path = app_settings["path"] + + # Prevent infinite redirect loop... + if domain + "/" != app_domain + app_path: + redirected_urls[domain + "/"] = app_domain + app_path + # New permission system for perm_name, perm_info in all_permissions.items(): diff --git a/src/domain.py b/src/domain.py index c94dc694d..1fdded817 100644 --- a/src/domain.py +++ b/src/domain.py @@ -454,6 +454,21 @@ class DomainConfigPanel(ConfigPanel): save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml" save_mode = "diff" + def _apply(self): + if ("default_app" in self.future_values and self.future_values["default_app"] != self.values["default_app"]): + from yunohost.app import app_ssowatconf, app_map + + if "/" in app_map(raw=True)[self.entity]: + raise YunohostValidationError( + "app_make_default_location_already_used", + app=self.future_values["default_app"], + domain=self.entity, + other_app=app_map(raw=True)[self.entity]["/"]["id"], + ) + + super()._apply() + app_ssowatconf() + def _get_toml(self): from yunohost.dns import _get_registrar_config_section diff --git a/src/utils/config.py b/src/utils/config.py index 90bc0be9f..1ba38c604 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -440,6 +440,7 @@ class ConfigPanel: "step", "accept", "redact", + "filters", ], "defaults": {}, }, @@ -705,6 +706,7 @@ class Question: self.ask = question.get("ask", {"en": self.name}) self.help = question.get("help") self.redact = question.get("redact", False) + self.filters = question.get("filters", []) # .current_value is the currently stored value self.current_value = question.get("current_value") # .value is the "proposed" value which we got from the user @@ -1126,6 +1128,28 @@ class DomainQuestion(Question): return value +class AppQuestion(Question): + argument_type = "app" + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + from yunohost.app import app_list + + super().__init__(question, context, hooks) + + apps = app_list(full=True)["apps"] + for _filter in self.filters: + apps = [app for app in apps if _filter in app and app[_filter]] + + def _app_display(app): + domain_path = f" ({app['domain_path']})" if 'domain_path' in app else "" + return app["label"] + domain_path + + self.choices = {"_none": "---"} + self.choices.update({app['id']: _app_display(app) for app in apps}) + + class UserQuestion(Question): argument_type = "user" @@ -1319,6 +1343,7 @@ ARGUMENTS_TYPE_PARSERS = { "alert": DisplayTextQuestion, "markdown": DisplayTextQuestion, "file": FileQuestion, + "app": AppQuestion, } diff --git a/src/utils/legacy.py b/src/utils/legacy.py index 910dfd5a3..4c61edb05 100644 --- a/src/utils/legacy.py +++ b/src/utils/legacy.py @@ -7,6 +7,7 @@ from moulinette.utils.filesystem import ( read_file, write_to_file, write_to_yaml, + write_to_json, read_yaml, ) @@ -68,6 +69,52 @@ def legacy_permission_label(app, permission_type): ) +def translate_legacy_default_app_in_ssowant_conf_json_persistent(): + from yunohost.app import app_list + from yunohost.domain import domain_config_set + + persistent_file_name = "/etc/ssowat/conf.json.persistent" + if not os.path.exists(persistent_file_name): + return + + # Ugly hack because for some reason so many people have tabs in their conf.json.persistent ... + os.system(r"sed -i 's/\t/ /g' /etc/ssowat/conf.json.persistent") + + # Ugly hack to try not to misarably fail migration + persistent = read_yaml(persistent_file_name) + + if "redirected_urls" not in persistent: + return + + redirected_urls = persistent["redirected_urls"] + + if not any(from_url.count('/') == 1 and from_url.endswith('/') for from_url in redirected_urls): + return + + apps = app_list()['apps'] + + if not any(app.get('domain_path') in redirected_urls.values() for app in apps): + return + + for from_url, dest_url in redirected_urls.copy().items(): + # Not a root domain, skip + if from_url.count('/') != 1 or not from_url.endswith('/'): + continue + for app in apps: + if app.get('domain_path') != dest_url: + continue + domain_config_set(from_url.strip('/'), "feature.app.default_app", app['id']) + del redirected_urls[from_url] + + persistent["redirected_urls"] = redirected_urls + + write_to_json(persistent_file_name, persistent, sort_keys=True, indent=4) + + logger.warning( + "YunoHost automatically translated some legacy redirections in /etc/ssowat/conf.json.persistent to match the new default application using domain configuration" + ) + + LEGACY_PHP_VERSION_REPLACEMENTS = [ ("/etc/php5", "/etc/php/7.4"), ("/etc/php/7.0", "/etc/php/7.4"),