diff --git a/conf/mdns/yunomdns.service b/conf/mdns/yunomdns.service index 1102f18f6..5fc98afbd 100644 --- a/conf/mdns/yunomdns.service +++ b/conf/mdns/yunomdns.service @@ -1,6 +1,7 @@ [Unit] Description=YunoHost mDNS service -After=network.target +Wants=network-online.target +After=network-online.target [Service] User=mdns diff --git a/debian/changelog b/debian/changelog index cbc6cfc99..7e35024c8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,35 @@ +yunohost (11.0.4) testing; urgency=low + + - [mod] certificate: drop unused 'staging' LE mode (4b78e8e3) + - [fix] cli: bash_completion was broken ([#1423](https://github.com/YunoHost/yunohost/pull/1423)) + - [enh] mdns: Wait for network to be fully up to start the service ([#1425](https://github.com/YunoHost/yunohost/pull/1425)) + - [fix] regenconf: make some systemctl enable/disable quiet (bccff1b4, 345e50ae) + - [fix] configpanels: Compute choices for the yunohost admin when installing an app ([#1427](https://github.com/YunoHost/yunohost/pull/1427)) + - [fix] configpanels: optimize _get_toml for domains to not load the whole DNS section stuff when just getting a simple info from another section (bf6252ac) + - [fix] configpanel: oopsies, could only change the default app for domain configs :P (0a59f863) + - [fix] php73_to_php74: another search&replace for synapse (f0a01ba2) + - [fix] php73_to_php74: stopping php7.3 before starting 7.4 should be more robust in case confs are conflicting (9ae7ec59) + - [i18n] Translations updated for French, Ukrainian + + Thanks to all contributors <3 ! (Éric Gaspar, Kay0u, Tagadda, tituspijean, Tymofii-Lytvynenko) + + -- Alexandre Aubin Sat, 29 Jan 2022 19:19:44 +0100 + +yunohost (11.0.3) testing; urgency=low + + - [enh] mail: Add SNI support for postfix and dovecot ([#1413](https://github.com/YunoHost/yunohost/pull/1413)) + - [fix] services: fix a couple edge cases (4571c5b2) + - [fix] services: Do not save php-fpm services in services.yml (5d0f8021) + - [fix] diagnosis: diagnosers were run in a funky order ([#1418](https://github.com/YunoHost/yunohost/pull/1418)) + - [fix] configpanels: config_get should return possible choices for domain, user questions (and other dynamic-choices questions) ([#1420](https://github.com/YunoHost/yunohost/pull/1420)) + - [enh] apps/domain: Clarify the default app mecanism, handle it fron domain config panel ([#1406](https://github.com/YunoHost/yunohost/pull/1406)) + - [fix] apps: When no main app permission found, fallback to default label instead of having a 'None' label to prevent the webadmin from displaying an empty app list (07396b8b) + - [i18n] Translations updated for Galician + + Thanks to all contributors <3 ! (José M, Kay0u, Tagadda, tituspijean) + + -- Alexandre Aubin Tue, 25 Jan 2022 13:06:10 +0100 + yunohost (11.0.2) testing; urgency=low - [mod] Various tweaks for Python 3.9, PHP 7.4, PostgreSQL 13, and other changes related to Buster->Bullseye ecosystem diff --git a/debian/install b/debian/install index db88462ab..5169d0b62 100644 --- a/debian/install +++ b/debian/install @@ -5,6 +5,6 @@ helpers/* /usr/share/yunohost/helpers.d/ conf/* /usr/share/yunohost/conf/ locales/* /usr/share/yunohost/locales/ doc/yunohost.8.gz /usr/share/man/man8/ -doc/bash-completion.sh /etc/bash_completion.d/yunohost +doc/bash_completion.d/* /etc/bash_completion.d/ conf/metronome/modules/* /usr/lib/metronome/modules/ src/* /usr/lib/python3/dist-packages/yunohost/ diff --git a/doc/generate_bash_completion.py b/doc/generate_bash_completion.py index 72a524210..d55973010 100644 --- a/doc/generate_bash_completion.py +++ b/doc/generate_bash_completion.py @@ -13,7 +13,8 @@ import yaml THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) ACTIONSMAP_FILE = THIS_SCRIPT_DIR + "/../share/actionsmap.yml" -BASH_COMPLETION_FILE = THIS_SCRIPT_DIR + "/bash-completion.sh" +BASH_COMPLETION_FOLDER = THIS_SCRIPT_DIR + "/bash_completion.d" +BASH_COMPLETION_FILE = BASH_COMPLETION_FOLDER + "/yunohost" def get_dict_actions(OPTION_SUBTREE, category): @@ -61,6 +62,8 @@ with open(ACTIONSMAP_FILE, "r") as stream: OPTION_TREE[category]["subcategories"], subcategory ) + os.makedirs(BASH_COMPLETION_FOLDER, exist_ok=True) + with open(BASH_COMPLETION_FILE, "w") as generated_file: # header of the file diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 1f6c143a6..ceab4b2f6 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -62,7 +62,7 @@ do_init_regen() { systemctl daemon-reload - systemctl enable yunohost-api.service + systemctl enable yunohost-api.service --quiet systemctl start yunohost-api.service # Yunohost-firewall is enabled only during postinstall, not init, not 100% sure why diff --git a/hooks/conf_regen/37-mdns b/hooks/conf_regen/37-mdns index 246b5b469..3a877970b 100755 --- a/hooks/conf_regen/37-mdns +++ b/hooks/conf_regen/37-mdns @@ -27,7 +27,7 @@ _generate_config() { do_init_regen() { do_pre_regen do_post_regen /etc/systemd/system/yunomdns.service - systemctl enable yunomdns + systemctl enable yunomdns --quiet } do_pre_regen() { @@ -53,12 +53,12 @@ do_post_regen() { systemctl daemon-reload fi - systemctl disable avahi-daemon.socket --now 2>&1|| true - systemctl disable avahi-daemon --now 2>&1 || true + systemctl disable avahi-daemon.socket --quiet --now 2>/dev/null || true + systemctl disable avahi-daemon --quiet --now 2>/dev/null || true # Legacy stuff to enable the new yunomdns service on legacy systems if [[ -e /etc/avahi/avahi-daemon.conf ]] && grep -q 'yunohost' /etc/avahi/avahi-daemon.conf; then - systemctl enable yunomdns --now + systemctl enable yunomdns --now --quiet sleep 2 fi diff --git a/locales/da.json b/locales/da.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/da.json @@ -0,0 +1 @@ +{} diff --git a/locales/en.json b/locales/en.json index d8126af34..e7769f794 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", @@ -500,7 +501,7 @@ "migration_0021_yunohost_upgrade": "Starting YunoHost core upgrade...", "migration_0023_not_enough_space": "Make sufficient space available in {path} to run the migration.", "migration_0023_postgresql_11_not_installed": "PostgreSQL was not installed on your system. Nothing to do.", - "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 is installed, but not postgresql 13!? Something weird might have happened on your system :(...", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 is installed, but not PostgreSQL 13!? Something weird might have happened on your system :(...", "migration_description_0021_migrate_to_bullseye": "Upgrade the system to Debian Bullseye and YunoHost 11.x", "migration_description_0022_php73_to_php74_pools": "Migrate php7.3-fpm 'pool' conf files to php7.4", "migration_description_0023_postgresql_11_to_13": "Migrate databases from PostgreSQL 11 to 13", diff --git a/locales/fr.json b/locales/fr.json index de88b79f8..a14850b8b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -676,5 +676,14 @@ "migration_0021_patch_yunohost_conflicts": "Application du correctif pour contourner le problème de conflit...", "migration_0021_not_buster": "La distribution Debian actuelle n'est pas Buster !", "migration_description_0021_migrate_to_bullseye": "Mise à niveau du système vers Debian Bullseye et YunoHost 11.x", - "global_settings_setting_security_ssh_password_authentication": "Autoriser l'authentification par mot de passe pour SSH" -} \ No newline at end of file + "global_settings_setting_security_ssh_password_authentication": "Autoriser l'authentification par mot de passe pour SSH", + "domain_config_default_app": "Application par défaut", + "migration_description_0022_php73_to_php74_pools": "Migration des fichiers de configuration php7.3-fpm 'pool' vers php7.4", + "migration_description_0023_postgresql_11_to_13": "Migration des bases de données de PostgreSQL 11 vers 13", + "service_description_postgresql": "Stocke les données d'application (base de données SQL)", + "tools_upgrade": "Mise à niveau des packages système", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 est installé, mais pas PostgreSQL 13 ! ? Quelque chose d'anormal s'est peut-être produit sur votre système :(...", + "tools_upgrade_failed": "Impossible de mettre à jour les paquets : {packages_list}", + "migration_0023_not_enough_space": "Prévoyez suffisamment d'espace disponible dans {path} pour exécuter la migration.", + "migration_0023_postgresql_11_not_installed": "PostgreSQL n'a pas été installé sur votre système. Il n'y a rien à faire." +} diff --git a/locales/gl.json b/locales/gl.json index 5ac7ea9da..e25c698c0 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -676,5 +676,13 @@ "migration_description_0021_migrate_to_bullseye": "Actualizar o sistema a Debian Bullseye e YunoHost 11.x", "migration_0021_system_not_fully_up_to_date": "O teu sistema non está completamente actualizado. Fai unha actualización normal antes de executar a migración a Bullseye.", "migration_0021_general_warning": "Ten en conta que a migración é unha operación delicada. O equipo de YunoHost fixo todo o que puido para revisalo e probalo, pero aínda así poderían acontecer fallos no sistema ou apps.\n\nAsí as cousas, é recomendable:\n - Facer unha copia de apoio dos datos e apps importantes. Máis info en https://yunohost.org/backup;\n - Ter paciencia unha vez inicias a migración: dependendo da túa conexión a internet e hardware, podería levarlle varias horas completar o proceso.", - "global_settings_setting_security_ssh_password_authentication": "Permitir autenticación con contrasinal para SSH" -} \ No newline at end of file + "global_settings_setting_security_ssh_password_authentication": "Permitir autenticación con contrasinal para SSH", + "tools_upgrade_failed": "Non se actualizaron os paquetes: {packages_list}", + "migration_0023_not_enough_space": "Crear espazo suficiente en {path} para realizar a migración.", + "migration_0023_postgresql_11_not_installed": "PostgreSQL non estaba instalado no sistema. Nada que facer.", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 está instalado, pero PostgreSQL 13 non!? Algo raro debeu pasarlle ao teu sistema :(...", + "migration_description_0022_php73_to_php74_pools": "Migrar ficheiros de configuración de php7.3-fpm 'pool' a php7.4", + "migration_description_0023_postgresql_11_to_13": "Migrar bases de datos de PostgreSQL 11 a 13", + "service_description_postgresql": "Almacena datos da app (Base datos SQL)", + "tools_upgrade": "Actualizando paquetes do sistema" +} diff --git a/locales/uk.json b/locales/uk.json index 1f99ba1b4..ae363bd35 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -676,5 +676,14 @@ "migration_0021_system_not_fully_up_to_date": "Ваша система не повністю оновлена. Будь ласка, виконайте регулярне оновлення перед запуском міграції на Bullseye.", "migration_0021_general_warning": "Будь ласка, зверніть увагу, що ця міграція є делікатною операцією. Команда YunoHost зробила все можливе, щоб перевірити і протестувати її, але міграція все ще може порушити частину системи або її застосунків.\n\nТому рекомендовано:\n - Виконати резервне копіювання всіх важливих даних або застосунків. Подробиці на сайті https://yunohost.org/backup; \n - Наберіться терпіння після запуску міграції: В залежності від вашого з'єднання з Інтернетом і апаратного забезпечення, оновлення може зайняти до декількох годин.", "migration_description_0021_migrate_to_bullseye": "Оновлення системи до Debian Bullseye і YunoHost 11.x", - "global_settings_setting_security_ssh_password_authentication": "Дозволити автентифікацію паролем для SSH" -} \ No newline at end of file + "global_settings_setting_security_ssh_password_authentication": "Дозволити автентифікацію паролем для SSH", + "service_description_postgresql": "Зберігає дані застосунків (база даних SQL)", + "domain_config_default_app": "Типовий застосунок", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 встановлено, але не PostgreSQL 13!? У вашій системі могло статися щось неприємне :(...", + "migration_description_0023_postgresql_11_to_13": "Перенесення баз даних з PostgreSQL 11 на 13", + "tools_upgrade": "Оновлення системних пакетів", + "tools_upgrade_failed": "Не вдалося оновити наступні пакети: {packages_list}", + "migration_0023_not_enough_space": "Звільніть достатньо місця в {path} для виконання міграції.", + "migration_0023_postgresql_11_not_installed": "PostgreSQL не було встановлено у вашій системі. Нічого робити.", + "migration_description_0022_php73_to_php74_pools": "Перенесення конфігураційних файлів php7.3-fpm 'pool' на php7.4" +} diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 06c03331e..4c59e0720 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -531,9 +531,6 @@ domain: --self-signed: help: Install self-signed certificate instead of Let's Encrypt action: store_true - --staging: - help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. - action: store_true ### certificate_renew() cert-renew: @@ -552,9 +549,6 @@ domain: --no-checks: help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) action: store_true - --staging: - help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. - action: store_true ### domain_url_available() url-available: @@ -677,9 +671,6 @@ domain: --self-signed: help: Install self-signed certificate instead of Let's Encrypt action: store_true - --staging: - help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. - action: store_true ### certificate_renew() renew: @@ -698,9 +689,6 @@ domain: --no-checks: help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) action: store_true - --staging: - help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. - action: store_true ############################# @@ -896,6 +884,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..65e755365 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" + filter = "is_webapp" + default = "_none" [feature.mail] #services = ['postfix', 'dovecot'] diff --git a/src/app.py b/src/app.py index 58ee7e017..da6560a68 100644 --- a/src/app.py +++ b/src/app.py @@ -58,6 +58,7 @@ from yunohost.utils.config import ( ask_questions_and_parse_answers, DomainQuestion, PathQuestion, + hydrate_questions_with_choices, ) from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError @@ -126,6 +127,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) @@ -162,6 +164,11 @@ 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") ) @@ -178,6 +185,7 @@ def app_info(app, full=False): if not ret["label"]: logger.warning(f"Failed to get label for app {app} ?") + ret["label"] = local_manifest["name"] return ret @@ -722,6 +730,9 @@ def app_manifest(app): shutil.rmtree(extracted_app_folder) + raw_questions = manifest.get("arguments", {}).get("install", []) + manifest["arguments"]["install"] = hydrate_questions_with_choices(raw_questions) + return manifest @@ -1069,6 +1080,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( @@ -1131,6 +1143,10 @@ 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") + if ret == 0: logger.success(m18n.n("app_removed", app=app)) hook_callback("post_app_remove", env=env_dict) @@ -1142,7 +1158,7 @@ def app_remove(operation_logger, app, purge=False): @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 @@ -1151,11 +1167,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 @@ -1164,36 +1179,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): @@ -1392,7 +1383,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() @@ -1430,6 +1421,23 @@ 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/certificate.py b/src/certificate.py index 2ad294605..2a9fb4ce9 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -60,8 +60,6 @@ KEY_SIZE = 3072 VALIDITY_LIMIT = 15 # days -# For tests -STAGING_CERTIFICATION_AUTHORITY = "https://acme-staging-v02.api.letsencrypt.org" # For prod PRODUCTION_CERTIFICATION_AUTHORITY = "https://acme-v02.api.letsencrypt.org" @@ -113,9 +111,7 @@ def certificate_status(domains, full=False): return {"certificates": certificates} -def certificate_install( - domain_list, force=False, no_checks=False, self_signed=False, staging=False -): +def certificate_install(domain_list, force=False, no_checks=False, self_signed=False): """ Install a Let's Encrypt certificate for given domains (all by default) @@ -130,7 +126,7 @@ def certificate_install( if self_signed: _certificate_install_selfsigned(domain_list, force) else: - _certificate_install_letsencrypt(domain_list, force, no_checks, staging) + _certificate_install_letsencrypt(domain_list, force, no_checks) def _certificate_install_selfsigned(domain_list, force=False): @@ -233,9 +229,7 @@ def _certificate_install_selfsigned(domain_list, force=False): operation_logger.error(msg) -def _certificate_install_letsencrypt( - domains, force=False, no_checks=False, staging=False -): +def _certificate_install_letsencrypt(domains, force=False, no_checks=False): from yunohost.domain import domain_list, _assert_domain_exists if not os.path.exists(ACCOUNT_KEY_FILE): @@ -264,11 +258,6 @@ def _certificate_install_letsencrypt( "certmanager_domain_cert_not_selfsigned", domain=domain ) - if staging: - logger.warning( - "Please note that you used the --staging option, and that no new certificate will actually be enabled !" - ) - # Actual install steps for domain in domains: @@ -284,12 +273,12 @@ def _certificate_install_letsencrypt( operation_logger = OperationLogger( "letsencrypt_cert_install", [("domain", domain)], - args={"force": force, "no_checks": no_checks, "staging": staging}, + args={"force": force, "no_checks": no_checks}, ) operation_logger.start() try: - _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) + _fetch_and_enable_new_certificate(domain, no_checks=no_checks) except Exception as e: msg = f"Certificate installation for {domain} failed !\nException: {e}" logger.error(msg) @@ -304,9 +293,7 @@ def _certificate_install_letsencrypt( operation_logger.success() -def certificate_renew( - domains, force=False, no_checks=False, email=False, staging=False -): +def certificate_renew(domains, force=False, no_checks=False, email=False): """ Renew Let's Encrypt certificate for given domains (all by default) @@ -373,11 +360,6 @@ def certificate_renew( "certmanager_acme_not_configured_for_domain", domain=domain ) - if staging: - logger.warning( - "Please note that you used the --staging option, and that no new certificate will actually be enabled !" - ) - # Actual renew steps for domain in domains: @@ -399,14 +381,13 @@ def certificate_renew( args={ "force": force, "no_checks": no_checks, - "staging": staging, "email": email, }, ) operation_logger.start() try: - _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) + _fetch_and_enable_new_certificate(domain, no_checks=no_checks) except Exception as e: import traceback from io import StringIO @@ -473,7 +454,7 @@ def _check_acme_challenge_configuration(domain): return "include /etc/nginx/conf.d/acme-challenge.conf.inc" in read_file(domain_conf) -def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): +def _fetch_and_enable_new_certificate(domain, no_checks=False): if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() @@ -507,11 +488,6 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): domain_csr_file = f"{TMP_FOLDER}/{domain}.csr" - if staging: - certification_authority = STAGING_CERTIFICATION_AUTHORITY - else: - certification_authority = PRODUCTION_CERTIFICATION_AUTHORITY - try: signed_certificate = sign_certificate( ACCOUNT_KEY_FILE, @@ -519,7 +495,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): WEBROOT_FOLDER, log=logger, disable_check=no_checks, - CA=certification_authority, + CA=PRODUCTION_CERTIFICATION_AUTHORITY, ) except ValueError as e: if "urn:acme:error:rateLimited" in str(e): @@ -539,12 +515,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): # Create corresponding directory date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") - if staging: - folder_flag = "staging" - else: - folder_flag = "letsencrypt" - - new_cert_folder = f"{CERT_FOLDER}/{domain}-history/{date_tag}-{folder_flag}" + new_cert_folder = f"{CERT_FOLDER}/{domain}-history/{date_tag}-letsencrypt" os.makedirs(new_cert_folder) @@ -563,9 +534,6 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): _set_permissions(domain_cert_file, "root", "ssl-cert", 0o640) - if staging: - return - _enable_certificate(domain, new_cert_folder) # Check the status of the certificate is now good @@ -677,12 +645,6 @@ def _get_status(domain): "verbose": "Let's Encrypt", } - elif cert_issuer.startswith("Fake LE"): - CA_type = { - "code": "fake-lets-encrypt", - "verbose": "Fake Let's Encrypt", - } - else: CA_type = { "code": "other-unknown", diff --git a/src/diagnosers/50-systemresources.py b/src/diagnosers/50-systemresources.py index 1096daebf..6ac7f0ec4 100644 --- a/src/diagnosers/50-systemresources.py +++ b/src/diagnosers/50-systemresources.py @@ -18,7 +18,7 @@ class MyDiagnoser(Diagnoser): def run(self): - MB = 1024 ** 2 + MB = 1024**2 GB = MB * 1024 # diff --git a/src/diagnosis.py b/src/diagnosis.py index 34bdd2e54..007719dfc 100644 --- a/src/diagnosis.py +++ b/src/diagnosis.py @@ -656,9 +656,10 @@ class Diagnoser: def _list_diagnosis_categories(): paths = glob.glob(os.path.dirname(__file__) + "/diagnosers/??-*.py") - names = [name.split("-")[-1] for name in sorted( - [os.path.basename(path)[: -len(".py")] for path in paths] - )] + names = [ + name.split("-")[-1] + for name in sorted([os.path.basename(path)[: -len(".py")] for path in paths]) + ] return names diff --git a/src/domain.py b/src/domain.py index c94dc694d..e40b4f03c 100644 --- a/src/domain.py +++ b/src/domain.py @@ -454,19 +454,50 @@ 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() + + # Reload ssowat if default app changed + if ( + "default_app" in self.future_values + and self.future_values["default_app"] != self.values["default_app"] + ): + app_ssowatconf() + def _get_toml(self): - from yunohost.dns import _get_registrar_config_section toml = super()._get_toml() toml["feature"]["xmpp"]["xmpp"]["default"] = ( 1 if self.entity == _get_maindomain() else 0 ) - toml["dns"]["registrar"] = _get_registrar_config_section(self.entity) - # FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ... - self.registar_id = toml["dns"]["registrar"]["registrar"]["value"] - del toml["dns"]["registrar"]["registrar"]["value"] + # Optimize wether or not to load the DNS section, + # e.g. we don't want to trigger the whole _get_registary_config_section + # when just getting the current value from the feature section + filter_key = self.filter_key.split(".") if self.filter_key != "" else [] + if not filter_key or filter_key[0] == "dns": + from yunohost.dns import _get_registrar_config_section + + toml["dns"]["registrar"] = _get_registrar_config_section(self.entity) + + # FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ... + self.registar_id = toml["dns"]["registrar"]["registrar"]["value"] + del toml["dns"]["registrar"]["registrar"]["value"] return toml @@ -476,7 +507,9 @@ class DomainConfigPanel(ConfigPanel): super()._load_current_values() # FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ... - self.values["registrar"] = self.registar_id + filter_key = self.filter_key.split(".") if self.filter_key != "" else [] + if not filter_key or filter_key[0] == "dns": + self.values["registrar"] = self.registar_id def _get_domain_settings(domain: str) -> dict: @@ -509,20 +542,16 @@ def domain_cert_status(domain_list, full=False): return certificate_status(domain_list, full) -def domain_cert_install( - domain_list, force=False, no_checks=False, self_signed=False, staging=False -): +def domain_cert_install(domain_list, force=False, no_checks=False, self_signed=False): from yunohost.certificate import certificate_install - return certificate_install(domain_list, force, no_checks, self_signed, staging) + return certificate_install(domain_list, force, no_checks, self_signed) -def domain_cert_renew( - domain_list, force=False, no_checks=False, email=False, staging=False -): +def domain_cert_renew(domain_list, force=False, no_checks=False, email=False): from yunohost.certificate import certificate_renew - return certificate_renew(domain_list, force, no_checks, email, staging) + return certificate_renew(domain_list, force, no_checks, email) def domain_dns_conf(domain): diff --git a/src/migrations/0021_migrate_to_bullseye.py b/src/migrations/0021_migrate_to_bullseye.py index b2abbb026..7e24cba77 100644 --- a/src/migrations/0021_migrate_to_bullseye.py +++ b/src/migrations/0021_migrate_to_bullseye.py @@ -293,7 +293,7 @@ class MyMigration(Migration): raise YunohostError("migration_0021_not_buster") # Have > 1 Go free space on /var/ ? - if free_space_in_directory("/var/") / (1024 ** 3) < 1.0: + if free_space_in_directory("/var/") / (1024**3) < 1.0: raise YunohostError("migration_0021_not_enough_free_space") # Check system is up to date diff --git a/src/migrations/0022_php73_to_php74_pools.py b/src/migrations/0022_php73_to_php74_pools.py index 3b6db919a..a2e5eae54 100644 --- a/src/migrations/0022_php73_to_php74_pools.py +++ b/src/migrations/0022_php73_to_php74_pools.py @@ -17,6 +17,10 @@ NEWPHP_POOLS = "/etc/php/7.4/fpm/pool.d" OLDPHP_SOCKETS_PREFIX = "/run/php/php7.3-fpm" NEWPHP_SOCKETS_PREFIX = "/run/php/php7.4-fpm" +# Because of synapse é_è +OLDPHP_SOCKETS_PREFIX2 = "/run/php7.3-fpm" +NEWPHP_SOCKETS_PREFIX2 = "/run/php7.4-fpm" + MIGRATION_COMMENT = ( "; YunoHost note : this file was automatically moved from {}".format(OLDPHP_POOLS) ) @@ -50,6 +54,10 @@ class MyMigration(Migration): OLDPHP_SOCKETS_PREFIX, NEWPHP_SOCKETS_PREFIX, dest ) os.system(c) + c = "sed -i -e 's@{}@{}@g' {}".format( + OLDPHP_SOCKETS_PREFIX2, NEWPHP_SOCKETS_PREFIX2, dest + ) + os.system(c) # Also add a comment that it was automatically moved from php7.3 # (for human traceability and backward migration) @@ -69,16 +77,20 @@ class MyMigration(Migration): OLDPHP_SOCKETS_PREFIX, NEWPHP_SOCKETS_PREFIX, nf ) os.system(c) + c = "sed -i -e 's@{}@{}@g' {}".format( + OLDPHP_SOCKETS_PREFIX2, NEWPHP_SOCKETS_PREFIX2, nf + ) + os.system(c) os.system( "rm /etc/logrotate.d/php7.3-fpm" ) # We remove this otherwise the logrotate cron will be unhappy # Reload/restart the php pools - _run_service_command("restart", "php7.4-fpm") - _run_service_command("enable", "php7.4-fpm") os.system("systemctl stop php7.3-fpm") os.system("systemctl disable php7.3-fpm") + _run_service_command("restart", "php7.4-fpm") + _run_service_command("enable", "php7.4-fpm") # Reload nginx _run_service_command("reload", "nginx") diff --git a/src/tests/test_app_config.py b/src/tests/test_app_config.py index 8de03bfd5..d6cf8045d 100644 --- a/src/tests/test_app_config.py +++ b/src/tests/test_app_config.py @@ -19,6 +19,7 @@ from yunohost.app import ( app_config_set, app_ssowatconf, ) +from yunohost.user import user_create, user_delete from yunohost.utils.error import YunohostError, YunohostValidationError @@ -101,6 +102,8 @@ def config_app(request): def test_app_config_get(config_app): + user_create("alice", "Alice", "White", _get_maindomain(), "test123Ynh") + assert isinstance(app_config_get(config_app), dict) assert isinstance(app_config_get(config_app, full=True), dict) assert isinstance(app_config_get(config_app, export=True), dict) @@ -108,6 +111,8 @@ def test_app_config_get(config_app): assert isinstance(app_config_get(config_app, "main.components"), dict) assert app_config_get(config_app, "main.components.boolean") == "0" + user_delete("alice") + def test_app_config_nopanel(legacy_app): diff --git a/src/tools.py b/src/tools.py index bffa50be3..16912e896 100644 --- a/src/tools.py +++ b/src/tools.py @@ -214,7 +214,7 @@ def tools_postinstall( main_space = sum( psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions ) - GB = 1024 ** 3 + GB = 1024**3 if not force_diskspace and main_space < 10 * GB: raise YunohostValidationError("postinstall_low_rootfsspace") @@ -440,7 +440,7 @@ def tools_upgrade(operation_logger, target=None): raise YunohostValidationError("dpkg_lock_not_available") if target not in ["apps", "system"]: - raise Exception( + raise YunohostValidationError( "Uhoh ?! tools_upgrade should have 'apps' or 'system' value for argument target" ) diff --git a/src/utils/config.py b/src/utils/config.py index 45989afaf..9ae75969b 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -285,8 +285,10 @@ class ConfigPanel: ask = m18n.n(self.config["i18n"] + "_" + option["id"]) if mode == "full": - # edit self.config directly option["ask"] = ask + question_class = ARGUMENTS_TYPE_PARSERS[option.get("type", "string")] + # FIXME : maybe other properties should be taken from the question, not just choices ?. + option["choices"] = question_class(option).choices else: result[key] = {"ask": ask} if "current_value" in option: @@ -438,6 +440,7 @@ class ConfigPanel: "step", "accept", "redact", + "filter", ], "defaults": {}, }, @@ -703,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.filter = question.get("filter", "true") # .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 @@ -1109,7 +1113,10 @@ class DomainQuestion(Question): if self.default is None: self.default = _get_maindomain() - self.choices = domain_list()["domains"] + self.choices = { + domain: domain + " ★" if domain == self.default else domain + for domain in domain_list()["domains"] + } @staticmethod def normalize(value, option={}): @@ -1124,6 +1131,33 @@ 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"] + + if self.filter: + apps = [ + app + for app in apps + if evaluate_simple_js_expression(self.filter, context=app) + ] + + def _app_display(app): + domain_path_or_id = f" ({app.get('domain_path', app['id'])})" + return app["label"] + domain_path_or_id + + self.choices = {"_none": "---"} + self.choices.update({app["id"]: _app_display(app) for app in apps}) + + class UserQuestion(Question): argument_type = "user" @@ -1134,7 +1168,11 @@ class UserQuestion(Question): from yunohost.domain import _get_maindomain super().__init__(question, context, hooks) - self.choices = list(user_list()["users"].keys()) + + self.choices = { + username: f"{infos['fullname']} ({infos['mail']})" + for username, infos in user_list()["users"].items() + } if not self.choices: raise YunohostValidationError( @@ -1145,7 +1183,7 @@ class UserQuestion(Question): if self.default is None: root_mail = "root@%s" % _get_maindomain() - for user in self.choices: + for user in self.choices.keys(): if root_mail in user_info(user).get("mail-aliases", []): self.default = user break @@ -1331,6 +1369,7 @@ ARGUMENTS_TYPE_PARSERS = { "alert": DisplayTextQuestion, "markdown": DisplayTextQuestion, "file": FileQuestion, + "app": AppQuestion, } @@ -1378,3 +1417,18 @@ def ask_questions_and_parse_answers( out.append(question) return out + + +def hydrate_questions_with_choices(raw_questions: List) -> List: + out = [] + + for raw_question in raw_questions: + question = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]( + raw_question + ) + if question.choices: + raw_question["choices"] = question.choices + raw_question["default"] = question.default + out.append(raw_question) + + return out diff --git a/src/utils/legacy.py b/src/utils/legacy.py index 910dfd5a3..85898f28d 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,55 @@ 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"),