From 30983a4efd89585b778257df695fb636616876bf Mon Sep 17 00:00:00 2001 From: Tagada <36127788+Tagadda@users.noreply.github.com> Date: Sat, 8 Jan 2022 00:12:45 +0100 Subject: [PATCH 1/3] [enh] Manage default applications with DomainConfigPanel [enh] makedefault with domain_config_set() [enh] Allow webadmin to reverse makedefault [legacy] translate legacy redirections to new domain config --- locales/en.json | 1 + share/actionsmap.yml | 4 +++ share/config_domain.toml | 5 ++++ src/app.py | 61 ++++++++++++++++++++-------------------- src/domain.py | 15 ++++++++++ src/utils/config.py | 19 +++++++++++++ src/utils/legacy.py | 46 ++++++++++++++++++++++++++++++ 7 files changed, 120 insertions(+), 31 deletions(-) diff --git a/locales/en.json b/locales/en.json index 91db42cb5..72aca192f 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 af13765e3..beee7799b 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 6fd1724b4..f2e7fd7f4 100644 --- a/src/domain.py +++ b/src/domain.py @@ -456,6 +456,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 99a002404..f3c8f8177 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -438,6 +438,7 @@ class ConfigPanel: "step", "accept", "redact", + "filters", ], "defaults": {}, }, @@ -703,6 +704,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 @@ -1123,6 +1125,22 @@ 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: + print(_filter) + apps = [ app for app in apps if _filter in app and app[_filter] ] + + self.choices = ["_none"] + [app['id'] for app in apps] class UserQuestion(Question): argument_type = "user" @@ -1315,6 +1333,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 306fcc87f..7a8a4540a 100644 --- a/src/utils/legacy.py +++ b/src/utils/legacy.py @@ -68,6 +68,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('domain_path' in app and app['domain_path'] in redirected_urls.values() for app in apps): + return + + for from_url, dest_url in redirected_urls.items(): + # Not a root domain, skip + if from_url.count('/') != 1 or not from_url.endswith('/'): + continue + for app in apps: + if 'domain_path' not in app or app['domain_path'] is not dest_url: + continue + domain_config_set(from_url.strip('/'), "feature.app.default", 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"), From cb7bfe7a669943e0a0cbaf9ce5cd8b92da22f0d1 Mon Sep 17 00:00:00 2001 From: Tagadda <36127788+Tagadda@users.noreply.github.com> Date: Thu, 20 Jan 2022 20:23:32 +0000 Subject: [PATCH 2/3] fix for bullseye --- src/utils/config.py | 1 - src/utils/legacy.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/config.py b/src/utils/config.py index f3c8f8177..837416ab5 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1137,7 +1137,6 @@ class AppQuestion(Question): apps = app_list(full=True)["apps"] for _filter in self.filters: - print(_filter) apps = [ app for app in apps if _filter in app and app[_filter] ] self.choices = ["_none"] + [app['id'] for app in apps] diff --git a/src/utils/legacy.py b/src/utils/legacy.py index 7a8a4540a..3d42af20b 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, ) @@ -92,17 +93,17 @@ def translate_legacy_default_app_in_ssowant_conf_json_persistent(): apps = app_list()['apps'] - if not any('domain_path' in app and app['domain_path'] in redirected_urls.values() for app in 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.items(): + 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 'domain_path' not in app or app['domain_path'] is not dest_url: + if app.get('domain_path') != dest_url: continue - domain_config_set(from_url.strip('/'), "feature.app.default", app['id']) + domain_config_set(from_url.strip('/'), "feature.app.default_app", app['id']) del redirected_urls[from_url] persistent["redirected_urls"] = redirected_urls From fde01fafd7c3375dd6323c449e39ea33419f8224 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 24 Jan 2022 18:31:04 +0100 Subject: [PATCH 3/3] app questions in config panel: handle choices with nice display names for webadmin --- src/utils/config.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/utils/config.py b/src/utils/config.py index 79d73b6ec..1ba38c604 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1127,6 +1127,7 @@ class DomainQuestion(Question): return value + class AppQuestion(Question): argument_type = "app" @@ -1139,9 +1140,15 @@ class AppQuestion(Question): apps = app_list(full=True)["apps"] for _filter in self.filters: - apps = [ app for app in apps if _filter in app and app[_filter] ] + 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}) - self.choices = ["_none"] + [app['id'] for app in apps] class UserQuestion(Question): argument_type = "user"