From f9afc19ed40622956076b936d384cf3bff8d7daa Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Thu, 30 Jun 2022 11:22:46 +0200 Subject: [PATCH 01/93] Added an optionnal "password" argument to the "yunohost dyndns subscribe" command --- locales/en.json | 4 +++- share/actionsmap.yml | 6 ++++++ src/dyndns.py | 12 ++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 2b2f10179..d7179cd7e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -356,6 +356,8 @@ "dyndns_key_generating": "Generating DNS key... It may take a while.", "dyndns_key_not_found": "DNS key not found for the domain", "dyndns_no_domain_registered": "No domain registered with DynDNS", + "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", + "dyndns_added_password": "Remember your recovery password, you can use it to delete this domain record.", "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", "dyndns_registered": "DynDNS domain registered", "dyndns_registration_failed": "Could not register DynDNS domain: {error}", @@ -685,4 +687,4 @@ "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - adding a first user through the 'Users' section of the webadmin (or 'yunohost user create ' in command-line);\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." -} \ No newline at end of file +} diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 89c6e914d..bf2f53371 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1406,6 +1406,12 @@ dyndns: -k: full: --key help: Public DNS key + -p: + full: --password + help: Password used to later delete the domain + extra: + pattern: *pattern_password + comment: dyndns_added_password ### dyndns_update() update: diff --git a/src/dyndns.py b/src/dyndns.py index 34f3dd5dc..39e8a7213 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -29,6 +29,7 @@ import json import glob import base64 import subprocess +import hashlib from moulinette import m18n from moulinette.core import MoulinetteError @@ -75,15 +76,19 @@ def _dyndns_available(domain): @is_unit_operation() -def dyndns_subscribe(operation_logger, domain=None, key=None): +def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): """ Subscribe to a DynDNS service Keyword argument: domain -- Full domain to subscribe with key -- Public DNS key + password -- Password that will be used to delete the domain """ + if password is None: + logger.warning(m18n.n('dyndns_no_recovery_password')) + if _guess_current_dyndns_domain() != (None, None): raise YunohostValidationError("domain_dyndns_already_subscribed") @@ -138,9 +143,12 @@ def dyndns_subscribe(operation_logger, domain=None, key=None): try: # Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever... b64encoded_key = base64.b64encode(secret.encode()).decode() + data = {"subdomain": domain} + if password: + data["recovery_password"]=hashlib.sha256((domain+":"+password.strip()).encode('utf-8')).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", - data={"subdomain": domain}, + data=data, timeout=30, ) except Exception as e: From 4a9080bdfd5057085dc962f733cd6b27c98bdef0 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Thu, 30 Jun 2022 12:23:51 +0200 Subject: [PATCH 02/93] Added a new command to delete dyndns records --- locales/en.json | 5 +++++ share/actionsmap.yml | 17 +++++++++++++++ src/dyndns.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/locales/en.json b/locales/en.json index d7179cd7e..0bbd41387 100644 --- a/locales/en.json +++ b/locales/en.json @@ -361,6 +361,10 @@ "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", "dyndns_registered": "DynDNS domain registered", "dyndns_registration_failed": "Could not register DynDNS domain: {error}", + "dyndns_unregistration_failed": "Could not unregister DynDNS domain: {error}", + "dyndns_unregistered": "Domain successfully deleted!", + "dyndns_unsubscribe_wrong_password": "Invalid password", + "dyndns_unsubscribe_wrong_domain": "Domain is not registered", "dyndns_unavailable": "The domain '{domain}' is unavailable.", "experimental_feature": "Warning: This feature is experimental and not considered stable, you should not use it unless you know what you are doing.", "extracting": "Extracting...", @@ -451,6 +455,7 @@ "log_domain_main_domain": "Make '{}' the main domain", "log_domain_remove": "Remove '{}' domain from system configuration", "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", + "log_dyndns_unsubscribe": "Unsubscribe to a YunoHost subdomain '{}'", "log_dyndns_update": "Update the IP associated with your YunoHost subdomain '{}'", "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help", "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}'", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index bf2f53371..f8d082a70 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1412,6 +1412,23 @@ dyndns: extra: pattern: *pattern_password comment: dyndns_added_password + + ### dyndns_unsubscribe() + unsubscribe: + action_help: Unsubscribe to a DynDNS service + arguments: + -d: + full: --domain + help: Full domain to subscribe with + extra: + pattern: *pattern_domain + required: True + -p: + full: --password + help: Password used to delete the domain + extra: + required: True + pattern: *pattern_password ### dyndns_update() update: diff --git a/src/dyndns.py b/src/dyndns.py index 39e8a7213..67a8b293d 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -178,6 +178,56 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): logger.success(m18n.n("dyndns_registered")) +@is_unit_operation() +def dyndns_unsubscribe(operation_logger, domain, password): + """ + Unsubscribe from a DynDNS service + + Keyword argument: + domain -- Full domain to unsubscribe with + password -- Password that is used to delete the domain ( defined when subscribing ) + """ + + operation_logger.start() + + # '165' is the convention identifier for hmac-sha512 algorithm + # '1234' is idk? doesnt matter, but the old format contained a number here... + key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" + + import requests # lazy loading this module for performance reasons + + # Send delete request + try: + r = requests.delete( + f"https://{DYNDNS_PROVIDER}/domains/{domain}", + data={"recovery_password":hashlib.sha256((str(domain)+":"+str(password).strip()).encode('utf-8')).hexdigest()}, + timeout=30, + ) + except Exception as e: + raise YunohostError("dyndns_unregistration_failed", error=str(e)) + + if r.status_code == 200: # Deletion was successful + rm(key_file, force=True) + # Yunohost regen conf will add the dyndns cron job if a key exists + # in /etc/yunohost/dyndns + regen_conf(["yunohost"]) + + # Add some dyndns update in 2 and 4 minutes from now such that user should + # not have to wait 10ish minutes for the conf to propagate + cmd = ( + "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" + ) + # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... + subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) + subprocess.check_call(["bash", "-c", cmd.format(t="4 min")]) + + logger.success(m18n.n("dyndns_unregistered")) + elif r.status_code == 403: # Wrong password + raise YunohostError("dyndns_unsubscribe_wrong_password") + elif r.status_code == 404: # Invalid domain + raise YunohostError("dyndns_unsubscribe_wrong_domain") + + @is_unit_operation() def dyndns_update( operation_logger, From fdca22ca5bea6268d282063146b15ef781d199f2 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Thu, 30 Jun 2022 12:26:53 +0200 Subject: [PATCH 03/93] Fixed typo --- share/actionsmap.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index f8d082a70..cfc1d6151 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1419,7 +1419,7 @@ dyndns: arguments: -d: full: --domain - help: Full domain to subscribe with + help: Full domain to unsubscribe with extra: pattern: *pattern_domain required: True From 4f2a111470219c04596b123e4731dba874bb7b8f Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 1 Jul 2022 10:38:25 +0200 Subject: [PATCH 04/93] We can now specify a password using the yunohost domain add command --- locales/en.json | 1 + share/actionsmap.yml | 6 ++++++ src/domain.py | 8 +++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 0bbd41387..60243b6bd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -338,6 +338,7 @@ "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", + "domain_password_no_dyndns": "The password is only used for subscribing to (and maybe later unsubscribing from) the DynDNS service", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index cfc1d6151..619b1207d 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -457,6 +457,12 @@ domain: full: --dyndns help: Subscribe to the DynDNS service action: store_true + -p: + full: --password + help: Password used to later delete the domain ( if subscribing to the DynDNS service ) + extra: + pattern: *pattern_password + comment: dyndns_added_password ### domain_remove() remove: diff --git a/src/domain.py b/src/domain.py index e40b4f03c..2426412c4 100644 --- a/src/domain.py +++ b/src/domain.py @@ -131,14 +131,14 @@ def _get_parent_domain_of(domain): @is_unit_operation() -def domain_add(operation_logger, domain, dyndns=False): +def domain_add(operation_logger, domain, dyndns=False,password=None): """ Create a custom domain Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS - + password -- Password used to later unsubscribe from DynDNS """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf @@ -183,7 +183,9 @@ def domain_add(operation_logger, domain, dyndns=False): from yunohost.dyndns import dyndns_subscribe # Actually subscribe - dyndns_subscribe(domain=domain) + dyndns_subscribe(domain=domain,password=password) + elif password: # If a password is provided, while not subscribing to a DynDNS service + logger.warning(m18n.n("domain_password_no_dyndns")) _certificate_install_selfsigned([domain], True) From 882c024bc8cdf2d03b3ccabf08eba76fbd6103f3 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 1 Jul 2022 14:16:50 +0200 Subject: [PATCH 05/93] `yunohost domain remove` now accepts a -p argument --- share/actionsmap.yml | 5 +++++ src/domain.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 619b1207d..ea1242825 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -481,6 +481,11 @@ domain: full: --force help: Do not ask confirmation to remove apps action: store_true + -p: + full: --password + help: Password used to delete the domain from DynDNS + extra: + pattern: *pattern_password ### domain_dns_conf() diff --git a/src/domain.py b/src/domain.py index 2426412c4..a8a9560cb 100644 --- a/src/domain.py +++ b/src/domain.py @@ -235,7 +235,7 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): @is_unit_operation() -def domain_remove(operation_logger, domain, remove_apps=False, force=False): +def domain_remove(operation_logger, domain, remove_apps=False, force=False, password=None): """ Delete domains @@ -244,7 +244,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): remove_apps -- Remove applications installed on the domain force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified - + password -- Recovery password used at the creation of the DynDNS domain """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove @@ -356,6 +356,13 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): hook_callback("post_domain_remove", args=[domain]) + # If a password is provided, delete the DynDNS record + if password: + from yunohost.dyndns import dyndns_unsubscribe + + # Actually unsubscribe + dyndns_unsubscribe(domain=domain,password=password) + logger.success(m18n.n("domain_deleted")) From 02a4a5fecfe18f4dacd79cdd8438a1c45925d801 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 1 Jul 2022 14:40:52 +0200 Subject: [PATCH 06/93] The option -d is deprecated, -p is preferred --- locales/en.json | 1 - share/actionsmap.yml | 4 ++-- src/domain.py | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index 60243b6bd..0bbd41387 100644 --- a/locales/en.json +++ b/locales/en.json @@ -338,7 +338,6 @@ "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", - "domain_password_no_dyndns": "The password is only used for subscribing to (and maybe later unsubscribing from) the DynDNS service", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index ea1242825..8f2e90569 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -455,11 +455,11 @@ domain: pattern: *pattern_domain -d: full: --dyndns - help: Subscribe to the DynDNS service + help: (Deprecated, using the -p option in order to set a password is recommended) Subscribe to the DynDNS service action: store_true -p: full: --password - help: Password used to later delete the domain ( if subscribing to the DynDNS service ) + help: Subscribe to the DynDNS service with a password, used to later delete the domain extra: pattern: *pattern_password comment: dyndns_added_password diff --git a/src/domain.py b/src/domain.py index a8a9560cb..3c5823037 100644 --- a/src/domain.py +++ b/src/domain.py @@ -163,6 +163,7 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): domain = domain.encode("idna").decode("utf-8") # DynDNS domain + dyndns = dyndns or (password!=None) # If a password is specified, then it is obviously a dyndns domain, no need for the extra option if dyndns: from yunohost.utils.dns import is_yunohost_dyndns_domain @@ -184,8 +185,6 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): # Actually subscribe dyndns_subscribe(domain=domain,password=password) - elif password: # If a password is provided, while not subscribing to a DynDNS service - logger.warning(m18n.n("domain_password_no_dyndns")) _certificate_install_selfsigned([domain], True) From 150614645028f54236cd9cccedfaab53865d846c Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Mon, 4 Jul 2022 10:07:30 +0200 Subject: [PATCH 07/93] Passwords can be set interactively --- share/actionsmap.yml | 9 ++++++++- src/domain.py | 2 +- src/dyndns.py | 22 +++++++++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 8f2e90569..fae7ab8f8 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -459,6 +459,8 @@ domain: action: store_true -p: full: --password + nargs: "?" + const: 0 help: Subscribe to the DynDNS service with a password, used to later delete the domain extra: pattern: *pattern_password @@ -483,6 +485,8 @@ domain: action: store_true -p: full: --password + nargs: "?" + const: 0 help: Password used to delete the domain from DynDNS extra: pattern: *pattern_password @@ -1419,6 +1423,8 @@ dyndns: help: Public DNS key -p: full: --password + nargs: "?" + const: 0 help: Password used to later delete the domain extra: pattern: *pattern_password @@ -1436,9 +1442,10 @@ dyndns: required: True -p: full: --password + nargs: "?" + const: 0 help: Password used to delete the domain extra: - required: True pattern: *pattern_password ### dyndns_update() diff --git a/src/domain.py b/src/domain.py index 3c5823037..5bdecf651 100644 --- a/src/domain.py +++ b/src/domain.py @@ -356,7 +356,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, pass hook_callback("post_domain_remove", args=[domain]) # If a password is provided, delete the DynDNS record - if password: + if password!=None: from yunohost.dyndns import dyndns_unsubscribe # Actually unsubscribe diff --git a/src/dyndns.py b/src/dyndns.py index 67a8b293d..a5532f101 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -31,7 +31,7 @@ import base64 import subprocess import hashlib -from moulinette import m18n +from moulinette import Moulinette, m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file, rm, chown, chmod @@ -144,7 +144,14 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): # Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever... b64encoded_key = base64.b64encode(secret.encode()).decode() data = {"subdomain": domain} - if password: + if password!=None: + from yunohost.utils.password import assert_password_is_strong_enough + # Ensure sufficiently complex password + if Moulinette.interface.type == "cli" and password==0: + password = Moulinette.prompt( + m18n.n("ask_password"), is_password=True, confirm=True + ) + assert_password_is_strong_enough("admin", password) data["recovery_password"]=hashlib.sha256((domain+":"+password.strip()).encode('utf-8')).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", @@ -179,7 +186,7 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): @is_unit_operation() -def dyndns_unsubscribe(operation_logger, domain, password): +def dyndns_unsubscribe(operation_logger, domain, password=None): """ Unsubscribe from a DynDNS service @@ -189,6 +196,15 @@ def dyndns_unsubscribe(operation_logger, domain, password): """ operation_logger.start() + + from yunohost.utils.password import assert_password_is_strong_enough + + # Ensure sufficiently complex password + if Moulinette.interface.type == "cli" and not password: + password = Moulinette.prompt( + m18n.n("ask_password"), is_password=True, confirm=True + ) + assert_password_is_strong_enough("admin", password) # '165' is the convention identifier for hmac-sha512 algorithm # '1234' is idk? doesnt matter, but the old format contained a number here... From 40fbc8d1ea8db5a58c46301c3022fa8ba48f8da4 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Mon, 4 Jul 2022 10:09:08 +0200 Subject: [PATCH 08/93] Clarification --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 0bbd41387..96946acd3 100644 --- a/locales/en.json +++ b/locales/en.json @@ -362,7 +362,7 @@ "dyndns_registered": "DynDNS domain registered", "dyndns_registration_failed": "Could not register DynDNS domain: {error}", "dyndns_unregistration_failed": "Could not unregister DynDNS domain: {error}", - "dyndns_unregistered": "Domain successfully deleted!", + "dyndns_unregistered": "DynDNS domain successfully unregistered", "dyndns_unsubscribe_wrong_password": "Invalid password", "dyndns_unsubscribe_wrong_domain": "Domain is not registered", "dyndns_unavailable": "The domain '{domain}' is unavailable.", From fbfcec4873b801ef9d252383a911e625e9bda280 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 09:06:52 +0200 Subject: [PATCH 09/93] `yunohost domain dns push` works with Dynette domains --- hooks/conf_regen/01-yunohost | 2 +- locales/en.json | 1 + share/actionsmap.yml | 71 ++++++++++++++++++++++++++++++++++-- src/dns.py | 34 ++++++++++++++--- src/domain.py | 37 +++++++++++++++---- src/dyndns.py | 4 +- 6 files changed, 128 insertions(+), 21 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index dc0bfc689..29da2b183 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -117,7 +117,7 @@ SHELL=/bin/bash # - if ip.yunohost.org answers ping (basic check to validate that we're connected to the internet and yunohost infra aint down) # - and if lock ain't already taken by another command # - trigger yunohost dyndns update -*/10 * * * * root : YunoHost DynDNS update; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || yunohost dyndns update >> /dev/null +*/10 * * * * root : YunoHost DynDNS update ; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || yunohost domain list --exclude-subdomains --output json | jq --raw-output '.domains[]' | grep -E "\.(noho\.st|nohost\.me|ynh\.fr)$" | xargs -I {} yunohost domain dns push "{}" >> /dev/null EOF else # (Delete cron if no dyndns domain found) diff --git a/locales/en.json b/locales/en.json index 96946acd3..a1a90d686 100644 --- a/locales/en.json +++ b/locales/en.json @@ -323,6 +323,7 @@ "domain_dns_conf_special_use_tld": "This domain is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", "domain_dns_push_already_up_to_date": "Records already up to date, nothing to do.", "domain_dns_push_failed": "Updating the DNS records failed miserably.", + "domain_dns_push_failed_domain": "Updating the DNS records for {domain} failed : {error}", "domain_dns_push_failed_to_authenticate": "Failed to authenticate on registrar's API for domain '{domain}'. Most probably the credentials are incorrect? (Error: {error})", "domain_dns_push_failed_to_list": "Failed to list current records using the registrar's API: {error}", "domain_dns_push_managed_in_parent_domain": "The automatic DNS configuration feature is managed in the parent domain {parent_domain}.", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index fae7ab8f8..85b240aa3 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -578,10 +578,69 @@ domain: help: The path to check (e.g. /coffee) subcategories: + dyndns: + subcategory_help: Subscribe and Update DynDNS Hosts + actions: + ### domain_dyndns_subscribe() + subscribe: + action_help: Subscribe to a DynDNS service + arguments: + -d: + full: --domain + help: Full domain to subscribe with + extra: + pattern: *pattern_domain + -k: + full: --key + help: Public DNS key + -p: + full: --password + nargs: "?" + const: 0 + help: Password used to later delete the domain + extra: + pattern: *pattern_password + comment: dyndns_added_password + + ### domain_dyndns_unsubscribe() + unsubscribe: + action_help: Unsubscribe from a DynDNS service + arguments: + -d: + full: --domain + help: Full domain to unsubscribe with + extra: + pattern: *pattern_domain + required: True + -p: + full: --password + nargs: "?" + const: 0 + help: Password used to delete the domain + extra: + pattern: *pattern_password config: subcategory_help: Domain settings actions: + ### domain_config_get() + get: + action_help: Display a domain configuration + api: GET /domains//config + arguments: + domain: + help: Domain name + key: + help: A specific panel, section or a question identifier + nargs: '?' + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true + -e: + full: --export + help: Only export key/values, meant to be reimported using "config set --args-file" + action: store_true ### domain_config_get() get: @@ -641,6 +700,7 @@ domain: arguments: domain: help: Domain name to push DNS conf for + nargs: "*" extra: pattern: *pattern_domain -d: @@ -1406,16 +1466,17 @@ firewall: # DynDNS # ############################# dyndns: - category_help: Subscribe and Update DynDNS Hosts + category_help: Subscribe and Update DynDNS Hosts ( deprecated, use 'yunohost domain dyndns' instead ) actions: ### dyndns_subscribe() subscribe: action_help: Subscribe to a DynDNS service + deprecated: true arguments: -d: full: --domain - help: Full domain to subscribe with + help: Full domain to subscribe with ( deprecated, use 'yunohost domain dyndns subscribe' instead ) extra: pattern: *pattern_domain -k: @@ -1432,7 +1493,8 @@ dyndns: ### dyndns_unsubscribe() unsubscribe: - action_help: Unsubscribe to a DynDNS service + action_help: Unsubscribe to a DynDNS service ( deprecated, use 'yunohost domain dyndns unsubscribe' instead ) + deprecated: true arguments: -d: full: --domain @@ -1450,7 +1512,8 @@ dyndns: ### dyndns_update() update: - action_help: Update IP on DynDNS platform + action_help: Update IP on DynDNS platform ( deprecated, use 'yunohost domain dns push DOMAIN' instead ) + deprecated: true arguments: -d: full: --domain diff --git a/src/dns.py b/src/dns.py index c8bebed41..144f8a3a2 100644 --- a/src/dns.py +++ b/src/dns.py @@ -47,6 +47,7 @@ from yunohost.utils.error import YunohostValidationError, YunohostError from yunohost.utils.network import get_public_ip from yunohost.log import is_unit_operation from yunohost.hook import hook_callback +from yunohost.dyndns import dyndns_update logger = getActionLogger("yunohost.domain") @@ -622,7 +623,17 @@ def _get_registar_settings(domain): @is_unit_operation() -def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge=False): +def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False): + if type(domains).__name__!="list": # If we provide only a domain as an argument + domains = [domains] + for domain in domains: + try: + domain_dns_push_unique(domain,dry_run=dry_run,force=force,purge=purge) + except YunohostError as e: + logger.error(m18n.n("domain_dns_push_failed_domain",domain=domain,error=str(e))) + +@is_unit_operation() +def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, purge=False): """ Send DNS records to the previously-configured registrar of the domain. """ @@ -643,12 +654,14 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= # FIXME: in the future, properly unify this with yunohost dyndns update if registrar == "yunohost": - logger.info(m18n.n("domain_dns_registrar_yunohost")) + #logger.info(m18n.n("domain_dns_registrar_yunohost")) + from yunohost.dyndns import dyndns_update + dyndns_update(domain=domain) return {} if registrar == "parent_domain": parent_domain = domain.split(".", 1)[1] - registar, registrar_credentials = _get_registar_settings(parent_domain) + registrar, registrar_credentials = _get_registar_settings(parent_domain) if any(registrar_credentials.values()): raise YunohostValidationError( "domain_dns_push_managed_in_parent_domain", @@ -656,9 +669,18 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= parent_domain=parent_domain, ) else: - raise YunohostValidationError( - "domain_registrar_is_not_configured", domain=parent_domain - ) + new_parent_domain = ".".join(parent_domain.split(".")[-3:]) + registrar, registrar_credentials = _get_registar_settings(new_parent_domain) + if registrar == "yunohost": + raise YunohostValidationError( + "domain_dns_push_managed_in_parent_domain", + domain=domain, + parent_domain=new_parent_domain, + ) + else: + raise YunohostValidationError( + "domain_registrar_is_not_configured", domain=parent_domain + ) if not all(registrar_credentials.values()): raise YunohostValidationError( diff --git a/src/domain.py b/src/domain.py index 5bdecf651..770c2931b 100644 --- a/src/domain.py +++ b/src/domain.py @@ -181,10 +181,8 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): operation_logger.start() if dyndns: - from yunohost.dyndns import dyndns_subscribe - # Actually subscribe - dyndns_subscribe(domain=domain,password=password) + domain_dyndns_subscribe(domain=domain,password=password) _certificate_install_selfsigned([domain], True) @@ -357,14 +355,37 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, pass # If a password is provided, delete the DynDNS record if password!=None: - from yunohost.dyndns import dyndns_unsubscribe - # Actually unsubscribe - dyndns_unsubscribe(domain=domain,password=password) + domain_dyndns_unsubscribe(domain=domain,password=password) logger.success(m18n.n("domain_deleted")) +def domain_dyndns_subscribe(**kwargs): + """ + Subscribe to a DynDNS domain + """ + from yunohost.dyndns import dyndns_subscribe + + dyndns_subscribe(**kwargs) + +def domain_dyndns_unsubscribe(**kwargs): + """ + Unsubscribe from a DynDNS domain + """ + from yunohost.dyndns import dyndns_unsubscribe + + dyndns_unsubscribe(**kwargs) + +def domain_dyndns_update(**kwargs): + """ + Update a DynDNS domain + """ + from yunohost.dyndns import dyndns_update + + dyndns_update(**kwargs) + + @is_unit_operation() def domain_main_domain(operation_logger, new_main_domain=None): """ @@ -572,7 +593,7 @@ def domain_dns_suggest(domain): return domain_dns_suggest(domain) -def domain_dns_push(domain, dry_run, force, purge): +def domain_dns_push(domain, dry_run=None, force=None, purge=None): from yunohost.dns import domain_dns_push - return domain_dns_push(domain, dry_run, force, purge) + return domain_dns_push(domain, dry_run=dry_run, force=force, purge=purge) diff --git a/src/dyndns.py b/src/dyndns.py index a5532f101..741bb81dc 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -176,7 +176,7 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): # Add some dyndns update in 2 and 4 minutes from now such that user should # not have to wait 10ish minutes for the conf to propagate cmd = ( - "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" + f"at -M now + {{t}} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost domain dns push {domain}'\"" ) # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) @@ -231,7 +231,7 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): # Add some dyndns update in 2 and 4 minutes from now such that user should # not have to wait 10ish minutes for the conf to propagate cmd = ( - "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" + f"at -M now + {{t}} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost domain dns push {domain}'\"" ) # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) From 11684675d7a0c10e1dbb7c36f4de45dbda997329 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 09:16:17 +0200 Subject: [PATCH 10/93] Fix --- share/actionsmap.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 85b240aa3..23e6094bf 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -642,25 +642,6 @@ domain: help: Only export key/values, meant to be reimported using "config set --args-file" action: store_true - ### domain_config_get() - get: - action_help: Display a domain configuration - api: GET /domains//config - arguments: - domain: - help: Domain name - key: - help: A specific panel, section or a question identifier - nargs: '?' - -f: - full: --full - help: Display all details (meant to be used by the API) - action: store_true - -e: - full: --export - help: Only export key/values, meant to be reimported using "config set --args-file" - action: store_true - ### domain_config_set() set: action_help: Apply a new configuration From 273c10f17d1fd3d405328a617c46c4102deae623 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 09:45:26 +0200 Subject: [PATCH 11/93] Updated locales --- locales/ca.json | 2 +- locales/de.json | 4 ++-- locales/en.json | 4 ++-- locales/es.json | 4 ++-- locales/eu.json | 4 ++-- locales/fa.json | 2 +- locales/fr.json | 4 ++-- locales/gl.json | 4 ++-- locales/it.json | 4 ++-- locales/uk.json | 4 ++-- locales/zh_Hans.json | 2 +- src/dns.py | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/locales/ca.json b/locales/ca.json index b660032d2..57ee0234c 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -531,7 +531,7 @@ "diagnosis_swap_tip": "Vigileu i tingueu en compte que els servidor està allotjant memòria d'intercanvi en una targeta SD o en l'emmagatzematge SSD, això pot reduir dràsticament l'esperança de vida del dispositiu.", "restore_already_installed_apps": "No s'han pogut restaurar les següents aplicacions perquè ja estan instal·lades: {apps}", "app_packaging_format_not_supported": "No es pot instal·lar aquesta aplicació ja que el format del paquet no és compatible amb la versió de YunoHost del sistema. Hauríeu de considerar actualitzar el sistema.", - "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost domain dns push DOMAIN --force.", "regenconf_need_to_explicitly_specify_ssh": "La configuració ssh ha estat modificada manualment, però heu d'especificar explícitament la categoria «ssh» amb --force per fer realment els canvis.", "global_settings_setting_backup_compress_tar_archives": "Comprimir els arxius (.tar.gz) en lloc d'arxius no comprimits (.tar) al crear noves còpies de seguretat. N.B.: activar aquesta opció permet fer arxius de còpia de seguretat més lleugers, però el procés inicial de còpia de seguretat serà significativament més llarg i més exigent a nivell de CPU.", "global_settings_setting_smtp_relay_host": "L'amfitrió de tramesa SMTP que s'ha d'utilitzar per enviar correus electrònics en lloc d'aquesta instància de YunoHost. És útil si esteu en una de les següents situacions: el port 25 està bloquejat per el vostre proveïdor d'accés a internet o proveïdor de servidor privat virtual, si teniu una IP residencial llistada a DUHL, si no podeu configurar el DNS invers o si el servidor no està directament exposat a internet i voleu utilitzar-ne un altre per enviar correus electrònics.", diff --git a/locales/de.json b/locales/de.json index 686eb9251..824616abd 100644 --- a/locales/de.json +++ b/locales/de.json @@ -312,7 +312,7 @@ "diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.", "diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!", "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden", - "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost dyndns update --force ein Update erzwingen.", + "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost domain dns push DOMAIN --force ein Update erzwingen.", "diagnosis_dns_point_to_doc": "Bitte schaue in der Dokumentation unter https://yunohost.org/dns_config nach, wenn du Hilfe bei der Konfiguration der DNS-Einträge benötigst.", "diagnosis_dns_discrepancy": "Der folgende DNS Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}", "diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration solltest du einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}", @@ -644,7 +644,7 @@ "log_app_config_set": "Konfiguration auf die Applikation '{}' anwenden", "log_user_import": "Konten importieren", "diagnosis_high_number_auth_failures": "In letzter Zeit gab es eine verdächtig hohe Anzahl von Authentifizierungsfehlern. Stelle sicher, dass fail2ban läuft und korrekt konfiguriert ist, oder verwende einen benutzerdefinierten Port für SSH, wie unter https://yunohost.org/security beschrieben.", - "domain_dns_registrar_yunohost": "Dies ist eine nohost.me / nohost.st / ynh.fr Domäne, ihre DNS-Konfiguration wird daher automatisch von YunoHost ohne weitere Konfiguration übernommen. (siehe Befehl 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Dies ist eine nohost.me / nohost.st / ynh.fr Domäne, ihre DNS-Konfiguration wird daher automatisch von YunoHost ohne weitere Konfiguration übernommen. (siehe Befehl 'yunohost domain dns push DOMAIN')", "domain_config_auth_entrypoint": "API-Einstiegspunkt", "domain_config_auth_application_key": "Anwendungsschlüssel", "domain_config_auth_application_secret": "Geheimer Anwendungsschlüssel", diff --git a/locales/en.json b/locales/en.json index a1a90d686..9ea928b1a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -192,7 +192,7 @@ "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}", "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help about configuring DNS records.", "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", - "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost domain dns push DOMAIN --force.", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", "diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains", "diagnosis_domain_expiration_not_found_details": "The WHOIS information for domain {domain} doesn't seem to contain the information about the expiration date?", @@ -336,7 +336,7 @@ "domain_dns_registrar_managed_in_parent_domain": "This domain is a subdomain of {parent_domain_link}. DNS registrar configuration should be managed in {parent_domain}'s configuration panel.", "domain_dns_registrar_not_supported": "YunoHost could not automatically detect the registrar handling this domain. You should manually configure your DNS records following the documentation at https://yunohost.org/dns.", "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", - "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", + "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "The domain already exists", diff --git a/locales/es.json b/locales/es.json index aebb959a8..93236189e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -491,7 +491,7 @@ "diagnosis_domain_expiration_not_found_details": "¿Parece que la información de WHOIS para el dominio {domain} no contiene información sobre la fecha de expiración?", "diagnosis_domain_not_found_details": "¡El dominio {domain} no existe en la base de datos WHOIS o ha expirado!", "diagnosis_domain_expiration_not_found": "No se pudo revisar la fecha de expiración para algunos dominios", - "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por YunoHost. Si no es el caso, puedes intentar forzar una actualización mediante yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por YunoHost. Si no es el caso, puedes intentar forzar una actualización mediante yunohost domain dns push DOMAIN --force.", "diagnosis_ip_local": "IP Local: {local}", "diagnosis_ip_no_ipv6_tip": "Tener IPv6 funcionando no es obligatorio para que su servidor funcione, pero es mejor para la salud del Internet en general. IPv6 debería ser configurado automáticamente por el sistema o su proveedor si está disponible. De otra manera, es posible que tenga que configurar varias cosas manualmente, tal y como se explica en esta documentación https://yunohost.org/#/ipv6. Si no puede habilitar IPv6 o si parece demasiado técnico, puede ignorar esta advertencia con toda seguridad.", "diagnosis_display_tip": "Para ver los problemas encontrados, puede ir a la sección de diagnóstico del webadmin, o ejecutar 'yunohost diagnosis show --issues --human-readable' en la línea de comandos.", @@ -616,7 +616,7 @@ "domain_config_auth_application_key": "LLave de Aplicación", "domain_dns_registrar_supported": "YunoHost detectó automáticamente que este dominio es manejado por el registrador **{registrar}**. Si lo desea, YunoHost configurará automáticamente esta zona DNS, si le proporciona las credenciales de API adecuadas. Puede encontrar documentación sobre cómo obtener sus credenciales de API en esta página: https://yunohost.org/registar_api_{registrar}. (También puede configurar manualmente sus registros DNS siguiendo la documentación en https://yunohost.org/dns)", "domain_dns_registrar_managed_in_parent_domain": "Este dominio es un subdominio de {parent_domain_link}. La configuración del registrador de DNS debe administrarse en el panel de configuración de {parent_domain}.", - "domain_dns_registrar_yunohost": "Este dominio es un nohost.me / nohost.st / ynh.fr y, por lo tanto, YunoHost maneja automáticamente su configuración de DNS sin ninguna configuración adicional. (vea el comando 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Este dominio es un nohost.me / nohost.st / ynh.fr y, por lo tanto, YunoHost maneja automáticamente su configuración de DNS sin ninguna configuración adicional. (vea el comando 'yunohost domain dns push DOMAIN')", "domain_dns_registrar_not_supported": "YunoHost no pudo detectar automáticamente el registrador que maneja este dominio. Debe configurar manualmente sus registros DNS siguiendo la documentación en https://yunohost.org/dns.", "global_settings_setting_security_nginx_redirect_to_https": "Redirija las solicitudes HTTP a HTTPs de forma predeterminada (¡NO LO DESACTIVE a menos que realmente sepa lo que está haciendo!)", "global_settings_setting_security_webadmin_allowlist": "Direcciones IP permitidas para acceder al webadmin. Separado por comas.", diff --git a/locales/eu.json b/locales/eu.json index e0ce226d5..60676d287 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -160,7 +160,7 @@ "certmanager_domain_not_diagnosed_yet": "Oraindik ez dago {domain} domeinurako diagnostikorik. Mesedez, berrabiarazi diagnostikoak 'DNS balioak' eta 'Web' ataletarako diagnostikoen gunean Let's Encrypt ziurtagirirako prest ote dagoen egiaztatzeko. (Edo zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztatzea desgaitzeko.)", "diagnosis_domain_expiration_warning": "Domeinu batzuk iraungitzear daude!", "app_packaging_format_not_supported": "Aplikazio hau ezin da instalatu YunoHostek ez duelako paketea ezagutzen. Sistema eguneratzea hausnartu beharko zenuke ziur asko.", - "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost dyndns update --force erabiliz.", + "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost domain dns push DOMAIN --force erabiliz.", "app_manifest_install_ask_path": "Aukeratu aplikazio hau instalatzeko URLaren bidea (domeinuaren atzeko aldean)", "app_manifest_install_ask_admin": "Aukeratu administrari bat aplikazio honetarako", "app_manifest_install_ask_password": "Aukeratu administrazio-pasahitz bat aplikazio honetarako", @@ -333,7 +333,7 @@ "domain_dns_push_not_applicable": "Ezin da {domain} domeinurako DNS konfigurazio automatiko funtzioa erabili. DNS erregistroak eskuz ezarri beharko zenituzke gidaorriei erreparatuz: https://yunohost.org/dns_config.", "domain_dns_push_managed_in_parent_domain": "DNS ezarpenak automatikoki konfiguratzeko funtzioa {parent_domain} domeinu nagusian kudeatzen da.", "domain_dns_registrar_managed_in_parent_domain": "Domeinu hau {parent_domain_link} (r)en azpidomeinua da. DNS ezarpenak {parent_domain}(r)en konfigurazio atalean kudeatu behar dira.", - "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost dyndns update' komandoa)", + "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost domain dns push DOMAIN' komandoa)", "domain_dns_registrar_not_supported": "YunoHostek ezin izan du domeinu honen erregistro-enpresa automatikoki antzeman. Eskuz konfiguratu beharko dituzu DNS ezarpenak gidalerroei erreparatuz: https://yunohost.org/dns.", "domain_dns_registrar_experimental": "Oraingoz, YunoHosten kideek ez dute **{registrar}** erregistro-enpresaren APIa nahi beste probatu eta aztertu. Funtzioa **oso esperimentala** da — kontuz!", "domain_config_mail_in": "Jasotako mezuak", diff --git a/locales/fa.json b/locales/fa.json index 599ab1ea7..d9e3f39b3 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -31,7 +31,7 @@ "diagnosis_domain_not_found_details": "دامنه {domain} در پایگاه داده WHOIS وجود ندارد یا منقضی شده است!", "diagnosis_domain_expiration_not_found": "بررسی تاریخ انقضا برخی از دامنه ها امکان پذیر نیست", "diagnosis_dns_specialusedomain": "دامنه {domain} بر اساس یک دامنه سطح بالا (TLD) مخصوص استفاده است و بنابراین انتظار نمی رود که دارای سوابق DNS واقعی باشد.", - "diagnosis_dns_try_dyndns_update_force": "پیکربندی DNS این دامنه باید به طور خودکار توسط YunoHost مدیریت شود. اگر اینطور نیست ، می توانید سعی کنید به زور یک به روز رسانی را با استفاده از yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "پیکربندی DNS این دامنه باید به طور خودکار توسط YunoHost مدیریت شود. اگر اینطور نیست ، می توانید سعی کنید به زور یک به روز رسانی را با استفاده از yunohost domain dns push DOMAIN --force.", "diagnosis_dns_point_to_doc": "لطفاً اسناد را در https://yunohost.org/dns_config برسی و مطالعه کنید، اگر در مورد پیکربندی سوابق DNS به کمک نیاز دارید.", "diagnosis_dns_discrepancy": "به نظر می رسد پرونده DNS زیر از پیکربندی توصیه شده پیروی نمی کند:
نوع: {type}
نام: {name}
ارزش فعلی: {current}
مقدار مورد انتظار: {value}", "diagnosis_dns_missing_record": "با توجه به پیکربندی DNS توصیه شده ، باید یک رکورد DNS با اطلاعات زیر اضافه کنید.
نوع: {type}
نام: {name}
ارزش: {value}", diff --git a/locales/fr.json b/locales/fr.json index 2773d0bee..6e96e500a 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -534,7 +534,7 @@ "diagnosis_swap_tip": "Merci d'être prudent et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire drastiquement l'espérance de vie du périphérique.", "restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}", "regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.", - "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost domain dns push DOMAIN --force.", "app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de YunoHost. Vous devriez probablement envisager de mettre à jour votre système.", "global_settings_setting_backup_compress_tar_archives": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar). N.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.", "diagnosis_processes_killed_by_oom_reaper": "Certains processus ont été arrêtés récemment par le système car il manquait de mémoire. Cela apparaît généralement quand le système manque de mémoire ou qu'un processus consomme trop de mémoire. Liste des processus tués :\n{kills_summary}", @@ -628,7 +628,7 @@ "service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}", "domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.", "domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.", - "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost domain dns push DOMAIN')", "domain_dns_registrar_supported": "YunoHost a détecté automatiquement que ce domaine est géré par le registrar **{registrar}**. Si vous le souhaitez, YunoHost configurera automatiquement cette zone DNS, si vous lui fournissez les identifiants API appropriés. Vous pouvez trouver de la documentation sur la façon d'obtenir vos identifiants API sur cette page : https://yunohost.org/registar_api_{registrar}. (Vous pouvez également configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns )", "domain_config_features_disclaimer": "Jusqu'à présent, l'activation/désactivation des fonctionnalités de messagerie ou XMPP n'a d'impact que sur la configuration DNS recommandée et automatique, et non sur les configurations système !", "domain_dns_push_managed_in_parent_domain": "La fonctionnalité de configuration DNS automatique est gérée dans le domaine parent {parent_domain}.", diff --git a/locales/gl.json b/locales/gl.json index 4a77645d6..8d53051f2 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -191,7 +191,7 @@ "diagnosis_domain_expiration_not_found_details": "A información WHOIS para o dominio {domain} non semella conter información acerca da data de caducidade?", "diagnosis_domain_not_found_details": "O dominio {domain} non existe na base de datos de WHOIS ou está caducado!", "diagnosis_domain_expiration_not_found": "Non se puido comprobar a data de caducidade para algúns dominios", - "diagnosis_dns_try_dyndns_update_force": "A xestión DNS deste dominio debería estar xestionada directamente por YunoHost. Se non fose o caso, podes intentar forzar unha actualización executando yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "A xestión DNS deste dominio debería estar xestionada directamente por YunoHost. Se non fose o caso, podes intentar forzar unha actualización executando yunohost domain dns push DOMAIN --force.", "diagnosis_swap_ok": "O sistema ten {total} de swap!", "diagnosis_swap_notsomuch": "O sistema só ten {total} de swap. Deberías considerar ter polo menos {recommended} para evitar situacións onde o sistema esgote a memoria.", "diagnosis_swap_none": "O sistema non ten partición swap. Deberías considerar engadir polo menos {recommended} de swap para evitar situación onde o sistema esgote a memoria.", @@ -648,7 +648,7 @@ "domain_config_auth_consumer_key": "Chave consumidora", "log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'", "other_available_options": "... e outras {n} opcións dispoñibles non mostradas", - "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost domain dns push DOMAIN')", "domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registrar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registrar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )", "domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.", "domain_config_auth_token": "Token de autenticación", diff --git a/locales/it.json b/locales/it.json index 844b756ea..27ab21473 100644 --- a/locales/it.json +++ b/locales/it.json @@ -321,7 +321,7 @@ "diagnosis_domain_expiration_not_found_details": "Le informazioni WHOIS per il dominio {domain} non sembrano contenere la data di scadenza, giusto?", "diagnosis_domain_not_found_details": "Il dominio {domain} non esiste nel database WHOIS o è scaduto!", "diagnosis_domain_expiration_not_found": "Non riesco a controllare la data di scadenza di alcuni domini", - "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da YunoHost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da YunoHost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost domain dns push DOMAIN --force.", "diagnosis_dns_point_to_doc": "Controlla la documentazione a https://yunohost.org/dns_config se hai bisogno di aiuto nel configurare i record DNS.", "diagnosis_dns_discrepancy": "Il record DNS non sembra seguire la configurazione DNS raccomandata:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", "diagnosis_dns_missing_record": "Stando alla configurazione DNS raccomandata, dovresti aggiungere un record DNS con le seguenti informazioni.
Type: {type}
Name: {name}
Value: {value}", @@ -641,7 +641,7 @@ "diagnosis_description_apps": "Applicazioni", "domain_registrar_is_not_configured": "Il registrar non è ancora configurato per il dominio {domain}.", "domain_dns_registrar_managed_in_parent_domain": "Questo dominio è un sotto-dominio di {parent_domain_link}. La configurazione del registrar DNS dovrebbe essere gestita dal pannello di configurazione di {parent_domain}.", - "domain_dns_registrar_yunohost": "Questo dominio è un nohost.me / nohost.st / ynh.fr, perciò la sua configurazione DNS è gestita automaticamente da YunoHost, senza alcuna ulteriore configurazione. (vedi il comando yunohost dyndns update)", + "domain_dns_registrar_yunohost": "Questo dominio è un nohost.me / nohost.st / ynh.fr, perciò la sua configurazione DNS è gestita automaticamente da YunoHost, senza alcuna ulteriore configurazione. (vedi il comando yunohost domain dns push DOMAIN)", "domain_dns_push_success": "Record DNS aggiornati!", "domain_dns_push_failed": "L’aggiornamento dei record DNS è miseramente fallito.", "domain_dns_push_partial_failure": "Record DNS parzialmente aggiornati: alcuni segnali/errori sono stati riportati.", diff --git a/locales/uk.json b/locales/uk.json index 9a32a597b..ba6737d7a 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -436,7 +436,7 @@ "diagnosis_domain_not_found_details": "Домен {domain} не існує в базі даних WHOIS або строк його дії сплив!", "diagnosis_domain_expiration_not_found": "Неможливо перевірити строк дії деяких доменів", "diagnosis_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) такого як .local або .test і тому не очікується, що у нього будуть актуальні записи DNS.", - "diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди yunohost domain dns push DOMAIN --force.", "diagnosis_dns_point_to_doc": "Якщо вам потрібна допомога з налаштування DNS-записів, зверніться до документації на сайті https://yunohost.org/dns_config.", "diagnosis_dns_discrepancy": "Наступний запис DNS, схоже, не відповідає рекомендованій конфігурації:
Тип: {type}
Назва: {name}
Поточне значення: {current}
Очікуване значення: {value}", "diagnosis_dns_missing_record": "Згідно рекомендованої конфігурації DNS, ви повинні додати запис DNS з наступними відомостями.
Тип: {type}
Назва: {name}
Значення: {value}", @@ -632,7 +632,7 @@ "diagnosis_http_special_use_tld": "Домен {domain} базується на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він буде відкритий за межами локальної мережі.", "domain_dns_push_managed_in_parent_domain": "Функцією автоконфігурації DNS керує батьківський домен {parent_domain}.", "domain_dns_registrar_managed_in_parent_domain": "Цей домен є піддоменом {parent_domain_link}. Конфігурацією реєстратора DNS слід керувати на панелі конфігурації {parent_domain}.", - "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost domain dns push DOMAIN')", "domain_dns_conf_special_use_tld": "Цей домен засновано на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він матиме актуальні записи DNS.", "domain_dns_registrar_supported": "YunoHost автоматично визначив, що цей домен обслуговується реєстратором **{registrar}**. Якщо ви хочете, YunoHost автоматично налаштує цю DNS-зону, якщо ви надасте йому відповідні облікові дані API. Ви можете знайти документацію про те, як отримати реєстраційні дані API на цій сторінці: https://yunohost.org/registar_api_{registrar}. (Ви також можете вручну налаштувати свої DNS-записи, дотримуючись документації на https://yunohost.org/dns)", "domain_dns_registrar_experimental": "Поки що інтерфейс з API **{registrar}** не був належним чином протестований і перевірений спільнотою YunoHost. Підтримка є **дуже експериментальною** - будьте обережні!", diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index 2daf45483..d3b12f778 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -477,7 +477,7 @@ "diagnosis_diskusage_low": "存储器{mountpoint}(在设备{device}上)只有{free} ({free_percent}%) 的空间。({free_percent}%)的剩余空间(在{total}中)。要小心。", "diagnosis_diskusage_verylow": "存储器{mountpoint}(在设备{device}上)仅剩余{free} ({free_percent}%) (剩余{total})个空间。您应该真正考虑清理一些空间!", "diagnosis_services_bad_status_tip": "你可以尝试重新启动服务,如果没有效果,可以看看webadmin中的服务日志(从命令行,你可以用yunohost service restart {service}yunohost service log {service})来做。", - "diagnosis_dns_try_dyndns_update_force": "该域的DNS配置应由YunoHost自动管理,如果不是这种情况,您可以尝试使用 yunohost dyndns update --force强制进行更新。", + "diagnosis_dns_try_dyndns_update_force": "该域的DNS配置应由YunoHost自动管理,如果不是这种情况,您可以尝试使用 yunohost domain dns push DOMAIN --force强制进行更新。", "diagnosis_dns_point_to_doc": "如果您需要有关配置DNS记录的帮助,请查看 https://yunohost.org/dns_config 上的文档。", "diagnosis_dns_discrepancy": "以下DNS记录似乎未遵循建议的配置:
类型: {type}
名称: {name}
代码> 当前值: {current}期望值: {value}", "log_backup_create": "创建备份档案", diff --git a/src/dns.py b/src/dns.py index 144f8a3a2..9cb2d4044 100644 --- a/src/dns.py +++ b/src/dns.py @@ -656,7 +656,7 @@ def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, if registrar == "yunohost": #logger.info(m18n.n("domain_dns_registrar_yunohost")) from yunohost.dyndns import dyndns_update - dyndns_update(domain=domain) + dyndns_update(domain=domain,force=force) return {} if registrar == "parent_domain": From ab37617f91d482a4ac7b75827f0e254859971a18 Mon Sep 17 00:00:00 2001 From: theo-is-taken <108329355+theo-is-taken@users.noreply.github.com> Date: Tue, 5 Jul 2022 09:54:23 +0200 Subject: [PATCH 12/93] Update src/dns.py Co-authored-by: ljf (zamentur) --- src/dns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dns.py b/src/dns.py index 9cb2d4044..00bb72d42 100644 --- a/src/dns.py +++ b/src/dns.py @@ -624,7 +624,8 @@ def _get_registar_settings(domain): @is_unit_operation() def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False): - if type(domains).__name__!="list": # If we provide only a domain as an argument + # If we provide only a domain as an argument + if isinstance(domains, str): domains = [domains] for domain in domains: try: From e58aaa6db607d2d51be9f842b1e8831afbf91ffc Mon Sep 17 00:00:00 2001 From: theo-is-taken <108329355+theo-is-taken@users.noreply.github.com> Date: Tue, 5 Jul 2022 09:55:19 +0200 Subject: [PATCH 13/93] Update src/dyndns.py Co-authored-by: ljf (zamentur) --- src/dyndns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index 741bb81dc..fd86df9ff 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -216,7 +216,8 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): try: r = requests.delete( f"https://{DYNDNS_PROVIDER}/domains/{domain}", - data={"recovery_password":hashlib.sha256((str(domain)+":"+str(password).strip()).encode('utf-8')).hexdigest()}, + secret = str(domain) + ":" + str(password).strip() + data = {"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, timeout=30, ) except Exception as e: From 0903460fc4a0d520e195ec1b43d43510b1bcba2f Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 09:59:15 +0200 Subject: [PATCH 14/93] Fix --- src/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index fd86df9ff..e755c803d 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -214,9 +214,9 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): # Send delete request try: + secret = str(domain) + ":" + str(password).strip() r = requests.delete( f"https://{DYNDNS_PROVIDER}/domains/{domain}", - secret = str(domain) + ":" + str(password).strip() data = {"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, timeout=30, ) From dd51adcd3f467968c2663f985832e43395d0b995 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 10:02:58 +0200 Subject: [PATCH 15/93] Removed useless call to dns push --- src/dyndns.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/dyndns.py b/src/dyndns.py index e755c803d..3db4b7521 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -229,15 +229,6 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): # in /etc/yunohost/dyndns regen_conf(["yunohost"]) - # Add some dyndns update in 2 and 4 minutes from now such that user should - # not have to wait 10ish minutes for the conf to propagate - cmd = ( - f"at -M now + {{t}} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost domain dns push {domain}'\"" - ) - # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... - subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) - subprocess.check_call(["bash", "-c", cmd.format(t="4 min")]) - logger.success(m18n.n("dyndns_unregistered")) elif r.status_code == 403: # Wrong password raise YunohostError("dyndns_unsubscribe_wrong_password") From ac60516638dc0fdf16cfc571ecc099a89da34797 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 10:15:56 +0200 Subject: [PATCH 16/93] Raise an actual error (instead of log) --- locales/en.json | 1 + src/dns.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/locales/en.json b/locales/en.json index 9ea928b1a..3c56c207b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -324,6 +324,7 @@ "domain_dns_push_already_up_to_date": "Records already up to date, nothing to do.", "domain_dns_push_failed": "Updating the DNS records failed miserably.", "domain_dns_push_failed_domain": "Updating the DNS records for {domain} failed : {error}", + "domain_dns_push_failed_domains": "Updating the DNS records for {domains} failed.", "domain_dns_push_failed_to_authenticate": "Failed to authenticate on registrar's API for domain '{domain}'. Most probably the credentials are incorrect? (Error: {error})", "domain_dns_push_failed_to_list": "Failed to list current records using the registrar's API: {error}", "domain_dns_push_managed_in_parent_domain": "The automatic DNS configuration feature is managed in the parent domain {parent_domain}.", diff --git a/src/dns.py b/src/dns.py index 00bb72d42..0a7ce7ea2 100644 --- a/src/dns.py +++ b/src/dns.py @@ -627,11 +627,15 @@ def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge # If we provide only a domain as an argument if isinstance(domains, str): domains = [domains] + error_domains = [] for domain in domains: try: domain_dns_push_unique(domain,dry_run=dry_run,force=force,purge=purge) except YunohostError as e: logger.error(m18n.n("domain_dns_push_failed_domain",domain=domain,error=str(e))) + error_domains.append(domain) + if len(error_domains)>0: + raise YunohostError("domain_dns_push_failed_domains",domains=', '.join(error_domains)) @is_unit_operation() def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, purge=False): From cf6eaf364d2deb9fb0aa81b25bafed2997ecd21a Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 10:45:21 +0200 Subject: [PATCH 17/93] Better password assert placement --- src/dyndns.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/dyndns.py b/src/dyndns.py index 3db4b7521..02ebe2cca 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -88,6 +88,14 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): if password is None: logger.warning(m18n.n('dyndns_no_recovery_password')) + else: + from yunohost.utils.password import assert_password_is_strong_enough + # Ensure sufficiently complex password + if Moulinette.interface.type == "cli" and password==0: + password = Moulinette.prompt( + m18n.n("ask_password"), is_password=True, confirm=True + ) + assert_password_is_strong_enough("admin", password) if _guess_current_dyndns_domain() != (None, None): raise YunohostValidationError("domain_dyndns_already_subscribed") @@ -145,13 +153,6 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): b64encoded_key = base64.b64encode(secret.encode()).decode() data = {"subdomain": domain} if password!=None: - from yunohost.utils.password import assert_password_is_strong_enough - # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and password==0: - password = Moulinette.prompt( - m18n.n("ask_password"), is_password=True, confirm=True - ) - assert_password_is_strong_enough("admin", password) data["recovery_password"]=hashlib.sha256((domain+":"+password.strip()).encode('utf-8')).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", @@ -195,17 +196,17 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): password -- Password that is used to delete the domain ( defined when subscribing ) """ - operation_logger.start() - from yunohost.utils.password import assert_password_is_strong_enough # Ensure sufficiently complex password if Moulinette.interface.type == "cli" and not password: password = Moulinette.prompt( m18n.n("ask_password"), is_password=True, confirm=True - ) + ) assert_password_is_strong_enough("admin", password) + operation_logger.start() + # '165' is the convention identifier for hmac-sha512 algorithm # '1234' is idk? doesnt matter, but the old format contained a number here... key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" From e4c631c171df131b4d7bb5efd4ae239a9696a25f Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 11:30:40 +0200 Subject: [PATCH 18/93] Added `yunohost domain dyndns list` command --- share/actionsmap.yml | 4 ++++ src/domain.py | 8 ++++++++ src/dyndns.py | 12 ++++++++++++ 3 files changed, 24 insertions(+) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 23e6094bf..5327edcb2 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -620,6 +620,10 @@ domain: extra: pattern: *pattern_password + ### domain_dyndns_list() + list: + action_help: List all subscribed DynDNS domains + config: subcategory_help: Domain settings actions: diff --git a/src/domain.py b/src/domain.py index 770c2931b..4203e1c0f 100644 --- a/src/domain.py +++ b/src/domain.py @@ -377,6 +377,14 @@ def domain_dyndns_unsubscribe(**kwargs): dyndns_unsubscribe(**kwargs) +def domain_dyndns_list(): + """ + Returns all currently subscribed DynDNS domains + """ + from yunohost.dyndns import dyndns_list + + return dyndns_list() + def domain_dyndns_update(**kwargs): """ Update a DynDNS domain diff --git a/src/dyndns.py b/src/dyndns.py index 02ebe2cca..1673b6d16 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -236,6 +236,18 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): elif r.status_code == 404: # Invalid domain raise YunohostError("dyndns_unsubscribe_wrong_domain") +def dyndns_list(): + """ + Returns all currently subscribed DynDNS domains ( deduced from the key files ) + """ + + files = glob.glob("/etc/yunohost/dyndns/K*key") + # Get the domain names + for i in range(len(files)): + files[i] = files[i].split(".+",1)[0] + files[i] = files[i].split("/etc/yunohost/dyndns/K")[1] + + return {"domains":files} @is_unit_operation() def dyndns_update( From 986b42fc1daa00ac79f748463703514997f6a262 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 14:37:14 +0200 Subject: [PATCH 19/93] `yunohost domain add` auto-detect DynDNS domains and asks for a --subscribe or --no-subscribe option --- share/actionsmap.yml | 13 +++++++------ src/domain.py | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 5327edcb2..bb9fa2ae7 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -453,15 +453,16 @@ domain: help: Domain name to add extra: pattern: *pattern_domain - -d: - full: --dyndns - help: (Deprecated, using the -p option in order to set a password is recommended) Subscribe to the DynDNS service + -n: + full: --no-subscribe + help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true - -p: - full: --password + -s: + full: --subscribe + metavar: PASSWORD nargs: "?" const: 0 - help: Subscribe to the DynDNS service with a password, used to later delete the domain + help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain extra: pattern: *pattern_password comment: dyndns_added_password diff --git a/src/domain.py b/src/domain.py index 4203e1c0f..e6a61cea0 100644 --- a/src/domain.py +++ b/src/domain.py @@ -40,6 +40,7 @@ from yunohost.app import ( from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf from yunohost.utils.config import ConfigPanel, Question from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.log import is_unit_operation logger = getActionLogger("yunohost.domain") @@ -131,7 +132,7 @@ def _get_parent_domain_of(domain): @is_unit_operation() -def domain_add(operation_logger, domain, dyndns=False,password=None): +def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): """ Create a custom domain @@ -163,10 +164,12 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): domain = domain.encode("idna").decode("utf-8") # DynDNS domain - dyndns = dyndns or (password!=None) # If a password is specified, then it is obviously a dyndns domain, no need for the extra option + dyndns = is_yunohost_dyndns_domain(domain) if dyndns: + print(subscribe,no_subscribe) + if ((subscribe==None) == (no_subscribe==False)): + raise YunohostValidationError("domain_dyndns_instruction_unclear") - from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.dyndns import _guess_current_dyndns_domain # Do not allow to subscribe to multiple dyndns domains... @@ -178,11 +181,14 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): if not is_yunohost_dyndns_domain(domain): raise YunohostValidationError("domain_dyndns_root_unknown") - operation_logger.start() - if dyndns: + operation_logger.start() + if not dyndns and (subscribe is not None or no_subscribe): + logger.warning("This domain is not a DynDNS one, no need for the --subscribe or --no-subscribe option") + + if dyndns and not no_subscribe: # Actually subscribe - domain_dyndns_subscribe(domain=domain,password=password) + domain_dyndns_subscribe(domain=domain,password=subscribe) _certificate_install_selfsigned([domain], True) From 840bed5222f23dd3cc07713226282d3505047ad2 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 15:10:04 +0200 Subject: [PATCH 20/93] `yunohost domain remove` auto-detects DynDNS domains and now uses --unsubscribe and --no-unsubscribe --- share/actionsmap.yml | 11 ++++++++--- src/domain.py | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index bb9fa2ae7..eb24de49f 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -484,11 +484,16 @@ domain: full: --force help: Do not ask confirmation to remove apps action: store_true - -p: - full: --password + -n: + full: --no-unsubscribe + help: If removing a DynDNS domain, only remove the domain, without unsubscribing from the DynDNS service + action: store_true + -u: + full: --unsubscribe + metavar: PASSWORD nargs: "?" const: 0 - help: Password used to delete the domain from DynDNS + help: If removing a DynDNS domain, unsubscribe from the DynDNS service with a password extra: pattern: *pattern_password diff --git a/src/domain.py b/src/domain.py index e6a61cea0..9bd6a05bd 100644 --- a/src/domain.py +++ b/src/domain.py @@ -139,7 +139,8 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS - password -- Password used to later unsubscribe from DynDNS + subscribe -- Password used to later unsubscribe from DynDNS + no_unsubscribe -- If we want to just add the DynDNS domain to the list, without subscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf @@ -166,7 +167,6 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): # DynDNS domain dyndns = is_yunohost_dyndns_domain(domain) if dyndns: - print(subscribe,no_subscribe) if ((subscribe==None) == (no_subscribe==False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") @@ -238,7 +238,7 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): @is_unit_operation() -def domain_remove(operation_logger, domain, remove_apps=False, force=False, password=None): +def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsubscribe=None,no_unsubscribe=False): """ Delete domains @@ -247,7 +247,8 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, pass remove_apps -- Remove applications installed on the domain force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified - password -- Recovery password used at the creation of the DynDNS domain + unsubscribe -- Recovery password used at the creation of the DynDNS domain + no_unsubscribe -- If we just remove the DynDNS domain, without unsubscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove @@ -312,9 +313,18 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, pass "domain_uninstall_app_first", apps="\n".join([x[1] for x in apps_on_that_domain]), ) + + # DynDNS domain + dyndns = is_yunohost_dyndns_domain(domain) + if dyndns: + if ((unsubscribe==None) == (no_unsubscribe==False)): + raise YunohostValidationError("domain_dyndns_instruction_unclear") operation_logger.start() + if not dyndns and (unsubscribe!=None or no_unsubscribe!=False): + logger.warning("This domain is not a DynDNS one, no need for the --unsubscribe or --no-unsubscribe option") + ldap = _get_ldap_interface() try: ldap.remove("virtualdomain=" + domain + ",ou=domains") @@ -360,9 +370,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, pass hook_callback("post_domain_remove", args=[domain]) # If a password is provided, delete the DynDNS record - if password!=None: + if dyndns and not no_unsubscribe: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain,password=password) + domain_dyndns_unsubscribe(domain=domain,password=unsubscribe) logger.success(m18n.n("domain_deleted")) From 3bd427afab266d34c3a8b0543c6625b6f5dd40e0 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 15:14:33 +0200 Subject: [PATCH 21/93] Password is only asked once if we unsubscribe --- src/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index 1673b6d16..f5531d518 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -201,7 +201,7 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): # Ensure sufficiently complex password if Moulinette.interface.type == "cli" and not password: password = Moulinette.prompt( - m18n.n("ask_password"), is_password=True, confirm=True + m18n.n("ask_password"), is_password=True ) assert_password_is_strong_enough("admin", password) From 7117c61bbff87c847695275e8d6ff89f9f607f39 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 15:25:43 +0200 Subject: [PATCH 22/93] Removed useless error --- locales/en.json | 1 - src/domain.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/locales/en.json b/locales/en.json index 3c56c207b..f5a38709a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -339,7 +339,6 @@ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", - "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", diff --git a/src/domain.py b/src/domain.py index 9bd6a05bd..f9597b813 100644 --- a/src/domain.py +++ b/src/domain.py @@ -176,12 +176,6 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): if _guess_current_dyndns_domain() != (None, None): raise YunohostValidationError("domain_dyndns_already_subscribed") - # Check that this domain can effectively be provided by - # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st) - if not is_yunohost_dyndns_domain(domain): - raise YunohostValidationError("domain_dyndns_root_unknown") - - operation_logger.start() if not dyndns and (subscribe is not None or no_subscribe): logger.warning("This domain is not a DynDNS one, no need for the --subscribe or --no-subscribe option") From bc3521fd0452295378177033496976f6d1813cda Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 6 Jul 2022 09:30:00 +0200 Subject: [PATCH 23/93] `yunohost tools postinstall` auto-detect DynDNS domains and asks for a --subscribe or --no-subscribe option --- share/actionsmap.yml | 14 ++++++++++++-- src/tools.py | 18 ++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index eb24de49f..d69a35f1f 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1574,9 +1574,19 @@ tools: pattern: *pattern_password required: True comment: good_practices_about_admin_password - --ignore-dyndns: - help: Do not subscribe domain to a DynDNS service + -n: + full: --no-subscribe + help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true + -s: + full: --subscribe + metavar: PASSWORD + nargs: "?" + const: 0 + help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain + extra: + pattern: *pattern_password + comment: dyndns_added_password --force-password: help: Use this if you really want to set a weak password action: store_true diff --git a/src/tools.py b/src/tools.py index bb7ded03a..543f835e6 100644 --- a/src/tools.py +++ b/src/tools.py @@ -186,6 +186,8 @@ def tools_postinstall( domain, password, ignore_dyndns=False, + subscribe=None, + no_subscribe=False, force_password=False, force_diskspace=False, ): @@ -230,10 +232,13 @@ def tools_postinstall( assert_password_is_strong_enough("admin", password) # If this is a nohost.me/noho.st, actually check for availability - if not ignore_dyndns and is_yunohost_dyndns_domain(domain): + if is_yunohost_dyndns_domain(domain): + if ((subscribe==None) == (no_subscribe==False)): + raise YunohostValidationError("domain_dyndns_instruction_unclear") + # Check if the domain is available... try: - available = _dyndns_available(domain) + _dyndns_available(domain) # If an exception is thrown, most likely we don't have internet # connectivity or something. Assume that this domain isn't manageable # and inform the user that we could not contact the dyndns host server. @@ -241,14 +246,7 @@ def tools_postinstall( logger.warning( m18n.n("dyndns_provider_unreachable", provider="dyndns.yunohost.org") ) - - if available: - dyndns = True - # If not, abort the postinstall - else: raise YunohostValidationError("dyndns_unavailable", domain=domain) - else: - dyndns = False if os.system("iptables -V >/dev/null 2>/dev/null") != 0: raise YunohostValidationError( @@ -260,7 +258,7 @@ def tools_postinstall( logger.info(m18n.n("yunohost_installing")) # New domain config - domain_add(domain, dyndns) + domain_add(domain, subscribe=subscribe,no_subscribe=no_subscribe) domain_main_domain(domain) # Update LDAP admin and create home dir From 01dfb778e9bd8b29b2980dcc6ac9d75ed6219dbf Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 6 Jul 2022 09:32:25 +0200 Subject: [PATCH 24/93] Added domain_dyndns_instruction_unclear --- locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/locales/en.json b/locales/en.json index f5a38709a..8e3d5c2b6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -339,6 +339,7 @@ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", + "domain_dyndns_instruction_unclear": "The --subscribe and --no-subscribe options are not compatible", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", From 940af74c2d58fa9a0b055c7c6cbb4dfdd4202e03 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 6 Jul 2022 15:52:27 +0200 Subject: [PATCH 25/93] `yunohost domain dns push` now accepts an --auto option Domains can be configured to be auto-pushed by a cron job --- hooks/conf_regen/01-yunohost | 5 +++-- locales/en.json | 2 ++ share/actionsmap.yml | 10 ++++++++-- share/config_domain.toml | 7 +++++++ src/dns.py | 9 +++++---- src/domain.py | 8 +++++--- src/dyndns.py | 4 ++++ 7 files changed, 34 insertions(+), 11 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 29da2b183..55accc4f4 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -116,8 +116,9 @@ SHELL=/bin/bash # - (sleep random 60 is here to spread requests over a 1-min window) # - if ip.yunohost.org answers ping (basic check to validate that we're connected to the internet and yunohost infra aint down) # - and if lock ain't already taken by another command -# - trigger yunohost dyndns update -*/10 * * * * root : YunoHost DynDNS update ; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || yunohost domain list --exclude-subdomains --output json | jq --raw-output '.domains[]' | grep -E "\.(noho\.st|nohost\.me|ynh\.fr)$" | xargs -I {} yunohost domain dns push "{}" >> /dev/null +# - check if some domains are flagged as autopush +# - trigger yunohost domain dns push --auto +*/10 * * * * root : YunoHost DynDNS update ; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || !(grep -nR "autopush: 1" /etc/yunohost/domains/*.yml > /dev/null) || yunohost domain dns push --auto >> /dev/null EOF else # (Delete cron if no dyndns domain found) diff --git a/locales/en.json b/locales/en.json index 8e3d5c2b6..d58790ba2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -312,6 +312,8 @@ "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_autopush": "Auto-push", + "domain_config_autopush_help": "Automatically update the domain's record", "domain_config_mail_in": "Incoming emails", "domain_config_mail_out": "Outgoing emails", "domain_config_xmpp": "Instant messaging (XMPP)", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index d69a35f1f..17b4c1f96 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -443,6 +443,9 @@ domain: --exclude-subdomains: help: Filter out domains that are obviously subdomains of other declared domains action: store_true + --auto-push: + help: Only display domains that are pushed automatically + action: store_true ### domain_add() add: @@ -689,8 +692,8 @@ domain: action_help: Push DNS records to registrar api: POST /domains//dns/push arguments: - domain: - help: Domain name to push DNS conf for + domains: + help: Domain names to push DNS conf for nargs: "*" extra: pattern: *pattern_domain @@ -704,6 +707,9 @@ domain: --purge: help: Delete all records action: store_true + --auto: + help: Push only domains that should be pushed automatically + action: store_true cert: subcategory_help: Manage domain certificates diff --git a/share/config_domain.toml b/share/config_domain.toml index 65e755365..ba0706749 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -46,6 +46,13 @@ i18n = "domain_config" default = 0 [dns] + + [dns.zone] + + [dns.zone.autopush] + type = "boolean" + default = 0 + help = "" [dns.registrar] optional = true diff --git a/src/dns.py b/src/dns.py index 0a7ce7ea2..8ba46011e 100644 --- a/src/dns.py +++ b/src/dns.py @@ -623,10 +623,11 @@ def _get_registar_settings(domain): @is_unit_operation() -def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False): - # If we provide only a domain as an argument - if isinstance(domains, str): - domains = [domains] +def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False, auto=False): + if auto: + domains = domain_list(exclude_subdomains=True,auto_push=True)["domains"] + elif len(domains)==0: + domains = domain_list(exclude_subdomains=True)["domains"] error_domains = [] for domain in domains: try: diff --git a/src/domain.py b/src/domain.py index f9597b813..df40577da 100644 --- a/src/domain.py +++ b/src/domain.py @@ -52,7 +52,7 @@ DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" domain_list_cache: Dict[str, Any] = {} -def domain_list(exclude_subdomains=False): +def domain_list(exclude_subdomains=False,auto_push=False): """ List domains @@ -78,6 +78,8 @@ def domain_list(exclude_subdomains=False): parent_domain = domain.split(".", 1)[1] if parent_domain in result: continue + if auto_push and not domain_config_get(domain, key="dns.zone.autopush"): + continue result_list.append(domain) @@ -611,7 +613,7 @@ def domain_dns_suggest(domain): return domain_dns_suggest(domain) -def domain_dns_push(domain, dry_run=None, force=None, purge=None): +def domain_dns_push(domains, dry_run=None, force=None, purge=None, auto=False): from yunohost.dns import domain_dns_push - return domain_dns_push(domain, dry_run=dry_run, force=force, purge=purge) + return domain_dns_push(domains, dry_run=dry_run, force=force, purge=purge, auto=auto) diff --git a/src/dyndns.py b/src/dyndns.py index f5531d518..4ddbf7396 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -170,6 +170,10 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): error = f'Server error, code: {r.status_code}. (Message: "{r.text}")' raise YunohostError("dyndns_registration_failed", error=error) + # Set the domain's config to autopush + from yunohost.domain import domain_config_set + domain_config_set(domain,key="dns.zone.autopush",value=1) + # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns regen_conf(["yunohost"]) From 9e44b33401f83ba5d720c4e62e2bf89e254768e9 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 6 Jul 2022 15:57:17 +0200 Subject: [PATCH 26/93] Clarification --- src/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index 4ddbf7396..070090d7f 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -82,7 +82,7 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): Keyword argument: domain -- Full domain to subscribe with - key -- Public DNS key + key -- TSIG Shared DNS key password -- Password that will be used to delete the domain """ From 06db6f7e0430847f1bbdd4bcf9e6fd2ae4ecbfa9 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 8 Jul 2022 09:21:08 +0200 Subject: [PATCH 27/93] Clearer locales --- locales/en.json | 3 ++- src/domain.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index d58790ba2..b8d22bbfa 100644 --- a/locales/en.json +++ b/locales/en.json @@ -341,7 +341,8 @@ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", - "domain_dyndns_instruction_unclear": "The --subscribe and --no-subscribe options are not compatible", + "domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --no-subscribe", + "domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --no-unsubscribe", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", diff --git a/src/domain.py b/src/domain.py index df40577da..68726f4f2 100644 --- a/src/domain.py +++ b/src/domain.py @@ -314,7 +314,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu dyndns = is_yunohost_dyndns_domain(domain) if dyndns: if ((unsubscribe==None) == (no_unsubscribe==False)): - raise YunohostValidationError("domain_dyndns_instruction_unclear") + raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") operation_logger.start() From 0b5c96e2495c6155f3a12ec56a7c949435f64742 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Mon, 11 Jul 2022 15:16:33 +0200 Subject: [PATCH 28/93] Added an API endpoint to check if a domain is a DynDNS one --- share/actionsmap.yml | 10 ++++++++++ src/domain.py | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 17b4c1f96..fa620b0b6 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -585,6 +585,16 @@ domain: pattern: *pattern_domain path: help: The path to check (e.g. /coffee) + + ### domain_isdyndns() + isdyndns: + action_help: Check if a domain is a dyndns one + api: GET /domain//isdyndns + arguments: + domain: + help: The domain to test (e.g. your.domain.tld) + extra: + pattern: *pattern_domain subcategories: dyndns: diff --git a/src/domain.py b/src/domain.py index 68726f4f2..750224da0 100644 --- a/src/domain.py +++ b/src/domain.py @@ -458,6 +458,14 @@ def domain_url_available(domain, path): return len(_get_conflicting_apps(domain, path)) == 0 +def domain_isdyndns(domain): + """ + Returns if a domain is a DynDNS one ( used via the web API ) + + Arguments: + domain -- the domain to check + """ + return is_yunohost_dyndns_domain(domain) def _get_maindomain(): with open("/etc/yunohost/current_host", "r") as f: From ba061a49e495154b8f5386c55c766fd87f153ced Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Mon, 11 Jul 2022 15:48:30 +0200 Subject: [PATCH 29/93] Added a --full option to `domain list` --- share/actionsmap.yml | 13 ++++--------- src/domain.py | 16 ++++++---------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index fa620b0b6..b4b8955f4 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -446,6 +446,10 @@ domain: --auto-push: help: Only display domains that are pushed automatically action: store_true + -f: + full: --full + action: store_true + help: Display more information ### domain_add() add: @@ -586,15 +590,6 @@ domain: path: help: The path to check (e.g. /coffee) - ### domain_isdyndns() - isdyndns: - action_help: Check if a domain is a dyndns one - api: GET /domain//isdyndns - arguments: - domain: - help: The domain to test (e.g. your.domain.tld) - extra: - pattern: *pattern_domain subcategories: dyndns: diff --git a/src/domain.py b/src/domain.py index 750224da0..0378cca51 100644 --- a/src/domain.py +++ b/src/domain.py @@ -52,7 +52,7 @@ DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" domain_list_cache: Dict[str, Any] = {} -def domain_list(exclude_subdomains=False,auto_push=False): +def domain_list(exclude_subdomains=False,auto_push=False,full=False): """ List domains @@ -97,6 +97,11 @@ def domain_list(exclude_subdomains=False,auto_push=False): if exclude_subdomains: return {"domains": result_list, "main": _get_maindomain()} + if full: + for i in range(len(result_list)): + domain = result_list[i] + result_list[i] = {'name':domain,'isdyndns': is_yunohost_dyndns_domain(domain)} + domain_list_cache = {"domains": result_list, "main": _get_maindomain()} return domain_list_cache @@ -458,15 +463,6 @@ def domain_url_available(domain, path): return len(_get_conflicting_apps(domain, path)) == 0 -def domain_isdyndns(domain): - """ - Returns if a domain is a DynDNS one ( used via the web API ) - - Arguments: - domain -- the domain to check - """ - return is_yunohost_dyndns_domain(domain) - def _get_maindomain(): with open("/etc/yunohost/current_host", "r") as f: maindomain = f.readline().rstrip() From a2a1eefbed051587bb08dbfda71de7d925cf17f8 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Mon, 11 Jul 2022 17:11:49 +0200 Subject: [PATCH 30/93] Small fix --- share/actionsmap.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index b4b8955f4..1155e6697 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -446,8 +446,7 @@ domain: --auto-push: help: Only display domains that are pushed automatically action: store_true - -f: - full: --full + --full: action: store_true help: Display more information From f67eaef90bc3950290eb20152e8b37193e1cd41c Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 12 Jul 2022 10:45:35 +0200 Subject: [PATCH 31/93] Ignore cache if "full" is specified --- src/domain.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/domain.py b/src/domain.py index 0378cca51..b7ab302b8 100644 --- a/src/domain.py +++ b/src/domain.py @@ -61,7 +61,7 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): """ global domain_list_cache - if not exclude_subdomains and domain_list_cache: + if not (exclude_subdomains or full) and domain_list_cache: return domain_list_cache from yunohost.utils.ldap import _get_ldap_interface @@ -93,7 +93,6 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): result_list = sorted(result_list, key=cmp_domain) - # Don't cache answer if using exclude_subdomains if exclude_subdomains: return {"domains": result_list, "main": _get_maindomain()} @@ -102,8 +101,13 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): domain = result_list[i] result_list[i] = {'name':domain,'isdyndns': is_yunohost_dyndns_domain(domain)} - domain_list_cache = {"domains": result_list, "main": _get_maindomain()} - return domain_list_cache + result = {"domains": result_list, "main": _get_maindomain()} + + # Cache answer only if not using exclude_subdomains or full + if not (full or exclude_subdomains): + domain_list_cache = result + + return result def _assert_domain_exists(domain): From 731f07817b4835a8f2f7e983fb5d3d2321fa740f Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 13 Jul 2022 11:03:16 +0200 Subject: [PATCH 32/93] Redact domain passwords in logs --- src/domain.py | 6 ++++++ src/dyndns.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/domain.py b/src/domain.py index b7ab302b8..a107c7635 100644 --- a/src/domain.py +++ b/src/domain.py @@ -158,6 +158,9 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): from yunohost.utils.ldap import _get_ldap_interface from yunohost.certificate import _certificate_install_selfsigned + if subscribe!=0 and subscribe!=None: + operation_logger.data_to_redact.append(subscribe) + if domain.startswith("xmpp-upload."): raise YunohostValidationError("domain_cannot_add_xmpp_upload") @@ -258,6 +261,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface + + if unsubscribe!=0 and unsubscribe!=None: + operation_logger.data_to_redact.append(unsubscribe) # the 'force' here is related to the exception happening in domain_add ... # we don't want to check the domain exists because the ldap add may have diff --git a/src/dyndns.py b/src/dyndns.py index 070090d7f..0baa1d428 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -95,6 +95,7 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): password = Moulinette.prompt( m18n.n("ask_password"), is_password=True, confirm=True ) + operation_logger.data_to_redact.append(password) assert_password_is_strong_enough("admin", password) if _guess_current_dyndns_domain() != (None, None): @@ -207,6 +208,7 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): password = Moulinette.prompt( m18n.n("ask_password"), is_password=True ) + operation_logger.data_to_redact.append(password) assert_password_is_strong_enough("admin", password) operation_logger.start() From 0084ce757c6937449c7f8e2e177a995710089b80 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 13 Jul 2022 11:34:04 +0200 Subject: [PATCH 33/93] Don't ask for the (un)subscribe options if this is a sub-subdomain --- src/domain.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/domain.py b/src/domain.py index a107c7635..d6a3aa095 100644 --- a/src/domain.py +++ b/src/domain.py @@ -93,9 +93,6 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): result_list = sorted(result_list, key=cmp_domain) - if exclude_subdomains: - return {"domains": result_list, "main": _get_maindomain()} - if full: for i in range(len(result_list)): domain = result_list[i] @@ -178,8 +175,8 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): # Non-latin characters (e.g. café.com => xn--caf-dma.com) domain = domain.encode("idna").decode("utf-8") - # DynDNS domain - dyndns = is_yunohost_dyndns_domain(domain) + # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 if dyndns: if ((subscribe==None) == (no_subscribe==False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") @@ -325,8 +322,8 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu apps="\n".join([x[1] for x in apps_on_that_domain]), ) - # DynDNS domain - dyndns = is_yunohost_dyndns_domain(domain) + # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 if dyndns: if ((unsubscribe==None) == (no_unsubscribe==False)): raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") From 02103c07a33d6242d6852b5cbbffd8c962e95891 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 13 Jul 2022 11:50:20 +0200 Subject: [PATCH 34/93] Don't flag sub-DynDNS domains as subscribed --- src/domain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain.py b/src/domain.py index d6a3aa095..b1544d46a 100644 --- a/src/domain.py +++ b/src/domain.py @@ -96,7 +96,8 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): if full: for i in range(len(result_list)): domain = result_list[i] - result_list[i] = {'name':domain,'isdyndns': is_yunohost_dyndns_domain(domain)} + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 + result_list[i] = {'name':domain,'isdyndns': dyndns} result = {"domains": result_list, "main": _get_maindomain()} From f015767f508d9ab2ba26216cb36ffb8981c24568 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 13:21:16 +0200 Subject: [PATCH 35/93] Added some tests for subscribing and unsubscribing --- src/tests/test_domains.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 95a33e0ba..6aab9f241 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -1,7 +1,9 @@ import pytest import os +import random from moulinette.core import MoulinetteError +from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.domain import ( @@ -16,6 +18,8 @@ from yunohost.domain import ( ) TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"] +TEST_DYNDNS_DOMAIN = "".join(chr(random.randint(ord("a"),ord("z"))) for x in range(15))+random.choice([".noho.st",".ynh.fr",".nohost.me"]) +TEST_DYNDNS_PASSWORD = "astrongandcomplicatedpassphrasethatisverysecure" def setup_function(function): @@ -35,9 +39,9 @@ def setup_function(function): # Clear other domains for domain in domains: - if domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]: + if (domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]) and domain != TEST_DYNDNS_DOMAIN: # Clean domains not used for testing - domain_remove(domain) + domain_remove(domain,no_unsubscribe=is_yunohost_dyndns_domain(domain)) elif domain in TEST_DOMAINS: # Reset settings if any os.system(f"rm -rf {DOMAIN_SETTINGS_DIR}/{domain}.yml") @@ -67,6 +71,12 @@ def test_domain_add(): assert TEST_DOMAINS[2] in domain_list()["domains"] +def test_domain_add_subscribe(): + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + domain_add(TEST_DYNDNS_DOMAIN,subscribe=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + + def test_domain_add_existing_domain(): with pytest.raises(MoulinetteError): assert TEST_DOMAINS[1] in domain_list()["domains"] @@ -79,6 +89,12 @@ def test_domain_remove(): assert TEST_DOMAINS[1] not in domain_list()["domains"] +def test_domain_remove_unsubscribe(): + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + domain_remove(TEST_DYNDNS_DOMAIN,unsubscribe=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + + def test_main_domain(): current_main_domain = _get_maindomain() assert domain_main_domain()["current_main_domain"] == current_main_domain From 863843a1cf21919a72f286dc875930c0100bcffd Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 14:26:21 +0200 Subject: [PATCH 36/93] The maximum number of subscribed DynDNS domains is configurable --- src/domain.py | 4 +- src/dyndns.py | 107 ++++++++++++++++++++++---------------------------- 2 files changed, 50 insertions(+), 61 deletions(-) diff --git a/src/domain.py b/src/domain.py index b1544d46a..bc04dd523 100644 --- a/src/domain.py +++ b/src/domain.py @@ -182,10 +182,10 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): if ((subscribe==None) == (no_subscribe==False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") - from yunohost.dyndns import _guess_current_dyndns_domain + from yunohost.dyndns import is_subscribing_allowed # Do not allow to subscribe to multiple dyndns domains... - if _guess_current_dyndns_domain() != (None, None): + if not is_subscribing_allowed(): raise YunohostValidationError("domain_dyndns_already_subscribed") operation_logger.start() diff --git a/src/dyndns.py b/src/dyndns.py index 0baa1d428..a12ef3355 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -48,6 +48,16 @@ logger = getActionLogger("yunohost.dyndns") DYNDNS_PROVIDER = "dyndns.yunohost.org" DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"] +MAX_DYNDNS_DOMAINS = 1 + +def is_subscribing_allowed(): + """ + Check if the limit of subscribed DynDNS domains has been reached + + Returns: + True if the limit is not reached, False otherwise + """ + return len(glob.glob("/etc/yunohost/dyndns/*.key"))[^\s\+]+)\.\+165.+\.key$") - - # Retrieve the first registered domain - paths = list(glob.iglob("/etc/yunohost/dyndns/K*.key")) - for path in paths: - match = DYNDNS_KEY_REGEX.match(path) - if not match: - continue - _domain = match.group("domain") - - # Verify if domain is registered (i.e., if it's available, skip - # current domain beause that's not the one we want to update..) - # If there's only 1 such key found, then avoid doing the request - # for nothing (that's very probably the one we want to find ...) - if len(paths) > 1 and _dyndns_available(_domain): - continue - else: - return (_domain, path) - - return (None, None) From eb3c3916242e402e7622534530958f069541e42f Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 14:33:11 +0200 Subject: [PATCH 37/93] Removed useless argument --- .gitlab/ci/install.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/install.gitlab-ci.yml b/.gitlab/ci/install.gitlab-ci.yml index e2662e9e2..335e07eb6 100644 --- a/.gitlab/ci/install.gitlab-ci.yml +++ b/.gitlab/ci/install.gitlab-ci.yml @@ -26,4 +26,4 @@ install-postinstall: script: - apt-get update -o Acquire::Retries=3 - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ./$YNH_BUILD_DIR/*.deb - - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace + - yunohost tools postinstall -d domain.tld -p the_password --force-diskspace From 5f2785c6c931e4990a2859125922451f2691cc92 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 15:11:45 +0200 Subject: [PATCH 38/93] Pleasing the linter --- src/dns.py | 19 ++++++++--------- src/domain.py | 36 +++++++++++++++++--------------- src/dyndns.py | 43 ++++++++++++++++++++++----------------- src/tests/test_domains.py | 8 ++++---- src/tools.py | 4 ++-- src/utils/config.py | 1 + 6 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/dns.py b/src/dns.py index 8ba46011e..9b27c18af 100644 --- a/src/dns.py +++ b/src/dns.py @@ -47,7 +47,6 @@ from yunohost.utils.error import YunohostValidationError, YunohostError from yunohost.utils.network import get_public_ip from yunohost.log import is_unit_operation from yunohost.hook import hook_callback -from yunohost.dyndns import dyndns_update logger = getActionLogger("yunohost.domain") @@ -625,18 +624,19 @@ def _get_registar_settings(domain): @is_unit_operation() def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False, auto=False): if auto: - domains = domain_list(exclude_subdomains=True,auto_push=True)["domains"] - elif len(domains)==0: - domains = domain_list(exclude_subdomains=True)["domains"] + domains = domain_list(exclude_subdomains=True, auto_push=True)["domains"] + elif len(domains) == 0: + domains = domain_list(exclude_subdomains=True)["domains"] error_domains = [] for domain in domains: try: - domain_dns_push_unique(domain,dry_run=dry_run,force=force,purge=purge) + domain_dns_push_unique(domain, dry_run=dry_run, force=force, purge=purge) except YunohostError as e: - logger.error(m18n.n("domain_dns_push_failed_domain",domain=domain,error=str(e))) + logger.error(m18n.n("domain_dns_push_failed_domain", domain=domain, error=str(e))) error_domains.append(domain) - if len(error_domains)>0: - raise YunohostError("domain_dns_push_failed_domains",domains=', '.join(error_domains)) + if len(error_domains) > 0: + raise YunohostError("domain_dns_push_failed_domains", domains=', '.join(error_domains)) + @is_unit_operation() def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, purge=False): @@ -660,9 +660,8 @@ def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, # FIXME: in the future, properly unify this with yunohost dyndns update if registrar == "yunohost": - #logger.info(m18n.n("domain_dns_registrar_yunohost")) from yunohost.dyndns import dyndns_update - dyndns_update(domain=domain,force=force) + dyndns_update(domain=domain, force=force) return {} if registrar == "parent_domain": diff --git a/src/domain.py b/src/domain.py index bc04dd523..4c4ed3472 100644 --- a/src/domain.py +++ b/src/domain.py @@ -52,7 +52,7 @@ DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" domain_list_cache: Dict[str, Any] = {} -def domain_list(exclude_subdomains=False,auto_push=False,full=False): +def domain_list(exclude_subdomains=False, auto_push=False, full=False): """ List domains @@ -96,11 +96,11 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): if full: for i in range(len(result_list)): domain = result_list[i] - dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 - result_list[i] = {'name':domain,'isdyndns': dyndns} + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 + result_list[i] = {'name': domain, 'isdyndns': dyndns} result = {"domains": result_list, "main": _get_maindomain()} - + # Cache answer only if not using exclude_subdomains or full if not (full or exclude_subdomains): domain_list_cache = result @@ -156,7 +156,7 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): from yunohost.utils.ldap import _get_ldap_interface from yunohost.certificate import _certificate_install_selfsigned - if subscribe!=0 and subscribe!=None: + if subscribe != 0 and subscribe is not None: operation_logger.data_to_redact.append(subscribe) if domain.startswith("xmpp-upload."): @@ -177,9 +177,9 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): domain = domain.encode("idna").decode("utf-8") # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) - dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((subscribe==None) == (no_subscribe==False)): + if ((subscribe is None) == (no_subscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") from yunohost.dyndns import is_subscribing_allowed @@ -194,7 +194,7 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): if dyndns and not no_subscribe: # Actually subscribe - domain_dyndns_subscribe(domain=domain,password=subscribe) + domain_dyndns_subscribe(domain=domain, password=subscribe) _certificate_install_selfsigned([domain], True) @@ -244,7 +244,7 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): @is_unit_operation() -def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsubscribe=None,no_unsubscribe=False): +def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsubscribe=None, no_unsubscribe=False): """ Delete domains @@ -259,8 +259,8 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface - - if unsubscribe!=0 and unsubscribe!=None: + + if unsubscribe != 0 and unsubscribe is not None: operation_logger.data_to_redact.append(unsubscribe) # the 'force' here is related to the exception happening in domain_add ... @@ -322,16 +322,16 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu "domain_uninstall_app_first", apps="\n".join([x[1] for x in apps_on_that_domain]), ) - + # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) - dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((unsubscribe==None) == (no_unsubscribe==False)): + if ((unsubscribe is None) == (no_unsubscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") operation_logger.start() - if not dyndns and (unsubscribe!=None or no_unsubscribe!=False): + if not dyndns and ((unsubscribe is not None) or (no_unsubscribe is not False)): logger.warning("This domain is not a DynDNS one, no need for the --unsubscribe or --no-unsubscribe option") ldap = _get_ldap_interface() @@ -381,7 +381,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu # If a password is provided, delete the DynDNS record if dyndns and not no_unsubscribe: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain,password=unsubscribe) + domain_dyndns_unsubscribe(domain=domain, password=unsubscribe) logger.success(m18n.n("domain_deleted")) @@ -394,6 +394,7 @@ def domain_dyndns_subscribe(**kwargs): dyndns_subscribe(**kwargs) + def domain_dyndns_unsubscribe(**kwargs): """ Unsubscribe from a DynDNS domain @@ -402,6 +403,7 @@ def domain_dyndns_unsubscribe(**kwargs): dyndns_unsubscribe(**kwargs) + def domain_dyndns_list(): """ Returns all currently subscribed DynDNS domains @@ -410,6 +412,7 @@ def domain_dyndns_list(): return dyndns_list() + def domain_dyndns_update(**kwargs): """ Update a DynDNS domain @@ -471,6 +474,7 @@ def domain_url_available(domain, path): return len(_get_conflicting_apps(domain, path)) == 0 + def _get_maindomain(): with open("/etc/yunohost/current_host", "r") as f: maindomain = f.readline().rstrip() diff --git a/src/dyndns.py b/src/dyndns.py index a12ef3355..a324b35a5 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -24,7 +24,6 @@ Subscribe and Update DynDNS Hosts """ import os -import re import json import glob import base64 @@ -50,6 +49,7 @@ DYNDNS_PROVIDER = "dyndns.yunohost.org" DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"] MAX_DYNDNS_DOMAINS = 1 + def is_subscribing_allowed(): """ Check if the limit of subscribed DynDNS domains has been reached @@ -57,7 +57,7 @@ def is_subscribing_allowed(): Returns: True if the limit is not reached, False otherwise """ - return len(glob.glob("/etc/yunohost/dyndns/*.key")) Date: Fri, 15 Jul 2022 15:29:04 +0200 Subject: [PATCH 39/93] Let the dynette cool down --- src/tests/test_domains.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 360a3b81f..cbdd412a7 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -1,5 +1,6 @@ import pytest import os +import time import random from moulinette.core import MoulinetteError @@ -18,7 +19,7 @@ from yunohost.domain import ( ) TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"] -TEST_DYNDNS_DOMAIN = "".join(chr(random.randint(ord("a"), ord("z"))) for x in range(15)) + random.choice([".noho.st", ".ynh.fr", ".nohost.me"]) +TEST_DYNDNS_DOMAIN = "ci-test-" + "".join(chr(random.randint(ord("a"), ord("z"))) for x in range(12)) + random.choice([".noho.st", ".ynh.fr", ".nohost.me"]) TEST_DYNDNS_PASSWORD = "astrongandcomplicatedpassphrasethatisverysecure" @@ -72,6 +73,8 @@ def test_domain_add(): def test_domain_add_subscribe(): + + time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] domain_add(TEST_DYNDNS_DOMAIN, subscribe=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] @@ -90,6 +93,8 @@ def test_domain_remove(): def test_domain_remove_unsubscribe(): + + time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] domain_remove(TEST_DYNDNS_DOMAIN, unsubscribe=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] From 4f303de7a48107b1524918f9b39227009ae5ab83 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 15:56:33 +0200 Subject: [PATCH 40/93] Removed the useless argument `key` from dyndns_subscribe --- share/actionsmap.yml | 6 ------ src/dyndns.py | 36 +++++++++++++++++------------------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 1155e6697..e3d315996 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -603,9 +603,6 @@ domain: help: Full domain to subscribe with extra: pattern: *pattern_domain - -k: - full: --key - help: Public DNS key -p: full: --password nargs: "?" @@ -1480,9 +1477,6 @@ dyndns: help: Full domain to subscribe with ( deprecated, use 'yunohost domain dyndns subscribe' instead ) extra: pattern: *pattern_domain - -k: - full: --key - help: Public DNS key -p: full: --password nargs: "?" diff --git a/src/dyndns.py b/src/dyndns.py index a324b35a5..6b64c1e78 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -86,13 +86,12 @@ def _dyndns_available(domain): @is_unit_operation() -def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): +def dyndns_subscribe(operation_logger, domain=None, password=None): """ Subscribe to a DynDNS service Keyword argument: domain -- Full domain to subscribe with - key -- TSIG Shared DNS key password -- Password that will be used to delete the domain """ @@ -133,29 +132,28 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): # '1234' is idk? doesnt matter, but the old format contained a number here... key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" - if key is None: - if not os.path.exists("/etc/yunohost/dyndns"): - os.makedirs("/etc/yunohost/dyndns") + if not os.path.exists("/etc/yunohost/dyndns"): + os.makedirs("/etc/yunohost/dyndns") - logger.debug(m18n.n("dyndns_key_generating")) + logger.debug(m18n.n("dyndns_key_generating")) - # Here, we emulate the behavior of the old 'dnssec-keygen' utility - # which since bullseye was replaced by ddns-keygen which is now - # in the bind9 package ... but installing bind9 will conflict with dnsmasq - # and is just madness just to have access to a tsig keygen utility -.- + # Here, we emulate the behavior of the old 'dnssec-keygen' utility + # which since bullseye was replaced by ddns-keygen which is now + # in the bind9 package ... but installing bind9 will conflict with dnsmasq + # and is just madness just to have access to a tsig keygen utility -.- - # Use 512 // 8 = 64 bytes for hmac-sha512 (c.f. https://git.hactrn.net/sra/tsig-keygen/src/master/tsig-keygen.py) - secret = base64.b64encode(os.urandom(512 // 8)).decode("ascii") + # Use 512 // 8 = 64 bytes for hmac-sha512 (c.f. https://git.hactrn.net/sra/tsig-keygen/src/master/tsig-keygen.py) + secret = base64.b64encode(os.urandom(512 // 8)).decode("ascii") - # Idk why but the secret is split in two parts, with the first one - # being 57-long char ... probably some DNS format - secret = f"{secret[:56]} {secret[56:]}" + # Idk why but the secret is split in two parts, with the first one + # being 57-long char ... probably some DNS format + secret = f"{secret[:56]} {secret[56:]}" - key_content = f"{domain}. IN KEY 0 3 165 {secret}" - write_to_file(key_file, key_content) + key_content = f"{domain}. IN KEY 0 3 165 {secret}" + write_to_file(key_file, key_content) - chmod("/etc/yunohost/dyndns", 0o600, recursive=True) - chown("/etc/yunohost/dyndns", "root", recursive=True) + chmod("/etc/yunohost/dyndns", 0o600, recursive=True) + chown("/etc/yunohost/dyndns", "root", recursive=True) import requests # lazy loading this module for performance reasons From bbc6dcc50b5101d8fdab04c91f38a18b4dde9966 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 16:30:57 +0200 Subject: [PATCH 41/93] Better logging for `domain dns push --auto` --- src/dns.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dns.py b/src/dns.py index 9b27c18af..44547d412 100644 --- a/src/dns.py +++ b/src/dns.py @@ -621,8 +621,7 @@ def _get_registar_settings(domain): return registrar, settings -@is_unit_operation() -def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False, auto=False): +def domain_dns_push(domains, dry_run=False, force=False, purge=False, auto=False): if auto: domains = domain_list(exclude_subdomains=True, auto_push=True)["domains"] elif len(domains) == 0: From e21c114b70202ab518f3b57c49a7a024e6d16961 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 16:54:53 +0200 Subject: [PATCH 42/93] Better log redacting --- src/domain.py | 4 ++-- src/dyndns.py | 2 -- src/tools.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/domain.py b/src/domain.py index 4c4ed3472..f49f96e46 100644 --- a/src/domain.py +++ b/src/domain.py @@ -140,7 +140,7 @@ def _get_parent_domain_of(domain): return _get_parent_domain_of(parent_domain) -@is_unit_operation() +@is_unit_operation(exclude=["subscribe"]) def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): """ Create a custom domain @@ -243,7 +243,7 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): logger.success(m18n.n("domain_created")) -@is_unit_operation() +@is_unit_operation(exclude=["unsubscribe"]) def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsubscribe=None, no_unsubscribe=False): """ Delete domains diff --git a/src/dyndns.py b/src/dyndns.py index 6b64c1e78..9fd25442c 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -106,7 +106,6 @@ def dyndns_subscribe(operation_logger, domain=None, password=None): is_password=True, confirm=True ) - operation_logger.data_to_redact.append(password) assert_password_is_strong_enough("admin", password) if not is_subscribing_allowed(): @@ -218,7 +217,6 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): m18n.n("ask_password"), is_password=True ) - operation_logger.data_to_redact.append(password) assert_password_is_strong_enough("admin", password) operation_logger.start() diff --git a/src/tools.py b/src/tools.py index b77279208..032bbea9f 100644 --- a/src/tools.py +++ b/src/tools.py @@ -180,12 +180,11 @@ def _detect_virt(): return out.split()[0] -@is_unit_operation() +@is_unit_operation(exclude=["subscribe","password"]) def tools_postinstall( operation_logger, domain, password, - ignore_dyndns=False, subscribe=None, no_subscribe=False, force_password=False, From 129f5cce9537fc9cbbbdd804091f1cdb662983ff Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 16:57:12 +0200 Subject: [PATCH 43/93] Linter fixes --- src/tests/test_domains.py | 6 +++--- src/tools.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index cbdd412a7..272f5bb4d 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -73,8 +73,8 @@ def test_domain_add(): def test_domain_add_subscribe(): - - time.sleep(35) # Dynette blocks requests that happen too frequently + + time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] domain_add(TEST_DYNDNS_DOMAIN, subscribe=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] @@ -94,7 +94,7 @@ def test_domain_remove(): def test_domain_remove_unsubscribe(): - time.sleep(35) # Dynette blocks requests that happen too frequently + time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] domain_remove(TEST_DYNDNS_DOMAIN, unsubscribe=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] diff --git a/src/tools.py b/src/tools.py index 032bbea9f..6dcc262ad 100644 --- a/src/tools.py +++ b/src/tools.py @@ -180,7 +180,7 @@ def _detect_virt(): return out.split()[0] -@is_unit_operation(exclude=["subscribe","password"]) +@is_unit_operation(exclude=["subscribe", "password"]) def tools_postinstall( operation_logger, domain, From dc5fbd5555aa1e59e97a228385da28fcab639996 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 18 Nov 2022 22:56:10 +0100 Subject: [PATCH 44/93] Fix OCSP stapling ... but using Google resolver :| --- conf/nginx/server.tpl.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index d5b1d3bef..183cce8b8 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -54,7 +54,7 @@ server { ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - resolver 127.0.0.1 127.0.1.1 valid=300s; + resolver 8.8.8.8 valid=300s; resolver_timeout 5s; {% endif %} @@ -110,7 +110,7 @@ server { ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - resolver 127.0.0.1 127.0.1.1 valid=300s; + resolver 8.8.8.8 valid=300s; resolver_timeout 5s; {% endif %} From a21567b27dda8b6b11f2fa683f41221c65b83a0e Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 8 Jan 2023 00:35:34 +0100 Subject: [PATCH 45/93] [enh] Semantic --- locales/en.json | 1 + share/actionsmap.yml | 9 +++----- src/domain.py | 48 ++++++++++++++++++++++++--------------- src/tests/test_domains.py | 2 +- src/tools.py | 8 +++---- 5 files changed, 39 insertions(+), 29 deletions(-) diff --git a/locales/en.json b/locales/en.json index b8d22bbfa..432462708 100644 --- a/locales/en.json +++ b/locales/en.json @@ -73,6 +73,7 @@ "ask_new_domain": "New domain", "ask_new_path": "New path", "ask_password": "Password", + "ask_dyndns_recovery_password": "DynDNS recovey password", "ask_user_domain": "Domain to use for the user's email address and XMPP account", "backup_abstract_method": "This backup method has yet to be implemented", "backup_actually_backuping": "Creating a backup archive from the collected files...", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index e3d315996..e437a812b 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -463,8 +463,7 @@ domain: full: --no-subscribe help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true - -s: - full: --subscribe + --dyndns-password-recovery: metavar: PASSWORD nargs: "?" const: 0 @@ -494,8 +493,7 @@ domain: full: --no-unsubscribe help: If removing a DynDNS domain, only remove the domain, without unsubscribing from the DynDNS service action: store_true - -u: - full: --unsubscribe + --dyndns-password-recovery: metavar: PASSWORD nargs: "?" const: 0 @@ -1582,8 +1580,7 @@ tools: full: --no-subscribe help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true - -s: - full: --subscribe + --dyndns-password-recovery: metavar: PASSWORD nargs: "?" const: 0 diff --git a/src/domain.py b/src/domain.py index f49f96e46..f6b99b5cb 100644 --- a/src/domain.py +++ b/src/domain.py @@ -140,24 +140,25 @@ def _get_parent_domain_of(domain): return _get_parent_domain_of(parent_domain) -@is_unit_operation(exclude=["subscribe"]) -def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): +@is_unit_operation(exclude=["dyndns_password_recovery"]) +def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subscribe=False): """ Create a custom domain Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS - subscribe -- Password used to later unsubscribe from DynDNS + dyndns_password_recovery -- Password used to later unsubscribe from DynDNS no_unsubscribe -- If we want to just add the DynDNS domain to the list, without subscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf from yunohost.utils.ldap import _get_ldap_interface + from yunohost.utils.password import assert_password_is_strong_enough from yunohost.certificate import _certificate_install_selfsigned - if subscribe != 0 and subscribe is not None: - operation_logger.data_to_redact.append(subscribe) + if dyndns_password_recovery != 0 and dyndns_password_recovery is not None: + operation_logger.data_to_redact.append(dyndns_password_recovery) if domain.startswith("xmpp-upload."): raise YunohostValidationError("domain_cannot_add_xmpp_upload") @@ -179,7 +180,18 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((subscribe is None) == (no_subscribe is False)): + if not no_subscribe and not dyndns_password_recovery: + if Moulinette.interface.type == "api": + raise YunohostValidationError("domain_dyndns_missing_password") + else: + dyndns_password_recovery = Moulinette.prompt( + m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True + ) + + # Ensure sufficiently complex password + assert_password_is_strong_enough("admin", dyndns_password_recovery) + + if ((dyndns_password_recovery is None) == (no_subscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") from yunohost.dyndns import is_subscribing_allowed @@ -189,12 +201,12 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): raise YunohostValidationError("domain_dyndns_already_subscribed") operation_logger.start() - if not dyndns and (subscribe is not None or no_subscribe): - logger.warning("This domain is not a DynDNS one, no need for the --subscribe or --no-subscribe option") + if not dyndns and (dyndns_password_recovery is not None or no_subscribe): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns-password-recovery or --no-subscribe option") if dyndns and not no_subscribe: # Actually subscribe - domain_dyndns_subscribe(domain=domain, password=subscribe) + domain_dyndns_subscribe(domain=domain, password=dyndns_password_recovery) _certificate_install_selfsigned([domain], True) @@ -243,8 +255,8 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): logger.success(m18n.n("domain_created")) -@is_unit_operation(exclude=["unsubscribe"]) -def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsubscribe=None, no_unsubscribe=False): +@is_unit_operation(exclude=["dyndns_password_recovery"]) +def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_password_recovery=None, no_unsubscribe=False): """ Delete domains @@ -253,15 +265,15 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu remove_apps -- Remove applications installed on the domain force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified - unsubscribe -- Recovery password used at the creation of the DynDNS domain + dyndns_password_recovery -- Recovery password used at the creation of the DynDNS domain no_unsubscribe -- If we just remove the DynDNS domain, without unsubscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface - if unsubscribe != 0 and unsubscribe is not None: - operation_logger.data_to_redact.append(unsubscribe) + if dyndns_password_recovery != 0 and dyndns_password_recovery is not None: + operation_logger.data_to_redact.append(dyndns_password_recovery) # the 'force' here is related to the exception happening in domain_add ... # we don't want to check the domain exists because the ldap add may have @@ -326,13 +338,13 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((unsubscribe is None) == (no_unsubscribe is False)): + if ((dyndns_password_recovery is None) == (no_unsubscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") operation_logger.start() - if not dyndns and ((unsubscribe is not None) or (no_unsubscribe is not False)): - logger.warning("This domain is not a DynDNS one, no need for the --unsubscribe or --no-unsubscribe option") + if not dyndns and ((dyndns_password_recovery is not None) or (no_unsubscribe is not False)): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns_password_recovery or --no-unsubscribe option") ldap = _get_ldap_interface() try: @@ -381,7 +393,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu # If a password is provided, delete the DynDNS record if dyndns and not no_unsubscribe: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain, password=unsubscribe) + domain_dyndns_unsubscribe(domain=domain, password=dyndns_password_recovery) logger.success(m18n.n("domain_deleted")) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 272f5bb4d..e09c3534b 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -76,7 +76,7 @@ def test_domain_add_subscribe(): time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] - domain_add(TEST_DYNDNS_DOMAIN, subscribe=TEST_DYNDNS_PASSWORD) + domain_add(TEST_DYNDNS_DOMAIN, dyndns_password_recovery=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] diff --git a/src/tools.py b/src/tools.py index 6dcc262ad..21262c64b 100644 --- a/src/tools.py +++ b/src/tools.py @@ -180,12 +180,12 @@ def _detect_virt(): return out.split()[0] -@is_unit_operation(exclude=["subscribe", "password"]) +@is_unit_operation(exclude=["dyndns_password_recovery", "password"]) def tools_postinstall( operation_logger, domain, password, - subscribe=None, + dyndns_password_recovery=None, no_subscribe=False, force_password=False, force_diskspace=False, @@ -232,7 +232,7 @@ def tools_postinstall( # If this is a nohost.me/noho.st, actually check for availability if is_yunohost_dyndns_domain(domain): - if ((subscribe is None) == (no_subscribe is False)): + if ((dyndns_password_recovery is None) == (no_subscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") # Check if the domain is available... @@ -257,7 +257,7 @@ def tools_postinstall( logger.info(m18n.n("yunohost_installing")) # New domain config - domain_add(domain, subscribe=subscribe, no_subscribe=no_subscribe) + domain_add(domain, dyndns_password_recovery=dyndns_password_recovery, no_subscribe=no_subscribe) domain_main_domain(domain) # Update LDAP admin and create home dir From 1e0fe76672e9d72ee1e60e5c58f8cf2e8c7810b5 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 10 Jan 2023 10:25:30 +0100 Subject: [PATCH 46/93] [fix] Test --- src/tests/test_domains.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index e09c3534b..b221d688e 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -96,7 +96,7 @@ def test_domain_remove_unsubscribe(): time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] - domain_remove(TEST_DYNDNS_DOMAIN, unsubscribe=TEST_DYNDNS_PASSWORD) + domain_remove(TEST_DYNDNS_DOMAIN, dyndns_password_recovery=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] From f0751aff17c65d4240591c6c4c658aee994208ff Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 27 Sep 2019 22:55:12 +0200 Subject: [PATCH 47/93] Allow system users to auth on the mail stack and send emails --- conf/dovecot/dovecot.conf | 12 ++++++++++++ conf/postfix/main.cf | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index e614c3796..e4ada80ab 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -38,16 +38,28 @@ ssl_prefer_server_ciphers = no ############################################################################### +# Regular Yunohost accounts passdb { args = /etc/dovecot/dovecot-ldap.conf driver = ldap } +# Internally, allow authentication from system user +# who might want to send emails (e.g. from apps) +passdb { + override_fields = allow_nets=127.0.0.1/32,::1/64 + driver = pam +} + userdb { args = /etc/dovecot/dovecot-ldap.conf driver = ldap } +userdb { + driver = passwd +} + protocol imap { imap_client_workarounds = mail_plugins = $mail_plugins imap_quota antispam diff --git a/conf/postfix/main.cf b/conf/postfix/main.cf index 19b40aefb..4d02fc5c0 100644 --- a/conf/postfix/main.cf +++ b/conf/postfix/main.cf @@ -107,7 +107,10 @@ virtual_alias_domains = virtual_minimum_uid = 100 virtual_uid_maps = static:vmail virtual_gid_maps = static:mail -smtpd_sender_login_maps= ldap:/etc/postfix/ldap-accounts.cf +smtpd_sender_login_maps= + ldap:/etc/postfix/ldap-accounts.cf, # Regular Yunohost accounts + hash:/etc/postfix/sender_login_maps # Extra maps for system users who need to send emails + # Dovecot LDA virtual_transport = dovecot From c48d9ec483241dc45aa3f2888c76226f55c8005f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 28 Feb 2023 17:56:49 +0100 Subject: [PATCH 48/93] appsv2/mail: add new 'allow_email' flag on app system users that will autogenerate a passwd-like file to be used by dovecot + map for postfix --- conf/dovecot/dovecot.conf | 12 +++++----- conf/postfix/main.cf | 6 +++-- hooks/conf_regen/19-postfix | 2 ++ hooks/conf_regen/25-dovecot | 2 ++ src/app.py | 45 +++++++++++++++++++++++++++++++++++++ src/utils/resources.py | 31 +++++++++++++++++++++++-- 6 files changed, 88 insertions(+), 10 deletions(-) diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index e4ada80ab..152f4c01c 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -44,20 +44,20 @@ passdb { driver = ldap } -# Internally, allow authentication from system user -# who might want to send emails (e.g. from apps) +# Internally, allow authentication from apps system user who have "enable_email = true" passdb { - override_fields = allow_nets=127.0.0.1/32,::1/64 - driver = pam + driver = passwd-file + args = /etc/dovecot/app-senders-passwd } userdb { - args = /etc/dovecot/dovecot-ldap.conf driver = ldap + args = /etc/dovecot/dovecot-ldap.conf } userdb { - driver = passwd + driver = passwd-file + args = /etc/dovecot/app-senders-passwd } protocol imap { diff --git a/conf/postfix/main.cf b/conf/postfix/main.cf index 4d02fc5c0..e30ca0874 100644 --- a/conf/postfix/main.cf +++ b/conf/postfix/main.cf @@ -108,8 +108,10 @@ virtual_minimum_uid = 100 virtual_uid_maps = static:vmail virtual_gid_maps = static:mail smtpd_sender_login_maps= - ldap:/etc/postfix/ldap-accounts.cf, # Regular Yunohost accounts - hash:/etc/postfix/sender_login_maps # Extra maps for system users who need to send emails + # Regular Yunohost accounts + ldap:/etc/postfix/ldap-accounts.cf, + # Extra maps for app system users who need to send emails + hash:/etc/postfix/app_senders_login_maps # Dovecot LDA diff --git a/hooks/conf_regen/19-postfix b/hooks/conf_regen/19-postfix index 3a2aead5d..d6ddcb5ee 100755 --- a/hooks/conf_regen/19-postfix +++ b/hooks/conf_regen/19-postfix @@ -80,6 +80,8 @@ do_post_regen() { postmap -F hash:/etc/postfix/sni + python3 -c 'from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix as r; r(only="postfix")' + [[ -z "$regen_conf_files" ]] \ || { systemctl restart postfix && systemctl restart postsrsd; } diff --git a/hooks/conf_regen/25-dovecot b/hooks/conf_regen/25-dovecot index 49ff0c9ba..54b4e5d37 100755 --- a/hooks/conf_regen/25-dovecot +++ b/hooks/conf_regen/25-dovecot @@ -53,6 +53,8 @@ do_post_regen() { chown root:mail /var/mail chmod 1775 /var/mail + python3 -c 'from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix as r; r(only="dovecot")' + [ -z "$regen_conf_files" ] && exit 0 # compile sieve script diff --git a/src/app.py b/src/app.py index 17ebe96ca..88d79e750 100644 --- a/src/app.py +++ b/src/app.py @@ -1582,6 +1582,9 @@ def app_setting(app, key, value=None, delete=False): if delete: if key in app_settings: del app_settings[key] + else: + # Don't call _set_app_settings to avoid unecessary writes... + return # SET else: @@ -3148,3 +3151,45 @@ def _ask_confirmation( if not answer: raise YunohostError("aborting") + + +def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): + + dovecot = True if only in [None, "dovecot"] else False + postfix = True if only in [None, "postfix"] else False + + from yunohost.user import _hash_user_password + + postfix_map = [] + dovecot_passwd = [] + for app in _installed_apps(): + + settings = _get_app_settings(app) + + if "domain" not in settings or "mail_pwd" not in settings: + continue + + if dovecot: + hashed_password = _hash_user_password(settings["mail_pwd"]) + dovecot_passwd.append(f"{app}:{hashed_password}::::::allow_nets=127.0.0.1/24") + if postfix: + postfix_map.append(f"{app}@{settings['domain']} {app}") + + if dovecot: + app_senders_passwd = "/etc/dovecot/app-senders-passwd" + content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!" + content += '\n' + '\n'.join(dovecot_passwd) + write_to_file(app_senders_passwd, content) + chmod(app_senders_passwd, 0o440) + chown(app_senders_passwd, "root", "dovecot") + + if postfix: + app_senders_map = "/etc/postfix/app_senders_login_maps" + content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!" + content += '\n' + '\n'.join(postfix_map) + write_to_file(app_senders_map, content) + chmod(app_senders_map, 0o440) + chown(app_senders_map, "postfix", "root") + os.system(f"postmap {app_senders_map} 2>/dev/null") + chmod(app_senders_map + ".db", 0o640) + chown(app_senders_map + ".db", "postfix", "root") diff --git a/src/utils/resources.py b/src/utils/resources.py index cff6c6b19..fe8e33b47 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -24,6 +24,7 @@ import tempfile from typing import Dict, Any, List from moulinette import m18n +from moulinette.utils.text import random_ascii from moulinette.utils.process import check_output from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import mkdir, chown, chmod, write_to_file @@ -473,6 +474,7 @@ class SystemuserAppResource(AppResource): default_properties: Dict[str, Any] = { "allow_ssh": False, "allow_sftp": False, + "allow_email": False, "home": "/var/www/__APP__", } @@ -480,9 +482,13 @@ class SystemuserAppResource(AppResource): allow_ssh: bool = False allow_sftp: bool = False + allow_email: bool = False home: str = "" def provision_or_update(self, context: Dict = {}): + + from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix + # FIXME : validate that no yunohost user exists with that name? # and/or that no system user exists during install ? @@ -527,7 +533,25 @@ class SystemuserAppResource(AppResource): f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd" ) + # Update mail-related stuff + if self.allow_email: + mail_pwd = self.get_setting("mail_pwd") + if not mail_pwd: + mail_pwd = random_ascii(24) + self.set_setting("mail_pwd", mail_pwd) + + regen_mail_app_user_config_for_dovecot_and_postfix() + else: + self.delete_setting("mail_pwd") + if os.system(f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps") == 0 \ + or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") == 0: + regen_mail_app_user_config_for_dovecot_and_postfix() + + def deprovision(self, context: Dict = {}): + + from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix + if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: os.system(f"deluser {self.app} >/dev/null") if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: @@ -542,6 +566,11 @@ class SystemuserAppResource(AppResource): f"Failed to delete system user for {self.app}", raw_msg=True ) + self.delete_setting("mail_pwd") + if os.system(f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps") == 0 \ + or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") == 0: + regen_mail_app_user_config_for_dovecot_and_postfix() + # FIXME : better logging and error handling, add stdout/stderr from the deluser/delgroup commands... @@ -1060,8 +1089,6 @@ class DatabaseAppResource(AppResource): self.set_setting("db_pwd", db_pwd) if not db_pwd: - from moulinette.utils.text import random_ascii - db_pwd = random_ascii(24) self.set_setting("db_pwd", db_pwd) From 8188c2816759bec61b24c5807272640e54fdd68b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 28 Feb 2023 18:10:27 +0100 Subject: [PATCH 49/93] appsv2: add documentation for previously introduced allow_email flag --- src/utils/resources.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/resources.py b/src/utils/resources.py index fe8e33b47..4e1907ab4 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -451,6 +451,7 @@ class SystemuserAppResource(AppResource): ##### Properties: - `allow_ssh`: (default: False) Adds the user to the ssh.app group, allowing SSH connection via this user - `allow_sftp`: (default: False) Adds the user to the sftp.app group, allowing SFTP connection via this user + - `allow_email`: (default: False) Enable authentication on the mail stack for the system user and send mail using `__APP__@__DOMAIN__`. A `mail_pwd` setting is automatically defined (similar to `db_pwd` for databases). You can then configure the app to use `__APP__` and `__MAIL_PWD__` as SMTP credentials (with host 127.0.0.1) - `home`: (default: `/var/www/__APP__`) Defines the home property for this user. NB: unfortunately you can't simply use `__INSTALL_DIR__` or `__DATA_DIR__` for now ##### Provision/Update: From d67e23167897ca3307e2dad89dda86fc824a1514 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 14:48:44 +0200 Subject: [PATCH 50/93] dydns-password-recovery -> dyndns-recovery-password --- share/actionsmap.yml | 6 +++--- src/domain.py | 42 +++++++++++++++++++-------------------- src/tests/test_domains.py | 4 ++-- src/tools.py | 8 ++++---- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 6e60655d0..294d00881 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -510,7 +510,7 @@ domain: full: --no-subscribe help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true - --dyndns-password-recovery: + --dyndns-recovery-password: metavar: PASSWORD nargs: "?" const: 0 @@ -540,7 +540,7 @@ domain: full: --no-unsubscribe help: If removing a DynDNS domain, only remove the domain, without unsubscribing from the DynDNS service action: store_true - --dyndns-password-recovery: + --dyndns-recovery-password: metavar: PASSWORD nargs: "?" const: 0 @@ -1692,7 +1692,7 @@ tools: full: --no-subscribe help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true - --dyndns-password-recovery: + --dyndns-recovery-password: metavar: PASSWORD nargs: "?" const: 0 diff --git a/src/domain.py b/src/domain.py index 67f39aa71..4301f9ab1 100644 --- a/src/domain.py +++ b/src/domain.py @@ -213,15 +213,15 @@ def _get_parent_domain_of(domain, return_self=False, topest=False): return domain if return_self else None -@is_unit_operation(exclude=["dyndns_password_recovery"]) -def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subscribe=False): +@is_unit_operation(exclude=["dyndns_recovery_password"]) +def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subscribe=False): """ Create a custom domain Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS - dyndns_password_recovery -- Password used to later unsubscribe from DynDNS + dyndns_recovery_password -- Password used to later unsubscribe from DynDNS no_unsubscribe -- If we want to just add the DynDNS domain to the list, without subscribing """ from yunohost.hook import hook_callback @@ -230,8 +230,8 @@ def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subsc from yunohost.utils.password import assert_password_is_strong_enough from yunohost.certificate import _certificate_install_selfsigned - if dyndns_password_recovery != 0 and dyndns_password_recovery is not None: - operation_logger.data_to_redact.append(dyndns_password_recovery) + if dyndns_recovery_password != 0 and dyndns_recovery_password is not None: + operation_logger.data_to_redact.append(dyndns_recovery_password) if domain.startswith("xmpp-upload."): raise YunohostValidationError("domain_cannot_add_xmpp_upload") @@ -256,18 +256,18 @@ def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subsc # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if not no_subscribe and not dyndns_password_recovery: + if not no_subscribe and not dyndns_recovery_password: if Moulinette.interface.type == "api": raise YunohostValidationError("domain_dyndns_missing_password") else: - dyndns_password_recovery = Moulinette.prompt( + dyndns_recovery_password = Moulinette.prompt( m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True ) # Ensure sufficiently complex password - assert_password_is_strong_enough("admin", dyndns_password_recovery) + assert_password_is_strong_enough("admin", dyndns_recovery_password) - if ((dyndns_password_recovery is None) == (no_subscribe is False)): + if ((dyndns_recovery_password is None) == (no_subscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") from yunohost.dyndns import is_subscribing_allowed @@ -277,12 +277,12 @@ def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subsc raise YunohostValidationError("domain_dyndns_already_subscribed") operation_logger.start() - if not dyndns and (dyndns_password_recovery is not None or no_subscribe): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns-password-recovery or --no-subscribe option") + if not dyndns and (dyndns_recovery_password is not None or no_subscribe): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns-recovery-password or --no-subscribe option") if dyndns and not no_subscribe: # Actually subscribe - domain_dyndns_subscribe(domain=domain, password=dyndns_password_recovery) + domain_dyndns_subscribe(domain=domain, password=dyndns_recovery_password) _certificate_install_selfsigned([domain], True) @@ -331,8 +331,8 @@ def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subsc logger.success(m18n.n("domain_created")) -@is_unit_operation(exclude=["dyndns_password_recovery"]) -def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_password_recovery=None, no_unsubscribe=False): +@is_unit_operation(exclude=["dyndns_recovery_password"]) +def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_recovery_password=None, no_unsubscribe=False): """ Delete domains @@ -341,15 +341,15 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd remove_apps -- Remove applications installed on the domain force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified - dyndns_password_recovery -- Recovery password used at the creation of the DynDNS domain + dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain no_unsubscribe -- If we just remove the DynDNS domain, without unsubscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface - if dyndns_password_recovery != 0 and dyndns_password_recovery is not None: - operation_logger.data_to_redact.append(dyndns_password_recovery) + if dyndns_recovery_password != 0 and dyndns_recovery_password is not None: + operation_logger.data_to_redact.append(dyndns_recovery_password) # the 'force' here is related to the exception happening in domain_add ... # we don't want to check the domain exists because the ldap add may have @@ -414,13 +414,13 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((dyndns_password_recovery is None) == (no_unsubscribe is False)): + if ((dyndns_recovery_password is None) == (no_unsubscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") operation_logger.start() - if not dyndns and ((dyndns_password_recovery is not None) or (no_unsubscribe is not False)): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns_password_recovery or --no-unsubscribe option") + if not dyndns and ((dyndns_recovery_password is not None) or (no_unsubscribe is not False)): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns_recovery_password or --no-unsubscribe option") ldap = _get_ldap_interface() try: @@ -469,7 +469,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd # If a password is provided, delete the DynDNS record if dyndns and not no_unsubscribe: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain, password=dyndns_password_recovery) + domain_dyndns_unsubscribe(domain=domain, password=dyndns_recovery_password) logger.success(m18n.n("domain_deleted")) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index b221d688e..43c04bee6 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -76,7 +76,7 @@ def test_domain_add_subscribe(): time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] - domain_add(TEST_DYNDNS_DOMAIN, dyndns_password_recovery=TEST_DYNDNS_PASSWORD) + domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] @@ -96,7 +96,7 @@ def test_domain_remove_unsubscribe(): time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] - domain_remove(TEST_DYNDNS_DOMAIN, dyndns_password_recovery=TEST_DYNDNS_PASSWORD) + domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] diff --git a/src/tools.py b/src/tools.py index 02b4f58d1..512986ff9 100644 --- a/src/tools.py +++ b/src/tools.py @@ -144,14 +144,14 @@ def _set_hostname(hostname, pretty_hostname=None): logger.debug(out) -@is_unit_operation(exclude=["dyndns_password_recovery", "password"]) +@is_unit_operation(exclude=["dyndns_recovery_password", "password"]) def tools_postinstall( operation_logger, domain, username, fullname, password, - dyndns_password_recovery=None, + dyndns_recovery_password=None, no_subscribe=False, force_diskspace=False, ): @@ -194,7 +194,7 @@ def tools_postinstall( # If this is a nohost.me/noho.st, actually check for availability if is_yunohost_dyndns_domain(domain): - if ((dyndns_password_recovery is None) == (no_subscribe is False)): + if ((dyndns_recovery_password is None) == (no_subscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") # Check if the domain is available... @@ -219,7 +219,7 @@ def tools_postinstall( logger.info(m18n.n("yunohost_installing")) # New domain config - domain_add(domain, dyndns_password_recovery=dyndns_password_recovery, no_subscribe=no_subscribe) + domain_add(domain, dyndns_recovery_password=dyndns_recovery_password, no_subscribe=no_subscribe) domain_main_domain(domain) user_create(username, domain, password, admin=True, fullname=fullname) From 81360723cc6284fa61e3668a844d412346322c23 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 15:17:41 +0200 Subject: [PATCH 51/93] dyndns: Misc semantic tweaks... --- locales/en.json | 4 ++-- share/actionsmap.yml | 13 +++++-------- src/domain.py | 26 +++++++++++++------------- src/dyndns.py | 26 +++++++++++++------------- src/tools.py | 7 ++++--- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/locales/en.json b/locales/en.json index 21ffdfdc2..8c6636322 100644 --- a/locales/en.json +++ b/locales/en.json @@ -375,8 +375,8 @@ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", - "domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --no-subscribe", - "domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --no-unsubscribe", + "domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --ignore-dyndns", + "domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --ignore-dyndns", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 294d00881..3124f3105 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -506,8 +506,7 @@ domain: help: Domain name to add extra: pattern: *pattern_domain - -n: - full: --no-subscribe + --ignore-dyndns: help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true --dyndns-recovery-password: @@ -536,8 +535,7 @@ domain: full: --force help: Do not ask confirmation to remove apps action: store_true - -n: - full: --no-unsubscribe + --ignore-dyndns: help: If removing a DynDNS domain, only remove the domain, without unsubscribing from the DynDNS service action: store_true --dyndns-recovery-password: @@ -662,7 +660,7 @@ domain: extra: pattern: *pattern_domain -p: - full: --password + full: --recovery-password nargs: "?" const: 0 help: Password used to later delete the domain @@ -681,7 +679,7 @@ domain: pattern: *pattern_domain required: True -p: - full: --password + full: --recovery-password nargs: "?" const: 0 help: Password used to delete the domain @@ -1688,8 +1686,7 @@ tools: pattern: *pattern_password required: True comment: good_practices_about_admin_password - -n: - full: --no-subscribe + --ingnore-dyndns: help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true --dyndns-recovery-password: diff --git a/src/domain.py b/src/domain.py index 4301f9ab1..020a707c7 100644 --- a/src/domain.py +++ b/src/domain.py @@ -214,7 +214,7 @@ def _get_parent_domain_of(domain, return_self=False, topest=False): @is_unit_operation(exclude=["dyndns_recovery_password"]) -def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subscribe=False): +def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_dyndns=False): """ Create a custom domain @@ -222,7 +222,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subsc domain -- Domain name to add dyndns -- Subscribe to DynDNS dyndns_recovery_password -- Password used to later unsubscribe from DynDNS - no_unsubscribe -- If we want to just add the DynDNS domain to the list, without subscribing + ignore_dyndns -- If we want to just add the DynDNS domain to the list, without subscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf @@ -256,7 +256,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subsc # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if not no_subscribe and not dyndns_recovery_password: + if not ignore_dyndns and not dyndns_recovery_password: if Moulinette.interface.type == "api": raise YunohostValidationError("domain_dyndns_missing_password") else: @@ -267,7 +267,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subsc # Ensure sufficiently complex password assert_password_is_strong_enough("admin", dyndns_recovery_password) - if ((dyndns_recovery_password is None) == (no_subscribe is False)): + if ((dyndns_recovery_password is None) == (ignore_dyndns is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") from yunohost.dyndns import is_subscribing_allowed @@ -277,10 +277,10 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subsc raise YunohostValidationError("domain_dyndns_already_subscribed") operation_logger.start() - if not dyndns and (dyndns_recovery_password is not None or no_subscribe): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns-recovery-password or --no-subscribe option") + if not dyndns and (dyndns_recovery_password is not None or ignore_dyndns): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns-recovery-password or --ignore-dyndns option") - if dyndns and not no_subscribe: + if dyndns and not ignore_dyndns: # Actually subscribe domain_dyndns_subscribe(domain=domain, password=dyndns_recovery_password) @@ -332,7 +332,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subsc @is_unit_operation(exclude=["dyndns_recovery_password"]) -def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_recovery_password=None, no_unsubscribe=False): +def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_recovery_password=None, ignore_dyndns=False): """ Delete domains @@ -342,7 +342,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain - no_unsubscribe -- If we just remove the DynDNS domain, without unsubscribing + ignore_dyndns -- If we just remove the DynDNS domain, without unsubscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove @@ -414,13 +414,13 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((dyndns_recovery_password is None) == (no_unsubscribe is False)): + if ((dyndns_recovery_password is None) == (ignore_dyndns is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") operation_logger.start() - if not dyndns and ((dyndns_recovery_password is not None) or (no_unsubscribe is not False)): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns_recovery_password or --no-unsubscribe option") + if not dyndns and ((dyndns_recovery_password is not None) or (ignore_dyndns is not False)): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns_recovery_password or --ignore-dyndns option") ldap = _get_ldap_interface() try: @@ -467,7 +467,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd hook_callback("post_domain_remove", args=[domain]) # If a password is provided, delete the DynDNS record - if dyndns and not no_unsubscribe: + if dyndns and not ignore_dyndns: # Actually unsubscribe domain_dyndns_unsubscribe(domain=domain, password=dyndns_recovery_password) diff --git a/src/dyndns.py b/src/dyndns.py index d1049e756..caeef9459 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -79,27 +79,27 @@ def _dyndns_available(domain): @is_unit_operation() -def dyndns_subscribe(operation_logger, domain=None, password=None): +def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): """ Subscribe to a DynDNS service Keyword argument: domain -- Full domain to subscribe with - password -- Password that will be used to delete the domain + recovery_password -- Password that will be used to delete the domain """ - if password is None: + if recovery_password is None: logger.warning(m18n.n('dyndns_no_recovery_password')) else: from yunohost.utils.password import assert_password_is_strong_enough # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and password == 0: - password = Moulinette.prompt( + if Moulinette.interface.type == "cli" and recovery_password == 0: + recovery_password = Moulinette.prompt( m18n.n("ask_password"), is_password=True, confirm=True ) - assert_password_is_strong_enough("admin", password) + assert_password_is_strong_enough("admin", recovery_password) if not is_subscribing_allowed(): raise YunohostValidationError("domain_dyndns_already_subscribed") @@ -155,7 +155,7 @@ def dyndns_subscribe(operation_logger, domain=None, password=None): b64encoded_key = base64.b64encode(secret.encode()).decode() data = {"subdomain": domain} if password is not None: - data["recovery_password"] = hashlib.sha256((domain + ":" + password.strip()).encode('utf-8')).hexdigest() + data["recovery_password"] = hashlib.sha256((domain + ":" + recovery_password.strip()).encode('utf-8')).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", data=data, @@ -193,24 +193,24 @@ def dyndns_subscribe(operation_logger, domain=None, password=None): @is_unit_operation() -def dyndns_unsubscribe(operation_logger, domain, password=None): +def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): """ Unsubscribe from a DynDNS service Keyword argument: domain -- Full domain to unsubscribe with - password -- Password that is used to delete the domain ( defined when subscribing ) + recovery_password -- Password that is used to delete the domain ( defined when subscribing ) """ from yunohost.utils.password import assert_password_is_strong_enough # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and not password: - password = Moulinette.prompt( + if Moulinette.interface.type == "cli" and not recovery_password: + recovery_password = Moulinette.prompt( m18n.n("ask_password"), is_password=True ) - assert_password_is_strong_enough("admin", password) + assert_password_is_strong_enough("admin", recovery_password) operation_logger.start() @@ -222,7 +222,7 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): # Send delete request try: - secret = str(domain) + ":" + str(password).strip() + secret = str(domain) + ":" + str(recovery_password).strip() r = requests.delete( f"https://{DYNDNS_PROVIDER}/domains/{domain}", data={"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, diff --git a/src/tools.py b/src/tools.py index 512986ff9..c0da7a37b 100644 --- a/src/tools.py +++ b/src/tools.py @@ -152,7 +152,7 @@ def tools_postinstall( fullname, password, dyndns_recovery_password=None, - no_subscribe=False, + ignore_dyndns=False, force_diskspace=False, ): @@ -194,7 +194,8 @@ def tools_postinstall( # If this is a nohost.me/noho.st, actually check for availability if is_yunohost_dyndns_domain(domain): - if ((dyndns_recovery_password is None) == (no_subscribe is False)): + + if (bool(dyndns_recovery_password), ignore_dyndns) in [(True, True), (False, False)]: raise YunohostValidationError("domain_dyndns_instruction_unclear") # Check if the domain is available... @@ -219,7 +220,7 @@ def tools_postinstall( logger.info(m18n.n("yunohost_installing")) # New domain config - domain_add(domain, dyndns_recovery_password=dyndns_recovery_password, no_subscribe=no_subscribe) + domain_add(domain, dyndns_recovery_password=dyndns_recovery_password, ignore_dyndns=ignore_dyndns) domain_main_domain(domain) user_create(username, domain, password, admin=True, fullname=fullname) From 789b1b2af93e330e7a3f0bed51cc98979f46869d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 15:49:52 +0200 Subject: [PATCH 52/93] dyndns: revert changes regarding auto-push ... it's not complete, and the PR about dyndns recovery password is already too complex... --- hooks/conf_regen/01-yunohost | 5 ++--- locales/ca.json | 2 +- locales/de.json | 4 ++-- locales/en.json | 8 ++------ locales/es.json | 4 ++-- locales/eu.json | 4 ++-- locales/fa.json | 2 +- locales/fr.json | 4 ++-- locales/gl.json | 4 ++-- locales/it.json | 4 ++-- locales/uk.json | 4 ++-- locales/zh_Hans.json | 2 +- share/actionsmap.yml | 29 +++-------------------------- share/config_domain.toml | 7 ------- src/dns.py | 18 +----------------- src/domain.py | 5 ++--- src/dyndns.py | 6 +----- src/tools.py | 10 +++++----- 18 files changed, 33 insertions(+), 89 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index a3fd13687..51022a4e5 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -116,9 +116,8 @@ SHELL=/bin/bash # - (sleep random 60 is here to spread requests over a 1-min window) # - if ip.yunohost.org answers ping (basic check to validate that we're connected to the internet and yunohost infra aint down) # - and if lock ain't already taken by another command -# - check if some domains are flagged as autopush -# - trigger yunohost domain dns push --auto -*/10 * * * * root : YunoHost DynDNS update ; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || !(grep -nR "autopush: 1" /etc/yunohost/domains/*.yml > /dev/null) || yunohost domain dns push --auto >> /dev/null +# - trigger yunohost dyndns update +*/10 * * * * root : YunoHost DynDNS update; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || yunohost dyndns update >> /dev/null EOF else # (Delete cron if no dyndns domain found) diff --git a/locales/ca.json b/locales/ca.json index 808354264..106d0af89 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -506,7 +506,7 @@ "diagnosis_swap_tip": "Vigileu i tingueu en compte que els servidor està allotjant memòria d'intercanvi en una targeta SD o en l'emmagatzematge SSD, això pot reduir dràsticament l'esperança de vida del dispositiu.", "restore_already_installed_apps": "No s'han pogut restaurar les següents aplicacions perquè ja estan instal·lades: {apps}", "app_packaging_format_not_supported": "No es pot instal·lar aquesta aplicació ja que el format del paquet no és compatible amb la versió de YunoHost del sistema. Hauríeu de considerar actualitzar el sistema.", - "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost dyndns update --force.", "regenconf_need_to_explicitly_specify_ssh": "La configuració ssh ha estat modificada manualment, però heu d'especificar explícitament la categoria «ssh» amb --force per fer realment els canvis.", "unknown_main_domain_path": "Domini o ruta desconeguda per a «{app}». Heu d'especificar un domini i una ruta per a poder especificar una URL per al permís.", "show_tile_cant_be_enabled_for_regex": "No podeu activar «show_title» ara, perquè la URL per al permís «{permission}» és una expressió regular", diff --git a/locales/de.json b/locales/de.json index 4a1db2961..5baa41687 100644 --- a/locales/de.json +++ b/locales/de.json @@ -290,7 +290,7 @@ "diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.", "diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!", "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden", - "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost domain dns push DOMAIN --force ein Update erzwingen.", + "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost dyndns update --force ein Update erzwingen.", "diagnosis_dns_point_to_doc": "Bitte schauen Sie in der Dokumentation unter https://yunohost.org/dns_config nach, wenn Sie Hilfe bei der Konfiguration der DNS-Einträge benötigen.", "diagnosis_dns_discrepancy": "Der folgende DNS Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}", "diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration solltest du einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}", @@ -612,7 +612,7 @@ "log_app_config_set": "Konfiguration auf die Applikation '{}' anwenden", "log_user_import": "Konten importieren", "diagnosis_high_number_auth_failures": "In letzter Zeit gab es eine verdächtig hohe Anzahl von Authentifizierungsfehlern. Stelle sicher, dass fail2ban läuft und korrekt konfiguriert ist, oder verwende einen benutzerdefinierten Port für SSH, wie unter https://yunohost.org/security beschrieben.", - "domain_dns_registrar_yunohost": "Dies ist eine nohost.me / nohost.st / ynh.fr Domäne, ihre DNS-Konfiguration wird daher automatisch von YunoHost ohne weitere Konfiguration übernommen. (siehe Befehl 'yunohost domain dns push DOMAIN')", + "domain_dns_registrar_yunohost": "Dies ist eine nohost.me / nohost.st / ynh.fr Domäne, ihre DNS-Konfiguration wird daher automatisch von YunoHost ohne weitere Konfiguration übernommen. (siehe Befehl 'yunohost dyndns update')", "domain_config_auth_entrypoint": "API-Einstiegspunkt", "domain_config_auth_application_key": "Anwendungsschlüssel", "domain_config_auth_application_secret": "Geheimer Anwendungsschlüssel", diff --git a/locales/en.json b/locales/en.json index 8c6636322..6065c75d8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -208,7 +208,7 @@ "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}", "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help configuring DNS records.", "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", - "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force.", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", "diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains", "diagnosis_domain_expiration_not_found_details": "The WHOIS information for domain {domain} doesn't seem to contain the information about the expiration date?", @@ -333,8 +333,6 @@ "domain_config_auth_key": "Authentication key", "domain_config_auth_secret": "Authentication secret", "domain_config_auth_token": "Authentication token", - "domain_config_autopush": "Auto-push", - "domain_config_autopush_help": "Automatically update the domain's record", "domain_config_cert_install": "Install Let's Encrypt certificate", "domain_config_cert_issuer": "Certification authority", "domain_config_cert_no_checks": "Ignore diagnosis checks", @@ -359,8 +357,6 @@ "domain_dns_conf_special_use_tld": "This domain is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", "domain_dns_push_already_up_to_date": "Records already up to date, nothing to do.", "domain_dns_push_failed": "Updating the DNS records failed miserably.", - "domain_dns_push_failed_domain": "Updating the DNS records for {domain} failed : {error}", - "domain_dns_push_failed_domains": "Updating the DNS records for {domains} failed.", "domain_dns_push_failed_to_authenticate": "Failed to authenticate on registrar's API for domain '{domain}'. Most probably the credentials are incorrect? (Error: {error})", "domain_dns_push_failed_to_list": "Failed to list current records using the registrar's API: {error}", "domain_dns_push_managed_in_parent_domain": "The automatic DNS configuration feature is managed in the parent domain {parent_domain}.", @@ -373,7 +369,7 @@ "domain_dns_registrar_managed_in_parent_domain": "This domain is a subdomain of {parent_domain_link}. DNS registrar configuration should be managed in {parent_domain}'s configuration panel.", "domain_dns_registrar_not_supported": "YunoHost could not automatically detect the registrar handling this domain. You should manually configure your DNS records following the documentation at https://yunohost.org/dns.", "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", - "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", + "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", "domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --ignore-dyndns", "domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --ignore-dyndns", diff --git a/locales/es.json b/locales/es.json index bfb111f26..8637c3da8 100644 --- a/locales/es.json +++ b/locales/es.json @@ -467,7 +467,7 @@ "diagnosis_domain_expiration_not_found_details": "¿Parece que la información de WHOIS para el dominio {domain} no contiene información sobre la fecha de expiración?", "diagnosis_domain_not_found_details": "¡El dominio {domain} no existe en la base de datos WHOIS o ha expirado!", "diagnosis_domain_expiration_not_found": "No se pudo revisar la fecha de expiración para algunos dominios", - "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por YunoHost. Si no es el caso, puedes intentar forzar una actualización mediante yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por YunoHost. Si no es el caso, puedes intentar forzar una actualización mediante yunohost dyndns update --force.", "diagnosis_ip_local": "IP Local: {local}", "diagnosis_ip_no_ipv6_tip": "Tener IPv6 funcionando no es obligatorio para que su servidor funcione, pero es mejor para la salud del Internet en general. IPv6 debería ser configurado automáticamente por el sistema o su proveedor si está disponible. De otra manera, es posible que tenga que configurar varias cosas manualmente, tal y como se explica en esta documentación https://yunohost.org/#/ipv6. Si no puede habilitar IPv6 o si parece demasiado técnico, puede ignorar esta advertencia con toda seguridad.", "diagnosis_display_tip": "Para ver los problemas encontrados, puede ir a la sección de diagnóstico del webadmin, o ejecutar 'yunohost diagnosis show --issues --human-readable' en la línea de comandos.", @@ -589,7 +589,7 @@ "domain_config_auth_application_key": "LLave de Aplicación", "domain_dns_registrar_supported": "YunoHost detectó automáticamente que este dominio es manejado por el registrador **{registrar}**. Si lo desea, YunoHost configurará automáticamente esta zona DNS, si le proporciona las credenciales de API adecuadas. Puede encontrar documentación sobre cómo obtener sus credenciales de API en esta página: https://yunohost.org/registar_api_{registrar}. (También puede configurar manualmente sus registros DNS siguiendo la documentación en https://yunohost.org/dns)", "domain_dns_registrar_managed_in_parent_domain": "Este dominio es un subdominio de {parent_domain_link}. La configuración del registrador de DNS debe administrarse en el panel de configuración de {parent_domain}.", - "domain_dns_registrar_yunohost": "Este dominio es un nohost.me / nohost.st / ynh.fr y, por lo tanto, YunoHost maneja automáticamente su configuración de DNS sin ninguna configuración adicional. (vea el comando 'yunohost domain dns push DOMAIN')", + "domain_dns_registrar_yunohost": "Este dominio es un nohost.me / nohost.st / ynh.fr y, por lo tanto, YunoHost maneja automáticamente su configuración de DNS sin ninguna configuración adicional. (vea el comando 'yunohost dyndns update')", "domain_dns_registrar_not_supported": "YunoHost no pudo detectar automáticamente el registrador que maneja este dominio. Debe configurar manualmente sus registros DNS siguiendo la documentación en https://yunohost.org/dns.", "migration_ldap_backup_before_migration": "Creación de una copia de seguridad de la base de datos LDAP y la configuración de las aplicaciones antes de la migración real.", "invalid_number": "Debe ser un miembro", diff --git a/locales/eu.json b/locales/eu.json index 47ff773da..d58289bf4 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -154,7 +154,7 @@ "certmanager_domain_not_diagnosed_yet": "Oraindik ez dago {domain} domeinurako diagnostikorik. Berrabiarazi diagnostikoak 'DNS balioak' eta 'Web' ataletarako diagnostikoen gunean Let's Encrypt ziurtagirirako prest ote dagoen egiaztatzeko. (Edo zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztatzea desgaitzeko.)", "diagnosis_domain_expiration_warning": "Domeinu batzuk iraungitzear daude!", "app_packaging_format_not_supported": "Aplikazio hau ezin da instalatu YunoHostek ez duelako paketea ezagutzen. Sistema eguneratzea hausnartu beharko zenuke ziur asko.", - "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost domain dns push DOMAIN --force erabiliz.", + "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost dyndns update --force erabiliz.", "app_manifest_install_ask_path": "Aukeratu aplikazio hau instalatzeko URLaren bidea (domeinuaren atzeko aldean)", "app_manifest_install_ask_admin": "Aukeratu administrari bat aplikazio honetarako", "app_manifest_install_ask_password": "Aukeratu administrazio-pasahitz bat aplikazio honetarako", @@ -316,7 +316,7 @@ "domain_dns_push_not_applicable": "Ezin da {domain} domeinurako DNS konfigurazio automatiko funtzioa erabili. DNS erregistroak eskuz ezarri beharko zenituzke gidaorriei erreparatuz: https://yunohost.org/dns_config.", "domain_dns_push_managed_in_parent_domain": "DNS ezarpenak automatikoki konfiguratzeko funtzioa {parent_domain} domeinu nagusian kudeatzen da.", "domain_dns_registrar_managed_in_parent_domain": "Domeinu hau {parent_domain_link}(r)en azpidomeinua da. DNS ezarpenak {parent_domain}(r)en konfigurazio atalean kudeatu behar dira.", - "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost domain dns push DOMAIN' komandoa)", + "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost dyndns update' komandoa)", "domain_dns_registrar_not_supported": "YunoHostek ezin izan du domeinu honen erregistro-enpresa automatikoki antzeman. Eskuz konfiguratu beharko dituzu DNS ezarpenak gidalerroei erreparatuz: https://yunohost.org/dns.", "domain_dns_registrar_experimental": "Oraingoz, YunoHosten kideek ez dute **{registrar}** erregistro-enpresaren APIa nahi beste probatu eta aztertu. Funtzioa **oso esperimentala** da — kontuz!", "domain_config_mail_in": "Jasotako mezuak", diff --git a/locales/fa.json b/locales/fa.json index afa86b13b..92e05bdad 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -28,7 +28,7 @@ "diagnosis_domain_not_found_details": "دامنه {domain} در پایگاه داده WHOIS وجود ندارد یا منقضی شده است!", "diagnosis_domain_expiration_not_found": "بررسی تاریخ انقضا برخی از دامنه ها امکان پذیر نیست", "diagnosis_dns_specialusedomain": "دامنه {domain} بر اساس یک دامنه سطح بالا (TLD) مخصوص استفاده است و بنابراین انتظار نمی رود که دارای سوابق DNS واقعی باشد.", - "diagnosis_dns_try_dyndns_update_force": "پیکربندی DNS این دامنه باید به طور خودکار توسط YunoHost مدیریت شود. اگر اینطور نیست ، می توانید سعی کنید به زور یک به روز رسانی را با استفاده از yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "پیکربندی DNS این دامنه باید به طور خودکار توسط YunoHost مدیریت شود. اگر اینطور نیست ، می توانید سعی کنید به زور یک به روز رسانی را با استفاده از yunohost dyndns update --force.", "diagnosis_dns_point_to_doc": "لطفاً اسناد را در https://yunohost.org/dns_config برسی و مطالعه کنید، اگر در مورد پیکربندی سوابق DNS به کمک نیاز دارید.", "diagnosis_dns_discrepancy": "به نظر می رسد پرونده DNS زیر از پیکربندی توصیه شده پیروی نمی کند:
نوع: {type}
نام: {name}
ارزش فعلی: {current}
مقدار مورد انتظار: {value}", "diagnosis_dns_missing_record": "با توجه به پیکربندی DNS توصیه شده ، باید یک رکورد DNS با اطلاعات زیر اضافه کنید.
نوع: {type}
نام: {name}
ارزش: {value}", diff --git a/locales/fr.json b/locales/fr.json index 9affb5869..959ef1a8d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -510,7 +510,7 @@ "diagnosis_swap_tip": "Soyez averti et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire considérablement l'espérance de vie de celui-ci.", "restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}", "regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.", - "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost dyndns update --force.", "app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de YunoHost. Vous devriez probablement envisager de mettre à jour votre système.", "global_settings_setting_backup_compress_tar_archives": "Compresser les archives de backup", "diagnosis_processes_killed_by_oom_reaper": "Certains processus ont été récemment arrêtés par le système car il manquait de mémoire. Ceci est typiquement symptomatique d'un manque de mémoire sur le système ou d'un processus consommant trop de mémoire. Liste des processus arrêtés :\n{kills_summary}", @@ -598,7 +598,7 @@ "service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}", "domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.", "domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.", - "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost domain dns push DOMAIN')", + "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost dyndns update')", "domain_dns_registrar_supported": "YunoHost a détecté automatiquement que ce domaine est géré par le registrar **{registrar}**. Si vous le souhaitez, YunoHost configurera automatiquement cette zone DNS, si vous lui fournissez les identifiants API appropriés. Vous pouvez trouver de la documentation sur la façon d'obtenir vos identifiants API sur cette page : https://yunohost.org/registar_api_{registrar}. (Vous pouvez également configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns )", "domain_config_features_disclaimer": "Jusqu'à présent, l'activation/désactivation des fonctionnalités de messagerie ou XMPP n'a d'impact que sur la configuration DNS recommandée et automatique, et non sur les configurations système !", "domain_dns_push_managed_in_parent_domain": "La fonctionnalité de configuration DNS automatique est gérée dans le domaine parent {parent_domain}.", diff --git a/locales/gl.json b/locales/gl.json index beaeec801..61af0b672 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -185,7 +185,7 @@ "diagnosis_domain_expiration_not_found_details": "A información WHOIS para o dominio {domain} non semella conter información acerca da data de caducidade?", "diagnosis_domain_not_found_details": "O dominio {domain} non existe na base de datos de WHOIS ou está caducado!", "diagnosis_domain_expiration_not_found": "Non se puido comprobar a data de caducidade para algúns dominios", - "diagnosis_dns_try_dyndns_update_force": "A xestión DNS deste dominio debería estar xestionada directamente por YunoHost. Se non fose o caso, podes intentar forzar unha actualización executando yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "A xestión DNS deste dominio debería estar xestionada directamente por YunoHost. Se non fose o caso, podes intentar forzar unha actualización executando yunohost dyndns update --force.", "diagnosis_swap_ok": "O sistema ten {total} de swap!", "diagnosis_swap_notsomuch": "O sistema só ten {total} de swap. Deberías considerar ter polo menos {recommended} para evitar situacións onde o sistema esgote a memoria.", "diagnosis_swap_none": "O sistema non ten partición swap. Deberías considerar engadir polo menos {recommended} de swap para evitar situación onde o sistema esgote a memoria.", @@ -615,7 +615,7 @@ "domain_config_auth_consumer_key": "Chave consumidora", "log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'", "other_available_options": "... e outras {n} opcións dispoñibles non mostradas", - "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost domain dns push DOMAIN')", + "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost dyndns update')", "domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registrar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registrar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )", "domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.", "domain_config_auth_token": "Token de autenticación", diff --git a/locales/it.json b/locales/it.json index 888b6cc62..9bb923c2a 100644 --- a/locales/it.json +++ b/locales/it.json @@ -299,7 +299,7 @@ "diagnosis_domain_expiration_not_found_details": "Le informazioni WHOIS per il dominio {domain} non sembrano contenere la data di scadenza, giusto?", "diagnosis_domain_not_found_details": "Il dominio {domain} non esiste nel database WHOIS o è scaduto!", "diagnosis_domain_expiration_not_found": "Non riesco a controllare la data di scadenza di alcuni domini", - "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da YunoHost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da YunoHost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost dyndns update --force.", "diagnosis_dns_point_to_doc": "Controlla la documentazione a https://yunohost.org/dns_config se hai bisogno di aiuto nel configurare i record DNS.", "diagnosis_dns_discrepancy": "Il record DNS non sembra seguire la configurazione DNS raccomandata:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", "diagnosis_dns_missing_record": "Stando alla configurazione DNS raccomandata, dovresti aggiungere un record DNS con le seguenti informazioni.
Type: {type}
Name: {name}
Value: {value}", @@ -608,7 +608,7 @@ "diagnosis_description_apps": "Applicazioni", "domain_registrar_is_not_configured": "Il registrar non è ancora configurato per il dominio {domain}.", "domain_dns_registrar_managed_in_parent_domain": "Questo dominio è un sotto-dominio di {parent_domain_link}. La configurazione del registrar DNS dovrebbe essere gestita dal pannello di configurazione di {parent_domain}.", - "domain_dns_registrar_yunohost": "Questo dominio è un nohost.me / nohost.st / ynh.fr, perciò la sua configurazione DNS è gestita automaticamente da YunoHost, senza alcuna ulteriore configurazione. (vedi il comando yunohost domain dns push DOMAIN)", + "domain_dns_registrar_yunohost": "Questo dominio è un nohost.me / nohost.st / ynh.fr, perciò la sua configurazione DNS è gestita automaticamente da YunoHost, senza alcuna ulteriore configurazione. (vedi il comando yunohost dyndns update)", "domain_dns_push_success": "Record DNS aggiornati!", "domain_dns_push_failed": "L’aggiornamento dei record DNS è miseramente fallito.", "domain_dns_push_partial_failure": "Record DNS parzialmente aggiornati: alcuni segnali/errori sono stati riportati.", diff --git a/locales/uk.json b/locales/uk.json index 2b168b65e..281f2dba7 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -409,7 +409,7 @@ "diagnosis_domain_not_found_details": "Домен {domain} не існує в базі даних WHOIS або строк його дії сплив!", "diagnosis_domain_expiration_not_found": "Неможливо перевірити строк дії деяких доменів", "diagnosis_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) такого як .local або .test і тому не очікується, що у нього будуть актуальні записи DNS.", - "diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди yunohost dyndns update --force.", "diagnosis_dns_point_to_doc": "Якщо вам потрібна допомога з налаштування DNS-записів, зверніться до документації на сайті https://yunohost.org/dns_config.", "diagnosis_dns_discrepancy": "Наступний запис DNS, схоже, не відповідає рекомендованій конфігурації:
Тип: {type}
Назва: {name}
Поточне значення: {current}
Очікуване значення: {value}", "diagnosis_dns_missing_record": "Згідно рекомендованої конфігурації DNS, ви повинні додати запис DNS з наступними відомостями.
Тип: {type}
Назва: {name}
Значення: {value}", @@ -599,7 +599,7 @@ "diagnosis_http_special_use_tld": "Домен {domain} базується на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він буде відкритий за межами локальної мережі.", "domain_dns_push_managed_in_parent_domain": "Функцією автоконфігурації DNS керує батьківський домен {parent_domain}.", "domain_dns_registrar_managed_in_parent_domain": "Цей домен є піддоменом {parent_domain_link}. Конфігурацією реєстратора DNS слід керувати на панелі конфігурації {parent_domain}.", - "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost domain dns push DOMAIN')", + "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost dyndns update')", "domain_dns_conf_special_use_tld": "Цей домен засновано на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він матиме актуальні записи DNS.", "domain_dns_registrar_supported": "YunoHost автоматично визначив, що цей домен обслуговується реєстратором **{registrar}**. Якщо ви хочете, YunoHost автоматично налаштує цю DNS-зону, якщо ви надасте йому відповідні облікові дані API. Ви можете знайти документацію про те, як отримати реєстраційні дані API на цій сторінці: https://yunohost.org/registar_api_{registrar}. (Ви також можете вручну налаштувати свої DNS-записи, дотримуючись документації на https://yunohost.org/dns)", "domain_dns_registrar_experimental": "Поки що інтерфейс з API **{registrar}** не був належним чином протестований і перевірений спільнотою YunoHost. Підтримка є **дуже експериментальною** - будьте обережні!", diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index b31d88217..8aecbbce3 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -449,7 +449,7 @@ "diagnosis_diskusage_low": "存储器{mountpoint}(在设备{device}上)只有{free} ({free_percent}%) 的空间。({free_percent}%)的剩余空间(在{total}中)。要小心。", "diagnosis_diskusage_verylow": "存储器{mountpoint}(在设备{device}上)仅剩余{free} ({free_percent}%) (剩余{total})个空间。您应该真正考虑清理一些空间!", "diagnosis_services_bad_status_tip": "你可以尝试重新启动服务,如果没有效果,可以看看webadmin中的服务日志(从命令行,你可以用yunohost service restart {service}yunohost service log {service})来做。", - "diagnosis_dns_try_dyndns_update_force": "该域的DNS配置应由YunoHost自动管理,如果不是这种情况,您可以尝试使用 yunohost domain dns push DOMAIN --force强制进行更新。", + "diagnosis_dns_try_dyndns_update_force": "该域的DNS配置应由YunoHost自动管理,如果不是这种情况,您可以尝试使用 yunohost dyndns update --force强制进行更新。", "diagnosis_dns_point_to_doc": "如果您需要有关配置DNS记录的帮助,请查看 https://yunohost.org/dns_config 上的文档。", "diagnosis_dns_discrepancy": "以下DNS记录似乎未遵循建议的配置:
类型: {type}
名称: {name}
代码> 当前值: {current}期望值: {value}", "log_backup_create": "创建备份档案", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 3124f3105..9fe077c23 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -751,9 +751,8 @@ domain: action_help: Push DNS records to registrar api: POST /domains//dns/push arguments: - domains: - help: Domain names to push DNS conf for - nargs: "*" + domain: + help: Domain name to push DNS conf for extra: pattern: *pattern_domain -d: @@ -766,9 +765,6 @@ domain: --purge: help: Delete all records action: store_true - --auto: - help: Push only domains that should be pushed automatically - action: store_true cert: subcategory_help: Manage domain certificates @@ -1572,7 +1568,7 @@ dyndns: extra: pattern: *pattern_domain -p: - full: --password + full: --recovery-password nargs: "?" const: 0 help: Password used to later delete the domain @@ -1580,25 +1576,6 @@ dyndns: pattern: *pattern_password comment: dyndns_added_password - ### dyndns_unsubscribe() - unsubscribe: - action_help: Unsubscribe to a DynDNS service ( deprecated, use 'yunohost domain dyndns unsubscribe' instead ) - deprecated: true - arguments: - -d: - full: --domain - help: Full domain to unsubscribe with - extra: - pattern: *pattern_domain - required: True - -p: - full: --password - nargs: "?" - const: 0 - help: Password used to delete the domain - extra: - pattern: *pattern_password - ### dyndns_update() update: action_help: Update IP on DynDNS platform ( deprecated, use 'yunohost domain dns push DOMAIN' instead ) diff --git a/share/config_domain.toml b/share/config_domain.toml index 0aae4df26..b1ec436c5 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -33,13 +33,6 @@ name = "Features" [dns] name = "DNS" - [dns.zone] - - [dns.zone.autopush] - type = "boolean" - default = 0 - help = "" - [dns.registrar] # This part is automatically generated in DomainConfigPanel diff --git a/src/dns.py b/src/dns.py index 085f47471..296ecfaaa 100644 --- a/src/dns.py +++ b/src/dns.py @@ -618,24 +618,8 @@ def _get_registar_settings(domain): return registrar, settings -def domain_dns_push(domains, dry_run=False, force=False, purge=False, auto=False): - if auto: - domains = domain_list(exclude_subdomains=True, auto_push=True)["domains"] - elif len(domains) == 0: - domains = domain_list(exclude_subdomains=True)["domains"] - error_domains = [] - for domain in domains: - try: - domain_dns_push_unique(domain, dry_run=dry_run, force=force, purge=purge) - except YunohostError as e: - logger.error(m18n.n("domain_dns_push_failed_domain", domain=domain, error=str(e))) - error_domains.append(domain) - if len(error_domains) > 0: - raise YunohostError("domain_dns_push_failed_domains", domains=', '.join(error_domains)) - - @is_unit_operation() -def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, purge=False): +def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge=False): """ Send DNS records to the previously-configured registrar of the domain. """ diff --git a/src/domain.py b/src/domain.py index 020a707c7..9dc884177 100644 --- a/src/domain.py +++ b/src/domain.py @@ -116,7 +116,6 @@ def domain_list(exclude_subdomains=False, tree=False, features=[]): domains_filtered = [] for domain in domains: config = domain_config_get(domain, key="feature", export=True) - config += domain_config_get(domain, key="dns.zone", export=True) if any(config.get(feature) == 1 for feature in features): domains_filtered.append(domain) domains = domains_filtered @@ -791,7 +790,7 @@ def domain_dns_suggest(domain): return domain_dns_suggest(domain) -def domain_dns_push(domains, dry_run=None, force=None, purge=None, auto=False): +def domain_dns_push(domain, dry_run, force, purge): from yunohost.dns import domain_dns_push - return domain_dns_push(domains, dry_run=dry_run, force=force, purge=purge, auto=auto) + return domain_dns_push(domain, dry_run, force, purge) diff --git a/src/dyndns.py b/src/dyndns.py index caeef9459..2f83038cc 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -172,10 +172,6 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): error = f'Server error, code: {r.status_code}. (Message: "{r.text}")' raise YunohostError("dyndns_registration_failed", error=error) - # Set the domain's config to autopush - from yunohost.domain import domain_config_set - domain_config_set(domain, key="dns.zone.autopush", value=1) - # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns regen_conf(["yunohost"]) @@ -183,7 +179,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): # Add some dyndns update in 2 and 4 minutes from now such that user should # not have to wait 10ish minutes for the conf to propagate cmd = ( - f"at -M now + {{t}} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost domain dns push {domain}'\"" + "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" ) # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) diff --git a/src/tools.py b/src/tools.py index c0da7a37b..79f10bc8c 100644 --- a/src/tools.py +++ b/src/tools.py @@ -200,15 +200,15 @@ def tools_postinstall( # Check if the domain is available... try: - _dyndns_available(domain) + available = _dyndns_available(domain) # If an exception is thrown, most likely we don't have internet # connectivity or something. Assume that this domain isn't manageable # and inform the user that we could not contact the dyndns host server. except Exception: - logger.warning( - m18n.n("dyndns_provider_unreachable", provider="dyndns.yunohost.org") - ) - raise YunohostValidationError("dyndns_unavailable", domain=domain) + raise YunohostValidationError("dyndns_provider_unreachable", provider="dyndns.yunohost.org") + else: + if not available: + raise YunohostValidationError("dyndns_unavailable", domain=domain) if os.system("iptables -V >/dev/null 2>/dev/null") != 0: raise YunohostValidationError( From e2da51b9a367fb7f4e8f7d1b39a081d19c58427b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 18:43:27 +0200 Subject: [PATCH 53/93] dyndns: various tweaks to simplify the code, improve UX ... --- locales/en.json | 8 ++-- share/actionsmap.yml | 2 +- src/domain.py | 42 +++++------------- src/dyndns.py | 102 +++++++++++++++++++------------------------ src/tools.py | 7 +-- 5 files changed, 62 insertions(+), 99 deletions(-) diff --git a/locales/en.json b/locales/en.json index 83ee34052..b3a1725e8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -90,7 +90,8 @@ "ask_new_domain": "New domain", "ask_new_path": "New path", "ask_password": "Password", - "ask_dyndns_recovery_password": "DynDNS recovey password", + "ask_dyndns_recovery_password_explain": "Please pick a recovery password for your DynDNS domain, in case you need to reset it later.", + "ask_dyndns_recovery_password": "DynDNS recovey passwory", "ask_user_domain": "Domain to use for the user's email address and XMPP account", "backup_abstract_method": "This backup method has yet to be implemented", "backup_actually_backuping": "Creating a backup archive from the collected files...", @@ -383,8 +384,6 @@ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", - "domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --ignore-dyndns", - "domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --ignore-dyndns", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", @@ -400,7 +399,6 @@ "dyndns_domain_not_provided": "DynDNS provider {provider} cannot provide domain {domain}.", "dyndns_ip_update_failed": "Could not update IP address to DynDNS", "dyndns_ip_updated": "Updated your IP on DynDNS", - "dyndns_key_generating": "Generating DNS key... It may take a while.", "dyndns_key_not_found": "DNS key not found for the domain", "dyndns_no_domain_registered": "No domain registered with DynDNS", "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", @@ -772,4 +770,4 @@ "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." -} \ No newline at end of file +} diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 412297440..e11073afc 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -493,7 +493,7 @@ domain: help: Display domains as a tree action: store_true --features: - help: List only domains with features enabled (xmpp, mail_in, mail_out, auto_push) + help: List only domains with features enabled (xmpp, mail_in, mail_out) nargs: "*" ### domain_info() diff --git a/src/domain.py b/src/domain.py index 35730483b..8d2758ab2 100644 --- a/src/domain.py +++ b/src/domain.py @@ -228,7 +228,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_d from yunohost.utils.password import assert_password_is_strong_enough from yunohost.certificate import _certificate_install_selfsigned - if dyndns_recovery_password != 0 and dyndns_recovery_password is not None: + if dyndns_recovery_password: operation_logger.data_to_redact.append(dyndns_recovery_password) if domain.startswith("xmpp-upload."): @@ -252,35 +252,19 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_d domain = domain.encode("idna").decode("utf-8") # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) - dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 + dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if not ignore_dyndns and not dyndns_recovery_password: - if Moulinette.interface.type == "api": - raise YunohostValidationError("domain_dyndns_missing_password") - else: - dyndns_recovery_password = Moulinette.prompt( - m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True - ) - - # Ensure sufficiently complex password - assert_password_is_strong_enough("admin", dyndns_recovery_password) - - if ((dyndns_recovery_password is None) == (ignore_dyndns is False)): - raise YunohostValidationError("domain_dyndns_instruction_unclear") - from yunohost.dyndns import is_subscribing_allowed - # Do not allow to subscribe to multiple dyndns domains... if not is_subscribing_allowed(): raise YunohostValidationError("domain_dyndns_already_subscribed") + if dyndns_recovery_password: + assert_password_is_strong_enough("admin", dyndns_recovery_password) operation_logger.start() - if not dyndns and (dyndns_recovery_password is not None or ignore_dyndns): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns-recovery-password or --ignore-dyndns option") - if dyndns and not ignore_dyndns: - # Actually subscribe - domain_dyndns_subscribe(domain=domain, password=dyndns_recovery_password) + if dyndns: + domain_dyndns_subscribe(domain=domain, recovery_password=dyndns_recovery_password) _certificate_install_selfsigned([domain], True) @@ -346,7 +330,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface - if dyndns_recovery_password != 0 and dyndns_recovery_password is not None: + if dyndns_recovery_password: operation_logger.data_to_redact.append(dyndns_recovery_password) # the 'force' here is related to the exception happening in domain_add ... @@ -410,16 +394,10 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd ) # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) - dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 - if dyndns: - if ((dyndns_recovery_password is None) == (ignore_dyndns is False)): - raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") + dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 operation_logger.start() - if not dyndns and ((dyndns_recovery_password is not None) or (ignore_dyndns is not False)): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns_recovery_password or --ignore-dyndns option") - ldap = _get_ldap_interface() try: ldap.remove("virtualdomain=" + domain + ",ou=domains") @@ -465,9 +443,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd hook_callback("post_domain_remove", args=[domain]) # If a password is provided, delete the DynDNS record - if dyndns and not ignore_dyndns: + if dyndns: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain, password=dyndns_recovery_password) + domain_dyndns_unsubscribe(domain=domain, recovery_password=dyndns_recovery_password) logger.success(m18n.n("domain_deleted")) diff --git a/src/dyndns.py b/src/dyndns.py index 3c9788af7..2da3de212 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -50,7 +50,7 @@ def is_subscribing_allowed(): Returns: True if the limit is not reached, False otherwise """ - return len(glob.glob("/etc/yunohost/dyndns/*.key")) < MAX_DYNDNS_DOMAINS + return len(dyndns_list()["domains"]) < MAX_DYNDNS_DOMAINS def _dyndns_available(domain): @@ -78,7 +78,7 @@ def _dyndns_available(domain): return r == f"Domain {domain} is available" -@is_unit_operation() +@is_unit_operation(exclude=["recovery_password"]) def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): """ Subscribe to a DynDNS service @@ -88,26 +88,6 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): recovery_password -- Password that will be used to delete the domain """ - if recovery_password is None: - logger.warning(m18n.n('dyndns_no_recovery_password')) - else: - from yunohost.utils.password import assert_password_is_strong_enough - # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and recovery_password == 0: - recovery_password = Moulinette.prompt( - m18n.n("ask_password"), - is_password=True, - confirm=True - ) - assert_password_is_strong_enough("admin", recovery_password) - - if not is_subscribing_allowed(): - raise YunohostValidationError("domain_dyndns_already_subscribed") - - if domain is None: - domain = _get_maindomain() - operation_logger.related_to.append(("domain", domain)) - # Verify if domain is provided by subscribe_host if not is_yunohost_dyndns_domain(domain): raise YunohostValidationError( @@ -118,6 +98,30 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): if not _dyndns_available(domain): raise YunohostValidationError("dyndns_unavailable", domain=domain) + # Check adding another dyndns domain is still allowed + if not is_subscribing_allowed(): + raise YunohostValidationError("domain_dyndns_already_subscribed") + + # Prompt for a password if running in CLI and no password provided + if not recovery_password and Moulinette.interface.type == "cli": + logger.warning(m18n.n("ask_dyndns_recovery_password_explain")) + recovery_password = Moulinette.prompt( + m18n.n("ask_dyndns_recovery_password"), + is_password=True, + confirm=True + ) + elif not recovery_password: + logger.warning(m18n.n("dyndns_no_recovery_password")) + + if recovery_password: + from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough("admin", recovery_password) + operation_logger.data_to_redact.append(recovery_password) + + if domain is None: + domain = _get_maindomain() + operation_logger.related_to.append(("domain", domain)) + operation_logger.start() # '165' is the convention identifier for hmac-sha512 algorithm @@ -127,8 +131,6 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): if not os.path.exists("/etc/yunohost/dyndns"): os.makedirs("/etc/yunohost/dyndns") - logger.debug(m18n.n("dyndns_key_generating")) - # Here, we emulate the behavior of the old 'dnssec-keygen' utility # which since bullseye was replaced by ddns-keygen which is now # in the bind9 package ... but installing bind9 will conflict with dnsmasq @@ -154,7 +156,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): # Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever... b64encoded_key = base64.b64encode(secret.encode()).decode() data = {"subdomain": domain} - if password is not None: + if recovery_password: data["recovery_password"] = hashlib.sha256((domain + ":" + recovery_password.strip()).encode('utf-8')).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", @@ -188,7 +190,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): logger.success(m18n.n("dyndns_registered")) -@is_unit_operation() +@is_unit_operation(exclude=["recovery_password"]) def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): """ Unsubscribe from a DynDNS service @@ -198,24 +200,19 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): recovery_password -- Password that is used to delete the domain ( defined when subscribing ) """ - from yunohost.utils.password import assert_password_is_strong_enough + import requests # lazy loading this module for performance reasons + + # FIXME : it should be possible to unsubscribe the domain just using the key file ... # Ensure sufficiently complex password if Moulinette.interface.type == "cli" and not recovery_password: recovery_password = Moulinette.prompt( - m18n.n("ask_password"), + m18n.n("ask_dyndns_recovery_password"), is_password=True ) - assert_password_is_strong_enough("admin", recovery_password) operation_logger.start() - # '165' is the convention identifier for hmac-sha512 algorithm - # '1234' is idk? doesnt matter, but the old format contained a number here... - key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" - - import requests # lazy loading this module for performance reasons - # Send delete request try: secret = str(domain) + ":" + str(recovery_password).strip() @@ -228,30 +225,30 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): raise YunohostError("dyndns_unregistration_failed", error=str(e)) if r.status_code == 200: # Deletion was successful - rm(key_file, force=True) + for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key"): + rm(key_file, force=True) # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns regen_conf(["yunohost"]) - - logger.success(m18n.n("dyndns_unregistered")) elif r.status_code == 403: # Wrong password raise YunohostError("dyndns_unsubscribe_wrong_password") elif r.status_code == 404: # Invalid domain raise YunohostError("dyndns_unsubscribe_wrong_domain") + logger.success(m18n.n("dyndns_unregistered")) + def dyndns_list(): """ Returns all currently subscribed DynDNS domains ( deduced from the key files ) """ - files = glob.glob("/etc/yunohost/dyndns/K*key") - # Get the domain names - for i in range(len(files)): - files[i] = files[i].split(".+", 1)[0] - files[i] = files[i].split("/etc/yunohost/dyndns/K")[1] + from yunohost.domain import domain_list - return {"domains": files} + domains = domain_list(exclude_subdomains=True)["domains"] + dyndns_domains = [d for d in domains if is_yunohost_dyndns_domain(d) and glob.glob(f"/etc/yunohost/dyndns/K{d}.+*.key")] + + return {"domains": dyndns_domains} @is_unit_operation() @@ -277,21 +274,14 @@ def dyndns_update( # If domain is not given, update all DynDNS domains if domain is None: - from yunohost.domain import domain_list + dyndns_domains = dyndns_list()["domains"] - domains = domain_list(exclude_subdomains=True, auto_push=True)["domains"] - pushed = 0 - for d in domains: - if is_yunohost_dyndns_domain(d): - dyndns_update(d, force=force, dry_run=dry_run) - pushed += 1 - if pushed == 0: + if not dyndns_domains: raise YunohostValidationError("dyndns_no_domain_registered") - return - elif type(domain).__name__ in ["list", "tuple"]: - for d in domain: - dyndns_update(d, force=force, dry_run=dry_run) + for domain in dyndns_domains: + dyndns_update(domain, force=force, dry_run=dry_run) + return # If key is not given, pick the first file we find with the domain given diff --git a/src/tools.py b/src/tools.py index 33cccd729..af6a2e61a 100644 --- a/src/tools.py +++ b/src/tools.py @@ -197,11 +197,8 @@ def tools_postinstall( assert_password_is_strong_enough("admin", password) # If this is a nohost.me/noho.st, actually check for availability - if not ignore_dyndns and is_yunohost_dyndns_domain(domain): - - if (bool(dyndns_recovery_password), ignore_dyndns) in [(True, True), (False, False)]: - raise YunohostValidationError("domain_dyndns_instruction_unclear") - + dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) + if dyndns: # Check if the domain is available... try: available = _dyndns_available(domain) From cbef40798c2843777a43db4b3b328e1d01abfcda Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 19:40:30 +0200 Subject: [PATCH 54/93] dyndns: be able to unsubscribe using the key + domain and i18n string consistency --- locales/en.json | 19 +++++++++-------- share/actionsmap.yml | 10 ++++----- src/dyndns.py | 50 +++++++++++++++++++++++++++++--------------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/locales/en.json b/locales/en.json index b3a1725e8..81d0b8a3e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -91,7 +91,8 @@ "ask_new_path": "New path", "ask_password": "Password", "ask_dyndns_recovery_password_explain": "Please pick a recovery password for your DynDNS domain, in case you need to reset it later.", - "ask_dyndns_recovery_password": "DynDNS recovey passwory", + "ask_dyndns_recovery_password": "DynDNS recovery password", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Please enter the recovery password for this DynDNS domain.", "ask_user_domain": "Domain to use for the user's email address and XMPP account", "backup_abstract_method": "This backup method has yet to be implemented", "backup_actually_backuping": "Creating a backup archive from the collected files...", @@ -401,15 +402,15 @@ "dyndns_ip_updated": "Updated your IP on DynDNS", "dyndns_key_not_found": "DNS key not found for the domain", "dyndns_no_domain_registered": "No domain registered with DynDNS", - "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", - "dyndns_added_password": "Remember your recovery password, you can use it to delete this domain record.", + "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", + "dyndns_added_password": "Remember your recovery password, you can use it to delete this domain record.", "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", - "dyndns_registered": "DynDNS domain registered", - "dyndns_registration_failed": "Could not register DynDNS domain: {error}", - "dyndns_unregistration_failed": "Could not unregister DynDNS domain: {error}", - "dyndns_unregistered": "DynDNS domain successfully unregistered", - "dyndns_unsubscribe_wrong_password": "Invalid password", - "dyndns_unsubscribe_wrong_domain": "Domain is not registered", + "dyndns_subscribed": "DynDNS domain subscribed", + "dyndns_subscribe_failed": "Could not subscribe DynDNS domain: {error}", + "dyndns_unsubscribe_failed": "Could not unsubscribe DynDNS domain: {error}", + "dyndns_unsubscribed": "DynDNS domain unsubscribed", + "dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials", + "dyndns_unsubscribe_already_unsubscribed": "Domain is already unsubscribed", "dyndns_unavailable": "The domain '{domain}' is unavailable.", "extracting": "Extracting...", "field_invalid": "Invalid field '{}'", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index e11073afc..b229be84a 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -663,9 +663,8 @@ domain: subscribe: action_help: Subscribe to a DynDNS service arguments: - -d: - full: --domain - help: Full domain to subscribe with + domain: + help: Domain to subscribe to the DynDNS service extra: pattern: *pattern_domain -p: @@ -681,9 +680,8 @@ domain: unsubscribe: action_help: Unsubscribe from a DynDNS service arguments: - -d: - full: --domain - help: Full domain to unsubscribe with + domain: + help: Domain to unsubscribe from the DynDNS service extra: pattern: *pattern_domain required: True diff --git a/src/dyndns.py b/src/dyndns.py index 2da3de212..4ed730ecc 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -165,14 +165,14 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): ) except Exception as e: rm(key_file, force=True) - raise YunohostError("dyndns_registration_failed", error=str(e)) + raise YunohostError("dyndns_subscribe_failed", error=str(e)) if r.status_code != 201: rm(key_file, force=True) try: error = json.loads(r.text)["error"] except Exception: error = f'Server error, code: {r.status_code}. (Message: "{r.text}")' - raise YunohostError("dyndns_registration_failed", error=error) + raise YunohostError("dyndns_subscribe_failed", error=error) # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns @@ -187,7 +187,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) subprocess.check_call(["bash", "-c", cmd.format(t="4 min")]) - logger.success(m18n.n("dyndns_registered")) + logger.success(m18n.n("dyndns_subscribed")) @is_unit_operation(exclude=["recovery_password"]) @@ -202,23 +202,37 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): import requests # lazy loading this module for performance reasons - # FIXME : it should be possible to unsubscribe the domain just using the key file ... + # Unsubscribe the domain using the key if available + keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") + if keys: + key = keys[0] + with open(key) as f: + key = f.readline().strip().split(" ", 6)[-1] + base64key = base64.b64encode(key.encode()).decode() + credential = {"key": base64key} + else: + # Ensure sufficiently complex password + if Moulinette.interface.type == "cli" and not recovery_password: + logger.warning(m18n.n("ask_dyndns_recovery_password_explain_during_unsubscribe")) + recovery_password = Moulinette.prompt( + m18n.n("ask_dyndns_recovery_password"), + is_password=True + ) - # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and not recovery_password: - recovery_password = Moulinette.prompt( - m18n.n("ask_dyndns_recovery_password"), - is_password=True - ) + if not recovery_password: + logger.error(f"Cannot unsubscribe the domain {domain}: no credential provided") + return + + secret = str(domain) + ":" + str(recovery_password).strip() + credential = {"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()} operation_logger.start() # Send delete request try: - secret = str(domain) + ":" + str(recovery_password).strip() r = requests.delete( f"https://{DYNDNS_PROVIDER}/domains/{domain}", - data={"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, + data=credential, timeout=30, ) except Exception as e: @@ -230,12 +244,14 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns regen_conf(["yunohost"]) - elif r.status_code == 403: # Wrong password - raise YunohostError("dyndns_unsubscribe_wrong_password") - elif r.status_code == 404: # Invalid domain - raise YunohostError("dyndns_unsubscribe_wrong_domain") + elif r.status_code == 403: + raise YunohostError("dyndns_unsubscribe_denied") + elif r.status_code == 409: + raise YunohostError("dyndns_unsubscribe_already_unsubscribed") + else: + raise YunohostError("dyndns_unsubscribe_failed", error=f"The server returned code {r.status_code}") - logger.success(m18n.n("dyndns_unregistered")) + logger.success(m18n.n("dyndns_unsubscribed")) def dyndns_list(): From 59a2c96921fe7d3b2083e1a1ae086bcc0edc3e90 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 19:49:28 +0200 Subject: [PATCH 55/93] dyndns: remove this 'comment' thing from the actionsmap, it's being displayed even for non-dyndns domains... --- locales/en.json | 1 - share/actionsmap.yml | 4 ---- 2 files changed, 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index 81d0b8a3e..e25911435 100644 --- a/locales/en.json +++ b/locales/en.json @@ -403,7 +403,6 @@ "dyndns_key_not_found": "DNS key not found for the domain", "dyndns_no_domain_registered": "No domain registered with DynDNS", "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", - "dyndns_added_password": "Remember your recovery password, you can use it to delete this domain record.", "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", "dyndns_subscribed": "DynDNS domain subscribed", "dyndns_subscribe_failed": "Could not subscribe DynDNS domain: {error}", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index b229be84a..9132a8545 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -525,7 +525,6 @@ domain: help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain extra: pattern: *pattern_password - comment: dyndns_added_password ### domain_remove() remove: @@ -674,7 +673,6 @@ domain: help: Password used to later delete the domain extra: pattern: *pattern_password - comment: dyndns_added_password ### domain_dyndns_unsubscribe() unsubscribe: @@ -1585,7 +1583,6 @@ dyndns: help: Password used to later delete the domain extra: pattern: *pattern_password - comment: dyndns_added_password ### dyndns_update() update: @@ -1684,7 +1681,6 @@ tools: help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain extra: pattern: *pattern_password - comment: dyndns_added_password --force-diskspace: help: Use this if you really want to install YunoHost on a setup with less than 10 GB on the root filesystem action: store_true From 58614add7905dc6ca361ee79aad66e9d0296c498 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 20:22:40 +0200 Subject: [PATCH 56/93] dyndns: add a 'set-recovery-password' command to set the recovery password using only the key --- locales/en.json | 5 +++++ share/actionsmap.yml | 20 ++++++++++++++++++-- src/domain.py | 21 +++++++++++++++------ src/dyndns.py | 45 ++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/locales/en.json b/locales/en.json index e25911435..bb0976fff 100644 --- a/locales/en.json +++ b/locales/en.json @@ -410,6 +410,11 @@ "dyndns_unsubscribed": "DynDNS domain unsubscribed", "dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials", "dyndns_unsubscribe_already_unsubscribed": "Domain is already unsubscribed", + "dyndns_set_recovery_password_denied": "Failed to set recovery password: invalid key", + "dyndns_set_recovery_password_unknown_domain": "Failed to set recovery password: domain not registered", + "dyndns_set_recovery_password_invalid_password": "Failed to set recovery password: password is not strong enough", + "dyndns_set_recovery_password_failed": "Failed to set recovery password: {error}", + "dyndns_set_recovery_password_success": "Recovery password set!", "dyndns_unavailable": "The domain '{domain}' is unavailable.", "extracting": "Extracting...", "field_invalid": "Invalid field '{}'", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 9132a8545..689c3da86 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -670,7 +670,7 @@ domain: full: --recovery-password nargs: "?" const: 0 - help: Password used to later delete the domain + help: Password used to later recover the domain if needed extra: pattern: *pattern_password @@ -687,7 +687,7 @@ domain: full: --recovery-password nargs: "?" const: 0 - help: Password used to delete the domain + help: Recovery password used to delete the domain extra: pattern: *pattern_password @@ -695,6 +695,22 @@ domain: list: action_help: List all subscribed DynDNS domains + ### domain_dyndns_set_recovery_password() + set-recovery-password: + action_help: Set recovery password + arguments: + domain: + help: Domain to set recovery password for + extra: + pattern: *pattern_domain + required: True + -p: + full: --recovery-password + help: The new recovery password + extra: + password: ask_dyndns_recovery_password + pattern: *pattern_password + config: subcategory_help: Domain settings actions: diff --git a/src/domain.py b/src/domain.py index 8d2758ab2..424b461e8 100644 --- a/src/domain.py +++ b/src/domain.py @@ -450,22 +450,22 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd logger.success(m18n.n("domain_deleted")) -def domain_dyndns_subscribe(**kwargs): +def domain_dyndns_subscribe(*args, **kwargs): """ Subscribe to a DynDNS domain """ from yunohost.dyndns import dyndns_subscribe - dyndns_subscribe(**kwargs) + dyndns_subscribe(*args, **kwargs) -def domain_dyndns_unsubscribe(**kwargs): +def domain_dyndns_unsubscribe(*args, **kwargs): """ Unsubscribe from a DynDNS domain """ from yunohost.dyndns import dyndns_unsubscribe - dyndns_unsubscribe(**kwargs) + dyndns_unsubscribe(*args, **kwargs) def domain_dyndns_list(): @@ -477,13 +477,22 @@ def domain_dyndns_list(): return dyndns_list() -def domain_dyndns_update(**kwargs): +def domain_dyndns_update(*args, **kwargs): """ Update a DynDNS domain """ from yunohost.dyndns import dyndns_update - dyndns_update(**kwargs) + dyndns_update(*args, **kwargs) + + +def domain_dyndns_set_recovery_password(*args, **kwargs): + """ + Set a recovery password for an already registered dyndns domain + """ + from yunohost.dyndns import dyndns_set_recovery_password + + dyndns_set_recovery_password(*args, **kwargs) @is_unit_operation() diff --git a/src/dyndns.py b/src/dyndns.py index 4ed730ecc..dca4e9c77 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -110,7 +110,8 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): is_password=True, confirm=True ) - elif not recovery_password: + + if not recovery_password: logger.warning(m18n.n("dyndns_no_recovery_password")) if recovery_password: @@ -210,8 +211,8 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): key = f.readline().strip().split(" ", 6)[-1] base64key = base64.b64encode(key.encode()).decode() credential = {"key": base64key} + # Otherwise, ask for the recovery password else: - # Ensure sufficiently complex password if Moulinette.interface.type == "cli" and not recovery_password: logger.warning(m18n.n("ask_dyndns_recovery_password_explain_during_unsubscribe")) recovery_password = Moulinette.prompt( @@ -254,6 +255,46 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): logger.success(m18n.n("dyndns_unsubscribed")) +def dyndns_set_recovery_password(domain, recovery_password): + + keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") + + if not keys: + raise YunohostValidationError("dyndns_key_not_found") + + from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough("admin", recovery_password) + secret = str(domain) + ":" + str(recovery_password).strip() + + key = keys[0] + with open(key) as f: + key = f.readline().strip().split(" ", 6)[-1] + base64key = base64.b64encode(key.encode()).decode() + + import requests # lazy loading this module for performance reasons + + # Send delete request + try: + r = requests.put( + f"https://{DYNDNS_PROVIDER}/domains/{domain}/recovery_password", + data={"key": base64key, "recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, + timeout=30, + ) + except Exception as e: + raise YunohostError("dyndns_set_recovery_password_failed", error=str(e)) + + if r.status_code == 200: + logger.success(m18n.n("dyndns_set_recovery_password_success")) + elif r.status_code == 403: + raise YunohostError("dyndns_set_recovery_password_denied") + elif r.status_code == 404: + raise YunohostError("dyndns_set_recovery_password_unknown_domain") + elif r.status_code == 409: + raise YunohostError("dyndns_set_recovery_password_invalid_password") + else: + raise YunohostError("dyndns_set_recovery_password_failed", error=f"The server returned code {r.status_code}") + + def dyndns_list(): """ Returns all currently subscribed DynDNS domains ( deduced from the key files ) From 91497afbfeb0713ca3170e4935bb5f52c16d24a3 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 12 Apr 2023 13:04:55 +0200 Subject: [PATCH 57/93] form: move option asking+prompt in external function --- src/tests/test_questions.py | 2 +- src/utils/form.py | 276 ++++++++++++++++++------------------ 2 files changed, 142 insertions(+), 136 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 190eb0cba..7ada38a1c 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -445,7 +445,7 @@ class BaseTest: assert option.name == id_ assert option.ask == {"en": id_} assert option.readonly is (True if is_special_readonly_option else False) - assert option.visible is None + assert option.visible is True # assert option.bind is None if is_special_readonly_option: diff --git a/src/utils/form.py b/src/utils/form.py index 12c3249c3..701632c30 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -35,6 +35,7 @@ from yunohost.utils.i18n import _value_for_locale logger = getActionLogger("yunohost.form") +Context = dict[str, Any] # ╭───────────────────────────────────────────────────────╮ # │ ┌─╴╷ ╷╭─┐╷ │ @@ -200,16 +201,14 @@ class BaseOption: def __init__( self, question: Dict[str, Any], - context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}, ): self.name = question["name"] - self.context = context self.hooks = hooks self.type = question.get("type", "string") self.default = question.get("default", None) self.optional = question.get("optional", False) - self.visible = question.get("visible", None) + self.visible = question.get("visible", True) self.readonly = question.get("readonly", False) # Don't restrict choices if there's none specified self.choices = question.get("choices", None) @@ -241,75 +240,11 @@ class BaseOption: value = value.strip() return value - def ask_if_needed(self): - if self.visible and not evaluate_simple_js_expression( - self.visible, context=self.context - ): - # FIXME There could be several use case if the question is not displayed: - # - we doesn't want to give a specific value - # - we want to keep the previous value - # - we want the default value - self.value = self.values[self.name] = None - return self.values + def is_visible(self, context: Context) -> bool: + if isinstance(self.visible, bool): + return self.visible - for i in range(5): - # Display question if no value filled or if it's a readonly message - if Moulinette.interface.type == "cli" and os.isatty(1): - text_for_user_input_in_cli = self._format_text_for_user_input_in_cli() - if self.readonly: - Moulinette.display(text_for_user_input_in_cli) - self.value = self.values[self.name] = self.current_value - return self.values - elif self.value is None: - self._prompt(text_for_user_input_in_cli) - - # Apply default value - class_default = getattr(self, "default_value", None) - if self.value in [None, ""] and ( - self.default is not None or class_default is not None - ): - self.value = class_default if self.default is None else self.default - - try: - # Normalize and validate - self.value = self.normalize(self.value, self) - self._value_pre_validator() - except YunohostValidationError as e: - # If in interactive cli, re-ask the current question - if i < 4 and Moulinette.interface.type == "cli" and os.isatty(1): - logger.error(str(e)) - self.value = None - continue - - # Otherwise raise the ValidationError - raise - - break - - self.value = self.values[self.name] = self._value_post_validator() - - # Search for post actions in hooks - post_hook = f"post_ask__{self.name}" - if post_hook in self.hooks: - self.values.update(self.hooks[post_hook](self)) - - return self.values - - def _prompt(self, text): - prefill = "" - if self.current_value is not None: - prefill = self.humanize(self.current_value, self) - elif self.default is not None: - prefill = self.humanize(self.default, self) - self.value = Moulinette.prompt( - message=text, - is_password=self.hide_user_input_in_prompt, - confirm=False, - prefill=prefill, - is_multiline=(self.type == "text"), - autocomplete=self.choices or [], - help=_value_for_locale(self.help), - ) + return evaluate_simple_js_expression(self.visible, context=context) def _format_text_for_user_input_in_cli(self): text_for_user_input_in_cli = _value_for_locale(self.ask) @@ -396,9 +331,9 @@ class DisplayTextOption(BaseOption): argument_type = "display_text" def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + self, question, hooks: Dict[str, Callable] = {} ): - super().__init__(question, context, hooks) + super().__init__(question, hooks) self.optional = True self.readonly = True @@ -424,13 +359,19 @@ class DisplayTextOption(BaseOption): class ButtonOption(BaseOption): argument_type = "button" - enabled = None + enabled = True def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + self, question, hooks: Dict[str, Callable] = {} ): - super().__init__(question, context, hooks) - self.enabled = question.get("enabled", None) + super().__init__(question, hooks) + self.enabled = question.get("enabled", True) + + def is_enabled(self, context: Context) -> bool: + if isinstance(self.enabled, bool): + return self.enabled + + return evaluate_simple_js_expression(self.enabled, context=context) # ╭───────────────────────────────────────────────────────╮ @@ -452,10 +393,8 @@ class PasswordOption(BaseOption): default_value = "" forbidden_chars = "{}" - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) self.redact = True if self.default is not None: raise YunohostValidationError( @@ -491,10 +430,8 @@ class NumberOption(BaseOption): argument_type = "number" default_value = None - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) self.min = question.get("min", None) self.max = question.get("max", None) self.step = question.get("step", None) @@ -549,10 +486,8 @@ class BooleanOption(BaseOption): yes_answers = ["1", "yes", "y", "true", "t", "on"] no_answers = ["0", "no", "n", "false", "f", "off"] - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) self.yes = question.get("yes", 1) self.no = question.get("no", 0) if self.default is None: @@ -716,10 +651,8 @@ class FileOption(BaseOption): argument_type = "file" upload_dirs: List[str] = [] - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) self.accept = question.get("accept", "") @classmethod @@ -830,15 +763,16 @@ class TagsOption(BaseOption): return super()._value_post_validator() +# ─ ENTITIES ────────────────────────────────────────────── + + class DomainOption(BaseOption): argument_type = "domain" - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): + def __init__(self, question, hooks: Dict[str, Callable] = {}): from yunohost.domain import domain_list, _get_maindomain - super().__init__(question, context, hooks) + super().__init__(question, hooks) if self.default is None: self.default = _get_maindomain() @@ -864,12 +798,10 @@ class DomainOption(BaseOption): class AppOption(BaseOption): argument_type = "app" - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): + def __init__(self, question, hooks: Dict[str, Callable] = {}): from yunohost.app import app_list - super().__init__(question, context, hooks) + super().__init__(question, hooks) apps = app_list(full=True)["apps"] @@ -891,13 +823,11 @@ class AppOption(BaseOption): class UserOption(BaseOption): argument_type = "user" - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): + def __init__(self, question, hooks: Dict[str, Callable] = {}): from yunohost.user import user_list, user_info from yunohost.domain import _get_maindomain - super().__init__(question, context, hooks) + super().__init__(question, hooks) self.choices = { username: f"{infos['fullname']} ({infos['mail']})" @@ -924,12 +854,10 @@ class UserOption(BaseOption): class GroupOption(BaseOption): argument_type = "group" - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): + def __init__(self, question, hooks: Dict[str, Callable] = {}): from yunohost.user import user_group_list - super().__init__(question, context) + super().__init__(question) self.choices = list( user_group_list(short=True, include_primary_groups=False)["groups"] @@ -981,12 +909,111 @@ OPTIONS = { # ╰───────────────────────────────────────────────────────╯ +def prompt_or_validate_form( + raw_options: dict[str, Any], + prefilled_answers: dict[str, Any] = {}, + context: Context = {}, + hooks: dict[str, Callable[[], None]] = {}, +) -> list[BaseOption]: + options = [] + answers = {**prefilled_answers} + + for name, raw_option in raw_options.items(): + raw_option["name"] = name + raw_option["value"] = answers.get(name) + question_class = OPTIONS[raw_option.get("type", "string")] + option = question_class(raw_option, hooks=hooks) + + interactive = Moulinette.interface.type == "cli" and os.isatty(1) + + if isinstance(option, ButtonOption): + if option.is_enabled(context): + continue + else: + raise YunohostValidationError( + "config_action_disabled", + action=option.name, + help=_value_for_locale(option.help), + ) + + if option.is_visible(context): + for i in range(5): + # Display question if no value filled or if it's a readonly message + if interactive: + text_for_user_input_in_cli = ( + option._format_text_for_user_input_in_cli() + ) + if option.readonly: + Moulinette.display(text_for_user_input_in_cli) + option.value = option.current_value + break + elif option.value is None: + prefill = "" + if option.current_value is not None: + prefill = option.humanize(option.current_value, option) + elif option.default is not None: + prefill = option.humanize(option.default, option) + option.value = Moulinette.prompt( + message=text_for_user_input_in_cli, + is_password=option.hide_user_input_in_prompt, + confirm=False, + prefill=prefill, + is_multiline=(option.type == "text"), + autocomplete=option.choices or [], + help=_value_for_locale(option.help), + ) + + # Apply default value + class_default = getattr(option, "default_value", None) + if option.value in [None, ""] and ( + option.default is not None or class_default is not None + ): + option.value = ( + class_default if option.default is None else option.default + ) + + try: + # Normalize and validate + option.value = option.normalize(option.value, option) + option._value_pre_validator() + except YunohostValidationError as e: + # If in interactive cli, re-ask the current question + if i < 4 and interactive: + logger.error(str(e)) + option.value = None + continue + + # Otherwise raise the ValidationError + raise + + break + + option.value = option.values[option.name] = option._value_post_validator() + + # Search for post actions in hooks + post_hook = f"post_ask__{option.name}" + if post_hook in option.hooks: + option.values.update(option.hooks[post_hook](option)) + else: + # FIXME There could be several use case if the question is not displayed: + # - we doesn't want to give a specific value + # - we want to keep the previous value + # - we want the default value + option.value = option.values[option.name] = None + + answers.update(option.values) + context.update(option.values) + options.append(option) + + return options + + def ask_questions_and_parse_answers( - raw_questions: Dict, + raw_options: dict[str, Any], prefilled_answers: Union[str, Mapping[str, Any]] = {}, current_values: Mapping[str, Any] = {}, hooks: Dict[str, Callable[[], None]] = {}, -) -> List[BaseOption]: +) -> list[BaseOption]: """Parse arguments store in either manifest.json or actions.json or from a config panel against the user answers when they are present. @@ -1013,31 +1040,10 @@ def ask_questions_and_parse_answers( answers = {} context = {**current_values, **answers} - out = [] - for name, raw_question in raw_questions.items(): - raw_question["name"] = name - question_class = OPTIONS[raw_question.get("type", "string")] - raw_question["value"] = answers.get(name) - question = question_class(raw_question, context=context, hooks=hooks) - if question.type == "button": - if question.enabled is None or evaluate_simple_js_expression( # type: ignore - question.enabled, context=context # type: ignore - ): # type: ignore - continue - else: - raise YunohostValidationError( - "config_action_disabled", - action=question.name, - help=_value_for_locale(question.help), - ) - - new_values = question.ask_if_needed() - answers.update(new_values) - context.update(new_values) - out.append(question) - - return out + return prompt_or_validate_form( + raw_options, prefilled_answers=answers, context=context, hooks=hooks + ) def hydrate_questions_with_choices(raw_questions: List) -> List: From 4261317e49cc2dff36028077224006469ad8de4b Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 12 Apr 2023 20:48:56 +0200 Subject: [PATCH 58/93] form: separate BaseOption into BaseReadonlyOption + BaseInputOption --- src/tests/test_questions.py | 7 +- src/utils/form.py | 191 ++++++++++++++++++++---------------- 2 files changed, 109 insertions(+), 89 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 7ada38a1c..706645f9b 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -17,6 +17,8 @@ from yunohost import app, domain, user from yunohost.utils.form import ( OPTIONS, ask_questions_and_parse_answers, + BaseInputOption, + BaseReadonlyOption, DisplayTextOption, PasswordOption, DomainOption, @@ -377,8 +379,7 @@ def _fill_or_prompt_one_option(raw_option, intake): answers = {id_: intake} if intake is not None else {} option = ask_questions_and_parse_answers(options, answers)[0] - - return (option, option.value) + return (option, option.value if isinstance(option, BaseInputOption) else None) def _test_value_is_expected_output(value, expected_output): @@ -438,7 +439,7 @@ class BaseTest: id_ = raw_option["id"] option, value = _fill_or_prompt_one_option(raw_option, None) - is_special_readonly_option = isinstance(option, DisplayTextOption) + is_special_readonly_option = isinstance(option, BaseReadonlyOption) assert isinstance(option, OPTIONS[raw_option["type"]]) assert option.type == raw_option["type"] diff --git a/src/utils/form.py b/src/utils/form.py index 701632c30..0da3f892d 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -195,9 +195,6 @@ def evaluate_simple_js_expression(expr, context={}): class BaseOption: - hide_user_input_in_prompt = False - pattern: Optional[Dict] = None - def __init__( self, question: Dict[str, Any], @@ -206,16 +203,101 @@ class BaseOption: self.name = question["name"] self.hooks = hooks self.type = question.get("type", "string") - self.default = question.get("default", None) - self.optional = question.get("optional", False) self.visible = question.get("visible", True) self.readonly = question.get("readonly", False) - # Don't restrict choices if there's none specified - self.choices = question.get("choices", None) - self.pattern = question.get("pattern", self.pattern) self.ask = question.get("ask", self.name) if not isinstance(self.ask, dict): self.ask = {"en": self.ask} + + def is_visible(self, context: Context) -> bool: + if isinstance(self.visible, bool): + return self.visible + + return evaluate_simple_js_expression(self.visible, context=context) + + def _format_text_for_user_input_in_cli(self) -> str: + return _value_for_locale(self.ask) + + +# ╭───────────────────────────────────────────────────────╮ +# │ DISPLAY OPTIONS │ +# ╰───────────────────────────────────────────────────────╯ + + +class BaseReadonlyOption(BaseOption): + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) + self.readonly = True + + +class DisplayTextOption(BaseReadonlyOption): + argument_type = "display_text" + + +class MarkdownOption(BaseReadonlyOption): + argument_type = "markdown" + + +class AlertOption(BaseReadonlyOption): + argument_type = "alert" + + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) + self.style = question.get("style", "info") + + def _format_text_for_user_input_in_cli(self) -> str: + text = _value_for_locale(self.ask) + + if self.style in ["success", "info", "warning", "danger"]: + color = { + "success": "green", + "info": "cyan", + "warning": "yellow", + "danger": "red", + } + prompt = m18n.g(self.style) if self.style != "danger" else m18n.n("danger") + return colorize(prompt, color[self.style]) + f" {text}" + else: + return text + + +class ButtonOption(BaseReadonlyOption): + argument_type = "button" + enabled = True + + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) + self.help = question.get("help") + self.style = question.get("style", "success") + self.enabled = question.get("enabled", True) + + def is_enabled(self, context: Context) -> bool: + if isinstance(self.enabled, bool): + return self.enabled + + return evaluate_simple_js_expression(self.enabled, context=context) + + +# ╭───────────────────────────────────────────────────────╮ +# │ INPUT OPTIONS │ +# ╰───────────────────────────────────────────────────────╯ + + +class BaseInputOption(BaseOption): + hide_user_input_in_prompt = False + pattern: Optional[Dict] = None + + def __init__( + self, + question: Dict[str, Any], + hooks: Dict[str, Callable] = {}, + ): + super().__init__(question, hooks) + self.default = question.get("default", None) + self.optional = question.get("optional", False) + # Don't restrict choices if there's none specified + self.choices = question.get("choices", None) + self.pattern = question.get("pattern", self.pattern) self.help = question.get("help") self.redact = question.get("redact", False) self.filter = question.get("filter", None) @@ -240,14 +322,8 @@ class BaseOption: value = value.strip() return value - def is_visible(self, context: Context) -> bool: - if isinstance(self.visible, bool): - return self.visible - - return evaluate_simple_js_expression(self.visible, context=context) - - def _format_text_for_user_input_in_cli(self): - text_for_user_input_in_cli = _value_for_locale(self.ask) + def _format_text_for_user_input_in_cli(self) -> str: + text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() if self.readonly: text_for_user_input_in_cli = colorize(text_for_user_input_in_cli, "purple") @@ -322,72 +398,15 @@ class BaseOption: return self.value -# ╭───────────────────────────────────────────────────────╮ -# │ DISPLAY OPTIONS │ -# ╰───────────────────────────────────────────────────────╯ - - -class DisplayTextOption(BaseOption): - argument_type = "display_text" - - def __init__( - self, question, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, hooks) - - self.optional = True - self.readonly = True - self.style = question.get( - "style", "info" if question["type"] == "alert" else "" - ) - - def _format_text_for_user_input_in_cli(self): - text = _value_for_locale(self.ask) - - if self.style in ["success", "info", "warning", "danger"]: - color = { - "success": "green", - "info": "cyan", - "warning": "yellow", - "danger": "red", - } - prompt = m18n.g(self.style) if self.style != "danger" else m18n.n("danger") - return colorize(prompt, color[self.style]) + f" {text}" - else: - return text - - -class ButtonOption(BaseOption): - argument_type = "button" - enabled = True - - def __init__( - self, question, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, hooks) - self.enabled = question.get("enabled", True) - - def is_enabled(self, context: Context) -> bool: - if isinstance(self.enabled, bool): - return self.enabled - - return evaluate_simple_js_expression(self.enabled, context=context) - - -# ╭───────────────────────────────────────────────────────╮ -# │ INPUT OPTIONS │ -# ╰───────────────────────────────────────────────────────╯ - - # ─ STRINGS ─────────────────────────────────────────────── -class StringOption(BaseOption): +class StringOption(BaseInputOption): argument_type = "string" default_value = "" -class PasswordOption(BaseOption): +class PasswordOption(BaseInputOption): hide_user_input_in_prompt = True argument_type = "password" default_value = "" @@ -426,7 +445,7 @@ class ColorOption(StringOption): # ─ NUMERIC ─────────────────────────────────────────────── -class NumberOption(BaseOption): +class NumberOption(BaseInputOption): argument_type = "number" default_value = None @@ -480,7 +499,7 @@ class NumberOption(BaseOption): # ─ BOOLEAN ─────────────────────────────────────────────── -class BooleanOption(BaseOption): +class BooleanOption(BaseInputOption): argument_type = "boolean" default_value = 0 yes_answers = ["1", "yes", "y", "true", "t", "on"] @@ -606,7 +625,7 @@ class EmailOption(StringOption): } -class WebPathOption(BaseOption): +class WebPathOption(BaseInputOption): argument_type = "path" default_value = "" @@ -647,7 +666,7 @@ class URLOption(StringOption): # ─ FILE ────────────────────────────────────────────────── -class FileOption(BaseOption): +class FileOption(BaseInputOption): argument_type = "file" upload_dirs: List[str] = [] @@ -713,7 +732,7 @@ class FileOption(BaseOption): # ─ CHOICES ─────────────────────────────────────────────── -class TagsOption(BaseOption): +class TagsOption(BaseInputOption): argument_type = "tags" default_value = "" @@ -766,7 +785,7 @@ class TagsOption(BaseOption): # ─ ENTITIES ────────────────────────────────────────────── -class DomainOption(BaseOption): +class DomainOption(BaseInputOption): argument_type = "domain" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -795,7 +814,7 @@ class DomainOption(BaseOption): return value -class AppOption(BaseOption): +class AppOption(BaseInputOption): argument_type = "app" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -820,7 +839,7 @@ class AppOption(BaseOption): self.choices.update({app["id"]: _app_display(app) for app in apps}) -class UserOption(BaseOption): +class UserOption(BaseInputOption): argument_type = "user" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -851,7 +870,7 @@ class UserOption(BaseOption): break -class GroupOption(BaseOption): +class GroupOption(BaseInputOption): argument_type = "group" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -877,8 +896,8 @@ class GroupOption(BaseOption): OPTIONS = { "display_text": DisplayTextOption, - "markdown": DisplayTextOption, - "alert": DisplayTextOption, + "markdown": MarkdownOption, + "alert": AlertOption, "button": ButtonOption, "string": StringOption, "text": StringOption, From 9e8e0497dd286ee4a0992d005bc893ea57e6ef38 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 12 Apr 2023 21:01:25 +0200 Subject: [PATCH 59/93] form: fix readonly prompting + + choices + tests --- src/tests/test_questions.py | 68 ++++++------------------ src/utils/configpanel.py | 10 ---- src/utils/form.py | 100 ++++++++++++++++++++++-------------- 3 files changed, 78 insertions(+), 100 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 706645f9b..9e7be5db4 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -662,9 +662,7 @@ class TestString(BaseTest): (" ##value \n \tvalue\n ", "##value \n \tvalue"), ], reason=r"should fail or without `\n`?"), # readonly - *xfail(scenarios=[ - ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), - ], reason="Should not be overwritten"), + ("overwrite", "expected value", {"readonly": True, "current_value": "expected value"}), ] # fmt: on @@ -701,9 +699,7 @@ class TestText(BaseTest): (r" ##value \n \tvalue\n ", r"##value \n \tvalue\n"), ], reason="Should not be stripped"), # readonly - *xfail(scenarios=[ - ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), - ], reason="Should not be overwritten"), + ("overwrite", "expected value", {"readonly": True, "current_value": "expected value"}), ] # fmt: on @@ -737,9 +733,7 @@ class TestPassword(BaseTest): ("secret", FAIL), *[("supersecret" + char, FAIL) for char in PasswordOption.forbidden_chars], # FIXME maybe add ` \n` to the list? # readonly - *xpass(scenarios=[ - ("s3cr3t!!", "s3cr3t!!", {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + ("s3cr3t!!", YunohostError, {"readonly": True, "current_value": "isforbidden"}), # readonly is forbidden ] # fmt: on @@ -780,9 +774,7 @@ class TestColor(BaseTest): ("yellow", "#ffff00"), ], reason="Should work with pydantic"), # readonly - *xfail(scenarios=[ - ("#ffff00", "#fe100", {"readonly": True, "default": "#fe100"}), - ], reason="Should not be overwritten"), + ("#ffff00", "#fe100", {"readonly": True, "current_value": "#fe100"}), ] # fmt: on @@ -824,9 +816,7 @@ class TestNumber(BaseTest): (-10, -10, {"default": 10}), (-10, -10, {"default": 10, "optional": True}), # readonly - *xfail(scenarios=[ - (1337, 10000, {"readonly": True, "default": 10000}), - ], reason="Should not be overwritten"), + (1337, 10000, {"readonly": True, "current_value": 10000}), ] # fmt: on # FIXME should `step` be some kind of "multiple of"? @@ -891,9 +881,7 @@ class TestBoolean(BaseTest): "scenarios": all_fails("", "y", "n", error=AssertionError), }, # readonly - *xfail(scenarios=[ - (1, 0, {"readonly": True, "default": 0}), - ], reason="Should not be overwritten"), + (1, 0, {"readonly": True, "current_value": 0}), ] @@ -931,9 +919,7 @@ class TestDate(BaseTest): ("12-01-10", FAIL), ("2022-02-29", FAIL), # readonly - *xfail(scenarios=[ - ("2070-12-31", "2024-02-29", {"readonly": True, "default": "2024-02-29"}), - ], reason="Should not be overwritten"), + ("2070-12-31", "2024-02-29", {"readonly": True, "current_value": "2024-02-29"}), ] # fmt: on @@ -966,9 +952,7 @@ class TestTime(BaseTest): ("23:1", FAIL), ("23:005", FAIL), # readonly - *xfail(scenarios=[ - ("00:00", "08:00", {"readonly": True, "default": "08:00"}), - ], reason="Should not be overwritten"), + ("00:00", "08:00", {"readonly": True, "current_value": "08:00"}), ] # fmt: on @@ -992,9 +976,7 @@ class TestEmail(BaseTest): *nones(None, "", output=""), ("\n Abc@example.tld ", "Abc@example.tld"), # readonly - *xfail(scenarios=[ - ("Abc@example.tld", "admin@ynh.local", {"readonly": True, "default": "admin@ynh.local"}), - ], reason="Should not be overwritten"), + ("Abc@example.tld", "admin@ynh.local", {"readonly": True, "current_value": "admin@ynh.local"}), # Next examples are from https://github.com/JoshData/python-email-validator/blob/main/tests/test_syntax.py # valid email values @@ -1107,9 +1089,7 @@ class TestWebPath(BaseTest): ("https://example.com/folder", "/https://example.com/folder") ], reason="Should fail or scheme+domain removed"), # readonly - *xfail(scenarios=[ - ("/overwrite", "/value", {"readonly": True, "default": "/value"}), - ], reason="Should not be overwritten"), + ("/overwrite", "/value", {"readonly": True, "current_value": "/value"}), # FIXME should path have forbidden_chars? ] # fmt: on @@ -1134,9 +1114,7 @@ class TestUrl(BaseTest): *nones(None, "", output=""), ("http://some.org/folder/file.txt", "http://some.org/folder/file.txt"), # readonly - *xfail(scenarios=[ - ("https://overwrite.org", "https://example.org", {"readonly": True, "default": "https://example.org"}), - ], reason="Should not be overwritten"), + ("https://overwrite.org", "https://example.org", {"readonly": True, "current_value": "https://example.org"}), # rest is taken from https://github.com/pydantic/pydantic/blob/main/tests/test_networks.py # valid *unchanged( @@ -1426,9 +1404,7 @@ class TestSelect(BaseTest): ] }, # readonly - *xfail(scenarios=[ - ("one", "two", {"readonly": True, "choices": ["one", "two"], "default": "two"}), - ], reason="Should not be overwritten"), + ("one", "two", {"readonly": True, "choices": ["one", "two"], "current_value": "two"}), ] # fmt: on @@ -1476,9 +1452,7 @@ class TestTags(BaseTest): *all_fails(*([t] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), *all_fails(*([str(t)] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), # readonly - *xfail(scenarios=[ - ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "default": "one,two"}), - ], reason="Should not be overwritten"), + ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "current_value": "one,two"}), ] # fmt: on @@ -1526,9 +1500,7 @@ class TestDomain(BaseTest): ("doesnt_exist.pouet", FAIL, {}), ("fake.com", FAIL, {"choices": ["fake.com"]}), # readonly - *xpass(scenarios=[ - (domains1[0], domains1[0], {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + (domains1[0], YunohostError, {"readonly": True}), # readonly is forbidden ] }, { @@ -1625,9 +1597,7 @@ class TestApp(BaseTest): (installed_non_webapp["id"], installed_non_webapp["id"]), (installed_non_webapp["id"], FAIL, {"filter": "is_webapp"}), # readonly - *xpass(scenarios=[ - (installed_non_webapp["id"], installed_non_webapp["id"], {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + (installed_non_webapp["id"], YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] @@ -1744,9 +1714,7 @@ class TestUser(BaseTest): ("", regular_username, {"default": regular_username}) ], reason="Should throw 'no default allowed'"), # readonly - *xpass(scenarios=[ - (admin_username, admin_username, {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + (admin_username, YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] @@ -1821,9 +1789,7 @@ class TestGroup(BaseTest): ("", "custom_group", {"default": "custom_group"}), ], reason="Should throw 'default must be in (None, 'all_users', 'visitors', 'admins')"), # readonly - *xpass(scenarios=[ - ("admins", "admins", {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + ("admins", YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 2c56eb754..355956574 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -465,20 +465,10 @@ class ConfigPanel: "max_progression", ] forbidden_keywords += format_description["sections"] - forbidden_readonly_types = ["password", "app", "domain", "user", "file"] for _, _, option in self._iterate(): if option["id"] in forbidden_keywords: raise YunohostError("config_forbidden_keyword", keyword=option["id"]) - if ( - option.get("readonly", False) - and option.get("type", "string") in forbidden_readonly_types - ): - raise YunohostError( - "config_forbidden_readonly_type", - type=option["type"], - id=option["id"], - ) return self.config diff --git a/src/utils/form.py b/src/utils/form.py index 0da3f892d..071bbaa21 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -204,7 +204,16 @@ class BaseOption: self.hooks = hooks self.type = question.get("type", "string") self.visible = question.get("visible", True) + self.readonly = question.get("readonly", False) + if self.readonly and self.type in {"password", "app", "domain", "user", "group", "file"}: + # FIXME i18n + raise YunohostError( + "config_forbidden_readonly_type", + type=self.type, + id=self.name, + ) + self.ask = question.get("ask", self.name) if not isinstance(self.ask, dict): self.ask = {"en": self.ask} @@ -328,9 +337,10 @@ class BaseInputOption(BaseOption): if self.readonly: text_for_user_input_in_cli = colorize(text_for_user_input_in_cli, "purple") if self.choices: - return ( - text_for_user_input_in_cli + f" {self.choices[self.current_value]}" - ) + choice = self.current_value + if isinstance(self.choices, dict) and choice is not None: + choice = self.choices[choice] + return f"{text_for_user_input_in_cli} {choice}" return text_for_user_input_in_cli + f" {self.humanize(self.current_value)}" elif self.choices: # Prevent displaying a shitload of choices @@ -348,7 +358,9 @@ class BaseInputOption(BaseOption): m18n.n("other_available_options", n=remaining_choices) ] - choices_to_display = " | ".join(choices_to_display) + choices_to_display = " | ".join( + str(choice) for choice in choices_to_display + ) text_for_user_input_in_cli += f" [{choices_to_display}]" @@ -946,7 +958,7 @@ def prompt_or_validate_form( interactive = Moulinette.interface.type == "cli" and os.isatty(1) if isinstance(option, ButtonOption): - if option.is_enabled(context): + if option.is_visible(context) and option.is_enabled(context): continue else: raise YunohostValidationError( @@ -955,32 +967,49 @@ def prompt_or_validate_form( help=_value_for_locale(option.help), ) - if option.is_visible(context): + # FIXME not sure why we do not append Buttons to returned options + options.append(option) + + if not option.is_visible(context): + if isinstance(option, BaseInputOption): + # FIXME There could be several use case if the question is not displayed: + # - we doesn't want to give a specific value + # - we want to keep the previous value + # - we want the default value + option.value = context[option.name] = None + + continue + + message = option._format_text_for_user_input_in_cli() + + if option.readonly: + if interactive: + Moulinette.display(message) + + if isinstance(option, BaseInputOption): + option.value = context[option.name] = option.current_value + + continue + + if isinstance(option, BaseInputOption): for i in range(5): - # Display question if no value filled or if it's a readonly message - if interactive: - text_for_user_input_in_cli = ( - option._format_text_for_user_input_in_cli() + if interactive and option.value is None: + prefill = "" + + if option.current_value is not None: + prefill = option.humanize(option.current_value, option) + elif option.default is not None: + prefill = option.humanize(option.default, option) + + option.value = Moulinette.prompt( + message=message, + is_password=isinstance(option, PasswordOption), + confirm=False, + prefill=prefill, + is_multiline=(option.type == "text"), + autocomplete=option.choices or [], + help=_value_for_locale(option.help), ) - if option.readonly: - Moulinette.display(text_for_user_input_in_cli) - option.value = option.current_value - break - elif option.value is None: - prefill = "" - if option.current_value is not None: - prefill = option.humanize(option.current_value, option) - elif option.default is not None: - prefill = option.humanize(option.default, option) - option.value = Moulinette.prompt( - message=text_for_user_input_in_cli, - is_password=option.hide_user_input_in_prompt, - confirm=False, - prefill=prefill, - is_multiline=(option.type == "text"), - autocomplete=option.choices or [], - help=_value_for_locale(option.help), - ) # Apply default value class_default = getattr(option, "default_value", None) @@ -1013,16 +1042,9 @@ def prompt_or_validate_form( post_hook = f"post_ask__{option.name}" if post_hook in option.hooks: option.values.update(option.hooks[post_hook](option)) - else: - # FIXME There could be several use case if the question is not displayed: - # - we doesn't want to give a specific value - # - we want to keep the previous value - # - we want the default value - option.value = option.values[option.name] = None - answers.update(option.values) - context.update(option.values) - options.append(option) + answers.update(option.values) + context.update(option.values) return options @@ -1070,7 +1092,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: for raw_question in raw_questions: question = OPTIONS[raw_question.get("type", "string")](raw_question) - if question.choices: + if isinstance(question, BaseInputOption) and question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default out.append(raw_question) From 07636fe21e15ebf0fcb77aabe9a752771d1c6901 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 02:36:18 +0200 Subject: [PATCH 60/93] form: rename text_cli_* to _get_prompt_message + message --- src/utils/form.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index 071bbaa21..57d4cabcc 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -193,6 +193,8 @@ def evaluate_simple_js_expression(expr, context={}): # │ ╰─╯╵ ╵ ╶┴╴╰─╯╵╰╯╶─╯ │ # ╰───────────────────────────────────────────────────────╯ +FORBIDDEN_READONLY_TYPES = {"password", "app", "domain", "user", "group"} + class BaseOption: def __init__( @@ -206,7 +208,7 @@ class BaseOption: self.visible = question.get("visible", True) self.readonly = question.get("readonly", False) - if self.readonly and self.type in {"password", "app", "domain", "user", "group", "file"}: + if self.readonly and self.type in FORBIDDEN_READONLY_TYPES: # FIXME i18n raise YunohostError( "config_forbidden_readonly_type", @@ -224,7 +226,7 @@ class BaseOption: return evaluate_simple_js_expression(self.visible, context=context) - def _format_text_for_user_input_in_cli(self) -> str: + def _get_prompt_message(self) -> str: return _value_for_locale(self.ask) @@ -254,7 +256,7 @@ class AlertOption(BaseReadonlyOption): super().__init__(question, hooks) self.style = question.get("style", "info") - def _format_text_for_user_input_in_cli(self) -> str: + def _get_prompt_message(self) -> str: text = _value_for_locale(self.ask) if self.style in ["success", "info", "warning", "danger"]: @@ -331,17 +333,17 @@ class BaseInputOption(BaseOption): value = value.strip() return value - def _format_text_for_user_input_in_cli(self) -> str: - text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() + def _get_prompt_message(self) -> str: + message = super()._get_prompt_message() if self.readonly: - text_for_user_input_in_cli = colorize(text_for_user_input_in_cli, "purple") + message = colorize(message, "purple") if self.choices: choice = self.current_value if isinstance(self.choices, dict) and choice is not None: choice = self.choices[choice] - return f"{text_for_user_input_in_cli} {choice}" - return text_for_user_input_in_cli + f" {self.humanize(self.current_value)}" + return f"{message} {choice}" + return message + f" {self.humanize(self.current_value)}" elif self.choices: # Prevent displaying a shitload of choices # (e.g. 100+ available users when choosing an app admin...) @@ -362,9 +364,9 @@ class BaseInputOption(BaseOption): str(choice) for choice in choices_to_display ) - text_for_user_input_in_cli += f" [{choices_to_display}]" + message += f" [{choices_to_display}]" - return text_for_user_input_in_cli + return message def _value_pre_validator(self): if self.value in [None, ""] and not self.optional: @@ -590,13 +592,13 @@ class BooleanOption(BaseInputOption): def get(self, key, default=None): return getattr(self, key, default) - def _format_text_for_user_input_in_cli(self): - text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() + def _get_prompt_message(self): + message = super()._get_prompt_message() if not self.readonly: - text_for_user_input_in_cli += " [yes | no]" + message += " [yes | no]" - return text_for_user_input_in_cli + return message # ─ TIME ────────────────────────────────────────────────── @@ -980,7 +982,7 @@ def prompt_or_validate_form( continue - message = option._format_text_for_user_input_in_cli() + message = option._get_prompt_message() if option.readonly: if interactive: From f0f89d8f2a4fb9b932b2f32d8cf66754fde3a077 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 13:58:24 +0200 Subject: [PATCH 61/93] form: restrict choices to select, tags, domain, app, user + group --- src/tests/test_questions.py | 85 +++---------------------- src/utils/form.py | 121 +++++++++++++++++++++++------------- 2 files changed, 84 insertions(+), 122 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 9e7be5db4..3e7927dce 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -17,9 +17,9 @@ from yunohost import app, domain, user from yunohost.utils.form import ( OPTIONS, ask_questions_and_parse_answers, + BaseChoicesOption, BaseInputOption, BaseReadonlyOption, - DisplayTextOption, PasswordOption, DomainOption, WebPathOption, @@ -490,14 +490,12 @@ class BaseTest: option, value = _fill_or_prompt_one_option(raw_option, None) expected_message = option.ask["en"] + choices = [] - if option.choices: - choices = ( - option.choices - if isinstance(option.choices, list) - else option.choices.keys() - ) - expected_message += f" [{' | '.join(choices)}]" + if isinstance(option, BaseChoicesOption): + choices = option.choices + if choices: + expected_message += f" [{' | '.join(choices)}]" if option.type == "boolean": expected_message += " [yes | no]" @@ -507,7 +505,7 @@ class BaseTest: confirm=False, # FIXME no confirm? prefill=prefill, is_multiline=option.type == "text", - autocomplete=option.choices or [], + autocomplete=choices, help=option.help["en"], ) @@ -1972,75 +1970,6 @@ def test_question_string_input_test_ask_with_example(): assert example_text in prompt.call_args[1]["message"] -def test_question_string_with_choice(): - questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} - answers = {"some_string": "fr"} - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "fr" - - -def test_question_string_with_choice_prompt(): - questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} - answers = {"some_string": "fr"} - with patch.object(Moulinette, "prompt", return_value="fr"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "fr" - - -def test_question_string_with_choice_bad(): - questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} - answers = {"some_string": "bad"} - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_string_with_choice_ask(): - ask_text = "some question" - choices = ["fr", "en", "es", "it", "ru"] - questions = { - "some_string": { - "ask": ask_text, - "choices": choices, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="ru") as prompt, patch.object( - os, "isatty", return_value=True - ): - ask_questions_and_parse_answers(questions, answers) - assert ask_text in prompt.call_args[1]["message"] - - for choice in choices: - assert choice in prompt.call_args[1]["message"] - - -def test_question_string_with_choice_default(): - questions = { - "some_string": { - "type": "string", - "choices": ["fr", "en"], - "default": "en", - } - } - answers = {} - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "en" - - @pytest.mark.skip # we should do something with this example def test_question_password_input_test_ask_with_example(): ask_text = "some question" diff --git a/src/utils/form.py b/src/utils/form.py index 57d4cabcc..02f51b6c4 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -306,8 +306,6 @@ class BaseInputOption(BaseOption): super().__init__(question, hooks) self.default = question.get("default", None) self.optional = question.get("optional", False) - # Don't restrict choices if there's none specified - self.choices = question.get("choices", None) self.pattern = question.get("pattern", self.pattern) self.help = question.get("help") self.redact = question.get("redact", False) @@ -338,33 +336,7 @@ class BaseInputOption(BaseOption): if self.readonly: message = colorize(message, "purple") - if self.choices: - choice = self.current_value - if isinstance(self.choices, dict) and choice is not None: - choice = self.choices[choice] - return f"{message} {choice}" - return message + f" {self.humanize(self.current_value)}" - elif self.choices: - # Prevent displaying a shitload of choices - # (e.g. 100+ available users when choosing an app admin...) - choices = ( - list(self.choices.keys()) - if isinstance(self.choices, dict) - else self.choices - ) - choices_to_display = choices[:20] - remaining_choices = len(choices[20:]) - - if remaining_choices > 0: - choices_to_display += [ - m18n.n("other_available_options", n=remaining_choices) - ] - - choices_to_display = " | ".join( - str(choice) for choice in choices_to_display - ) - - message += f" [{choices_to_display}]" + return f"{message} {self.humanize(self.current_value)}" return message @@ -374,13 +346,6 @@ class BaseInputOption(BaseOption): # we have an answer, do some post checks if self.value not in [None, ""]: - if self.choices and self.value not in self.choices: - raise YunohostValidationError( - "app_argument_choice_invalid", - name=self.name, - value=self.value, - choices=", ".join(str(choice) for choice in self.choices), - ) if self.pattern and not re.match(self.pattern["regexp"], str(self.value)): raise YunohostValidationError( self.pattern["error"], @@ -746,7 +711,72 @@ class FileOption(BaseInputOption): # ─ CHOICES ─────────────────────────────────────────────── -class TagsOption(BaseInputOption): +class BaseChoicesOption(BaseInputOption): + def __init__( + self, + question: Dict[str, Any], + hooks: Dict[str, Callable] = {}, + ): + super().__init__(question, hooks) + # Don't restrict choices if there's none specified + self.choices = question.get("choices", None) + + def _get_prompt_message(self) -> str: + message = super()._get_prompt_message() + + if self.readonly: + message = message + choice = self.current_value + + if isinstance(self.choices, dict) and choice is not None: + choice = self.choices[choice] + + return f"{colorize(message, 'purple')} {choice}" + + if self.choices: + # Prevent displaying a shitload of choices + # (e.g. 100+ available users when choosing an app admin...) + choices = ( + list(self.choices.keys()) + if isinstance(self.choices, dict) + else self.choices + ) + choices_to_display = choices[:20] + remaining_choices = len(choices[20:]) + + if remaining_choices > 0: + choices_to_display += [ + m18n.n("other_available_options", n=remaining_choices) + ] + + choices_to_display = " | ".join( + str(choice) for choice in choices_to_display + ) + + return f"{message} [{choices_to_display}]" + + return message + + def _value_pre_validator(self): + super()._value_pre_validator() + + # we have an answer, do some post checks + if self.value not in [None, ""]: + if self.choices and self.value not in self.choices: + raise YunohostValidationError( + "app_argument_choice_invalid", + name=self.name, + value=self.value, + choices=", ".join(str(choice) for choice in self.choices), + ) + + +class SelectOption(BaseChoicesOption): + argument_type = "select" + default_value = "" + + +class TagsOption(BaseChoicesOption): argument_type = "tags" default_value = "" @@ -799,7 +829,7 @@ class TagsOption(BaseInputOption): # ─ ENTITIES ────────────────────────────────────────────── -class DomainOption(BaseInputOption): +class DomainOption(BaseChoicesOption): argument_type = "domain" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -828,7 +858,7 @@ class DomainOption(BaseInputOption): return value -class AppOption(BaseInputOption): +class AppOption(BaseChoicesOption): argument_type = "app" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -853,7 +883,7 @@ class AppOption(BaseInputOption): self.choices.update({app["id"]: _app_display(app) for app in apps}) -class UserOption(BaseInputOption): +class UserOption(BaseChoicesOption): argument_type = "user" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -884,7 +914,7 @@ class UserOption(BaseInputOption): break -class GroupOption(BaseInputOption): +class GroupOption(BaseChoicesOption): argument_type = "group" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -926,7 +956,7 @@ OPTIONS = { "path": WebPathOption, "url": URLOption, "file": FileOption, - "select": StringOption, + "select": SelectOption, "tags": TagsOption, "domain": DomainOption, "app": AppOption, @@ -997,6 +1027,9 @@ def prompt_or_validate_form( for i in range(5): if interactive and option.value is None: prefill = "" + choices = ( + option.choices if isinstance(option, BaseChoicesOption) else [] + ) if option.current_value is not None: prefill = option.humanize(option.current_value, option) @@ -1009,7 +1042,7 @@ def prompt_or_validate_form( confirm=False, prefill=prefill, is_multiline=(option.type == "text"), - autocomplete=option.choices or [], + autocomplete=choices, help=_value_for_locale(option.help), ) @@ -1094,7 +1127,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: for raw_question in raw_questions: question = OPTIONS[raw_question.get("type", "string")](raw_question) - if isinstance(question, BaseInputOption) and question.choices: + if isinstance(question, BaseChoicesOption) and question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default out.append(raw_question) From c439c47d67b5fe7e0dece709212d4d6d3b18549c Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 14:00:04 +0200 Subject: [PATCH 62/93] form: restrict filter to AppOption --- src/utils/form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/form.py b/src/utils/form.py index 02f51b6c4..edae7717b 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -309,7 +309,6 @@ class BaseInputOption(BaseOption): self.pattern = question.get("pattern", self.pattern) self.help = question.get("help") self.redact = question.get("redact", False) - self.filter = question.get("filter", None) # .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 @@ -865,6 +864,7 @@ class AppOption(BaseChoicesOption): from yunohost.app import app_list super().__init__(question, hooks) + self.filter = question.get("filter", None) apps = app_list(full=True)["apps"] From fe2761da4ab2289d5b622ab061d323ad603f9f2a Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 14:05:03 +0200 Subject: [PATCH 63/93] configpanel: fix choices --- src/utils/configpanel.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 355956574..2914ae11f 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -30,6 +30,8 @@ from moulinette.utils.log import getActionLogger from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.form import ( OPTIONS, + BaseChoicesOption, + BaseInputOption, BaseOption, FileOption, ask_questions_and_parse_answers, @@ -148,9 +150,11 @@ class ConfigPanel: option["ask"] = ask question_class = OPTIONS[option.get("type", "string")] # FIXME : maybe other properties should be taken from the question, not just choices ?. - option["choices"] = question_class(option).choices - option["default"] = question_class(option).default - option["pattern"] = question_class(option).pattern + if issubclass(question_class, BaseChoicesOption): + option["choices"] = question_class(option).choices + if issubclass(question_class, BaseInputOption): + option["default"] = question_class(option).default + option["pattern"] = question_class(option).pattern else: result[key] = {"ask": ask} if "current_value" in option: From 1c7d427be0fea47d64a3abf67cf7a86b631c4d9a Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 14:28:00 +0200 Subject: [PATCH 64/93] form: remove hooks from Option's attrs --- src/utils/form.py | 68 ++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index edae7717b..b455fe812 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -200,10 +200,8 @@ class BaseOption: def __init__( self, question: Dict[str, Any], - hooks: Dict[str, Callable] = {}, ): self.name = question["name"] - self.hooks = hooks self.type = question.get("type", "string") self.visible = question.get("visible", True) @@ -236,8 +234,8 @@ class BaseOption: class BaseReadonlyOption(BaseOption): - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.readonly = True @@ -252,8 +250,8 @@ class MarkdownOption(BaseReadonlyOption): class AlertOption(BaseReadonlyOption): argument_type = "alert" - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.style = question.get("style", "info") def _get_prompt_message(self) -> str: @@ -276,8 +274,8 @@ class ButtonOption(BaseReadonlyOption): argument_type = "button" enabled = True - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.help = question.get("help") self.style = question.get("style", "success") self.enabled = question.get("enabled", True) @@ -298,12 +296,8 @@ class BaseInputOption(BaseOption): hide_user_input_in_prompt = False pattern: Optional[Dict] = None - def __init__( - self, - question: Dict[str, Any], - hooks: Dict[str, Callable] = {}, - ): - super().__init__(question, hooks) + def __init__(self, question: Dict[str, Any]): + super().__init__(question) self.default = question.get("default", None) self.optional = question.get("optional", False) self.pattern = question.get("pattern", self.pattern) @@ -390,8 +384,8 @@ class PasswordOption(BaseInputOption): default_value = "" forbidden_chars = "{}" - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.redact = True if self.default is not None: raise YunohostValidationError( @@ -427,8 +421,8 @@ class NumberOption(BaseInputOption): argument_type = "number" default_value = None - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.min = question.get("min", None) self.max = question.get("max", None) self.step = question.get("step", None) @@ -483,8 +477,8 @@ class BooleanOption(BaseInputOption): yes_answers = ["1", "yes", "y", "true", "t", "on"] no_answers = ["0", "no", "n", "false", "f", "off"] - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.yes = question.get("yes", 1) self.no = question.get("no", 0) if self.default is None: @@ -648,8 +642,8 @@ class FileOption(BaseInputOption): argument_type = "file" upload_dirs: List[str] = [] - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.accept = question.get("accept", "") @classmethod @@ -714,9 +708,8 @@ class BaseChoicesOption(BaseInputOption): def __init__( self, question: Dict[str, Any], - hooks: Dict[str, Callable] = {}, ): - super().__init__(question, hooks) + super().__init__(question) # Don't restrict choices if there's none specified self.choices = question.get("choices", None) @@ -831,10 +824,10 @@ class TagsOption(BaseChoicesOption): class DomainOption(BaseChoicesOption): argument_type = "domain" - def __init__(self, question, hooks: Dict[str, Callable] = {}): + def __init__(self, question): from yunohost.domain import domain_list, _get_maindomain - super().__init__(question, hooks) + super().__init__(question) if self.default is None: self.default = _get_maindomain() @@ -860,10 +853,10 @@ class DomainOption(BaseChoicesOption): class AppOption(BaseChoicesOption): argument_type = "app" - def __init__(self, question, hooks: Dict[str, Callable] = {}): + def __init__(self, question): from yunohost.app import app_list - super().__init__(question, hooks) + super().__init__(question) self.filter = question.get("filter", None) apps = app_list(full=True)["apps"] @@ -886,11 +879,11 @@ class AppOption(BaseChoicesOption): class UserOption(BaseChoicesOption): argument_type = "user" - def __init__(self, question, hooks: Dict[str, Callable] = {}): + def __init__(self, question): from yunohost.user import user_list, user_info from yunohost.domain import _get_maindomain - super().__init__(question, hooks) + super().__init__(question) self.choices = { username: f"{infos['fullname']} ({infos['mail']})" @@ -917,7 +910,7 @@ class UserOption(BaseChoicesOption): class GroupOption(BaseChoicesOption): argument_type = "group" - def __init__(self, question, hooks: Dict[str, Callable] = {}): + def __init__(self, question): from yunohost.user import user_group_list super().__init__(question) @@ -972,11 +965,14 @@ OPTIONS = { # ╰───────────────────────────────────────────────────────╯ +Hooks = dict[str, Callable[[BaseInputOption], Any]] + + def prompt_or_validate_form( raw_options: dict[str, Any], prefilled_answers: dict[str, Any] = {}, context: Context = {}, - hooks: dict[str, Callable[[], None]] = {}, + hooks: Hooks = {}, ) -> list[BaseOption]: options = [] answers = {**prefilled_answers} @@ -985,7 +981,7 @@ def prompt_or_validate_form( raw_option["name"] = name raw_option["value"] = answers.get(name) question_class = OPTIONS[raw_option.get("type", "string")] - option = question_class(raw_option, hooks=hooks) + option = question_class(raw_option) interactive = Moulinette.interface.type == "cli" and os.isatty(1) @@ -1075,8 +1071,8 @@ def prompt_or_validate_form( # Search for post actions in hooks post_hook = f"post_ask__{option.name}" - if post_hook in option.hooks: - option.values.update(option.hooks[post_hook](option)) + if post_hook in hooks: + option.values.update(hooks[post_hook](option)) answers.update(option.values) context.update(option.values) @@ -1088,7 +1084,7 @@ def ask_questions_and_parse_answers( raw_options: dict[str, Any], prefilled_answers: Union[str, Mapping[str, Any]] = {}, current_values: Mapping[str, Any] = {}, - hooks: Dict[str, Callable[[], None]] = {}, + hooks: Hooks = {}, ) -> list[BaseOption]: """Parse arguments store in either manifest.json or actions.json or from a config panel against the user answers when they are present. From e87f8ef93a417b4287c6bed3a88210b0be196569 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 15:10:23 +0200 Subject: [PATCH 65/93] form: use Enum for Option's type --- src/utils/configpanel.py | 25 ++++--- src/utils/form.py | 154 ++++++++++++++++++++++++++------------- 2 files changed, 118 insertions(+), 61 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 2914ae11f..f5d802356 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -34,6 +34,7 @@ from yunohost.utils.form import ( BaseInputOption, BaseOption, FileOption, + OptionType, ask_questions_and_parse_answers, evaluate_simple_js_expression, ) @@ -148,7 +149,7 @@ class ConfigPanel: if mode == "full": option["ask"] = ask - question_class = OPTIONS[option.get("type", "string")] + question_class = OPTIONS[option.get("type", OptionType.string)] # FIXME : maybe other properties should be taken from the question, not just choices ?. if issubclass(question_class, BaseChoicesOption): option["choices"] = question_class(option).choices @@ -158,7 +159,7 @@ class ConfigPanel: else: result[key] = {"ask": ask} if "current_value" in option: - question_class = OPTIONS[option.get("type", "string")] + question_class = OPTIONS[option.get("type", OptionType.string)] result[key]["value"] = question_class.humanize( option["current_value"], option ) @@ -243,7 +244,7 @@ class ConfigPanel: self.filter_key = "" self._get_config_panel() for panel, section, option in self._iterate(): - if option["type"] == "button": + if option["type"] == OptionType.button: key = f"{panel['id']}.{section['id']}.{option['id']}" actions[key] = _value_for_locale(option["ask"]) @@ -425,7 +426,7 @@ class ConfigPanel: subnode["name"] = key # legacy subnode.setdefault("optional", raw_infos.get("optional", True)) # If this section contains at least one button, it becomes an "action" section - if subnode.get("type") == "button": + if subnode.get("type") == OptionType.button: out["is_action_section"] = True out.setdefault(sublevel, []).append(subnode) # Key/value are a property @@ -500,13 +501,13 @@ class ConfigPanel: # Hydrating config panel with current value for _, section, option in self._iterate(): if option["id"] not in self.values: - allowed_empty_types = [ - "alert", - "display_text", - "markdown", - "file", - "button", - ] + allowed_empty_types = { + OptionType.alert, + OptionType.display_text, + OptionType.markdown, + OptionType.file, + OptionType.button, + } if section["is_action_section"] and option.get("default") is not None: self.values[option["id"]] = option["default"] @@ -587,7 +588,7 @@ class ConfigPanel: section["options"] = [ option for option in section["options"] - if option.get("type", "string") != "button" + if option.get("type", OptionType.string) != OptionType.button or option["id"] == action ] diff --git a/src/utils/form.py b/src/utils/form.py index b455fe812..62750657b 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -23,7 +23,8 @@ import re import shutil import tempfile import urllib.parse -from typing import Any, Callable, Dict, List, Mapping, Optional, Union +from enum import Enum +from typing import Any, Callable, Dict, List, Literal, Mapping, Optional, Union from moulinette import Moulinette, m18n from moulinette.interfaces.cli import colorize @@ -193,7 +194,50 @@ def evaluate_simple_js_expression(expr, context={}): # │ ╰─╯╵ ╵ ╶┴╴╰─╯╵╰╯╶─╯ │ # ╰───────────────────────────────────────────────────────╯ -FORBIDDEN_READONLY_TYPES = {"password", "app", "domain", "user", "group"} + +class OptionType(str, Enum): + # display + display_text = "display_text" + markdown = "markdown" + alert = "alert" + # action + button = "button" + # text + string = "string" + text = "text" + password = "password" + color = "color" + # numeric + number = "number" + range = "range" + # boolean + boolean = "boolean" + # time + date = "date" + time = "time" + # location + email = "email" + path = "path" + url = "url" + # file + file = "file" + # choice + select = "select" + tags = "tags" + # entity + domain = "domain" + app = "app" + user = "user" + group = "group" + + +FORBIDDEN_READONLY_TYPES = { + OptionType.password, + OptionType.app, + OptionType.domain, + OptionType.user, + OptionType.group, +} class BaseOption: @@ -202,7 +246,7 @@ class BaseOption: question: Dict[str, Any], ): self.name = question["name"] - self.type = question.get("type", "string") + self.type = question.get("type", OptionType.string) self.visible = question.get("visible", True) self.readonly = question.get("readonly", False) @@ -240,15 +284,15 @@ class BaseReadonlyOption(BaseOption): class DisplayTextOption(BaseReadonlyOption): - argument_type = "display_text" + type: Literal[OptionType.display_text] = OptionType.display_text class MarkdownOption(BaseReadonlyOption): - argument_type = "markdown" + type: Literal[OptionType.markdown] = OptionType.markdown class AlertOption(BaseReadonlyOption): - argument_type = "alert" + type: Literal[OptionType.alert] = OptionType.alert def __init__(self, question): super().__init__(question) @@ -271,7 +315,7 @@ class AlertOption(BaseReadonlyOption): class ButtonOption(BaseReadonlyOption): - argument_type = "button" + type: Literal[OptionType.button] = OptionType.button enabled = True def __init__(self, question): @@ -373,14 +417,21 @@ class BaseInputOption(BaseOption): # ─ STRINGS ─────────────────────────────────────────────── -class StringOption(BaseInputOption): - argument_type = "string" +class BaseStringOption(BaseInputOption): default_value = "" +class StringOption(BaseStringOption): + type: Literal[OptionType.string] = OptionType.string + + +class TextOption(BaseStringOption): + type: Literal[OptionType.text] = OptionType.text + + class PasswordOption(BaseInputOption): + type: Literal[OptionType.password] = OptionType.password hide_user_input_in_prompt = True - argument_type = "password" default_value = "" forbidden_chars = "{}" @@ -407,7 +458,8 @@ class PasswordOption(BaseInputOption): assert_password_is_strong_enough("user", self.value) -class ColorOption(StringOption): +class ColorOption(BaseStringOption): + type: Literal[OptionType.color] = OptionType.color pattern = { "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", "error": "config_validate_color", # i18n: config_validate_color @@ -418,7 +470,7 @@ class ColorOption(StringOption): class NumberOption(BaseInputOption): - argument_type = "number" + type: Literal[OptionType.number, OptionType.range] = OptionType.number default_value = None def __init__(self, question): @@ -472,7 +524,7 @@ class NumberOption(BaseInputOption): class BooleanOption(BaseInputOption): - argument_type = "boolean" + type: Literal[OptionType.boolean] = OptionType.boolean default_value = 0 yes_answers = ["1", "yes", "y", "true", "t", "on"] no_answers = ["0", "no", "n", "false", "f", "off"] @@ -562,7 +614,8 @@ class BooleanOption(BaseInputOption): # ─ TIME ────────────────────────────────────────────────── -class DateOption(StringOption): +class DateOption(BaseStringOption): + type: Literal[OptionType.date] = OptionType.date pattern = { "regexp": r"^\d{4}-\d\d-\d\d$", "error": "config_validate_date", # i18n: config_validate_date @@ -580,7 +633,8 @@ class DateOption(StringOption): raise YunohostValidationError("config_validate_date") -class TimeOption(StringOption): +class TimeOption(BaseStringOption): + type: Literal[OptionType.time] = OptionType.time pattern = { "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", "error": "config_validate_time", # i18n: config_validate_time @@ -590,7 +644,8 @@ class TimeOption(StringOption): # ─ LOCATIONS ───────────────────────────────────────────── -class EmailOption(StringOption): +class EmailOption(BaseStringOption): + type: Literal[OptionType.email] = OptionType.email pattern = { "regexp": r"^.+@.+", "error": "config_validate_email", # i18n: config_validate_email @@ -598,7 +653,7 @@ class EmailOption(StringOption): class WebPathOption(BaseInputOption): - argument_type = "path" + type: Literal[OptionType.path] = OptionType.path default_value = "" @staticmethod @@ -628,7 +683,8 @@ class WebPathOption(BaseInputOption): return "/" + value.strip().strip(" /") -class URLOption(StringOption): +class URLOption(BaseStringOption): + type: Literal[OptionType.url] = OptionType.url pattern = { "regexp": r"^https?://.*$", "error": "config_validate_url", # i18n: config_validate_url @@ -639,7 +695,7 @@ class URLOption(StringOption): class FileOption(BaseInputOption): - argument_type = "file" + type: Literal[OptionType.file] = OptionType.file upload_dirs: List[str] = [] def __init__(self, question): @@ -764,12 +820,12 @@ class BaseChoicesOption(BaseInputOption): class SelectOption(BaseChoicesOption): - argument_type = "select" + type: Literal[OptionType.select] = OptionType.select default_value = "" class TagsOption(BaseChoicesOption): - argument_type = "tags" + type: Literal[OptionType.tags] = OptionType.tags default_value = "" @staticmethod @@ -822,7 +878,7 @@ class TagsOption(BaseChoicesOption): class DomainOption(BaseChoicesOption): - argument_type = "domain" + type: Literal[OptionType.domain] = OptionType.domain def __init__(self, question): from yunohost.domain import domain_list, _get_maindomain @@ -851,7 +907,7 @@ class DomainOption(BaseChoicesOption): class AppOption(BaseChoicesOption): - argument_type = "app" + type: Literal[OptionType.app] = OptionType.app def __init__(self, question): from yunohost.app import app_list @@ -877,7 +933,7 @@ class AppOption(BaseChoicesOption): class UserOption(BaseChoicesOption): - argument_type = "user" + type: Literal[OptionType.user] = OptionType.user def __init__(self, question): from yunohost.user import user_list, user_info @@ -908,7 +964,7 @@ class UserOption(BaseChoicesOption): class GroupOption(BaseChoicesOption): - argument_type = "group" + type: Literal[OptionType.group] = OptionType.group def __init__(self, question): from yunohost.user import user_group_list @@ -932,29 +988,29 @@ class GroupOption(BaseChoicesOption): OPTIONS = { - "display_text": DisplayTextOption, - "markdown": MarkdownOption, - "alert": AlertOption, - "button": ButtonOption, - "string": StringOption, - "text": StringOption, - "password": PasswordOption, - "color": ColorOption, - "number": NumberOption, - "range": NumberOption, - "boolean": BooleanOption, - "date": DateOption, - "time": TimeOption, - "email": EmailOption, - "path": WebPathOption, - "url": URLOption, - "file": FileOption, - "select": SelectOption, - "tags": TagsOption, - "domain": DomainOption, - "app": AppOption, - "user": UserOption, - "group": GroupOption, + OptionType.display_text: DisplayTextOption, + OptionType.markdown: MarkdownOption, + OptionType.alert: AlertOption, + OptionType.button: ButtonOption, + OptionType.string: StringOption, + OptionType.text: StringOption, + OptionType.password: PasswordOption, + OptionType.color: ColorOption, + OptionType.number: NumberOption, + OptionType.range: NumberOption, + OptionType.boolean: BooleanOption, + OptionType.date: DateOption, + OptionType.time: TimeOption, + OptionType.email: EmailOption, + OptionType.path: WebPathOption, + OptionType.url: URLOption, + OptionType.file: FileOption, + OptionType.select: SelectOption, + OptionType.tags: TagsOption, + OptionType.domain: DomainOption, + OptionType.app: AppOption, + OptionType.user: UserOption, + OptionType.group: GroupOption, } @@ -1122,7 +1178,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: out = [] for raw_question in raw_questions: - question = OPTIONS[raw_question.get("type", "string")](raw_question) + question = OPTIONS[raw_question.get("type", OptionType.string)](raw_question) if isinstance(question, BaseChoicesOption) and question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default From c1f0ac04c7597467b8f179fdaaff30d36f3ae0ce Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 15:54:56 +0200 Subject: [PATCH 66/93] rename Option.name to Option.id --- src/app.py | 20 +++++++------- src/tests/test_questions.py | 10 +++---- src/utils/configpanel.py | 6 ++--- src/utils/form.py | 54 ++++++++++++++++++------------------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/app.py b/src/app.py index 97227ed0c..c64a5d860 100644 --- a/src/app.py +++ b/src/app.py @@ -1099,7 +1099,7 @@ def app_install( raw_questions = manifest["install"] questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) args = { - question.name: question.value + question.id: question.value for question in questions if question.value is not None } @@ -1147,7 +1147,7 @@ def app_install( if question.type == "password": continue - app_settings[question.name] = question.value + app_settings[question.id] = question.value _set_app_settings(app_instance_name, app_settings) @@ -1202,16 +1202,16 @@ def app_install( # Reinject user-provider passwords which are not in the app settings # (cf a few line before) if question.type == "password": - env_dict[question.name] = question.value + env_dict[question.id] = question.value # We want to hav the env_dict in the log ... but not password values env_dict_for_logging = env_dict.copy() for question in questions: # Or should it be more generally question.redact ? if question.type == "password": - del env_dict_for_logging[f"YNH_APP_ARG_{question.name.upper()}"] - if question.name in env_dict_for_logging: - del env_dict_for_logging[question.name] + del env_dict_for_logging[f"YNH_APP_ARG_{question.id.upper()}"] + if question.id in env_dict_for_logging: + del env_dict_for_logging[question.id] operation_logger.extra.update({"env": env_dict_for_logging}) @@ -2358,17 +2358,17 @@ def _set_default_ask_questions(questions, script_name="install"): ), # i18n: app_manifest_install_ask_init_admin_permission ] - for question_name, question in questions.items(): - question["name"] = question_name + for question_id, question in questions.items(): + question["id"] = question_id # If this question corresponds to a question with default ask message... if any( - (question.get("type"), question["name"]) == question_with_default + (question.get("type"), question["id"]) == question_with_default for question_with_default in questions_with_default ): # The key is for example "app_manifest_install_ask_domain" question["ask"] = m18n.n( - f"app_manifest_{script_name}_ask_{question['name']}" + f"app_manifest_{script_name}_ask_{question['id']}" ) # Also it in fact doesn't make sense for any of those questions to have an example value nor a default value... diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 3e7927dce..7ef678d19 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -33,7 +33,7 @@ from yunohost.utils.error import YunohostError, YunohostValidationError """ Argument default format: { - "the_name": { + "the_id": { "type": "one_of_the_available_type", // "sting" is not specified "ask": { "en": "the question in english", @@ -50,7 +50,7 @@ Argument default format: } User answers: -{"the_name": "value", ...} +{"the_id": "value", ...} """ @@ -443,7 +443,7 @@ class BaseTest: assert isinstance(option, OPTIONS[raw_option["type"]]) assert option.type == raw_option["type"] - assert option.name == id_ + assert option.id == id_ assert option.ask == {"en": id_} assert option.readonly is (True if is_special_readonly_option else False) assert option.visible is True @@ -1913,7 +1913,7 @@ def test_options_query_string(): ) def _assert_correct_values(options, raw_options): - form = {option.name: option.value for option in options} + form = {option.id: option.value for option in options} for k, v in results.items(): if k == "file_id": @@ -1945,7 +1945,7 @@ def test_question_string_default_type(): out = ask_questions_and_parse_answers(questions, answers)[0] - assert out.name == "some_string" + assert out.id == "some_string" assert out.type == "string" assert out.value == "some_value" diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index f5d802356..42a030cbc 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -521,7 +521,7 @@ class ConfigPanel: f"Config panel question '{option['id']}' should be initialized with a value during install or upgrade.", raw_msg=True, ) - value = self.values[option["name"]] + value = self.values[option["id"]] # Allow to use value instead of current_value in app config script. # e.g. apps may write `echo 'value: "foobar"'` in the config file (which is more intuitive that `echo 'current_value: "foobar"'` @@ -600,14 +600,14 @@ class ConfigPanel: prefilled_answers.update(self.new_values) questions = ask_questions_and_parse_answers( - {question["name"]: question for question in section["options"]}, + {question["id"]: question for question in section["options"]}, prefilled_answers=prefilled_answers, current_values=self.values, hooks=self.hooks, ) self.new_values.update( { - question.name: question.value + question.id: question.value for question in questions if question.value is not None } diff --git a/src/utils/form.py b/src/utils/form.py index 62750657b..57cb1cd5b 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -245,7 +245,7 @@ class BaseOption: self, question: Dict[str, Any], ): - self.name = question["name"] + self.id = question["id"] self.type = question.get("type", OptionType.string) self.visible = question.get("visible", True) @@ -255,10 +255,10 @@ class BaseOption: raise YunohostError( "config_forbidden_readonly_type", type=self.type, - id=self.name, + id=self.id, ) - self.ask = question.get("ask", self.name) + self.ask = question.get("ask", self.id) if not isinstance(self.ask, dict): self.ask = {"en": self.ask} @@ -379,14 +379,14 @@ class BaseInputOption(BaseOption): def _value_pre_validator(self): if self.value in [None, ""] and not self.optional: - raise YunohostValidationError("app_argument_required", name=self.name) + raise YunohostValidationError("app_argument_required", name=self.id) # we have an answer, do some post checks if self.value not in [None, ""]: if self.pattern and not re.match(self.pattern["regexp"], str(self.value)): raise YunohostValidationError( self.pattern["error"], - name=self.name, + name=self.id, value=self.value, ) @@ -440,7 +440,7 @@ class PasswordOption(BaseInputOption): self.redact = True if self.default is not None: raise YunohostValidationError( - "app_argument_password_no_default", name=self.name + "app_argument_password_no_default", name=self.id ) def _value_pre_validator(self): @@ -496,7 +496,7 @@ class NumberOption(BaseInputOption): option = option.__dict__ if isinstance(option, BaseOption) else option raise YunohostValidationError( "app_argument_invalid", - name=option.get("name"), + name=option.get("id"), error=m18n.n("invalid_number"), ) @@ -508,14 +508,14 @@ class NumberOption(BaseInputOption): if self.min is not None and int(self.value) < self.min: raise YunohostValidationError( "app_argument_invalid", - name=self.name, + name=self.id, error=m18n.n("invalid_number_min", min=self.min), ) if self.max is not None and int(self.value) > self.max: raise YunohostValidationError( "app_argument_invalid", - name=self.name, + name=self.id, error=m18n.n("invalid_number_max", max=self.max), ) @@ -554,7 +554,7 @@ class BooleanOption(BaseInputOption): raise YunohostValidationError( "app_argument_choice_invalid", - name=option.get("name"), + name=option.get("id"), value=value, choices="yes/no", ) @@ -594,7 +594,7 @@ class BooleanOption(BaseInputOption): raise YunohostValidationError( "app_argument_choice_invalid", - name=option.get("name"), + name=option.get("id"), value=strvalue, choices="yes/no", ) @@ -663,7 +663,7 @@ class WebPathOption(BaseInputOption): if not isinstance(value, str): raise YunohostValidationError( "app_argument_invalid", - name=option.get("name"), + name=option.get("id"), error="Argument for path should be a string.", ) @@ -676,7 +676,7 @@ class WebPathOption(BaseInputOption): elif option.get("optional") is False: raise YunohostValidationError( "app_argument_invalid", - name=option.get("name"), + name=option.get("id"), error="Option is mandatory", ) @@ -725,7 +725,7 @@ class FileOption(BaseInputOption): ): raise YunohostValidationError( "app_argument_invalid", - name=self.name, + name=self.id, error=m18n.n("file_does_not_exist", path=str(self.value)), ) @@ -740,7 +740,7 @@ class FileOption(BaseInputOption): FileOption.upload_dirs += [upload_dir] - logger.debug(f"Saving file {self.name} for file question into {file_path}") + logger.debug(f"Saving file {self.id} for file question into {file_path}") def is_file_path(s): return isinstance(s, str) and s.startswith("/") and os.path.exists(s) @@ -813,7 +813,7 @@ class BaseChoicesOption(BaseInputOption): if self.choices and self.value not in self.choices: raise YunohostValidationError( "app_argument_choice_invalid", - name=self.name, + name=self.id, value=self.value, choices=", ".join(str(choice) for choice in self.choices), ) @@ -853,13 +853,13 @@ class TagsOption(BaseChoicesOption): if self.choices: raise YunohostValidationError( "app_argument_choice_invalid", - name=self.name, + name=self.id, value=self.value, choices=", ".join(str(choice) for choice in self.choices), ) raise YunohostValidationError( "app_argument_invalid", - name=self.name, + name=self.id, error=f"'{str(self.value)}' is not a list", ) @@ -949,7 +949,7 @@ class UserOption(BaseChoicesOption): if not self.choices: raise YunohostValidationError( "app_argument_invalid", - name=self.name, + name=self.id, error="You should create a YunoHost user first.", ) @@ -1033,9 +1033,9 @@ def prompt_or_validate_form( options = [] answers = {**prefilled_answers} - for name, raw_option in raw_options.items(): - raw_option["name"] = name - raw_option["value"] = answers.get(name) + for id_, raw_option in raw_options.items(): + raw_option["id"] = id_ + raw_option["value"] = answers.get(id_) question_class = OPTIONS[raw_option.get("type", "string")] option = question_class(raw_option) @@ -1047,7 +1047,7 @@ def prompt_or_validate_form( else: raise YunohostValidationError( "config_action_disabled", - action=option.name, + action=option.id, help=_value_for_locale(option.help), ) @@ -1060,7 +1060,7 @@ def prompt_or_validate_form( # - we doesn't want to give a specific value # - we want to keep the previous value # - we want the default value - option.value = context[option.name] = None + option.value = context[option.id] = None continue @@ -1071,7 +1071,7 @@ def prompt_or_validate_form( Moulinette.display(message) if isinstance(option, BaseInputOption): - option.value = context[option.name] = option.current_value + option.value = context[option.id] = option.current_value continue @@ -1123,10 +1123,10 @@ def prompt_or_validate_form( break - option.value = option.values[option.name] = option._value_post_validator() + option.value = option.values[option.id] = option._value_post_validator() # Search for post actions in hooks - post_hook = f"post_ask__{option.name}" + post_hook = f"post_ask__{option.id}" if post_hook in hooks: option.values.update(hooks[post_hook](option)) From 4df7e4681dcdca089a269bb9bb63ce6355b19896 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 30 Apr 2023 17:15:40 +0200 Subject: [PATCH 67/93] form: force option type to 'select' if there's 'choices' + add test --- src/tests/test_questions.py | 15 +++++++++++++++ src/utils/form.py | 23 ++++++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 7ef678d19..7737c4546 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -1950,6 +1950,21 @@ def test_question_string_default_type(): assert out.value == "some_value" +def test_option_default_type_with_choices_is_select(): + questions = { + "some_choices": {"choices": ["a", "b"]}, + # LEGACY (`choices` in option `string` used to be valid) + # make sure this result as a `select` option + "some_legacy": {"type": "string", "choices": ["a", "b"]} + } + answers = {"some_choices": "a", "some_legacy": "a"} + + options = ask_questions_and_parse_answers(questions, answers) + for option in options: + assert option.type == "select" + assert option.value == "a" + + @pytest.mark.skip # we should do something with this example def test_question_string_input_test_ask_with_example(): ask_text = "some question" diff --git a/src/utils/form.py b/src/utils/form.py index 57cb1cd5b..1ca03373e 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1014,6 +1014,22 @@ OPTIONS = { } +def hydrate_option_type(raw_option: dict[str, Any]) -> dict[str, Any]: + type_ = raw_option.get( + "type", OptionType.select if "choices" in raw_option else OptionType.string + ) + # LEGACY (`choices` in option `string` used to be valid) + if "choices" in raw_option and type_ == OptionType.string: + logger.warning( + f"Packagers: option {raw_option['id']} has 'choices' but has type 'string', use 'select' instead to remove this warning." + ) + type_ = OptionType.select + + raw_option["type"] = type_ + + return raw_option + + # ╭───────────────────────────────────────────────────────╮ # │ ╷ ╷╶┬╴╶┬╴╷ ╭─╴ │ # │ │ │ │ │ │ ╰─╮ │ @@ -1036,8 +1052,8 @@ def prompt_or_validate_form( for id_, raw_option in raw_options.items(): raw_option["id"] = id_ raw_option["value"] = answers.get(id_) - question_class = OPTIONS[raw_option.get("type", "string")] - option = question_class(raw_option) + raw_option = hydrate_option_type(raw_option) + option = OPTIONS[raw_option["type"]](raw_option) interactive = Moulinette.interface.type == "cli" and os.isatty(1) @@ -1178,7 +1194,8 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: out = [] for raw_question in raw_questions: - question = OPTIONS[raw_question.get("type", OptionType.string)](raw_question) + raw_question = hydrate_option_type(raw_question) + question = OPTIONS[raw_question["type"]](raw_question) if isinstance(question, BaseChoicesOption) and question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default From fb4693be3959d6306eb4a23c62b13992b2c547d2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 20 Jun 2023 17:59:09 +0200 Subject: [PATCH 68/93] apps: be able to customize the user-part and domain-part of email when using allow_email on system user --- src/app.py | 4 +++- src/utils/resources.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app.py b/src/app.py index 88d79e750..a7d25cfa6 100644 --- a/src/app.py +++ b/src/app.py @@ -3173,7 +3173,9 @@ def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): hashed_password = _hash_user_password(settings["mail_pwd"]) dovecot_passwd.append(f"{app}:{hashed_password}::::::allow_nets=127.0.0.1/24") if postfix: - postfix_map.append(f"{app}@{settings['domain']} {app}") + mail_user = settings.get("mail_user", app) + mail_domain = settings.get("mail_domain", settings["domain"]) + postfix_map.append(f"{mail_user}@{mail_domain} {app}") if dovecot: app_senders_passwd = "/etc/dovecot/app-senders-passwd" diff --git a/src/utils/resources.py b/src/utils/resources.py index 4e1907ab4..925ce6ee8 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -451,7 +451,7 @@ class SystemuserAppResource(AppResource): ##### Properties: - `allow_ssh`: (default: False) Adds the user to the ssh.app group, allowing SSH connection via this user - `allow_sftp`: (default: False) Adds the user to the sftp.app group, allowing SFTP connection via this user - - `allow_email`: (default: False) Enable authentication on the mail stack for the system user and send mail using `__APP__@__DOMAIN__`. A `mail_pwd` setting is automatically defined (similar to `db_pwd` for databases). You can then configure the app to use `__APP__` and `__MAIL_PWD__` as SMTP credentials (with host 127.0.0.1) + - `allow_email`: (default: False) Enable authentication on the mail stack for the system user and send mail using `__APP__@__DOMAIN__`. A `mail_pwd` setting is automatically defined (similar to `db_pwd` for databases). You can then configure the app to use `__APP__` and `__MAIL_PWD__` as SMTP credentials (with host 127.0.0.1). You can also tweak the user-part of the domain-part of the email used by manually defining a custom setting `mail_user` or `mail_domain` - `home`: (default: `/var/www/__APP__`) Defines the home property for this user. NB: unfortunately you can't simply use `__INSTALL_DIR__` or `__DATA_DIR__` for now ##### Provision/Update: From fd7136446ec42ee99a3ee30dfbd9e8cb78967dfe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Jul 2023 18:01:49 +0200 Subject: [PATCH 69/93] Simplify ynh_add_fpm_config helper --- helpers/php | 80 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/helpers/php b/helpers/php index 417dbbc61..c9e5b1cb8 100644 --- a/helpers/php +++ b/helpers/php @@ -7,33 +7,44 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} # Create a dedicated PHP-FPM config # -# usage 1: ynh_add_fpm_config [--phpversion=7.X] [--use_template] [--package=packages] [--dedicated_service] -# | arg: -v, --phpversion= - Version of PHP to use. -# | arg: -t, --use_template - Use this helper in template mode. -# | arg: -p, --package= - Additionnal PHP packages to install -# | arg: -d, --dedicated_service - Use a dedicated PHP-FPM service instead of the common one. +# usage: ynh_add_fpm_config # -# ----------------------------------------------------------------------------- +# Case 1 (recommended) : your provided a snippet conf/extra_php-fpm.conf # -# usage 2: ynh_add_fpm_config [--phpversion=7.X] --usage=usage --footprint=footprint [--package=packages] [--dedicated_service] -# | arg: -v, --phpversion= - Version of PHP to use. -# | arg: -f, --footprint= - Memory footprint of the service (low/medium/high). +# The actual PHP configuration will be automatically generated, +# and your extra_php-fpm.conf will be appended (typically contains PHP upload limits) +# +# The resulting configuration will be deployed to the appropriate place, /etc/php/$phpversion/fpm/pool.d/$app.conf +# +# Performance-related options in the PHP conf, such as : +# pm.max_children, pm.start_servers, pm.min_spare_servers pm.max_spare_servers +# are computed from two parameters called "usage" and "footprint" which can be set to low/medium/high. (cf details below) +# +# If you wish to tweak those, please initialize the settings `fpm_usage` and `fpm_footprint` +# *prior* to calling this helper. Otherwise, "low" will be used as a default for both values. +# +# Otherwise, if you want the user to have control over these, we encourage to create a config panel +# (which should ultimately be standardized by the core ...) +# +# Case 2 (deprecate) : you provided an entire conf/php-fpm.conf +# +# The configuration will be hydrated, replacing __FOOBAR__ placeholders with $foobar values, etc. +# +# The resulting configuration will be deployed to the appropriate place, /etc/php/$phpversion/fpm/pool.d/$app.conf +# +# ---------------------- +# +# fpm_footprint: Memory footprint of the service (low/medium/high). # low - Less than 20 MB of RAM by pool. # medium - Between 20 MB and 40 MB of RAM by pool. # high - More than 40 MB of RAM by pool. -# Or specify exactly the footprint, the load of the service as MB by pool instead of having a standard value. -# To have this value, use the following command and stress the service. -# watch -n0.5 ps -o user,cmd,%cpu,rss -u APP +# N - Or you can specify a quantitative footprint as MB by pool (use watch -n0.5 ps -o user,cmd,%cpu,rss -u APP) # -# | arg: -u, --usage= - Expected usage of the service (low/medium/high). +# fpm_usage: Expected usage of the service (low/medium/high). # low - Personal usage, behind the SSO. # medium - Low usage, few people or/and publicly accessible. # high - High usage, frequently visited website. # -# | arg: -p, --package= - Additionnal PHP packages to install for a specific version of PHP -# | arg: -d, --dedicated_service - Use a dedicated PHP-FPM service instead of the common one. -# -# # The footprint of the service will be used to defined the maximum footprint we can allow, which is half the maximum RAM. # So it will be used to defined 'pm.max_children' # A lower value for the footprint will allow more children for 'pm.max_children'. And so for @@ -59,10 +70,9 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} ynh_add_fpm_config() { local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. - local legacy_args=vtufpd - local -A args_array=([v]=phpversion= [t]=use_template [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service) + local legacy_args=vufpd + local -A args_array=([v]=phpversion= [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service) local phpversion - local use_template local usage local footprint local package @@ -72,11 +82,28 @@ ynh_add_fpm_config() { package=${package:-} # The default behaviour is to use the template. - use_template="${use_template:-1}" + local autogenconf=false usage="${usage:-}" footprint="${footprint:-}" - if [ -n "$usage" ] || [ -n "$footprint" ]; then - use_template=0 + if [ -n "$usage" ] || [ -n "$footprint" ] || [[ -e $YNH_APP_BASEDIR/conf/extra_php-fpm.conf ]]; then + autogenconf=true + + # If no usage provided, default to the value existing in setting ... or to low + local fpm_usage_in_setting=$(ynh_app_setting_get --app=$app --key=fpm_usage) + if [ -z "$usage" ] + then + usage=${fpm_usage_in_setting:-low} + ynh_app_setting_set --app=$app --key=fpm_usage --value=$usage + fi + + # If no footprint provided, default to the value existing in setting ... or to low + local fpm_footprint_in_setting=$(ynh_app_setting_get --app=$app --key=fpm_footprint) + if [ -z "$footprint" ] + then + footprint=${fpm_footprint_in_setting:-low} + ynh_app_setting_set --app=$app --key=fpm_footprint --value=$footprint + fi + fi # Do not use a dedicated service by default dedicated_service=${dedicated_service:-0} @@ -111,6 +138,7 @@ ynh_add_fpm_config() { fi if [ $dedicated_service -eq 1 ]; then + ynh_print_warn --message "Argument --dedicated_service of ynh_add_fpm_config is deprecated and to be removed in the future" local fpm_service="${app}-phpfpm" local fpm_config_dir="/etc/php/$phpversion/dedicated-fpm" else @@ -141,7 +169,7 @@ ynh_add_fpm_config() { fi fi - if [ $use_template -eq 1 ]; then + if [ $autogenconf == "false" ]; then # Usage 1, use the template in conf/php-fpm.conf local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf" # Make sure now that the template indeed exists @@ -149,10 +177,6 @@ ynh_add_fpm_config() { else # Usage 2, generate a PHP-FPM config file with ynh_get_scalable_phpfpm - # Store settings - ynh_app_setting_set --app=$app --key=fpm_footprint --value=$footprint - ynh_app_setting_set --app=$app --key=fpm_usage --value=$usage - # Define the values to use for the configuration of PHP. ynh_get_scalable_phpfpm --usage=$usage --footprint=$footprint From 7924bb2b28436e6be7949b559c9eaa22981b3de4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Jul 2023 23:29:36 +0200 Subject: [PATCH 70/93] tests: fix my_webapp test that has been failing for a while --- src/tests/test_apps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index 1a3f5e97b..e6e1342ba 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -330,7 +330,7 @@ def test_app_from_catalog(): app_install( "my_webapp", - args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0&phpversion=none", + args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&init_main_permission=visitors&with_mysql=0&phpversion=none", ) app_map_ = app_map(raw=True) assert main_domain in app_map_ @@ -339,7 +339,7 @@ def test_app_from_catalog(): assert app_map_[main_domain]["/site"]["id"] == "my_webapp" assert app_is_installed(main_domain, "my_webapp") - assert app_is_exposed_on_http(main_domain, "/site", "Custom Web App") + assert app_is_exposed_on_http(main_domain, "/site", "you have just installed My Webapp") # Try upgrade, should do nothing app_upgrade("my_webapp") From 4152cb0dd1d76107cf1322e34db2ecbe6abc3923 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 17:34:21 +0200 Subject: [PATCH 71/93] apps: fix a bug where YunoHost would complain that 'it needs X RAM but only Y left' with Y > X because some apps have a higher runtime RAM requirement than build time ... --- src/app.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index a90e273a2..cce0aa51c 100644 --- a/src/app.py +++ b/src/app.py @@ -2782,10 +2782,18 @@ def _check_manifest_requirements( ram_requirement["runtime"] ) + # Some apps have a higher runtime value than build ... + if ram_requirement["build"] != "?" and ram_requirement["runtime"] != "?": + max_build_runtime = (ram_requirement["build"] + if human_to_binary(ram_requirement["build"]) > human_to_binary(ram_requirement["runtime"]) + else ram_requirement["runtime"]) + else: + max_build_runtime = ram_requirement["build"] + yield ( "ram", can_build and can_run, - {"current": binary_to_human(ram), "required": ram_requirement["build"]}, + {"current": binary_to_human(ram), "required": max_build_runtime}, "app_not_enough_ram", # i18n: app_not_enough_ram ) From b98ac21a0663b5e1078d7505deb51d114b32e5c5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 18 Jun 2023 15:45:44 +0200 Subject: [PATCH 72/93] apps: fix version.parse now refusing to parse legacy version numbers --- src/app.py | 65 ++++++++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/src/app.py b/src/app.py index cce0aa51c..64bb8c530 100644 --- a/src/app.py +++ b/src/app.py @@ -241,8 +241,8 @@ def _app_upgradable(app_infos): # Determine upgradability app_in_catalog = app_infos.get("from_catalog") - installed_version = version.parse(app_infos.get("version", "0~ynh0")) - version_in_catalog = version.parse( + installed_version = _parse_app_version(app_infos.get("version", "0~ynh0")) + version_in_catalog = _parse_app_version( app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0") ) @@ -257,25 +257,7 @@ def _app_upgradable(app_infos): ): return "bad_quality" - # If the app uses the standard version scheme, use it to determine - # upgradability - if "~ynh" in str(installed_version) and "~ynh" in str(version_in_catalog): - if installed_version < version_in_catalog: - return "yes" - else: - return "no" - - # Legacy stuff for app with old / non-standard version numbers... - - # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded - if not app_infos["from_catalog"].get("lastUpdate") or not app_infos[ - "from_catalog" - ].get("git"): - return "url_required" - - settings = app_infos["settings"] - local_update_time = settings.get("update_time", settings.get("install_time", 0)) - if app_infos["from_catalog"]["lastUpdate"] > local_update_time: + if installed_version < version_in_catalog: return "yes" else: return "no" @@ -620,9 +602,11 @@ def app_upgrade( # Manage upgrade type and avoid any upgrade if there is nothing to do upgrade_type = "UNKNOWN" # Get current_version and new version - app_new_version = version.parse(manifest.get("version", "?")) - app_current_version = version.parse(app_dict.get("version", "?")) - if "~ynh" in str(app_current_version) and "~ynh" in str(app_new_version): + app_new_version_raw = manifest.get("version", "?") + app_current_version_raw = app_dict.get("version", "?") + app_new_version = _parse_app_version(app_new_version_raw) + app_current_version = _parse_app_version(app_current_version_raw) + if "~ynh" in str(app_current_version_raw) and "~ynh" in str(app_new_version_raw): if app_current_version >= app_new_version and not force: # In case of upgrade from file or custom repository # No new version available @@ -642,10 +626,10 @@ def app_upgrade( upgrade_type = "UPGRADE_FORCED" else: app_current_version_upstream, app_current_version_pkg = str( - app_current_version + app_current_version_raw ).split("~ynh") app_new_version_upstream, app_new_version_pkg = str( - app_new_version + app_new_version_raw ).split("~ynh") if app_current_version_upstream == app_new_version_upstream: upgrade_type = "UPGRADE_PACKAGE" @@ -675,7 +659,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["PRE_UPGRADE"], - current_version=app_current_version, + current_version=app_current_version_raw, data=settings, ) _display_notifications(notifications, force=force) @@ -732,8 +716,8 @@ def app_upgrade( env_dict_more = { "YNH_APP_UPGRADE_TYPE": upgrade_type, - "YNH_APP_MANIFEST_VERSION": str(app_new_version), - "YNH_APP_CURRENT_VERSION": str(app_current_version), + "YNH_APP_MANIFEST_VERSION": str(app_new_version_raw), + "YNH_APP_CURRENT_VERSION": str(app_current_version_raw), } if manifest["packaging_format"] < 2: @@ -916,7 +900,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["POST_UPGRADE"], - current_version=app_current_version, + current_version=app_current_version_raw, data=settings, ) if Moulinette.interface.type == "cli": @@ -2054,6 +2038,20 @@ def _set_app_settings(app, settings): yaml.safe_dump(settings, f, default_flow_style=False) +def _parse_app_version(v): + + if v == "?": + return (0,0) + + try: + if "~" in v: + return (version.parse(v.split("~")[0]), int(v.split("~")[1].replace("ynh", ""))) + else: + return (version.parse(v), 0) + except Exception as e: + raise YunohostError(f"Failed to parse app version '{v}' : {e}", raw_msg=True) + + def _get_manifest_of_app(path): "Get app manifest stored in json or in toml" @@ -3158,12 +3156,7 @@ def _notification_is_dismissed(name, settings): def _filter_and_hydrate_notifications(notifications, current_version=None, data={}): def is_version_more_recent_than_current_version(name, current_version): current_version = str(current_version) - # Boring code to handle the fact that "0.1 < 9999~ynh1" is False - - if "~" in name: - return version.parse(name) > version.parse(current_version) - else: - return version.parse(name) > version.parse(current_version.split("~")[0]) + return _parse_app_version(name) > _parse_app_version(current_version) return { # Should we render the markdown maybe? idk From 798a5469eb772982e6d1874a19d24ec543c417cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Sun, 18 Jun 2023 05:06:05 +0000 Subject: [PATCH 73/93] Translated using Weblate (Galician) Currently translated at 100.0% (768 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/locales/gl.json b/locales/gl.json index b8b6e5cd0..3aaacd9c9 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -762,5 +762,9 @@ "log_resource_snippet": "Aprovisionamento/desaprovisionamento/actualización dun recurso", "app_resource_failed": "Fallou o aprovisionamento, desaprovisionamento ou actualización de recursos para {app}: {error}", "app_failed_to_download_asset": "Fallou a descarga do recurso '{source_id}' ({url}) para {app}: {out}", - "app_corrupt_source": "YunoHost foi quen de descargar o recurso '{source_id}' ({url}) para {app}, pero a suma de comprobación para o recurso non concorda. Pode significar que houbo un fallo temporal na conexión do servidor á rede, OU que o recurso sufreu, dalgún xeito, cambios desde que os desenvolvedores orixinais (ou unha terceira parte maliciosa?), o equipo de YunoHost ten que investigar e actualizar o manifesto da app para mostrar este cambio.\n Suma sha256 agardada: {expected_sha256} \n Suma sha256 do descargado: {computed_sha256}\n Tamaño do ficheiro: {size}" -} \ No newline at end of file + "app_corrupt_source": "YunoHost foi quen de descargar o recurso '{source_id}' ({url}) para {app}, pero a suma de comprobación para o recurso non concorda. Pode significar que houbo un fallo temporal na conexión do servidor á rede, OU que o recurso sufreu, dalgún xeito, cambios desde que os desenvolvedores orixinais (ou unha terceira parte maliciosa?), o equipo de YunoHost ten que investigar e actualizar o manifesto da app para mostrar este cambio.\n Suma sha256 agardada: {expected_sha256} \n Suma sha256 do descargado: {computed_sha256}\n Tamaño do ficheiro: {size}", + "group_mailalias_add": "Vaise engadir o alias de correo '{mail}' ao grupo '{group}'", + "group_mailalias_remove": "Vaise quitar o alias de email '{mail}' do grupo '{group}'", + "group_user_add": "Vaise engadir a '{user}' ao grupo '{grupo}'", + "group_user_remove": "Vaise quitar a '{user}' do grupo '{grupo}'" +} From e0a1f8ba0b74728c9aa9a440382c5ad72ad9e384 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Sun, 18 Jun 2023 16:08:28 +0000 Subject: [PATCH 74/93] Translated using Weblate (Basque) Currently translated at 96.7% (743 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/eu/ --- locales/eu.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/eu.json b/locales/eu.json index 0d424e6ca..bfdf54500 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -669,12 +669,12 @@ "migration_description_0024_rebuild_python_venv": "Konpondu Python aplikazioa Bullseye eguneraketa eta gero", "migration_0024_rebuild_python_venv_disclaimer_base": "Debian Bullseye eguneraketa dela-eta, Python aplikazio batzuk birsortu behar dira Debianekin datorren Pythonen bertsiora egokitzeko (teknikoki 'virtualenv' deritzaiona birsortu behar da). Egin artean, litekeena da Python aplikazio horiek ez funtzionatzea. YunoHost saia daiteke beherago ageri diren aplikazioen virtualenv edo ingurune birtualak birsortzen. Beste aplikazio batzuen kasuan, edo birsortze saiakerak kale egingo balu, aplikazio horien eguneraketa behartu beharko duzu.", "migration_0021_not_buster2": "Zerbitzariak darabilen Debian bertsioa ez da Buster! Dagoeneko Buster -> Bullseye migrazioa exekutatu baduzu, errore honek migrazioa erabat arrakastatsua izan ez zela esan nahi du (bestela YunoHostek amaitutzat markatuko luke). Komenigarria izango litzateke, laguntza taldearekin batera, zer gertatu zen aztertzea. Horretarako `migrazioaren erregistro **osoa** beharko duzue, Tresnak > Erregistroak atalean eskuragarri dagoena.", - "admins": "Administratzaileak", + "admins": "Administratzaileek", "app_action_failed": "{app} aplikaziorako {action} eragiketak huts egin du", "config_action_disabled": "Ezin izan da '{action}' eragiketa exekutatu ezgaituta dagoelako, egiaztatu bere mugak betetzen dituzula. Laguntza: {help}", - "all_users": "YunoHosten erabiltzaile guztiak", + "all_users": "YunoHosten erabiltzaile guztiek", "app_manifest_install_ask_init_admin_permission": "Nork izan beharko luke aplikazio honetako administrazio aukeretara sarbidea? (Aldatzea dago)", - "app_manifest_install_ask_init_main_permission": "Nor izan beharko luke aplikazio honetara sarbidea? (Aldatzea dago)", + "app_manifest_install_ask_init_main_permission": "Nork izan beharko luke aplikazio honetara sarbidea? (Aldatzea dago)", "ask_admin_fullname": "Administratzailearen izen osoa", "ask_admin_username": "Administratzailearen erabiltzaile-izena", "ask_fullname": "Izen osoa", @@ -689,7 +689,7 @@ "log_settings_reset": "Berrezarri ezarpenak", "log_settings_reset_all": "Berrezarri ezarpen guztiak", "root_password_changed": "root pasahitza aldatu da", - "visitors": "Bisitariak", + "visitors": "Bisitariek", "global_settings_setting_security_experimental_enabled": "Segurtasun ezaugarri esperimentalak", "registrar_infos": "Erregistro-enpresaren informazioa", "global_settings_setting_pop3_enabled": "Gaitu POP3", @@ -763,4 +763,4 @@ "app_failed_to_download_asset": "{app} aplikaziorako '{source_id}' ({url}) baliabidea deskargatzeak huts egin du: {out}", "apps_failed_to_upgrade": "Aplikazio hauen bertsio-berritzeak huts egin du: {apps}", "apps_failed_to_upgrade_line": "\n * {app_id} (dagokion erregistroa ikusteko, exekutatu 'yunohost log show {operation_logger_name}')" -} \ No newline at end of file +} From 9c3895300fbfabb2d39958b8cc384bfc59ce7217 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Sat, 1 Jul 2023 14:35:44 +0000 Subject: [PATCH 75/93] Translated using Weblate (Basque) Currently translated at 97.2% (747 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/eu/ --- locales/eu.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/eu.json b/locales/eu.json index bfdf54500..0267b3366 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -762,5 +762,9 @@ "app_not_upgraded_broken_system_continue": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du eta sistema hondatu du (beraz, --continue-on-failure aukerari muzin egin zaio) eta ondorengo aplikazioen bertsio-berritzeak ezeztatu dira: {apps}", "app_failed_to_download_asset": "{app} aplikaziorako '{source_id}' ({url}) baliabidea deskargatzeak huts egin du: {out}", "apps_failed_to_upgrade": "Aplikazio hauen bertsio-berritzeak huts egin du: {apps}", - "apps_failed_to_upgrade_line": "\n * {app_id} (dagokion erregistroa ikusteko, exekutatu 'yunohost log show {operation_logger_name}')" + "apps_failed_to_upgrade_line": "\n * {app_id} (dagokion erregistroa ikusteko, exekutatu 'yunohost log show {operation_logger_name}')", + "group_mailalias_add": "'{mail}' ePosta aliasa jarri zaio '{group}' taldeari", + "group_mailalias_remove": "'{mail}' ePosta aliasa kendu zaio '{group}' taldeari", + "group_user_remove": "'{user}' erabiltzailea '{group}' taldetik kenduko da", + "group_user_add": "'{user}' erabiltzailea '{group}' taldera gehituko da" } From 48c81a4175341d9df016e900286dbb0515d8e783 Mon Sep 17 00:00:00 2001 From: Grzegorz Cichocki Date: Sun, 2 Jul 2023 22:32:15 +0000 Subject: [PATCH 76/93] Translated using Weblate (Polish) Currently translated at 33.4% (257 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 64 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index 0b3dc5e73..52f2de3ca 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -29,7 +29,7 @@ "system_upgraded": "Zaktualizowano system", "diagnosis_description_regenconf": "Konfiguracja systemu", "diagnosis_description_apps": "Aplikacje", - "diagnosis_description_basesystem": "Podstawowy system", + "diagnosis_description_basesystem": "Baza systemu", "unlimit": "Brak limitu", "global_settings_setting_pop3_enabled": "Włącz POP3", "domain_created": "Utworzono domenę", @@ -214,5 +214,65 @@ "confirm_app_insufficient_ram": "UWAGA! Ta aplikacja wymaga {required} pamięci RAM do zainstalowania/aktualizacji, a obecnie dostępne jest tylko {current}. Nawet jeśli aplikacja mogłaby działać, proces instalacji/aktualizacji wymaga dużej ilości pamięci RAM, więc serwer może się zawiesić i niepowodzenie może być katastrofalne. Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'", "app_not_upgraded_broken_system": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu. W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}", "app_not_upgraded_broken_system_continue": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu (parametr --continue-on-failure jest ignorowany). W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}", - "certmanager_domain_http_not_working": "Domena {domain} wydaje się niedostępna przez HTTP. Sprawdź kategorię 'Strona internetowa' diagnostyki, aby uzyskać więcej informacji. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)" + "certmanager_domain_http_not_working": "Domena {domain} wydaje się niedostępna przez HTTP. Sprawdź kategorię 'Strona internetowa' diagnostyki, aby uzyskać więcej informacji. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", + "migration_0021_system_not_fully_up_to_date": "Twój system nie jest w pełni zaktualizowany! Proszę, wykonaj zwykłą aktualizację oprogramowania zanim rozpoczniesz migrację na system Bullseye.", + "global_settings_setting_smtp_relay_port": "Port przekaźnika SMTP", + "domain_config_cert_renew": "Odnów certyfikat Let's Encrypt", + "root_password_changed": "Hasło root zostało zmienione", + "diagnosis_services_running": "Usługa {service} działa!", + "global_settings_setting_admin_strength": "Wymogi dotyczące siły hasła administratora", + "global_settings_setting_admin_strength_help": "Wymagania te są egzekwowane tylko podczas inicjalizacji lub zmiany hasła", + "global_settings_setting_pop3_enabled_help": "Włącz protokołu POP3 dla serwera poczty", + "global_settings_setting_postfix_compatibility": "Kompatybilność Postfix", + "global_settings_setting_smtp_relay_user": "Nazwa użytkownika przekaźnika SMTP", + "global_settings_setting_ssh_password_authentication_help": "Zezwól na logowanie hasłem przez SSH", + "diagnosis_apps_allgood": "Wszystkie zainstalowane aplikacje są zgodne z podstawowymi zasadami pakowania", + "diagnosis_basesystem_hardware": "Architektura sprzętowa serwera to {virt} {arch}", + "diagnosis_ip_connected_ipv4": "Serwer jest połączony z Internet z użyciem IPv4!", + "diagnosis_ip_no_ipv6": "Serwer nie ma działającego połączenia z użyciem IPv6.", + "diagnosis_http_hairpinning_issue": "Wygląda na to, że sieć lokalna nie ma \"hairpinning\".", + "backup_unable_to_organize_files": "Nie można użyć szybkiej metody porządkowania plików w archiwum", + "log_letsencrypt_cert_renew": "Odnów '{}' certyfikat Let's Encrypt", + "global_settings_setting_passwordless_sudo": "Umożliw administratorom korzystania z 'sudo' bez konieczności ponownego wpisywania hasła", + "global_settings_setting_smtp_relay_enabled": "Włącz przekaźnik SMTP", + "global_settings_setting_smtp_relay_host": "Host przekaźnika SMTP", + "global_settings_setting_user_strength": "Wymagania dotyczące siły hasła użytkownika", + "domain_config_mail_in": "Odbieranie maili", + "global_settings_setting_webadmin_allowlist_enabled_help": "Zezwól tylko kilku adresom IP na dostęp do panelu webadmin.", + "diagnosis_basesystem_kernel": "Serwer działa pod kontrolą jądra Linuksa {kernel_version}", + "diagnosis_dns_good_conf": "Rekordy DNS zostały poprawnie skonfigurowane dla domeny {domain} (category {category})", + "diagnosis_ram_ok": "System nadal ma {available} ({available_percent}%) wolnej pamięci RAM z całej puli {total}.", + "diagnosis_http_ok": "Domena {domain} jest dostępna przez HTTP z poziomu sieci zewnętrznej.", + "diagnosis_swap_tip": "Pamiętaj, że wykorzystywanie partycji swap na karcie pamięci SD lub na dysku SSD może znacznie skrócić czas działania tego urządzenia.", + "diagnosis_basesystem_host": "Serwer działa pod kontrolą systemu Debian {debian_version}", + "diagnosis_basesystem_ynh_main_version": "Serwer działa pod kontrolą oprogramowania YunoHost {main_version} ({repo})", + "diagnosis_diskusage_verylow": "Przestrzeń {mountpoint} (na dysku {device}) ma tylko {free} ({free_percent}%) wolnego miejsca z całej puli {total}! Rozważ pozbycie się niepotrzebnych plików!", + "global_settings_setting_root_password": "Nowe hasło root", + "global_settings_setting_root_password_confirm": "Powtórz nowe hasło root", + "global_settings_setting_security_experimental_enabled": "Eksperymentalne funkcje bezpieczeństwa", + "global_settings_setting_smtp_relay_password": "Hasło przekaźnika SMTP", + "global_settings_setting_user_strength_help": "Wymagania te są egzekwowane tylko podczas inicjalizacji lub zmiany hasła", + "global_settings_setting_webadmin_allowlist_enabled": "Włącz listę dozwolonych adresów IP dla panelu webadmin", + "root_password_desynchronized": "Hasło administratora zostało zmienione, ale YunoHost nie mógł wykorzystać tego hasła jako hasło root!", + "service_already_started": "Usługa '{service}' już jest włączona", + "diagnosis_ip_dnsresolution_working": "Rozpoznawanie nazw domen działa!", + "diagnosis_regenconf_manually_modified": "Wygląda na to, że plik konfiguracyjny {file} został zmodyfikowany ręcznie.", + "diagnosis_diskusage_ok": "Przestrzeń {mountpoint} (na dysku {device}) nadal ma {free} ({free_percent}%) wolnego miejsca z całej puli {total}!", + "diagnosis_diskusage_low": "Przestrzeń {mountpoint} (na dysku {device}) ma tylko {free} ({free_percent}%) wolnego miejsca z całej puli {total}! Uważaj na możliwe zapełnienie dysku w bliskiej przyszłości.", + "diagnosis_ip_connected_ipv6": "Serwer nie jest połączony z internetem z użyciem IPv6!", + "global_settings_setting_smtp_relay_enabled_help": "Włączenie przekaźnika SMTP, który ma być używany do wysyłania poczty zamiast tej instancji yunohost może być przydatne, jeśli znajdujesz się w jednej z następujących sytuacji: Twój port 25 jest zablokowany przez dostawcę usług internetowych lub dostawcę VPS, masz adres IP zamieszkania wymieniony w DUHL, nie jesteś w stanie skonfigurować odwrotnego DNS lub ten serwer nie jest bezpośrednio widoczny w Internecie i chcesz użyć innego do wysyłania wiadomości e-mail.", + "global_settings_setting_backup_compress_tar_archives_help": "Podczas tworzenia nowych kopii zapasowych archiwa będą skompresowane (.tar.gz), a nie nieskompresowane jak dotychczas (.tar). Uwaga: włączenie tej opcji oznacza tworzenie mniejszych archiwów kopii zapasowych, ale początkowa procedura tworzenia kopii zapasowej będzie znacznie dłuższa i mocniej obciąży procesor.", + "domain_config_mail_out": "Wysyłanie maili", + "domain_dns_registrar_supported": "YunoHost automatycznie wykrył, że ta domena jest obsługiwana przez rejestratora **{registrar}**. Jeśli chcesz, YunoHost automatycznie skonfiguruje rekordy DNS, ale musisz podać odpowiednie dane uwierzytelniające API. Dokumentację dotyczącą uzyskiwania poświadczeń API można znaleźć na tej stronie: https://yunohost.org/registar_api_{registrar}. (Można również ręcznie skonfigurować rekordy DNS zgodnie z dokumentacją na stronie https://yunohost.org/dns )", + "domain_config_cert_summary_letsencrypt": "Świetnie! Wykorzystujesz właściwy certyfikaty Let's Encrypt!", + "global_settings_setting_portal_theme": "Motyw portalu", + "global_settings_setting_portal_theme_help": "Więcej informacji na temat tworzenia niestandardowych motywów portalu można znaleźć na stronie https://yunohost.org/theming", + "global_settings_setting_dns_exposure": "Wersje IP do uwzględnienia w konfiguracji i diagnostyce DNS", + "domain_config_auth_token": "Token uwierzytelniający", + "global_settings_setting_dns_exposure_help": "Uwaga: Ma to wpływ tylko na zalecaną konfigurację DNS i kontrole diagnostyczne. Nie ma to wpływu na konfigurację systemu.", + "global_settings_setting_security_experimental_enabled_help": "Uruchom eksperymentalne funkcje bezpieczeństwa (nie włączaj, jeśli nie wiesz co robisz!)", + "global_settings_setting_smtp_allow_ipv6_help": "Zezwól na wykorzystywanie IPv7 do odbierania i wysyłania maili", + "global_settings_setting_ssh_password_authentication": "Logowanie hasłem", + "diagnosis_backports_in_sources_list": "Wygląda na to że apt (menedżer pakietów) został skonfigurowany tak, aby wykorzystywać repozytorium backported. Nie zalecamy wykorzystywania repozytorium backported, ponieważ może powodować problemy ze stabilnością i/lub konflikty z konfiguracją. No chyba, że wiesz co robisz.", + "domain_config_xmpp_help": "Uwaga: niektóre funkcje XMPP będą wymagały aktualizacji rekordów DNS i odnowienia certyfikatu Lets Encrypt w celu ich włączenia" } From 76481dae22cebe37bbdb55f7ea10f688790dd14d Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 9 Jul 2023 04:32:52 +0200 Subject: [PATCH 77/93] Added translation using Weblate (Japanese) --- locales/ja.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/ja.json diff --git a/locales/ja.json b/locales/ja.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/ja.json @@ -0,0 +1 @@ +{} From 392695535e99eec745ce116beffb74326d91decf Mon Sep 17 00:00:00 2001 From: motcha Date: Sun, 9 Jul 2023 05:49:40 +0000 Subject: [PATCH 78/93] Translated using Weblate (Japanese) Currently translated at 0.1% (1 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ja/ --- locales/ja.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index 0967ef424..a76ec9f48 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1 +1,3 @@ -{} +{ + "password_too_simple_1": "パスワードは少なくとも8文字必要です" +} From 3f0a23105edc75fb44f8e0c5155c156f6d7092f5 Mon Sep 17 00:00:00 2001 From: motcha Date: Sun, 9 Jul 2023 15:17:43 +0000 Subject: [PATCH 79/93] Translated using Weblate (Japanese) Currently translated at 70.8% (544 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ja/ --- locales/ja.json | 769 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 768 insertions(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index a76ec9f48..90645193b 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1,3 +1,770 @@ { - "password_too_simple_1": "パスワードは少なくとも8文字必要です" + "password_too_simple_1": "パスワードは少なくとも8文字必要です", + "aborting": "中止します。", + "action_invalid": "不正なアクション ’ {action}’", + "additional_urls_already_added": "アクセス許可 '{permission}' に対する追加URLには ‘{url}’ が既に追加されています", + "admin_password": "管理者パスワード", + "app_action_cannot_be_ran_because_required_services_down": "このアクションを実行するには、次の必要なサービスが実行されている必要があります: {services} 。続行するには再起動してみてください (そして何故ダウンしているのか調査してください)。", + "app_action_failed": "‘{name}’ アプリのアクション ’{action}' に失敗しました", + "app_argument_invalid": "引数 '{name}' の有効な値を選択してください: {error}", + "app_argument_password_no_default": "パスワード引数 '{name}' の解析中にエラーが発生しました: セキュリティ上の理由から、パスワード引数にデフォルト値を設定することはできません", + "app_argument_required": "‘{name}’ は必要です。", + "app_change_url_failed": "{app}のURLを変更できませんでした:{error}", + "app_change_url_identical_domains": "古いドメインと新しいドメイン/url_pathは同一であり( '{domain}{path}')、何もしません。", + "app_change_url_script_failed": "URL 変更スクリプト内でエラーが発生しました", + "app_failed_to_upgrade_but_continue": "アプリの{failed_app}アップグレードに失敗しました。要求に応じて次のアップグレードに進みます。「yunohostログショー{operation_logger_name}」を実行して失敗ログを表示します", + "app_full_domain_unavailable": "申し訳ありませんが、このアプリは独自のドメインにインストールする必要がありますが、他のアプリは既にドメイン '{domain}' にインストールされています。代わりに、このアプリ専用のサブドメインを使用できます。", + "app_id_invalid": "不正なアプリID", + "app_install_failed": "インストールできません {app}:{error}", + "app_manifest_install_ask_password": "このアプリの管理パスワードを選択してください", + "app_manifest_install_ask_path": "このアプリをインストールするURLパス(ドメインの後)を選択します", + "app_not_properly_removed": "{app}が正しく削除されていません", + "app_not_upgraded": "アプリ「{failed_app}」のアップグレードに失敗したため、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_start_remove": "‘{app}’ を削除しています…", + "app_start_restore": "‘{app}’ をリストアしています…", + "ask_main_domain": "メインドメイン", + "ask_new_admin_password": "新しい管理者パスワード", + "ask_new_domain": "新しいドメイン", + "ask_new_path": "新しいパス", + "ask_password": "パスワード", + "ask_user_domain": "ユーザーのメールアドレスと XMPP アカウントに使用するドメイン", + "backup_abstract_method": "このバックアップ方法はまだ実装されていません", + "backup_actually_backuping": "収集したファイルからバックアップアーカイブを作成しています...", + "backup_archive_corrupted": "バックアップアーカイブ ’{archive}’ は破損しているようです: {error}", + "backup_archive_name_exists": "この名前のバックアップアーカイブはすでに存在します。", + "backup_archive_name_unknown": "「{name}」という名前の不明なローカルバックアップアーカイブ", + "backup_archive_open_failed": "バックアップアーカイブを開けませんでした", + "backup_archive_system_part_not_available": "このバックアップでは、システム部分 '{part}' を使用できません", + "backup_method_custom_finished": "カスタム バックアップ方法 '{method}' が完了しました", + "certmanager_attempt_to_replace_valid_cert": "ドメイン {domain} の適切で有効な証明書を上書きしようとしています。(—force でバイパスする)", + "certmanager_cannot_read_cert": "ドメイン {domain} (ファイル: {file}) の現在の証明書を開こうとしたときに問題が発生しました。理由: {reason}", + "certmanager_cert_install_failed": "{domains}のLet’s Encrypt 証明書のインストールに失敗しました", + "certmanager_cert_install_failed_selfsigned": "{domains} ドメインの自己署名証明書のインストールに失敗しました", + "certmanager_cert_install_success": "Let’s Encrypt 証明書が ‘{domain}’ にインストールされました", + "certmanager_cert_install_success_selfsigned": "ドメイン「{domain}」に自己署名証明書がインストールされました", + "certmanager_domain_dns_ip_differs_from_public_ip": "ドメイン '{domain}' の DNS レコードは、このサーバーの IP とは異なります。詳細については、診断の「DNSレコード」(基本)カテゴリを確認してください。最近 A レコードを変更した場合は、反映されるまでお待ちください (一部の DNS 伝達チェッカーはオンラインで入手できます)。(何をしているかがわかっている場合は、 '--no-checks'を使用してこれらのチェックをオフにします。", + "certmanager_domain_http_not_working": "ドメイン{domain}はHTTP経由でアクセスできないようです。詳細については、診断の「Web」カテゴリを確認してください。(何をしているかがわかっている場合は、 '--no-checks'を使用してこれらのチェックをオフにします。", + "certmanager_unable_to_parse_self_CA_name": "自己署名機関の名前を解析できませんでした (ファイル: {file})", + "certmanager_domain_not_diagnosed_yet": "ドメイン{domain}の診断結果はまだありません。診断セクションのカテゴリ「DNSレコード」と「Web」の診断を再実行して、ドメインが暗号化の準備ができているかどうかを確認してください。(または、何をしているかがわかっている場合は、「--no-checks」を使用してこれらのチェックをオフにします。", + "confirm_app_insufficient_ram": "危険!このアプリのインストール/アップグレードには{required}RAMが必要ですが、現在利用可能なのは{current}つだけです。このアプリを実行できたとしても、そのインストール/アップグレードプロセスには大量のRAMが必要なため、サーバーがフリーズして惨めに失敗する可能性があります。とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", + "confirm_notifications_read": "警告:続行する前に上記のアプリ通知を確認する必要があります、知っておくべき重要なことがあるかもしれません。[{answers}]", + "custom_app_url_required": "カスタム App をアップグレードするには URL を指定する必要があります{app}", + "danger": "危険:", + "diagnosis_cant_run_because_of_dep": "{dep}に関連する重要な問題がある間、{category}診断を実行できません。", + "diagnosis_description_apps": "アプリケーション", + "diagnosis_description_basesystem": "システム", + "diagnosis_description_dnsrecords": "DNS レコード", + "diagnosis_description_ip": "インターネット接続", + "diagnosis_description_mail": "メールアドレス", + "diagnosis_description_ports": "ポート開放", + "diagnosis_high_number_auth_failures": "最近、疑わしいほど多くの認証失敗が発生しています。fail2banが実行されていて正しく構成されていることを確認するか、https://yunohost.org/security で説明されているようにSSHにカスタムポートを使用することをお勧めします。", + "diagnosis_http_bad_status_code": "サーバーの代わりに別のマシン(おそらくインターネットルーター)が応答したようです。
1.この問題の最も一般的な原因は、ポート80(および443)が サーバーに正しく転送されていないことです。
2.より複雑なセットアップでは、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_http_hairpinning_issue_details": "これはおそらくISPボックス/ルーターが原因です。その結果、ローカルネットワークの外部の人々は期待どおりにサーバーにアクセスできますが、ドメイン名またはグローバルIPを使用する場合、ローカルネットワーク内の人々(おそらくあなたのような人)はアクセスできません。https://yunohost.org/dns_local_network を見ることによって状況を改善できるかもしれません", + "diagnosis_ignored_issues": "(+{nb_ignored}無視された問題)", + "diagnosis_ip_dnsresolution_working": "ドメイン名前解決は機能しています!", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 は通常、システムまたはプロバイダー (使用可能な場合) によって自動的に構成されます。それ以外の場合は、こちらのドキュメントで説明されているように、いくつかのことを手動で構成する必要があります: https://yunohost.org/#/ipv6。", + "diagnosis_ip_not_connected_at_all": "サーバーがインターネットに接続されていないようですね!?", + "diagnosis_ip_weird_resolvconf": "DNS名前解決は機能しているようですが、カスタムされた/etc/resolv.confを使用しているようです。", + "diagnosis_ip_weird_resolvconf_details": "ファイルは/etc/resolv.conf、(dnsmasq)を指す127.0.0.1それ自体への/etc/resolvconf/run/resolv.confシンボリックリンクである必要があります。DNSリゾルバーを手動で設定する場合は、編集/etc/resolv.dnsmasq.confしてください。", + "diagnosis_mail_blacklist_listed_by": "あなたのIPまたはドメイン {item} はブラックリスト {blacklist_name} に登録されています", + "diagnosis_mail_blacklist_ok": "このサーバーが使用するIPとドメインはブラックリストに登録されていないようです", + "diagnosis_mail_ehlo_could_not_diagnose_details": "エラー: {error}", + "diagnosis_mail_fcrdns_ok": "逆引きDNSが正しく構成されています!", + "diagnosis_mail_fcrdns_nok_alternatives_4": "一部のプロバイダーでは、逆引きDNSを構成できません(または機能が壊れている可能性があります…)。そのせいで問題が発生している場合は、次の解決策を検討してください。
- 一部のISPが提供するメールサーバーリレーを使用する ことで代替できますが、ISPが電子メールトラフィックを盗み見る可能性があることを意味します。
- プライバシーに配慮した代替手段は、この種の制限を回避するために*専用のパブリックIP*を持つVPNを使用することです。https://yunohost.org/#/vpn_advantage を見る
-または、別のプロバイダーに切り替えることが可能です", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "一部のプロバイダーは、ネット中立性を気にしないため、送信ポート25のブロックを解除することを許可しません。
-それらのいくつかは 、メールサーバーリレーを使用する 代替手段を提供しますが、リレーが電子メールトラフィックをスパイできることを意味します。
- プライバシーに配慮した代替手段は、*専用のパブリックIP*を持つVPNを使用して、これらの種類の制限を回避することです。https://yunohost.org/#/vpn_advantage を見る
-よりネット中立性に優しいプロバイダーへの切り替えを検討することもできます", + "diagnosis_mail_outgoing_port_25_ok": "SMTP メール サーバーは電子メールを送信できます (送信ポート 25 はブロックされません)。", + "diagnosis_mail_queue_ok": "メールキュー内の保留中のメール{nb_pending}", + "diagnosis_mail_queue_too_big": "メールキュー内の保留中のメールが多すぎます({nb_pending}メール)", + "diagnosis_mail_queue_unavailable": "キュー内の保留中の電子メールの数を調べることはできません", + "diagnosis_mail_queue_unavailable_details": "エラー: {error}", + "diagnosis_no_cache": "カテゴリ '{category}' の診断キャッシュがまだありません", + "diagnosis_ports_forwarding_tip": "この問題を解決するには、ほとんどの場合、https://yunohost.org/isp_box_config で説明されているように、インターネットルーターでポート転送を構成する必要があります", + "diagnosis_ports_needed_by": "このポートの公開は、{category}機能 (サービス {service}) に必要です。", + "diagnosis_ports_ok": "ポート {port} は外部から到達可能です。", + "diagnosis_ports_partially_unreachable": "ポート {port} は、IPv{failed} では外部から到達できません。", + "diagnosis_security_vulnerable_to_meltdown": "Meltdown(重大なセキュリティの脆弱性)に対して脆弱に見えます", + "diagnosis_services_conf_broken": "サービス{service}の構成が壊れています!", + "diagnosis_services_running": "サービス{service}が実行されています!", + "diagnosis_sshd_config_inconsistent": "SSHポートが/ etc / ssh / sshd_configで手動で変更されたようです。YunoHost 4.2以降、手動で構成を編集する必要がないように、新しいグローバル設定「security.ssh.ssh_port」を使用できます。", + "diagnosis_swap_none": "システムにスワップがまったくない。システムのメモリ不足の状況を回避するために、少なくとも {recommended} つのスワップを追加することを検討する必要があります。", + "diagnosis_swap_notsomuch": "システムにはスワップが {total} しかありません。システムのメモリ不足の状況を回避するために、少なくとも {recommended} のスワップを用意することを検討してください。", + "diagnosis_swap_ok": "システムには {total} のスワップがあります!", + "domain_cert_gen_failed": "証明書を生成できませんでした", + "domain_config_acme_eligible": "ACMEの資格", + "domain_config_cert_summary": "証明書の状態", + "domain_config_cert_summary_abouttoexpire": "現在の証明書の有効期限が近づいています。すぐに自動的に更新されるはずです。", + "domain_config_cert_summary_expired": "クリティカル: 現在の証明書が無効です!HTTPSはまったく機能しません!", + "domain_config_cert_validity": "データの入力規則", + "domain_config_xmpp": "インスタント メッセージング (XMPP)", + "domain_dns_conf_is_just_a_recommendation": "このコマンドは、*推奨*構成を表示します。実際にはDNS構成は設定されません。この推奨事項に従って、レジストラーで DNS ゾーンを構成するのはユーザーの責任です。", + "domain_dns_conf_special_use_tld": "このドメインは、.local や .test などの特殊な用途のトップレベル ドメイン (TLD) に基づいているため、実際の DNS レコードを持つことは想定されていません。", + "domain_dns_push_already_up_to_date": "レコードはすでに最新であり、何もする必要はありません。", + "domain_dns_push_failed": "DNS レコードの更新が失敗しました。", + "domain_dyndns_already_subscribed": "すでに DynDNS ドメインにサブスクライブしている", + "dyndns_key_generating": "DNS キーを生成しています...しばらく時間がかかる場合があります。", + "dyndns_key_not_found": "ドメインの DNS キーが見つかりません", + "firewall_reload_failed": "バックアップアーカイブを開けませんでした", + "global_settings_setting_postfix_compatibility_help": "Postfix サーバーの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します", + "global_settings_setting_root_password": "新しい管理者パスワード", + "global_settings_setting_root_password_confirm": "新しい管理者パスワード", + "global_settings_setting_smtp_allow_ipv6": "IPv6 を許可する", + "global_settings_setting_user_strength_help": "これらの要件は、パスワードを初期化または変更する場合にのみ適用されます", + "group_cannot_be_deleted": "グループ{group}を手動で削除することはできません。", + "group_created": "グループ '{group}' が作成されました", + "group_mailalias_add": "メール エイリアス '{mail}' がグループ '{group}' に追加されます。", + "group_mailalias_remove": "メール エイリアス '{mail}' がグループ '{group}' から削除されます。", + "group_no_change": "グループ '{group}' に対して変更はありません", + "group_unknown": "グループ '{group}' は不明です", + "group_user_already_in_group": "ユーザー {user} は既にグループ {group} に所属しています", + "group_user_not_in_group": "ユーザー {user}がグループ {group} にない", + "group_user_remove": "ユーザー '{user}' はグループ '{group}' から削除されます。", + "hook_exec_failed": "スクリプトを実行できませんでした: {path}", + "hook_exec_not_terminated": "スクリプトが正しく終了しませんでした: {path}", + "log_app_install": "‘{}’ アプリをインストールする", + "log_user_permission_update": "アクセス許可 '{}' のアクセスを更新する", + "log_user_update": "ユーザー '{}' の情報を更新する", + "mail_alias_remove_failed": "電子メール エイリアス '{mail}' を削除できませんでした", + "mail_domain_unknown": "ドメイン '{domain}' の電子メール アドレスが無効です。このサーバーによって管理されているドメインを使用してください。", + "mail_forward_remove_failed": "電子メール転送 '{mail}' を削除できませんでした", + "mail_unavailable": "この電子メール アドレスは、管理者グループ用に予約されています", + "migration_0021_start": "Bullseyeへの移行開始", + "migration_0021_yunohost_upgrade": "YunoHostコアのアップグレードを開始しています...", + "migration_description_0026_new_admins_group": "新しい「複数の管理者」システムに移行する", + "migration_ldap_backup_before_migration": "実際の移行の前に、LDAP データベースとアプリ設定のバックアップを作成します。", + "migration_ldap_can_not_backup_before_migration": "移行が失敗する前に、システムのバックアップを完了できませんでした。エラー: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "移行できませんでした...システムをロールバックしようとしています。", + "permission_updated": "アクセス許可 '{permission}' が更新されました", + "restore_confirm_yunohost_installed": "すでにインストールされているシステムを復元しますか?[{answers}]", + "restore_extracting": "アーカイブから必要なファイルを抽出しています...", + "restore_failed": "バックアップを復元する ‘{name}’", + "restore_hook_unavailable": "「{part}」の復元スクリプトは、システムで使用できず、アーカイブでも利用できません", + "restore_not_enough_disk_space": "十分なスペースがありません(スペース:{free_space} B、必要なスペース:{needed_space} B、セキュリティマージン:{margin} B)", + "restore_nothings_done": "何も復元されませんでした", + "restore_removing_tmp_dir_failed": "古い一時ディレクトリを削除できませんでした", + "restore_running_app_script": "アプリ「{app}」を復元しています...", + "restore_running_hooks": "復元フックを実行しています...", + "restore_system_part_failed": "「{part}」システム部分を復元できませんでした", + "root_password_changed": "パスワード確認", + "server_reboot": "サーバーが再起動します", + "server_shutdown_confirm": "サーバーはすぐにシャットダウンしますが、よろしいですか?[{answers}]", + "service_add_failed": "サービス '{service}' を追加できませんでした", + "service_added": "サービス '{service}' が追加されました", + "service_already_started": "サービス '{service}' は既に実行されています", + "service_description_dnsmasq": "ドメイン名解決 (DNS) を処理します。", + "service_description_dovecot": "電子メールクライアントが電子メールにアクセス/フェッチすることを許可します(IMAPおよびPOP3経由)", + "service_description_fail2ban": "インターネットからのブルートフォース攻撃やその他の種類の攻撃から保護します", + "service_description_metronome": "XMPP インスタント メッセージング アカウントを管理する", + "service_description_mysql": "アプリ データの格納 (SQL データベース)", + "service_description_postfix": "電子メールの送受信に使用", + "service_description_postgresql": "アプリ データの格納 (SQL データベース)", + "service_enable_failed": "起動時にサービス '{service}' を自動的に開始できませんでした。\n\n最近のサービスログ:{logs}", + "service_enabled": "サービス '{service}' は、システムの起動時に自動的に開始されるようになりました。", + "service_reloaded": "サービス '{service}' がリロードされました", + "service_not_reloading_because_conf_broken": "構成が壊れているため、サービス「{name}」をリロード/再起動しません:{errors}", + "show_tile_cant_be_enabled_for_regex": "権限 '{permission}' の URL は正規表現であるため、現在 'show_tile' を有効にすることはできません。", + "show_tile_cant_be_enabled_for_url_not_defined": "最初にアクセス許可 '{permission}' の URL を定義する必要があるため、現在 'show_tile' を有効にすることはできません。", + "ssowat_conf_generated": "SSOワット構成の再生成", + "system_upgraded": "システムのアップグレード", + "unlimit": "クォータなし", + "update_apt_cache_failed": "APT (Debian のパッケージマネージャ) のキャッシュを更新できません。問題のある行を特定するのに役立つ可能性のあるsources.list行のダンプを次に示します。\n{sourceslist}", + "update_apt_cache_warning": "APT(Debianのパッケージマネージャー)のキャッシュを更新中に問題が発生しました。問題のある行を特定するのに役立つ可能性のあるsources.list行のダンプを次に示します。\n{sourceslist}", + "admins": "管理者", + "all_users": "YunoHostの全ユーザー", + "already_up_to_date": "何もすることはありません。すべて最新です。", + "app_action_broke_system": "このアクションは、これらの重要なサービスを壊したようです: {services}", + "app_already_installed": "アプリ '{app}' は既にインストール済み", + "app_already_installed_cant_change_url": "このアプリは既にインストールされています。この機能だけではURLを変更することはできません。利用可能な場合は、`app changeurl`を確認してください。", + "app_already_up_to_date": "{app} アプリは既に最新です", + "app_arch_not_supported": "このアプリはアーキテクチャ {required} にのみインストールできますが、サーバーのアーキテクチャは{current} です", + "app_argument_choice_invalid": "引数 '{name}' に有効な値を選択してください: '{value}' は使用可能な選択肢に含まれていません ({choices})", + "app_change_url_no_script": "アプリ「{app_name}」はまだURLの変更をサポートしていません。多分あなたはそれをアップグレードする必要があります。", + "app_change_url_require_full_domain": "{app}は完全なドメイン(つまり、path = /)を必要とするため、この新しいURLに移動できません。", + "app_change_url_success": "{app} URL が{domain}{path}されました", + "app_config_unable_to_apply": "設定パネルの値を適用できませんでした。", + "app_config_unable_to_read": "設定パネルの値の読み取りに失敗しました。", + "app_corrupt_source": "YunoHost はアセット '{source_id}' ({url}) を {app} 用にダウンロードできましたが、アセットのチェックサムが期待されるものと一致しません。これは、あなたのサーバーで一時的なネットワーク障害が発生したか、もしくはアセットがアップストリームメンテナ(または悪意のあるアクター?)によって何らかの形で変更され、YunoHostパッケージャーがアプリマニフェストを調査/更新する必要があることを意味する可能性があります。\n 期待される sha256 チェックサム: {expected_sha256}\n ダウンロードしたsha256チェックサム: {computed_sha256}\n ダウンロードしたファイルサイズ: {size}", + "app_extraction_failed": "インストール ファイルを抽出できませんでした", + "app_failed_to_download_asset": "{app}のアセット「{source_id}」({url})をダウンロードできませんでした:{out}", + "app_install_files_invalid": "これらのファイルはインストールできません", + "app_install_script_failed": "アプリのインストールスクリプト内部でエラーが発生しました", + "app_label_deprecated": "このコマンドは非推奨です。新しいコマンド ’yunohost user permission update’ を使用して、アプリラベルを管理してください。", + "app_location_unavailable": "この URL は利用できないか、既にインストールされているアプリと競合しています。\n{apps}", + "app_make_default_location_already_used": "「{app}」をドメインのデフォルトアプリにすることはできません。「{domain}」は「{other_app}」によってすでに使用されています", + "app_manifest_install_ask_admin": "このアプリの管理者ユーザーを選択する", + "app_manifest_install_ask_domain": "このアプリをインストールするドメインを選択してください", + "app_manifest_install_ask_init_admin_permission": "このアプリの管理機能にアクセスできるのは誰ですか?(これは後で変更できます)", + "app_manifest_install_ask_init_main_permission": "誰がこのアプリにアクセスできる必要がありますか?(これは後で変更できます)", + "app_manifest_install_ask_is_public": "このアプリは匿名の訪問者に公開する必要がありますか?", + "app_not_correctly_installed": "{app}が正しくインストールされていないようです", + "app_not_enough_disk": "このアプリには{required}の空き容量が必要です。", + "app_not_enough_ram": "このアプリのインストール/アップグレードには{required} のRAMが必要ですが、現在利用可能なのは {current} だけです。", + "app_not_installed": "インストールされているアプリのリストに{app}が見つかりませんでした: {all_apps}", + "app_not_upgraded_broken_system": "アプリ「{failed_app}」はアップグレードに失敗し、システムを壊れた状態にしたため、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_not_upgraded_broken_system_continue": "アプリ ’{failed_app}’ はアップグレードに失敗し、システムを壊れた状態にした(そのためcontinue-on-failureは無視されます)ので、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_restore_failed": "{app}を復元できませんでした: {error}", + "app_restore_script_failed": "アプリのリストアスクリプト内でエラーが発生しました", + "app_sources_fetch_failed": "ソースファイルをフェッチできませんでしたが、URLは正しいですか?", + "app_packaging_format_not_supported": "このアプリは、パッケージ形式がYunoHostバージョンでサポートされていないため、インストールできません。おそらく、システムのアップグレードを検討する必要があります。", + "app_remove_after_failed_install": "インストールの失敗後にアプリを削除しています...", + "app_removed": "'{app}' はアンインストール済", + "app_requirements_checking": "{app} の依存関係を確認しています…", + "app_resource_failed": "{app}のリソースのプロビジョニング、プロビジョニング解除、または更新に失敗しました: {error}", + "app_start_backup": "{app}用にバックアップするファイルを収集しています...", + "app_start_install": "‘{app}’ をインストールしています…", + "app_unknown": "未知のアプリ", + "app_unsupported_remote_type": "アプリで使用されている、サポートされないリモートの種類", + "apps_catalog_init_success": "アプリ カタログ システムが初期化されました!", + "apps_catalog_obsolete_cache": "アプリケーションカタログキャッシュが空であるか、古くなっています。", + "apps_catalog_update_success": "アプリケーションカタログを更新しました!", + "apps_catalog_updating": "アプリケーションカタログを更新しています...", + "app_upgrade_app_name": "'{app}' をアップグレードしています…", + "app_upgrade_failed": "アップグレードに失敗しました {app}: {error}", + "app_upgrade_script_failed": "アプリのアップグレードスクリプト内でエラーが発生しました", + "app_upgrade_several_apps": "次のアプリがアップグレードされます: {apps}", + "app_upgrade_some_app_failed": "一部のアプリをアップグレードできませんでした", + "app_upgraded": "'{app}' アップグレード済", + "app_yunohost_version_not_supported": "このアプリは YunoHost >= {required} を必要としますが、現在インストールされているバージョンは{current} です", + "apps_already_up_to_date": "全てのアプリが最新になりました!", + "apps_catalog_failed_to_download": "{apps_catalog} アプリ カタログをダウンロードできません: {error}", + "apps_failed_to_upgrade": "これらのアプリケーションのアップグレードに失敗しました: {apps}", + "apps_failed_to_upgrade_line": "\n * {app_id} (対応するログを表示するには、’yunohost log show {operation_logger_name}’ を実行してください)", + "ask_admin_fullname": "管理者 フルネーム", + "ask_admin_username": "管理者ユーザー名", + "ask_fullname": "フルネーム", + "backup_app_failed": "{app}をバックアップできませんでした", + "backup_applying_method_copy": "すべてのファイルをバックアップにコピーしています...", + "backup_applying_method_custom": "カスタムバックアップメソッド ’{method}’ を呼び出しています...", + "backup_applying_method_tar": "バックアップ TAR アーカイブを作成しています...", + "backup_archive_app_not_found": "バックアップアーカイブに{app}が見つかりませんでした", + "backup_archive_broken_link": "バックアップアーカイブにアクセスできませんでした({path}へのリンクが壊れています)", + "backup_archive_cant_retrieve_info_json": "アーカイブ '{archive}' の情報を読み込めませんでした... info.json ファイルを取得できません (または有効な json ではありません)。", + "backup_archive_writing_error": "圧縮アーカイブ '{archive}' にバックアップするファイル '{source}' (アーカイブ '{dest}' で指定) を追加できませんでした", + "backup_ask_for_copying_if_needed": "一時的に{size}MBを使用してバックアップを実行しますか?(この方法は、より効率的な方法で準備できなかったファイルがあるため、この方法が使用されます。", + "backup_cant_mount_uncompress_archive": "非圧縮アーカイブを書き込み保護としてマウントできませんでした", + "backup_cleaning_failed": "一時バックアップフォルダをクリーンアップできませんでした", + "backup_copying_to_organize_the_archive": "アーカイブを整理するために{size}MBをコピーしています", + "backup_couldnt_bind": "{src}を{dest}にバインドできませんでした。", + "backup_create_size_estimation": "アーカイブには約{size}のデータが含まれます。", + "backup_created": "バックアップを作成しました: {name}'", + "backup_creation_failed": "バックアップ作成できませんでした", + "backup_csv_addition_failed": "バックアップするファイルをCSVファイルに追加できませんでした", + "backup_csv_creation_failed": "復元に必要な CSV ファイルを作成できませんでした", + "backup_custom_backup_error": "カスタムバックアップ方法は「バックアップ」ステップを通過できませんでした", + "backup_custom_mount_error": "カスタムバックアップ方法は「マウント」ステップを通過できませんでした", + "backup_delete_error": "‘{path}’ を削除する", + "backup_deleted": "バックアップは削除されました: {name}", + "backup_nothings_done": "保存するものがありません", + "backup_output_directory_forbidden": "別の出力ディレクトリを選択します。バックアップは、/bin、/boot、/dev、/etc、/lib、/root、/run、/sbin、/sys、/usr、/var、または/home/yunohost.backup/archives のサブフォルダには作成できません", + "backup_output_directory_not_empty": "空の出力ディレクトリを選択する必要があります", + "backup_output_directory_required": "バックアップ用の出力ディレクトリを指定する必要があります", + "backup_hook_unknown": "バックアップ フック '{hook}' が不明です", + "backup_method_copy_finished": "バックアップコピーがファイナライズされました", + "backup_method_tar_finished": "TARバックアップアーカイブが作成されました", + "backup_output_symlink_dir_broken": "アーカイブディレクトリ '{path}' は壊れたシンボリックリンクです。たぶん、あなたはそれが指す記憶媒体を再/マウントまたは差し込むのを忘れました。", + "backup_mount_archive_for_restore": "復元のためにアーカイブを準備しています...", + "backup_no_uncompress_archive_dir": "そのような圧縮されていないアーカイブディレクトリはありません", + "certmanager_warning_subdomain_dns_record": "サブドメイン '{subdomain}' は '{domain}' と同じ IP アドレスに解決されません。一部の機能は、これを修正して証明書を再生成するまで使用できません。", + "config_action_disabled": "アクション '{action}' は無効になっているため実行できませんでした。制約を満たしていることを確認してください。ヘルプ: {help}", + "backup_permission": "{app}のバックアップ権限", + "backup_running_hooks": "バックアップフックを実行しています...", + "backup_system_part_failed": "「{part}」システム部分をバックアップできませんでした", + "backup_unable_to_organize_files": "簡単な方法を使用してアーカイブ内のファイルを整理できませんでした", + "backup_with_no_backup_script_for_app": "アプリ「{app}」にはバックアップスクリプトがありません。無視。", + "backup_with_no_restore_script_for_app": "{app}には復元スクリプトがないため、このアプリのバックアップを自動的に復元することはできません。", + "certmanager_acme_not_configured_for_domain": "ACMEチャレンジは、nginx confに対応するコードスニペットがないため、現在{domain}実行できません...'yunohost tools regen-conf nginx --dry-run --with-diff' を使用して、nginx の設定が最新であることを確認してください。", + "certmanager_attempt_to_renew_nonLE_cert": "ドメイン '{domain}' の証明書は、Let's Encryptによって発行されていません。自動的に更新できません!", + "certmanager_attempt_to_renew_valid_cert": "ドメイン '{domain}' の証明書の有効期限が近づいていません。(あなたが何をしているのかわかっている場合は、--forceを使用できます)", + "certmanager_cert_renew_failed": "{domains}のLet’s Encrypt 証明書更新に失敗しました", + "certmanager_cert_renew_success": "{domains}のLet’s Encrypt 証明書が更新されました", + "certmanager_cert_signing_failed": "新しい証明書に署名できませんでした", + "certmanager_certificate_fetching_or_enabling_failed": "{domain}に新しい証明書を使用しようとしましたが、機能しませんでした...", + "certmanager_domain_cert_not_selfsigned": "ドメイン {domain} の証明書は自己署名されていません。置き換えてよろしいですか(これを行うには '--force' を使用してください)?", + "certmanager_hit_rate_limit": "最近{domain}、この正確なドメインのセットに対して既に発行されている証明書が多すぎます。しばらくしてからもう一度お試しください。詳細については、https://letsencrypt.org/docs/rate-limits/ を参照してください。", + "certmanager_no_cert_file": "ドメイン {domain} (ファイル: {file}) の証明書ファイルを読み取れませんでした。", + "certmanager_self_ca_conf_file_not_found": "自己署名機関の設定ファイルが見つかりませんでした(ファイル:{file})", + "config_forbidden_readonly_type": "型 '{type}' は読み取り専用として設定できず、別の型を使用してこの値をレンダリングします (関連する引数 ID: '{id}')。", + "config_no_panel": "設定パネルが見つかりません。", + "config_unknown_filter_key": "フィルター キー '{filter_key}' が正しくありません。", + "config_validate_color": "有効な RGB 16 進色である必要があります", + "config_validate_date": "YYYY-MM-DD の形式のような有効な日付である必要があります。", + "config_validate_email": "有効なメールアドレスである必要があります", + "config_action_failed": "アクション '{action}' の実行に失敗しました: {error}", + "config_apply_failed": "新しい構成の適用に失敗しました: {error}", + "config_cant_set_value_on_section": "構成セクション全体に 1 つの値を設定することはできません。", + "config_forbidden_keyword": "キーワード '{keyword}' は予約されており、この ID の質問を含む設定パネルを作成または使用することはできません。", + "config_validate_time": "HH:MM のような有効な時刻である必要があります", + "config_validate_url": "有効なウェブ URL である必要があります", + "confirm_app_install_danger": "危険!このアプリはまだ実験的であることが知られています(明示的に動作していない場合)!あなたが何をしているのかわからない限り、おそらくそれをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません...とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", + "confirm_app_install_thirdparty": "危険!このアプリはYunoHostのアプリカタログの一部ではありません。サードパーティのアプリをインストールすると、システムの整合性とセキュリティが損なわれる可能性があります。あなたが何をしているのかわからない限り、おそらくそれをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません...とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", + "confirm_app_install_warning": "警告:このアプリは動作する可能性がありますが、YunoHostにうまく統合されていません。シングル サインオンやバックアップ/復元などの一部の機能は使用できない場合があります。とにかくインストールしますか?[{answers}] ", + "diagnosis_apps_allgood": "インストールされているすべてのアプリは、基本的なパッケージ化プラクティスを尊重します", + "diagnosis_apps_bad_quality": "このアプリケーションは現在、YunoHostのアプリケーションカタログで壊れているとフラグが付けられています。これは、メンテナが問題を修正しようとしている間の一時的な問題である可能性があります。それまでの間、このアプリのアップグレードは無効になります。", + "diagnosis_apps_broken": "このアプリケーションは現在、YunoHostのアプリケーションカタログで壊れているとフラグが付けられています。これは、メンテナが問題を修正しようとしている間の一時的な問題である可能性があります。それまでの間、このアプリのアップグレードは無効になります。", + "diagnosis_apps_deprecated_practices": "このアプリのインストール済みバージョンでは、非常に古い非推奨のパッケージ化プラクティスがまだ使用されています。あなたは本当にそれをアップグレードすることを検討する必要があります。", + "diagnosis_basesystem_hardware": "サーバーのハードウェア アーキテクチャが{virt} {arch}", + "diagnosis_basesystem_hardware_model": "サーバーモデルが{model}", + "diagnosis_apps_issue": "アプリ '{app}' をアップグレードする", + "diagnosis_apps_not_in_app_catalog": "このアプリケーションは、YunoHostのアプリケーションカタログにはありません。過去に存在し、削除された場合は、アップグレードを受け取らず、システムの整合性とセキュリティが損なわれる可能性があるため、このアプリのアンインストールを検討する必要があります。", + "diagnosis_apps_outdated_ynh_requirement": "このアプリのインストール済みバージョンには、yunohost >= 2.xまたは3.xのみが必要であり、推奨されるパッケージングプラクティスとヘルパーが最新ではないことを示す傾向があります。あなたは本当にそれをアップグレードすることを検討する必要があります。", + "diagnosis_backports_in_sources_list": "apt(パッケージマネージャー)はバックポートリポジトリを使用するように構成されているようです。あなたが何をしているのか本当にわからない限り、バックポートからパッケージをインストールすることは、システムに不安定性や競合を引き起こす可能性があるため、強くお勧めしません。", + "diagnosis_basesystem_host": "サーバは Debian {debian_version} を実行しています", + "diagnosis_basesystem_kernel": "サーバーはLinuxカーネル{kernel_version}を実行しています", + "diagnosis_basesystem_ynh_inconsistent_versions": "一貫性のないバージョンのYunoHostパッケージを実行しています...ほとんどの場合、アップグレードの失敗または部分的なことが原因です。", + "diagnosis_basesystem_ynh_main_version": "サーバーがYunoHost{main_version}を実行しています({repo})", + "diagnosis_basesystem_ynh_single_version": "{package}バージョン:{version}({repo})", + "diagnosis_cache_still_valid": "(キャッシュは{category}診断に有効です。まだ再診断しません!", + "diagnosis_description_regenconf": "システム設定", + "diagnosis_description_services": "サービスステータスチェック", + "diagnosis_description_systemresources": "システムリソース", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "ストレージ<0>(デバイス<1>上)には、( )残りの領域({free_percent} )しかありません{free}。{total}注意してください。", + "diagnosis_diskusage_ok": "ストレージ<0>(デバイス<1>上)にはまだ({free_percent}%)スペースが{free}残っています(から{total})!", + "diagnosis_diskusage_verylow": "ストレージ<0>(デバイス<1>上)には、( )残りの領域({free_percent} )しかありません{free}。{total}あなたは本当にいくつかのスペースをきれいにすることを検討する必要があります!", + "diagnosis_display_tip": "見つかった問題を確認するには、ウェブ管理者の診断セクションに移動するか、コマンドラインから「yunohost診断ショー--問題--人間が読める」を実行します。", + "diagnosis_dns_bad_conf": "一部の DNS レコードが見つからないか、ドメイン {domain} (カテゴリ {category}) が正しくない", + "diagnosis_dns_discrepancy": "次の DNS レコードは、推奨される構成に従っていないようです。
種類: <0>
名前: <1>
現在の値: <2>
期待値: <3>", + "diagnosis_dns_good_conf": "DNS レコードがドメイン {domain} (カテゴリ {category}) 用に正しく構成されている", + "diagnosis_dns_missing_record": "推奨される DNS 構成に従って、次の情報を含む DNS レコードを追加する必要があります。
種類: <0>
名前: <1>
価値: <2>", + "diagnosis_dns_point_to_doc": "DNS レコードの構成についてサポートが必要な場合は 、https://yunohost.org/dns_config のドキュメントを確認してください。", + "diagnosis_dns_specialusedomain": "ドメイン {domain} は、.local や .test などの特殊な用途のトップレベル ドメイン (TLD) に基づいているため、実際の DNS レコードを持つことは想定されていません。", + "diagnosis_dns_try_dyndns_update_force": "このドメインのDNS設定は、YunoHostによって自動的に管理されます。そうでない場合は、 yunohost dyndns update --force を使用して更新を強制することができます。", + "diagnosis_domain_expiration_error": "一部のドメインはすぐに期限切れになります!", + "diagnosis_failed_for_category": "カテゴリ '{category}' の診断に失敗しました: {error}", + "diagnosis_domain_expiration_not_found": "一部のドメインの有効期限を確認できない", + "diagnosis_domain_expiration_not_found_details": "ドメイン{domain}のWHOIS情報に有効期限に関する情報が含まれていないようですね?", + "diagnosis_found_errors": "{category}に関連する{errors}重大な問題が見つかりました!", + "diagnosis_domain_expiration_success": "ドメインは登録されており、すぐに期限切れになることはありません。", + "diagnosis_domain_expiration_warning": "一部のドメインはまもなく期限切れになります!", + "diagnosis_domain_expires_in": "{domain} の有効期限は {days}日です。", + "diagnosis_found_errors_and_warnings": "{category}に関連する重大な問題が{errors}(および{warnings}の警告)見つかりました!", + "diagnosis_found_warnings": "{category}{warnings}改善できるアイテムが見つかりました。", + "diagnosis_domain_not_found_details": "ドメイン{domain}がWHOISデータベースに存在しないか、有効期限が切れています!", + "diagnosis_everything_ok": "{category}はすべて大丈夫そうです!", + "diagnosis_failed": "カテゴリ '{category}' の診断結果を取得できませんでした: {error}", + "diagnosis_http_connection_error": "接続エラー: 要求されたドメインに接続できませんでした。到達できない可能性が非常に高いです。", + "diagnosis_http_could_not_diagnose": "ドメインが IPv{ipversion} の外部から到達可能かどうかを診断できませんでした。", + "diagnosis_http_could_not_diagnose_details": "エラー: {error}", + "diagnosis_http_hairpinning_issue": "ローカルネットワークでヘアピニングが有効になっていないようです。", + "diagnosis_http_nginx_conf_not_up_to_date": "このドメインのnginx設定は手動で変更されたようで、YunoHostがHTTPで到達可能かどうかを診断できません。", + "diagnosis_http_nginx_conf_not_up_to_date_details": "状況を修正するには、コマンドラインからの違いを調べて、 yunohostツールregen-conf nginx --dry-run --with-diff を使用し、問題がない場合は、 yunohostツールregen-conf nginx --forceで変更を適用します。", + "diagnosis_http_ok": "ドメイン {domain} は、ローカル ネットワークの外部から HTTP 経由で到達できます。", + "diagnosis_http_partially_unreachable": "ドメイン {domain} は、IPv{passed} では機能しますが、IPv{failed} ではローカル ネットワークの外部から HTTP 経由で到達できないように見えます。", + "diagnosis_http_special_use_tld": "ドメイン {domain} は、.local や .test などの特殊な用途のトップレベル ドメイン (TLD) に基づいているため、ローカル ネットワークの外部に公開されることは想定されていません。", + "diagnosis_http_timeout": "外部からサーバーに接続しようとしているときにタイムアウトしました。到達できないようです。
1.この問題の最も一般的な原因は、ポート80(および443)が サーバーに正しく転送されていないことです。
2. サービスnginxが実行されていることも確認する必要があります
3.より複雑なセットアップでは、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_http_unreachable": "ドメイン {domain} は、ローカル ネットワークの外部から HTTP 経由で到達できないように見えます。", + "diagnosis_ip_broken_dnsresolution": "ドメイン名の解決が何らかの理由で壊れているようです...ファイアウォールはDNSリクエストをブロックしていますか?", + "diagnosis_ip_broken_resolvconf": "ドメインの名前解決がサーバー上で壊れているようですが、これは/etc/resolv.conf127.0.0.1を指定していないことに関連しているようです。", + "diagnosis_ip_connected_ipv4": "サーバーはIPv4経由でインターネットに接続されています!", + "diagnosis_ip_connected_ipv6": "サーバーはIPv6経由でインターネットに接続されています!", + "diagnosis_ip_global": "グローバルIP: {global}", + "diagnosis_ip_local": "ローカル IP: {local}", + "diagnosis_ip_no_ipv4": "サーバーに機能している IPv4 がありません。", + "diagnosis_ip_no_ipv6": "サーバーに機能している IPv6 がありません。", + "diagnosis_ip_no_ipv6_tip": "IPv6を機能させることは、サーバーが機能するために必須ではありませんが、インターネット全体の健全性にとってはより良いことです。IPv6 は通常、システムまたはプロバイダー (使用可能な場合) によって自動的に構成されます。それ以外の場合は、こちらのドキュメントで説明されているように、いくつかのことを手動で構成する必要があります。 https://yunohost.org/#/ipv6。IPv6を有効にできない場合、または技術的に難しすぎると思われる場合は、この警告を無視しても問題ありません。", + "diagnosis_mail_blacklist_reason": "ブラックリストの登録理由は次のとおりです: {reason}", + "diagnosis_mail_blacklist_website": "リストされている理由を特定して修正した後、IPまたはドメインを削除するように依頼してください: {blacklist_website}", + "diagnosis_mail_ehlo_bad_answer": "SMTP 以外のサービスが IPv{ipversion} のポート 25 で応答しました", + "diagnosis_mail_ehlo_bad_answer_details": "あなたのサーバーの代わりに別のマシンが応答していることが原因である可能性があります。", + "diagnosis_mail_ehlo_could_not_diagnose": "メール サーバ(postfix)が IPv{ipversion} の外部から到達可能かどうかを診断できませんでした。", + "diagnosis_mail_ehlo_ok": "SMTPメールサーバーは外部から到達可能であるため、電子メールを受信できます!", + "diagnosis_mail_ehlo_unreachable": "SMTP メール サーバは、IPv{ipversion} の外部から到達できません。メールを受信できません。", + "diagnosis_mail_ehlo_unreachable_details": "ポート 25 で IPv{ipversion} のサーバーへの接続を開くことができませんでした。到達できないようです。
1.この問題の最も一般的な原因は、ポート25 がサーバーに正しく転送されていないことです。
2. また、サービス接尾辞が実行されていることも確認する必要があります。
3.より複雑なセットアップでは、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_mail_ehlo_wrong": "別の SMTP メール サーバーが IPv{ipversion} で応答します。サーバーはおそらく電子メールを受信できないでしょう。", + "diagnosis_mail_ehlo_wrong_details": "リモート診断ツールが IPv{ipversion} で受信した EHLO は、サーバーのドメインとは異なります。
受信したEHLO: <1>
期待: <2>
この問題の最も一般的な原因は、ポート 25 が サーバーに正しく転送されていないことです。または、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "逆引き DNS が IPv{ipversion} 用に正しく構成されていません。一部のメールは配信されないか、スパムとしてフラグが立てられる場合があります。", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "現在の逆引きDNS: <0>
期待値: <1>", + "diagnosis_mail_fcrdns_dns_missing": "IPv{ipversion} では逆引き DNS は定義されていません。一部のメールは配信されないか、スパムとしてフラグが立てられる場合があります。", + "diagnosis_mail_fcrdns_nok_alternatives_6": "一部のプロバイダーでは、逆引きDNSを構成できません(または機能が壊れている可能性があります...)。逆引きDNSがIPv4用に正しく設定されている場合は、 yunohost設定email.smtp.smtp_allow_ipv6-vオフに設定して、メールを送信するときにIPv6の使用を無効にしてみてください。注:この最後の解決策は、そこにあるいくつかのIPv6専用サーバーから電子メールを送受信できないことを意味します。", + "diagnosis_mail_fcrdns_nok_details": "まず、インターネットルーターインターフェイスまたはホスティングプロバイダーインターフェイスで <0> 逆引きDNSを構成してみてください。(一部のホスティングプロバイダーでは、このためのサポートチケットを送信する必要がある場合があります)。", + "diagnosis_mail_outgoing_port_25_blocked": "送信ポート 25 が IPv{ipversion} でブロックされているため、SMTP メール サーバーは他のサーバーに電子メールを送信できません。", + "diagnosis_mail_outgoing_port_25_blocked_details": "まず、インターネットルーターインターフェイスまたはホスティングプロバイダーインターフェイスの送信ポート25のブロックを解除する必要があります。(一部のホスティングプロバイダーでは、このために問い合わせを行う必要がある場合があります)。", + "diagnosis_never_ran_yet": "このサーバーは最近セットアップされたようで、表示する診断レポートはまだありません。Web管理画面またはコマンドラインから ’yunohost diagnosis run’ を実行して、完全な診断を実行することから始める必要があります。", + "diagnosis_package_installed_from_sury": "一部のシステムパッケージはダウングレードする必要があります", + "diagnosis_processes_killed_by_oom_reaper": "一部のプロセスは、メモリが不足したため、最近システムによって強制終了されました。これは通常、システム上のメモリ不足、またはプロセスがメモリを消費しすぎていることを示しています。強制終了されたプロセスの概要:\n{kills_summary}", + "diagnosis_ram_low": "システムには{available}({available_percent}%)の使用可能なRAMがあります({total}のうち)。注意してください。", + "diagnosis_package_installed_from_sury_details": "一部のパッケージは、Suryと呼ばれるサードパーティのリポジトリから誤ってインストールされました。YunoHostチームはこれらのパッケージを処理する戦略を改善しましたが、Stretchを使用している間にPHP7.3アプリをインストールした一部のセットアップには、いくつかの矛盾が残っていると予想されます。この状況を修正するには、次のコマンドを実行してみてください。 {cmd_to_fix}", + "diagnosis_ports_could_not_diagnose": "IPv{ipversion} で外部からポートに到達できるかどうかを診断できませんでした。", + "diagnosis_ports_could_not_diagnose_details": "エラー: {error}", + "diagnosis_ports_unreachable": "ポート {port} は外部から到達できません。", + "diagnosis_regenconf_allgood": "すべての構成ファイルは、推奨される構成と一致しています!", + "diagnosis_regenconf_manually_modified": "{file} 構成ファイルが手動で変更されたようです。", + "diagnosis_regenconf_manually_modified_details": "あなたが何をしているのかを知っていれば、これはおそらく大丈夫です!YunoHostはこのファイルの自動更新を停止します... ただし、YunoHostのアップグレードには重要な推奨変更が含まれている可能性があることに注意してください。必要に応じて、yunohost tools regen-conf {category} --dry-run --with-diffで違いを調べ、yunohost tools regen-conf {category} --forceを使用して推奨構成に強制的にリセットすることができます", + "diagnosis_rootfstotalspace_critical": "ルートファイルシステムには合計{space}しかありませんが、これは非常に心配な値です!ディスク容量がすぐに枯渇する可能性があります。ルートファイルシステム用には少なくとも16GBを用意することをお勧めします。", + "diagnosis_ram_ok": "システムには、{total}のうち{available} ({available_percent}%) の RAM がまだ使用可能です。", + "diagnosis_ram_verylow": "システムには{available}({available_percent}%)のRAMしか使用できません。({total}のうち)", + "diagnosis_rootfstotalspace_warning": "ルートファイルシステムには合計{space}しかありません。これは問題ないかもしれませんが、最終的にはディスク容量がすぐに枯渇する可能性があるため、注意してください... ルートファイルシステム用に少なくとも16GBを用意することをお勧めします。", + "diagnosis_security_vulnerable_to_meltdown_details": "これを修正するには、システムをアップグレードして再起動し、新しいLinuxカーネルをロードする必要があります(または、これが機能しない場合はサーバープロバイダーに連絡してください)。詳細については、https://meltdownattack.com/ を参照してください。", + "diagnosis_services_bad_status": "サービス{service} のステータスは {status} です :(", + "diagnosis_services_bad_status_tip": "サービスの再起動を試みることができ、それが機能しない場合は、webadminのサービスログを確認してください(コマンドラインから、yunohostサービスの再起動{service}とyunohostサービスログ{service}を使用してこれを行うことができます)。。", + "diagnosis_sshd_config_inconsistent_details": "security.ssh.ssh_port -v YOUR_SSH_PORT に設定された yunohost 設定を実行して SSH ポートを定義し、yunohost tools regen-conf ssh --dry-run --with-diff および yunohost tools regen-conf ssh --force をチェックして、会議を YunoHost の推奨事項にリセットしてください。", + "diagnosis_sshd_config_insecure": "SSH構成は手動で変更されたようで、許可されたユーザーへのアクセスを制限するための「許可グループ」または「許可ユーザー」ディレクティブが含まれていないため、安全ではありません。", + "diagnosis_swap_tip": "サーバーがSDカードまたはSSDストレージでスワップをホストしている場合、デバイスの平均寿命が大幅に短くなる可能性があることに注意してください。", + "diagnosis_unknown_categories": "次のカテゴリは不明です: {categories}", + "diagnosis_using_stable_codename": "apt (システムのパッケージマネージャ) は現在、現在の Debian バージョン (bullseye) のコードネームではなく、コードネーム 'stable' からパッケージをインストールするように設定されています。", + "disk_space_not_sufficient_install": "このアプリケーションをインストールするのに十分なディスク領域が残っていません", + "diagnosis_using_stable_codename_details": "これは通常、ホスティングプロバイダーからの構成が正しくないことが原因です。なぜなら、Debian の次のバージョンが新しい「安定版」になるとすぐに、apt は適切な移行手順を経ずにすべてのシステムパッケージをアップグレードしたくなるからです。ベース Debian リポジトリの apt ソースを編集してこれを修正し、安定版キーワードを bullseye に置き換えることをお勧めします。対応する設定ファイルは /etc/apt/sources.list、または /etc/apt/sources.list.d/ 内のファイルでなければなりません。", + "diagnosis_using_yunohost_testing": "apt (システムのパッケージマネージャー)は現在、YunoHostコアの「テスト」アップグレードをインストールするように構成されています。", + "diagnosis_using_yunohost_testing_details": "自分が何をしているのかを知っていれば、これはおそらく問題ありませんが、YunoHostのアップグレードをインストールする前にリリースノートに注意してください!「テスト版」のアップグレードを無効にしたい場合は、/etc/apt/sources.list.d/yunohost.list から testing キーワードを削除する必要があります。", + "disk_space_not_sufficient_update": "このアプリケーションを更新するのに十分なディスク領域が残っていません", + "domain_cannot_add_muc_upload": "「muc.」で始まるドメインを追加することはできません。この種の名前は、YunoHostに統合されたXMPPマルチユーザーチャット機能のために予約されています。", + "domain_cannot_add_xmpp_upload": "「xmpp-upload」で始まるドメインを追加することはできません。この種の名前は、YunoHostに統合されたXMPPアップロード機能のために予約されています。", + "domain_cannot_remove_main": "'{domain}'はメインドメインなので削除できないので、まず「yunohost domain main-domain -n」を使用して別のドメインをメインドメインとして設定する必要があります。 候補 ドメインのリストは次のとおりです。 {other_domains}", + "domain_config_api_protocol": "API プロトコル", + "domain_cannot_remove_main_add_new_one": "「{domain}」はメインドメインであり唯一のドメインであるため、最初に「yunohostドメイン追加」を使用して別のドメインを追加し、次に「yunohostドメインメインドメイン-n 」を使用してメインドメインとして設定し、「yunohostドメイン削除{domain}」を使用してドメイン「{domain}」を削除する必要があります。", + "domain_config_acme_eligible_explain": "このドメインは、Let's Encrypt証明書の準備ができていないようです。DNS 構成と HTTP サーバーの到達可能性を確認してください。 診断ページの 「DNSレコード」と「Web」セクションは、何が誤って構成されているかを理解するのに役立ちます。", + "domain_config_auth_application_key": "アプリケーションキー", + "domain_config_auth_application_secret": "アプリケーション秘密鍵", + "domain_config_auth_consumer_key": "消費者キー", + "domain_config_auth_entrypoint": "API エントリ ポイント", + "domain_config_default_app": "デフォルトのアプリ", + "domain_config_default_app_help": "このドメインを開くと、ユーザーは自動的にこのアプリにリダイレクトされます。アプリが指定されていない場合、ユーザーはユーザーポータルのログインフォームにリダイレクトされます。", + "domain_config_mail_in": "受信メール", + "domain_config_auth_key": "認証キー", + "domain_config_auth_secret": "認証シークレット", + "domain_config_auth_token": "認証トークン", + "domain_config_cert_install": "Let's Encrypt証明書をインストールする", + "domain_config_cert_issuer": "証明機関", + "domain_config_cert_no_checks": "診断チェックを無視する", + "domain_config_cert_renew": "Let’s Encrypt証明書を更新する", + "domain_config_cert_renew_help": "証明書は、有効期間の最後の 15 日間に自動的に更新されます。必要に応じて手動で更新できます(推奨されません)。", + "domain_config_cert_summary_letsencrypt": "やった!有効なLet's Encrypt証明書を使用しています!", + "domain_config_cert_summary_ok": "さて、現在の証明書は良さそうです!", + "domain_config_cert_summary_selfsigned": "警告: 現在の証明書は自己署名です。ブラウザは新しい訪問者に不気味な警告を表示します!", + "domain_config_mail_out": "送信メール", + "domain_config_xmpp_help": "注意: 一部のXMPP機能では、DNSレコードを更新し、Lets Encrypt 証明書を再生成して有効にする必要があります", + "domain_created": "作成されたドメイン", + "domain_creation_failed": "ドメイン {domain}を作成できません: {error}", + "domain_deleted": "ドメインが削除されました", + "domain_deletion_failed": "ドメイン {domain}を削除できません: {error}", + "domain_dns_push_failed_to_authenticate": "ドメイン '{domain}' のレジストラーの API で認証に失敗しました。おそらく資格情報が正しくないようです?(エラー: {error})", + "domain_dns_push_failed_to_list": "レジストラの API を使用して現在のレコードを一覧表示できませんでした: {error}", + "domain_dns_push_not_applicable": "自動 DNS 構成機能は、ドメイン {domain}には適用されません。https://yunohost.org/dns_config のドキュメントに従って、DNS レコードを手動で構成する必要があります。", + "domain_dns_push_managed_in_parent_domain": "自動 DNS 構成機能は、親ドメイン {parent_domain}で管理されます。", + "domain_dns_push_partial_failure": "DNS レコードが部分的に更新されました: いくつかの警告/エラーが報告されました。", + "domain_dns_push_record_failed": "{action} {type}/{name} の記録に失敗しました: {error}", + "domain_dns_push_success": "DNS レコードが更新されました!", + "domain_dns_pushing": "DNS レコードをプッシュしています...", + "domain_dns_registrar_experimental": "これまでのところ、**{registrar}**のAPIとのインターフェースは、YunoHostコミュニティによって適切にテストおよびレビューされていません。サポートは**非常に実験的**です-注意してください!", + "domain_dns_registrar_managed_in_parent_domain": "このドメインは{parent_domain_link}のサブドメインです。DNS レジストラーの構成は、{parent_domain}の設定パネルで管理する必要があります。", + "domain_dns_registrar_not_supported": "YunoHost は、このドメインを処理するレジストラを自動的に検出できませんでした。DNS レコードは、https://yunohost.org/dns のドキュメントに従って手動で構成する必要があります。", + "domain_dns_registrar_supported": "YunoHost は、このドメインがレジストラ**{registrar}**によって処理されていることを自動的に検出しました。必要に応じて、適切なAPI資格情報を提供すると、YunoHostはこのDNSゾーンを自動的に構成します。API 資格情報の取得方法に関するドキュメントは、https://yunohost.org/registar_api_{registrar} ページにあります。(https://yunohost.org/dns のドキュメントに従ってDNSレコードを手動で構成することもできます)", + "domain_dns_registrar_yunohost": "このドメインは nohost.me / nohost.st / ynh.fr であるため、そのDNS構成は、それ以上の構成なしでYunoHostによって自動的に処理されます。(「YunoHost Dyndns Update」コマンドを参照)", + "domain_dyndns_root_unknown": "'{domain}' ドメインのルートを '{name}' にリダイレクト", + "domain_exists": "この名前のバックアップアーカイブはすでに存在します。", + "domain_hostname_failed": "新しいホスト名を設定できません。これにより、後で問題が発生する可能性があります(問題ない可能性があります)。", + "domain_registrar_is_not_configured": "レジストラーは、ドメイン {domain} 用にまだ構成されていません。", + "domain_remove_confirm_apps_removal": "このドメインを削除すると、これらのアプリケーションが削除されます。\n{apps}\n\nよろしいですか?[{answers}]", + "domain_uninstall_app_first": "これらのアプリケーションは、ドメインに引き続きインストールされます。\n{apps}\n\nドメインの削除に進む前に、「yunohostアプリ削除the_app_id」を使用してアンインストールするか、「yunohostアプリ変更URL the_app_id」を使用して別のドメインに移動してください。", + "domain_unknown": "ドメイン {domain}を作成できません: {error}", + "domains_available": "ドメイン管理", + "done": "完了", + "downloading": "ダウンロード中...", + "dpkg_is_broken": "dpkg / APT(システムパッケージマネージャー)が壊れた状態にあるように見えるため、現在はこれを行うことができません...SSH経由で接続し、 'sudo apt install --fix-broken'および/または 'sudo dpkg --configure -a'および/または 'sudo dpkg --audit'を実行することで、この問題を解決しようとすることができます。", + "dpkg_lock_not_available": "別のプログラムがdpkg(システムパッケージマネージャー)のロックを使用しているように見えるため、このコマンドは現在実行できません", + "dyndns_could_not_check_available": "{domain}{provider}で利用できるかどうかを確認できませんでした。", + "dyndns_ip_update_failed": "IP アドレスを DynDNS に更新できませんでした", + "dyndns_ip_updated": "DynDNSでIPを更新しました", + "dyndns_no_domain_registered": "このカテゴリーにログが登録されていません", + "dyndns_provider_unreachable": "DynDNSプロバイダー{provider}に到達できません:YunoHostがインターネットに正しく接続されていないか、ダイネットサーバーがダウンしています。", + "dyndns_registered": "このカテゴリーにログが登録されていません", + "dyndns_registration_failed": "DynDNS ドメインを登録できませんでした: {error}", + "dyndns_unavailable": "ドメイン {domain}を作成できません: {error}", + "dyndns_domain_not_provided": "DynDNS プロバイダー{provider}ドメイン{domain}を提供できません。", + "extracting": "抽出。。。", + "field_invalid": "フィールドは必要です。", + "file_does_not_exist": "ファイル {path}が存在しません。", + "firewall_reloaded": "ファイアウォールがリロードされました", + "firewall_rules_cmd_failed": "一部のファイアウォール規則コマンドが失敗しました。ログの詳細情報。", + "global_settings_reset_success": "グローバルIP: {global}", + "global_settings_setting_admin_strength": "管理者パスワードの強度要件", + "global_settings_setting_admin_strength_help": "これらの要件は、パスワードを初期化または変更する場合にのみ適用されます", + "global_settings_setting_backup_compress_tar_archives": "バックアップの圧縮", + "global_settings_setting_backup_compress_tar_archives_help": "新しいバックアップを作成するときは、圧縮されていないアーカイブ (.tar) ではなく、アーカイブを圧縮 (.tar.gz) します。注意:このオプションを有効にすると、バックアップアーカイブの作成が軽くなりますが、最初のバックアップ手順が大幅に長くなり、CPUに負担がかかります。", + "global_settings_setting_dns_exposure": "DNS の構成と診断で考慮すべき IP バージョン", + "global_settings_setting_dns_exposure_help": "注意:これは、推奨されるDNS構成と診断チェックにのみ影響します。これはシステム構成には影響しません。", + "global_settings_setting_nginx_compatibility": "NGINXの互換性", + "global_settings_setting_nginx_compatibility_help": "WebサーバーNGINXの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します", + "global_settings_setting_nginx_redirect_to_https": "HTTPSを強制", + "global_settings_setting_nginx_redirect_to_https_help": "デフォルトでHTTPリクエストをHTTPにリダイレクトします(あなたが何をしているのか本当にわからない限り、オフにしないでください!", + "global_settings_setting_passwordless_sudo": "管理者がパスワードを再入力せずに「sudo」を使用できるようにする", + "global_settings_setting_portal_theme_help": "カスタム ポータル テーマの作成の詳細については、https://yunohost.org/theming を参照してください。", + "global_settings_setting_postfix_compatibility": "後置の互換性", + "global_settings_setting_pop3_enabled": "POP3 を有効にする", + "global_settings_setting_pop3_enabled_help": "メール サーバーの POP3 プロトコルを有効にする", + "global_settings_setting_portal_theme": "ユーザーポータルでタイルに表示する", + "global_settings_setting_root_access_explain": "Linux システムでは、「ルート」が絶対管理者です。YunoHost のコンテキストでは、サーバーのローカルネットワークからを除き、直接の「ルート」SSH ログインはデフォルトで無効になっています。'admins' グループのメンバーは、sudo コマンドを使用して、コマンドラインから root として動作できます。ただし、何らかの理由で通常の管理者がログインできなくなった場合に、システムをデバッグするための(堅牢な)rootパスワードがあると便利です。", + "global_settings_setting_security_experimental_enabled": "実験的なセキュリティ機能", + "global_settings_setting_security_experimental_enabled_help": "実験的なセキュリティ機能を有効にします(何をしているのかわからない場合は有効にしないでください)。", + "global_settings_setting_smtp_allow_ipv6_help": "IPv6 を使用したメールの送受信を許可する", + "global_settings_setting_smtp_relay_enabled": "SMTP リレーを有効にする", + "global_settings_setting_smtp_relay_enabled_help": "この yunohost インスタンスの代わりにメールを送信するために使用する SMTP リレーを有効にします。このような状況のいずれかにある場合に便利です:25ポートがISPまたはVPSプロバイダーによってブロックされている、DUHLにリストされている住宅用IPがある、逆引きDNSを構成できない、またはこのサーバーがインターネットに直接公開されておらず、他のものを使用してメールを送信したい。", + "global_settings_setting_smtp_relay_host": "SMTP リレー ホスト", + "global_settings_setting_smtp_relay_password": "SMTP リレー パスワード", + "global_settings_setting_smtp_relay_port": "SMTP リレー ポート", + "global_settings_setting_smtp_relay_user": "SMTP リレー ユーザー", + "global_settings_setting_ssh_compatibility": "SSH の互換性", + "global_settings_setting_ssh_compatibility_help": "SSHサーバーの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します。詳細については、https://infosec.mozilla.org/guidelines/openssh を参照してください。", + "global_settings_setting_ssh_password_authentication": "パスワード認証", + "global_settings_setting_ssh_password_authentication_help": "SSH のパスワード認証を許可する", + "global_settings_setting_ssh_port": "SSH ポート", + "global_settings_setting_ssowat_panel_overlay_enabled": "アプリで小さな「YunoHost」ポータルショートカットの正方形を有効にします", + "global_settings_setting_user_strength": "ユーザー パスワードの強度要件", + "global_settings_setting_webadmin_allowlist_help": "ウェブ管理者へのアクセスを許可されたIPアドレス。", + "global_settings_setting_webadmin_allowlist": "ウェブ管理者 IP 許可リスト", + "global_settings_setting_webadmin_allowlist_enabled": "ウェブ管理 IP 許可リストを有効にする", + "global_settings_setting_webadmin_allowlist_enabled_help": "一部の IP のみにウェブ管理者へのアクセスを許可します。", + "good_practices_about_admin_password": "次に、新しい管理パスワードを定義しようとしています。パスワードは8文字以上である必要がありますが、より長いパスワード(パスフレーズなど)を使用したり、さまざまな文字(大文字、小文字、数字、特殊文字)を使用したりすることをお勧めします。", + "good_practices_about_user_password": "次に、新しいユーザー・パスワードを定義しようとしています。パスワードは少なくとも8文字の長さである必要がありますが、より長いパスワード(パスフレーズなど)や、さまざまな文字(大文字、小文字、数字、特殊文字)を使用することをお勧めします。", + "group_already_exist": "グループ {group} は既に存在します", + "group_already_exist_on_system": "グループ {group} はシステム グループに既に存在します。", + "group_already_exist_on_system_but_removing_it": "グループ{group}はすでにシステムグループに存在しますが、YunoHostはそれを削除します...", + "group_cannot_edit_all_users": "グループ 'all_users' は手動で編集できません。これは、YunoHostに登録されているすべてのユーザーを含むことを目的とした特別なグループです", + "invalid_shell": "無効なシェル: {shell}", + "ip6tables_unavailable": "ここではip6tablesを使うことはできません。あなたはコンテナ内にいるか、カーネルがサポートしていません", + "group_cannot_edit_primary_group": "グループ '{group}' を手動で編集することはできません。これは、特定のユーザーを 1 人だけ含むためのプライマリ グループです。", + "group_cannot_edit_visitors": "グループの「訪問者」を手動で編集することはできません。匿名の訪問者を代表する特別なグループです", + "group_creation_failed": "グループ '{group}' を作成できませんでした: {error}", + "group_deleted": "グループ '{group}' が削除されました", + "group_deletion_failed": "グループ '{group}' を削除できませんでした: {error}", + "group_update_aliases": "グループ '{group}' のエイリアスの更新", + "group_update_failed": "グループ '{group}' を更新できませんでした: {error}", + "group_updated": "グループ '{group}' が更新されました", + "group_user_add": "ユーザー '{user}' がグループ '{group}' に追加されます。", + "hook_json_return_error": "フック{path}からリターンを読み取れませんでした。エラー: {msg}. 生のコンテンツ: {raw_content}", + "hook_list_by_invalid": "このプロパティは、フックを一覧表示するために使用することはできません", + "hook_name_unknown": "不明なフック名 '{name}'", + "installation_complete": "インストールが完了しました", + "invalid_credentials": "無効なパスワードまたはユーザー名", + "invalid_number": "数値にする必要があります", + "invalid_number_max": "{max}より小さくする必要があります", + "invalid_number_min": "{min}より大きい値にする必要があります", + "invalid_regex": "無効な正規表現: '{regex}'", + "iptables_unavailable": "ここではiptablesを使うことはできません。あなたはコンテナ内にいるか、カーネルがサポートしていません", + "ldap_attribute_already_exists": "LDAP 属性 '{attribute}' は、値 '{value}' で既に存在します。", + "ldap_server_down": "LDAP サーバーに到達できません", + "ldap_server_is_down_restart_it": "LDAP サービスがダウンしています。再起動を試みます...", + "log_app_action_run": "{} アプリのアクションの実行", + "log_app_change_url": "{} アプリのアクセスURLを変更", + "log_app_config_set": "‘{}’ アプリに設定を適用する", + "log_app_makedefault": "‘{}’ をデフォルトのアプリにする", + "log_app_remove": "「{}」アプリを削除する", + "log_app_upgrade": "「{}」アプリをアップグレードする", + "log_available_on_yunopaste": "このログは、{url}", + "log_backup_create": "バックアップアーカイブを作成する", + "log_backup_restore_app": "バックアップを復元する ‘{name}’", + "log_backup_restore_system": "収集したファイルからバックアップアーカイブを作成しています...", + "log_corrupted_md_file": "ログに関連付けられている YAML メタデータ ファイルが破損しています: '{md_file}\nエラー: {error}'", + "log_does_exists": "「{log}」という名前の操作ログはありません。「yunohostログリスト」を使用して、利用可能なすべての操作ログを表示します", + "log_domain_add": "ドメイン ‘{name}’ を追加する", + "log_domain_config_set": "ドメイン '{}' の構成を更新する", + "log_domain_dns_push": "‘{name}’ DNSレコードを登録する", + "log_domain_main_domain": "「{}」をメインドメインにする", + "log_domain_remove": "システム構成から「{}」ドメインを削除する", + "log_dyndns_subscribe": "YunoHostコアのアップグレードを開始しています...", + "log_dyndns_update": "YunoHostサブドメイン「{}」に関連付けられているIPを更新します", + "log_help_to_get_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、「yunohostログ共有{name}」コマンドを使用してこの操作の完全なログを共有してください", + "log_help_to_get_log": "操作「{desc}」のログを表示するには、「yunohostログショー{name}」コマンドを使用します。", + "log_letsencrypt_cert_install": "「{}」ドメインにLet's Encrypt証明書をインストールする", + "log_letsencrypt_cert_renew": "Let’s Encrypt証明書を更新する", + "log_link_to_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、 ここをクリックして この操作の完全なログを提供してください", + "log_link_to_log": "この操作の完全なログ: ''{desc}", + "log_operation_unit_unclosed_properly": "操作ユニットが正しく閉じられていません", + "log_permission_create": "作成権限 '{}'", + "log_permission_delete": "削除権限 '{}'", + "log_permission_url": "権限 '{}' に関連する URL を更新する", + "log_regen_conf": "システム設定", + "log_remove_on_failed_install": "インストールに失敗した後に「{}」を削除します", + "log_resource_snippet": "リソースのプロビジョニング/プロビジョニング解除/更新", + "log_selfsigned_cert_install": "「{}」ドメインに自己署名証明書をインストールする", + "log_user_create": "「{}」ユーザーを追加する", + "log_user_delete": "「{}」ユーザーの削除", + "log_user_group_create": "「{}」グループの作成", + "log_settings_reset": "設定をリセット", + "log_settings_reset_all": "すべての設定をリセット", + "log_settings_set": "設定を適用", + "log_tools_migrations_migrate_forward": "移行を実行する", + "log_tools_postinstall": "YunoHostサーバーをポストインストールします", + "log_tools_reboot": "サーバーを再起動", + "log_tools_shutdown": "サーバーをシャットダウン", + "log_tools_upgrade": "システムパッケージのアップグレード", + "log_user_group_delete": "「{}」グループの削除", + "log_user_group_update": "'{}' グループを更新", + "log_user_import": "ユーザーのインポート", + "mailbox_used_space_dovecot_down": "使用済みメールボックススペースをフェッチする場合は、Dovecotメールボックスサービスが稼働している必要があります", + "log_user_permission_reset": "アクセス許可 '{}' をリセットします", + "mailbox_disabled": "ユーザーの{user}に対して電子メールがオフになっている", + "main_domain_change_failed": "メインドメインを変更できません", + "main_domain_changed": "メインドメインが変更されました", + "migration_0021_cleaning_up": "キャッシュとパッケージのクリーンアップはもう役に立たなくなりました...", + "migration_0021_general_warning": "この移行はデリケートな操作であることに注意してください。YunoHostチームはそれをレビューしてテストするために最善を尽くしましたが、移行によってシステムまたはそのアプリの一部が破損する可能性があります。\n\nしたがって、次のことをお勧めします。\n - 重要なデータやアプリのバックアップを実行します。関する詳細情報: https://yunohost.org/backup\n - 移行を開始した後はしばらくお待ちください: インターネット接続とハードウェアによっては、すべてがアップグレードされるまでに最大数時間かかる場合があります。", + "migration_0021_main_upgrade": "メインアップグレードを開始しています...", + "migration_0021_not_enough_free_space": "/var/の空き容量はかなり少ないです!この移行を実行するには、少なくとも 1 GB の空き容量が必要です。", + "migration_0021_modified_files": "次のファイルは手動で変更されていることが判明し、アップグレード後に上書きされる可能性があることに注意してください: {manually_modified_files}", + "migration_0021_not_buster2": "現在の Debian ディストリビューションは Buster ではありません!すでにBuster->Bullseyeの移行を実行している場合、このエラーは移行手順が100% s成功しなかったという事実の兆候です(そうでなければ、YunoHostは完了のフラグを立てます)。Webadminのツール>ログにある移行の**完全な**ログを必要とするサポートチームで何が起こったのかを調査することをお勧めします。", + "migration_0021_patch_yunohost_conflicts": "競合の問題を回避するためにパッチを適用しています...", + "migration_0021_patching_sources_list": "sources.listsにパッチを適用しています...", + "migration_0021_problematic_apps_warning": "以下の問題のあるインストール済みアプリが検出されました。これらはYunoHostアプリカタログからインストールされていないか、「working」としてフラグが立てられていないようです。したがって、アップグレード後も動作することを保証することはできません: {problematic_apps}", + "migration_0021_still_on_buster_after_main_upgrade": "メインのアップグレード中に問題が発生しましたが、システムはまだDebian Busterです", + "migration_0021_system_not_fully_up_to_date": "システムが完全に最新ではありません。Bullseyeへの移行を実行する前に、まずは通常のアップグレードを実行してください。", + "migration_0023_not_enough_space": "移行を実行するのに十分な領域を {path} で使用できるようにします。", + "migration_0023_postgresql_11_not_installed": "PostgreSQL がシステムにインストールされていません。何もすることはありません。", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11はインストールされていますが、PostgreSQL 13はインストールされてい!?:(システムで何か奇妙なことが起こった可能性があります...", + "migration_0024_rebuild_python_venv_broken_app": "このアプリ用にvirtualenvを簡単に再構築できないため、{app}スキップします。代わりに、「yunohostアプリのアップグレード-{app}を強制」を使用してこのアプリを強制的にアップグレードして、状況を修正する必要があります。", + "migration_0024_rebuild_python_venv_disclaimer_base": "Debian Bullseye へのアップグレード後、Debian に同梱されている新しい Python バージョンに変換するために、いくつかの Python アプリケーションを部分的に再構築する必要があります (技術的には、「virtualenv」と呼ばれるものを再作成する必要があります)。それまでの間、これらのPythonアプリケーションは機能しない可能性があります。YunoHostは、以下に詳述するように、それらのいくつかについて仮想環境の再構築を試みることができます。他のアプリの場合、または再構築の試行が失敗した場合は、それらのアプリのアップグレードを手動で強制する必要があります。", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "これらのアプリに対して Virtualenvs を自動的に再構築することはできません。あなたはそれらのアップグレードを強制する必要があります、それはコマンドラインから行うことができます: 'yunohostアプリのアップグレード - -force APP':{ignored_apps}", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "virtualenvの再構築は、次のアプリに対して試行されます(注意:操作には時間がかかる場合があります)。 {rebuild_apps}", + "migration_0024_rebuild_python_venv_failed": "{app} の Python virtualenv の再構築に失敗しました。これが解決されない限り、アプリは機能しない場合があります。「yunohostアプリのアップグレード--強制{app}」を使用してこのアプリのアップグレードを強制して、状況を修正する必要があります。", + "migration_0024_rebuild_python_venv_in_progress": "現在、 '{app}'のPython仮想環境を再構築しようとしています", + "migration_description_0021_migrate_to_bullseye": "システムを Debian ブルズアイと YunoHost 11.x にアップグレードする", + "migration_description_0022_php73_to_php74_pools": "php7.3-fpm 'pool' conf ファイルを php7.4 に移行します。", + "migration_description_0023_postgresql_11_to_13": "PostgreSQL 11 から 13 へのデータベースの移行", + "migration_description_0024_rebuild_python_venv": "ブルズアイ移行後にPythonアプリを修復する", + "migration_description_0025_global_settings_to_configpanel": "従来のグローバル設定の命名法を新しい最新の命名法に移行する", + "migration_ldap_rollback_success": "システムがロールバックされました。", + "migrations_already_ran": "これらの移行は既に完了しています: {ids}", + "migrations_dependencies_not_satisfied": "移行{id}の前に、次の移行を実行します: '{dependencies_id}'。", + "migrations_exclusive_options": "'--auto'、'--skip'、および '--force-rerun' は相互に排他的なオプションです。", + "migrations_failed_to_load_migration": "移行{id}を読み込めませんでした: {error}", + "migrations_list_conflict_pending_done": "'--previous' と '--done' の両方を同時に使用することはできません。", + "migrations_loading_migration": "移行{id}を読み込んでいます...", + "migrations_migration_has_failed": "移行{id}が完了しなかったため、中止されました。エラー: {exception}", + "migrations_must_provide_explicit_targets": "'--skip' または '--force-rerun' を使用する場合は、明示的なターゲットを指定する必要があります。", + "migrations_need_to_accept_disclaimer": "移行{id}を実行するには、次の免責事項に同意する必要があります。\n---\n{disclaimer}\n---\n移行の実行に同意する場合は、'--accept-disclaimer' オプションを指定してコマンドを再実行してください。", + "migrations_running_forward": "移行{id}を実行しています...", + "migrations_skip_migration": "移行{id}スキップしています...", + "migrations_success_forward": "移行{id}完了しました", + "migrations_to_be_ran_manually": "移行{id}は手動で実行する必要があります。ウェブ管理ページの移行→ツールに移動するか、「yunohostツールの移行実行」を実行してください。", + "not_enough_disk_space": "'{path}'に十分な空き容量がありません", + "operation_interrupted": "操作は手動で中断されたようですね?", + "migrations_no_migrations_to_run": "実行する移行はありません", + "migrations_no_such_migration": "「{id}」と呼ばれる移行はありません", + "other_available_options": "...および{n}個の表示されない他の使用可能なオプション", + "migrations_not_pending_cant_skip": "これらの移行は保留中ではないため、スキップすることはできません。 {ids}", + "migrations_pending_cant_rerun": "これらの移行はまだ保留中であるため、再度実行することはできません{ids}", + "password_confirmation_not_the_same": "パスワードが一致しません", + "password_listed": "このパスワードは、世界で最も使用されているパスワードの1つです。もっとユニークなものを選んでください。", + "password_too_long": "127文字未満のパスワードを選択してください", + "password_too_simple_2": "パスワードは8文字以上で、数字、大文字、小文字を含める必要があります", + "password_too_simple_3": "パスワードは8文字以上で、数字、大文字、小文字、特殊文字を含める必要があります", + "password_too_simple_4": "パスワードは12文字以上で、数字、大文字、小文字、特殊文字を含める必要があります", + "pattern_backup_archive_name": "最大 30 文字、英数字、-_ を含む有効なファイル名である必要があります。文字のみ", + "pattern_domain": "有効なドメイン名である必要があります(例:my-domain.org)", + "pattern_email": "「+」記号のない有効な電子メールアドレスである必要があります(例:someone@example.com)", + "pattern_email_forward": "有効な電子メールアドレスである必要があり、「+」記号が受け入れられます(例:someone+tag@example.com)", + "pattern_firstname": "有効な名前(3 文字以上)である必要があります。", + "pattern_fullname": "有効なフルネーム (3 文字以上) である必要があります。", + "pattern_lastname": "有効な姓 (3 文字以上) である必要があります。", + "pattern_mailbox_quota": "クォータを持たない場合は、接尾辞が b/k/M/G/T または 0 を含むサイズである必要があります", + "pattern_password": "3 文字以上である必要があります", + "pattern_password_app": "申し訳ありませんが、パスワードに次の文字を含めることはできません: {forbidden_chars}", + "pattern_port_or_range": "有効なポート番号(例:0-65535)またはポート範囲(例:100:200)である必要があります", + "pattern_username": "小文字の英数字とアンダースコア(_)のみにする必要があります", + "permission_already_allowed": "グループ '{group}' には既にアクセス許可 '{permission}' が有効になっています", + "permission_already_disallowed": "グループ '{group}' には既にアクセス許可 '{permission}' が無効になっています", + "permission_already_exist": "アクセス許可 '{permission}' は既に存在します", + "permission_already_up_to_date": "追加/削除要求が既に現在の状態と一致しているため、アクセス許可は更新されませんでした。", + "permission_cannot_remove_main": "メイン権限の削除は許可されていません", + "permission_cant_add_to_all_users": "権限{permission}すべてのユーザーに追加することはできません。", + "permission_created": "アクセス許可 '{permission}' が作成されました", + "permission_creation_failed": "アクセス許可 '{permission}' を作成できませんでした: {error}", + "permission_currently_allowed_for_all_users": "このアクセス許可は現在、他のユーザーに加えてすべてのユーザーに付与されています。「all_users」権限を削除するか、現在付与されている他のグループを削除することをお勧めします。", + "permission_deleted": "権限 '{permission}' が削除されました", + "permission_deletion_failed": "アクセス許可 '{permission}' を削除できませんでした: {error}", + "permission_not_found": "アクセス許可 '{permission}' が見つかりません", + "permission_protected": "アクセス許可{permission}は保護されています。このアクセス許可に対して訪問者グループを追加または削除することはできません。", + "permission_require_account": "権限{permission}は、アカウントを持つユーザーに対してのみ意味があるため、訪問者に対して有効にすることはできません。", + "permission_update_failed": "アクセス許可 '{permission}' を更新できませんでした: {error}", + "port_already_closed": "ポート {port} は既に{ip_version}接続のために閉じられています", + "port_already_opened": "ポート {port} は既に{ip_version}接続用に開かれています", + "postinstall_low_rootfsspace": "ルートファイルシステムの総容量は10GB未満で、かなり気になります。ディスク容量がすぐに不足する可能性があります。ルートファイルシステム用に少なくとも16GBを用意することをお勧めします。この警告にもかかわらずYunoHostをインストールする場合は、--force-diskspaceを使用してポストインストールを再実行してください", + "regenconf_dry_pending_applying": "カテゴリ '{category}' に適用された保留中の構成を確認しています...", + "regenconf_failed": "カテゴリの設定を再生成できませんでした: {categories}", + "regenconf_file_backed_up": "構成ファイル '{conf}' が '{backup}' にバックアップされました", + "regenconf_file_copy_failed": "新しい構成ファイル '{new}' を '{conf}' にコピーできませんでした", + "regenconf_file_kept_back": "設定ファイル '{conf}' は regen-conf (カテゴリ {category}) によって削除される予定でしたが、元に戻されました。", + "regenconf_file_manually_modified": "構成ファイル '{conf}' は手動で変更されており、更新されません", + "regenconf_file_manually_removed": "構成ファイル '{conf}' は手動で削除され、作成されません", + "regenconf_file_remove_failed": "構成ファイル '{conf}' を削除できませんでした", + "regenconf_file_removed": "構成ファイル '{conf}' が削除されました", + "regenconf_file_updated": "構成ファイル '{conf}' が更新されました", + "regenconf_need_to_explicitly_specify_ssh": "ssh構成は手動で変更されていますが、実際に変更を適用するには、--forceでカテゴリ「ssh」を明示的に指定する必要があります。", + "regenconf_now_managed_by_yunohost": "設定ファイル '{conf}' が YunoHost (カテゴリ {category}) によって管理されるようになりました。", + "regenconf_pending_applying": "カテゴリ '{category}' に保留中の構成を適用しています...", + "regenconf_up_to_date": "カテゴリ '{category}' の設定は既に最新です", + "regenconf_updated": "このカテゴリーにログが登録されていません", + "regenconf_would_be_updated": "カテゴリ '{category}' の構成が更新されているはずです。", + "regex_incompatible_with_tile": "パッケージャー!アクセス許可 '{permission}' show_tile が 'true' に設定されているため、正規表現 URL をメイン URL として定義できません", + "regex_with_only_domain": "ドメインに正規表現を使用することはできませんが、パスにのみ使用できます", + "registrar_infos": "レジストラ情報", + "restore_already_installed_app": "'{name}' の ‘{id}’ パネル設定をアップデートする", + "restore_already_installed_apps": "次のアプリは既にインストールされているため復元できません。 {apps}", + "restore_backup_too_old": "このバックアップアーカイブは、古すぎるYunoHostバージョンからのものであるため、復元できません。", + "restore_cleaning_failed": "一時復元ディレクトリをクリーンアップできませんでした", + "restore_complete": "復元が完了しました", + "restore_may_be_not_enough_disk_space": "システムに十分なスペースがないようです(空き:{free_space} B、必要なスペース:{needed_space} B、セキュリティマージン:{margin} B)", + "root_password_desynchronized": "管理者パスワードが変更されましたが、YunoHostはこれをrootパスワードに伝播できませんでした!", + "server_reboot_confirm": "サーバーはすぐに再起動しますが、よろしいですか?[{answers}]", + "server_shutdown": "サーバーがシャットダウンします", + "service_already_stopped": "サービス '{service}' は既に停止されています", + "service_cmd_exec_failed": "コマンド '{command}' を実行できませんでした", + "service_description_nginx": "サーバーでホストされているすべてのWebサイトへのアクセスを提供または提供します", + "service_description_redis-server": "高速データ・アクセス、タスク・キュー、およびプログラム間の通信に使用される特殊なデータベース", + "service_description_rspamd": "スパムやその他の電子メール関連機能をフィルタリングします", + "service_description_slapd": "ユーザー、ドメイン、関連情報を格納します", + "service_description_ssh": "ターミナル経由でサーバーにリモート接続できます(SSHプロトコル)", + "service_description_yunohost-api": "YunoHostウェブインターフェイスとシステム間の相互作用を管理します", + "service_description_yunohost-firewall": "サービスへの接続ポートの開閉を管理", + "service_description_yunomdns": "ローカルネットワークで「yunohost.local」を使用してサーバーに到達できます", + "service_disable_failed": "起動時にサービス '{service}' を開始できませんでした。\n\n最近のサービスログ:{logs}", + "service_disabled": "システムの起動時にサービス '{service}' は開始されなくなります。", + "service_reload_failed": "サービス '{service}' をリロードできませんでした\n\n最近のサービスログ:{logs}", + "service_reload_or_restart_failed": "サービス '{service}' をリロードまたは再起動できませんでした\n\n最近のサービスログ:{logs}", + "service_reloaded_or_restarted": "サービス '{service}' が再読み込みまたは再起動されました", + "service_remove_failed": "サービス '{service}' を削除できませんでした", + "service_removed": "サービス '{service}' が削除されました", + "service_restart_failed": "サービス '{service}' を再起動できませんでした\n\n最近のサービスログ:{logs}", + "service_restarted": "サービス '{service}' が再起動しました", + "service_start_failed": "サービス '{service}' を開始できませんでした\n\n最近のサービスログ:{logs}", + "service_started": "サービス '{service}' が開始されました", + "service_stop_failed": "サービス '{service}' を停止できません\n\n最近のサービスログ:{logs}", + "service_stopped": "サービス '{service}' が停止しました", + "service_unknown": "不明なサービス '{service}'", + "system_username_exists": "ユーザー名はシステムユーザーのリストにすでに存在します", + "this_action_broke_dpkg": "このアクションはdpkg / APT(システムパッケージマネージャ)を壊しました...SSH経由で接続し、「sudo apt install --fix-broken」および/または「sudo dpkg --configure -a」を実行することで、この問題を解決できます。", + "tools_upgrade": "システムパッケージのアップグレード", + "tools_upgrade_failed": "パッケージをアップグレードできませんでした: {packages_list}", + "unbackup_app": "{app}は保存されません", + "unexpected_error": "予期しない問題が発生しました:{error}", + "unknown_main_domain_path": "'{app}' の不明なドメインまたはパス。アクセス許可の URL を指定できるようにするには、ドメインとパスを指定する必要があります。", + "unrestore_app": "{app}は復元されません", + "updating_apt_cache": "システムパッケージの利用可能なアップグレードを取得しています...", + "upgrade_complete": "アップグレート完了", + "upgrading_packages": "パッケージをアップグレードしています...", + "upnp_dev_not_found": "UPnP デバイスが見つかりません", + "upnp_disabled": "UPnP がオフになりました", + "upnp_enabled": "UPnP がオンになりました", + "upnp_port_open_failed": "UPnP 経由でポートを開けませんでした", + "user_already_exists": "ユーザー '{user}' は既に存在します", + "user_created": "ユーザーが作成されました。", + "user_creation_failed": "ユーザー {user}を作成できませんでした: {error}", + "user_deleted": "ユーザーが削除されました", + "user_deletion_failed": "ユーザー {user}を削除できませんでした: {error}", + "user_home_creation_failed": "ユーザーのホームフォルダ '{home}' を作成できませんでした", + "user_import_bad_file": "CSVファイルが正しくフォーマットされていないため、データ損失の可能性を回避するために無視されます", + "user_import_bad_line": "行{line}が正しくありません: {details}", + "user_import_failed": "ユーザーのインポート操作が完全に失敗しました", + "user_import_missing_columns": "次の列がありません: {columns}", + "user_import_nothing_to_do": "インポートする必要があるユーザーはいません", + "user_import_partial_failed": "ユーザーのインポート操作が部分的に失敗しました", + "user_import_success": "ユーザーが正常にインポートされました", + "user_unknown": "不明なユーザー: {user}", + "user_update_failed": "ユーザー {user}を更新できませんでした: {error}", + "user_updated": "ユーザー情報が変更されました", + "visitors": "訪問者", + "yunohost_already_installed": "YunoHostはすでにインストールされています", + "yunohost_configured": "YunoHost が構成されました", + "yunohost_installing": "YunoHostをインストールしています...", + "yunohost_not_installed": "YunoHostが正しくインストールされていません。’yunohost tools postinstall’ を実行してください", + "yunohost_postinstall_end_tip": "インストール後処理が完了しました!セットアップを完了するには、次の点を考慮してください。\n - ウェブ管理画面の「診断」セクション(またはコマンドラインで’yunohost diagnosis run’)を通じて潜在的な問題を診断します。\n - 管理ドキュメントの「セットアップの最終処理」と「YunoHostを知る」の部分を読む: https://yunohost.org/admindoc。", + "additional_urls_already_removed": "アクセス許可 ‘{permission}’ に対する追加URLで ‘{url}’ は既に削除されています" } From 0d0740826d104ec71f544587ba51a2fe9a2b8157 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 17:43:31 +0200 Subject: [PATCH 80/93] Revert "apps: fix version.parse now refusing to parse legacy version numbers" This reverts commit b98ac21a0663b5e1078d7505deb51d114b32e5c5. --- src/app.py | 65 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/src/app.py b/src/app.py index 64bb8c530..cce0aa51c 100644 --- a/src/app.py +++ b/src/app.py @@ -241,8 +241,8 @@ def _app_upgradable(app_infos): # Determine upgradability app_in_catalog = app_infos.get("from_catalog") - installed_version = _parse_app_version(app_infos.get("version", "0~ynh0")) - version_in_catalog = _parse_app_version( + installed_version = version.parse(app_infos.get("version", "0~ynh0")) + version_in_catalog = version.parse( app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0") ) @@ -257,7 +257,25 @@ def _app_upgradable(app_infos): ): return "bad_quality" - if installed_version < version_in_catalog: + # If the app uses the standard version scheme, use it to determine + # upgradability + if "~ynh" in str(installed_version) and "~ynh" in str(version_in_catalog): + if installed_version < version_in_catalog: + return "yes" + else: + return "no" + + # Legacy stuff for app with old / non-standard version numbers... + + # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded + if not app_infos["from_catalog"].get("lastUpdate") or not app_infos[ + "from_catalog" + ].get("git"): + return "url_required" + + settings = app_infos["settings"] + local_update_time = settings.get("update_time", settings.get("install_time", 0)) + if app_infos["from_catalog"]["lastUpdate"] > local_update_time: return "yes" else: return "no" @@ -602,11 +620,9 @@ def app_upgrade( # Manage upgrade type and avoid any upgrade if there is nothing to do upgrade_type = "UNKNOWN" # Get current_version and new version - app_new_version_raw = manifest.get("version", "?") - app_current_version_raw = app_dict.get("version", "?") - app_new_version = _parse_app_version(app_new_version_raw) - app_current_version = _parse_app_version(app_current_version_raw) - if "~ynh" in str(app_current_version_raw) and "~ynh" in str(app_new_version_raw): + app_new_version = version.parse(manifest.get("version", "?")) + app_current_version = version.parse(app_dict.get("version", "?")) + if "~ynh" in str(app_current_version) and "~ynh" in str(app_new_version): if app_current_version >= app_new_version and not force: # In case of upgrade from file or custom repository # No new version available @@ -626,10 +642,10 @@ def app_upgrade( upgrade_type = "UPGRADE_FORCED" else: app_current_version_upstream, app_current_version_pkg = str( - app_current_version_raw + app_current_version ).split("~ynh") app_new_version_upstream, app_new_version_pkg = str( - app_new_version_raw + app_new_version ).split("~ynh") if app_current_version_upstream == app_new_version_upstream: upgrade_type = "UPGRADE_PACKAGE" @@ -659,7 +675,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["PRE_UPGRADE"], - current_version=app_current_version_raw, + current_version=app_current_version, data=settings, ) _display_notifications(notifications, force=force) @@ -716,8 +732,8 @@ def app_upgrade( env_dict_more = { "YNH_APP_UPGRADE_TYPE": upgrade_type, - "YNH_APP_MANIFEST_VERSION": str(app_new_version_raw), - "YNH_APP_CURRENT_VERSION": str(app_current_version_raw), + "YNH_APP_MANIFEST_VERSION": str(app_new_version), + "YNH_APP_CURRENT_VERSION": str(app_current_version), } if manifest["packaging_format"] < 2: @@ -900,7 +916,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["POST_UPGRADE"], - current_version=app_current_version_raw, + current_version=app_current_version, data=settings, ) if Moulinette.interface.type == "cli": @@ -2038,20 +2054,6 @@ def _set_app_settings(app, settings): yaml.safe_dump(settings, f, default_flow_style=False) -def _parse_app_version(v): - - if v == "?": - return (0,0) - - try: - if "~" in v: - return (version.parse(v.split("~")[0]), int(v.split("~")[1].replace("ynh", ""))) - else: - return (version.parse(v), 0) - except Exception as e: - raise YunohostError(f"Failed to parse app version '{v}' : {e}", raw_msg=True) - - def _get_manifest_of_app(path): "Get app manifest stored in json or in toml" @@ -3156,7 +3158,12 @@ def _notification_is_dismissed(name, settings): def _filter_and_hydrate_notifications(notifications, current_version=None, data={}): def is_version_more_recent_than_current_version(name, current_version): current_version = str(current_version) - return _parse_app_version(name) > _parse_app_version(current_version) + # Boring code to handle the fact that "0.1 < 9999~ynh1" is False + + if "~" in name: + return version.parse(name) > version.parse(current_version) + else: + return version.parse(name) > version.parse(current_version.split("~")[0]) return { # Should we render the markdown maybe? idk From af93524c362abf67e23b81457215157081fd964d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 17:55:21 +0200 Subject: [PATCH 81/93] regenconf: fix a stupid bug using chown instead of chmod ... --- hooks/conf_regen/43-dnsmasq | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hooks/conf_regen/43-dnsmasq b/hooks/conf_regen/43-dnsmasq index 648a128c2..90e3ed2d7 100755 --- a/hooks/conf_regen/43-dnsmasq +++ b/hooks/conf_regen/43-dnsmasq @@ -62,7 +62,8 @@ do_post_regen() { regen_conf_files=$1 # Force permission (to cover some edge cases where root's umask is like 027 and then dnsmasq cant read this file) - chown 644 /etc/resolv.dnsmasq.conf + chown root /etc/resolv.dnsmasq.conf + chmod 644 /etc/resolv.dnsmasq.conf # Fuck it, those domain/search entries from dhclient are usually annoying # lying shit from the ISP trying to MiTM From 5b726bb8c00a0eb4d463eb803595495d6015b9dc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 17:56:33 +0200 Subject: [PATCH 82/93] Update changelog for 11.1.22 --- debian/changelog | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/debian/changelog b/debian/changelog index 2c33e3917..428d02b05 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,21 @@ +yunohost (11.1.22) stable; urgency=low + + - security: replace $http_host by $host in nginx conf, cf https://github.com/yandex/gixy/blob/master/docs/en/plugins/hostspoofing.md / Credit to A.Wolski (3957b10e) + - security: keep fail2ban rule when reloading firewall ([#1661](https://github.com/yunohost/yunohost/pull/1661)) + - regenconf: fix a stupid bug using chown instead of chmod ... (af93524c) + - postinstall: crash early if the username already exists on the system (e87ee09b) + - diagnosis: Support multiple TXT entries for TLD ([#1680](https://github.com/yunohost/yunohost/pull/1680)) + - apps: Support gitea's URL format ([#1683](https://github.com/yunohost/yunohost/pull/1683)) + - apps: fix a bug where YunoHost would complain that 'it needs X RAM but only Y left' with Y > X because some apps have a higher runtime RAM requirement than build time ... (4152cb0d) + - apps: Enhance app_shell() : prevent from taking the lock + improve php context with a 'phpflags' setting ([#1681](https://github.com/yunohost/yunohost/pull/1681)) + - apps resources: Allow passing an actual list in the manifest.toml for the apt resource packages ([#1670](https://github.com/yunohost/yunohost/pull/1670)) + - apps resources: fix a bug where port automigration between v1->v2 wouldnt work (36a17dfd) + - i18n: Translations updated for Basque, Galician, Japanese, Polish + + Thanks to all contributors <3 ! (Félix Piédallu, Grzegorz Cichocki, José M, Kayou, motcha, Nicolas Palix, orhtej2, tituspijean, xabirequejo, Yann Autissier) + + -- Alexandre Aubin Mon, 10 Jul 2023 17:43:56 +0200 + yunohost (11.1.21.4) stable; urgency=low - regenconf: Get rid of previous tmp hack about /dev/null for people that went through the very first 11.1.21, because it's causing issue in unpriviledged LXC or similar context (8242cab7) From 739e02eaf8ce99d6cd834aa1cd9a934a7ffa5ce0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 18:22:48 +0200 Subject: [PATCH 83/93] Typo/wording --- share/actionsmap.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 689c3da86..e64045367 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1596,7 +1596,7 @@ dyndns: full: --recovery-password nargs: "?" const: 0 - help: Password used to later delete the domain + help: Password used to later recover the domain if needed extra: pattern: *pattern_password @@ -1687,14 +1687,14 @@ tools: pattern: *pattern_password required: True comment: good_practices_about_admin_password - --ingnore-dyndns: + --ignore-dyndns: help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true --dyndns-recovery-password: metavar: PASSWORD nargs: "?" const: 0 - help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain + help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later recover the domain if needed extra: pattern: *pattern_password --force-diskspace: From 4a1b7c30ba68346225d80059a12f601ea9d379d1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 18:46:36 +0200 Subject: [PATCH 84/93] dyndns update is not deprecated because 'dns push' is not ready for dyndns ... --- share/actionsmap.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 97d2b5387..3624a9011 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1610,8 +1610,7 @@ dyndns: ### dyndns_update() update: - action_help: Update IP on DynDNS platform ( deprecated, use 'yunohost domain dns push DOMAIN' instead ) - deprecated: true + action_help: Update IP on DynDNS platform arguments: -d: full: --domain From 14040b8fd2ee18e93e051202a8dd3ee3cc9b8fe2 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Mon, 10 Jul 2023 17:05:52 +0000 Subject: [PATCH 85/93] [CI] Format code with Black --- src/app.py | 9 ++++++--- src/diagnosers/12-dnsrecords.py | 2 +- src/tests/test_apps.py | 4 +++- src/tests/test_appurl.py | 10 +++++++--- src/utils/resources.py | 22 +++++++++++++--------- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/app.py b/src/app.py index cce0aa51c..a77cf51b8 100644 --- a/src/app.py +++ b/src/app.py @@ -2784,9 +2784,12 @@ def _check_manifest_requirements( # Some apps have a higher runtime value than build ... if ram_requirement["build"] != "?" and ram_requirement["runtime"] != "?": - max_build_runtime = (ram_requirement["build"] - if human_to_binary(ram_requirement["build"]) > human_to_binary(ram_requirement["runtime"]) - else ram_requirement["runtime"]) + max_build_runtime = ( + ram_requirement["build"] + if human_to_binary(ram_requirement["build"]) + > human_to_binary(ram_requirement["runtime"]) + else ram_requirement["runtime"] + ) else: max_build_runtime = ram_requirement["build"] diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py index be9bf5418..196a2e1f9 100644 --- a/src/diagnosers/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -182,7 +182,7 @@ class MyDiagnoser(Diagnoser): if success != "ok": return None else: - if type_ == "TXT" and isinstance(answers,list): + if type_ == "TXT" and isinstance(answers, list): for part in answers: if part.startswith('"v=spf1'): return part diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index e6e1342ba..d7a591a36 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -339,7 +339,9 @@ def test_app_from_catalog(): assert app_map_[main_domain]["/site"]["id"] == "my_webapp" assert app_is_installed(main_domain, "my_webapp") - assert app_is_exposed_on_http(main_domain, "/site", "you have just installed My Webapp") + assert app_is_exposed_on_http( + main_domain, "/site", "you have just installed My Webapp" + ) # Try upgrade, should do nothing app_upgrade("my_webapp") diff --git a/src/tests/test_appurl.py b/src/tests/test_appurl.py index 996a5a2c3..d0c55f732 100644 --- a/src/tests/test_appurl.py +++ b/src/tests/test_appurl.py @@ -71,10 +71,14 @@ def test_repo_url_definition(): ### Gitea assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh") - assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh/src/branch/branch_name") + assert _is_app_repo_url( + "https://gitea.instance.tld/user/repo_ynh/src/branch/branch_name" + ) assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh/src/tag/tag_name") - assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh/src/commit/abcd1234") - + assert _is_app_repo_url( + "https://gitea.instance.tld/user/repo_ynh/src/commit/abcd1234" + ) + ### Invalid patterns # no schema diff --git a/src/utils/resources.py b/src/utils/resources.py index 7f6f263de..8d33c3bac 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1065,9 +1065,11 @@ class AptDependenciesAppResource(AppResource): if isinstance(values.get("packages"), str): values["packages"] = [value.strip() for value in values["packages"].split(",")] # type: ignore - if not isinstance(values.get("repo"), str) \ - or not isinstance(values.get("key"), str) \ - or not isinstance(values.get("packages"), list): + if ( + not isinstance(values.get("repo"), str) + or not isinstance(values.get("key"), str) + or not isinstance(values.get("packages"), list) + ): raise YunohostError( "In apt resource in the manifest: 'extras' repo should have the keys 'repo', 'key' defined as strings and 'packages' defined as list", raw_msg=True, @@ -1076,12 +1078,14 @@ class AptDependenciesAppResource(AppResource): def provision_or_update(self, context: Dict = {}): script = " ".join(["ynh_install_app_dependencies", *self.packages]) for repo, values in self.extras.items(): - script += "\n" + " ".join([ - "ynh_install_extra_app_dependencies", - f"--repo='{values['repo']}'", - f"--key='{values['key']}'", - f"--package='{' '.join(values['packages'])}'" - ]) + script += "\n" + " ".join( + [ + "ynh_install_extra_app_dependencies", + f"--repo='{values['repo']}'", + f"--key='{values['key']}'", + f"--package='{' '.join(values['packages'])}'", + ] + ) # FIXME : we're feeding the raw value of values['packages'] to the helper .. if we want to be consistent, may they should be comma-separated, though in the majority of cases, only a single package is installed from an extra repo.. self._run_script("provision_or_update", script) From c0c0fcaf54520459cbe7517ca0cfa0a67dfe2e33 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 19:10:54 +0200 Subject: [PATCH 86/93] ocsp stapling: Use 1.1.1.1 and 9.9.9.9 instead of 8.8.8.8 --- conf/nginx/server.tpl.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index 183cce8b8..5103e9081 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -54,7 +54,7 @@ server { ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - resolver 8.8.8.8 valid=300s; + resolver 1.1.1.1 9.9.9.9 valid=300s; resolver_timeout 5s; {% endif %} @@ -110,7 +110,7 @@ server { ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - resolver 8.8.8.8 valid=300s; + resolver 1.1.1.1 9.9.9.9 valid=300s; resolver_timeout 5s; {% endif %} From 432a9ab544800782dcdaa0bef9ae84480c7d77ba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 19:19:28 +0200 Subject: [PATCH 87/93] regenconf/ssh: disable Banner by default --- conf/ssh/sshd_config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/ssh/sshd_config b/conf/ssh/sshd_config index eaa0c7380..4a239d2ad 100644 --- a/conf/ssh/sshd_config +++ b/conf/ssh/sshd_config @@ -64,7 +64,7 @@ PasswordAuthentication no {% endif %} # Post-login stuff -Banner /etc/issue.net +# Banner none PrintMotd no PrintLastLog yes ClientAliveInterval 60 From 1927875924b16b08f8f850142a5e17c0f08b3bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Th=C3=A9o=20LAURET?= <118362885+eldertek@users.noreply.github.com> Date: Mon, 10 Jul 2023 21:28:22 +0400 Subject: [PATCH 88/93] [fix/enh] Rewrite of yunopaste CLI tool (#1667) * rewrite python * Modify to pipe * alexAubin review * Fix "output" var not existing ... * yunopaste: anonymize_output is too harsh and not yunopaste's job + print_usage ain't called ... * yunopaste: return link to the raw version, less confusing than haste's ui ... --------- Co-authored-by: Alexandre Aubin --- bin/yunopaste | 93 ++++++++++++++------------------------------------- 1 file changed, 25 insertions(+), 68 deletions(-) diff --git a/bin/yunopaste b/bin/yunopaste index edf8d55c8..f6bdecae2 100755 --- a/bin/yunopaste +++ b/bin/yunopaste @@ -1,77 +1,34 @@ -#!/bin/bash +#!/usr/bin/env python3 -set -e -set -u +import sys +import requests +import json -PASTE_URL="https://paste.yunohost.org" +SERVER_URL = "https://paste.yunohost.org" +TIMEOUT = 3 -_die() { - printf "Error: %s\n" "$*" - exit 1 -} +def create_snippet(data): + try: + url = SERVER_URL + "/documents" + response = requests.post(url, data=data.encode('utf-8'), timeout=TIMEOUT) + response.raise_for_status() + dockey = json.loads(response.text)['key'] + return SERVER_URL + "/raw/" + dockey + except requests.exceptions.RequestException as e: + print("\033[31mError: {}\033[0m".format(e)) + sys.exit(1) -check_dependencies() { - curl -V > /dev/null 2>&1 || _die "This script requires curl." -} -paste_data() { - json=$(curl -X POST -s -d "$1" "${PASTE_URL}/documents") - [[ -z "$json" ]] && _die "Unable to post the data to the server." +def main(): + output = sys.stdin.read() - key=$(echo "$json" \ - | python3 -c 'import json,sys;o=json.load(sys.stdin);print(o["key"])' \ - 2>/dev/null) - [[ -z "$key" ]] && _die "Unable to parse the server response." + if not output: + print("\033[31mError: No input received from stdin.\033[0m") + sys.exit(1) - echo "${PASTE_URL}/${key}" -} + url = create_snippet(output) -usage() { - printf "Usage: ${0} [OPTION]... + print("\033[32mURL: {}\033[0m".format(url)) -Read from input stream and paste the data to the YunoHost -Haste server. - -For example, to paste the output of the YunoHost diagnosis, you -can simply execute the following: - yunohost diagnosis show | ${0} - -It will return the URL where you can access the pasted data. - -Options: - -h, --help show this help message and exit -" -} - -main() { - # parse options - while (( ${#} )); do - case "${1}" in - --help|-h) - usage - exit 0 - ;; - *) - echo "Unknown parameter detected: ${1}" >&2 - echo >&2 - usage >&2 - exit 1 - ;; - esac - - shift 1 - done - - # check input stream - read -t 0 || { - echo -e "Invalid usage: No input is provided.\n" >&2 - usage - exit 1 - } - - paste_data "$(cat)" -} - -check_dependencies - -main "${@}" +if __name__ == "__main__": + main() From dfc51ed7c525c61bd0a352002f0d8609da4a0c46 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 19:29:34 +0200 Subject: [PATCH 89/93] Revert "[fix/enh] Rewrite of yunopaste CLI tool (#1667)" This reverts commit 1927875924b16b08f8f850142a5e17c0f08b3bc3. --- bin/yunopaste | 93 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 25 deletions(-) diff --git a/bin/yunopaste b/bin/yunopaste index f6bdecae2..edf8d55c8 100755 --- a/bin/yunopaste +++ b/bin/yunopaste @@ -1,34 +1,77 @@ -#!/usr/bin/env python3 +#!/bin/bash -import sys -import requests -import json +set -e +set -u -SERVER_URL = "https://paste.yunohost.org" -TIMEOUT = 3 +PASTE_URL="https://paste.yunohost.org" -def create_snippet(data): - try: - url = SERVER_URL + "/documents" - response = requests.post(url, data=data.encode('utf-8'), timeout=TIMEOUT) - response.raise_for_status() - dockey = json.loads(response.text)['key'] - return SERVER_URL + "/raw/" + dockey - except requests.exceptions.RequestException as e: - print("\033[31mError: {}\033[0m".format(e)) - sys.exit(1) +_die() { + printf "Error: %s\n" "$*" + exit 1 +} +check_dependencies() { + curl -V > /dev/null 2>&1 || _die "This script requires curl." +} -def main(): - output = sys.stdin.read() +paste_data() { + json=$(curl -X POST -s -d "$1" "${PASTE_URL}/documents") + [[ -z "$json" ]] && _die "Unable to post the data to the server." - if not output: - print("\033[31mError: No input received from stdin.\033[0m") - sys.exit(1) + key=$(echo "$json" \ + | python3 -c 'import json,sys;o=json.load(sys.stdin);print(o["key"])' \ + 2>/dev/null) + [[ -z "$key" ]] && _die "Unable to parse the server response." - url = create_snippet(output) + echo "${PASTE_URL}/${key}" +} - print("\033[32mURL: {}\033[0m".format(url)) +usage() { + printf "Usage: ${0} [OPTION]... -if __name__ == "__main__": - main() +Read from input stream and paste the data to the YunoHost +Haste server. + +For example, to paste the output of the YunoHost diagnosis, you +can simply execute the following: + yunohost diagnosis show | ${0} + +It will return the URL where you can access the pasted data. + +Options: + -h, --help show this help message and exit +" +} + +main() { + # parse options + while (( ${#} )); do + case "${1}" in + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown parameter detected: ${1}" >&2 + echo >&2 + usage >&2 + exit 1 + ;; + esac + + shift 1 + done + + # check input stream + read -t 0 || { + echo -e "Invalid usage: No input is provided.\n" >&2 + usage + exit 1 + } + + paste_data "$(cat)" +} + +check_dependencies + +main "${@}" From ba2159de7358c3b36d30472c8742d932dcf5191d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Th=C3=A9o=20LAURET?= <118362885+eldertek@users.noreply.github.com> Date: Mon, 10 Jul 2023 21:28:22 +0400 Subject: [PATCH 90/93] [fix/enh] Rewrite of yunopaste CLI tool (#1667) * rewrite python * Modify to pipe * alexAubin review * Fix "output" var not existing ... * yunopaste: anonymize_output is too harsh and not yunopaste's job + print_usage ain't called ... * yunopaste: return link to the raw version, less confusing than haste's ui ... --------- Co-authored-by: Alexandre Aubin --- bin/yunopaste | 93 ++++++++++++++------------------------------------- 1 file changed, 25 insertions(+), 68 deletions(-) diff --git a/bin/yunopaste b/bin/yunopaste index edf8d55c8..f6bdecae2 100755 --- a/bin/yunopaste +++ b/bin/yunopaste @@ -1,77 +1,34 @@ -#!/bin/bash +#!/usr/bin/env python3 -set -e -set -u +import sys +import requests +import json -PASTE_URL="https://paste.yunohost.org" +SERVER_URL = "https://paste.yunohost.org" +TIMEOUT = 3 -_die() { - printf "Error: %s\n" "$*" - exit 1 -} +def create_snippet(data): + try: + url = SERVER_URL + "/documents" + response = requests.post(url, data=data.encode('utf-8'), timeout=TIMEOUT) + response.raise_for_status() + dockey = json.loads(response.text)['key'] + return SERVER_URL + "/raw/" + dockey + except requests.exceptions.RequestException as e: + print("\033[31mError: {}\033[0m".format(e)) + sys.exit(1) -check_dependencies() { - curl -V > /dev/null 2>&1 || _die "This script requires curl." -} -paste_data() { - json=$(curl -X POST -s -d "$1" "${PASTE_URL}/documents") - [[ -z "$json" ]] && _die "Unable to post the data to the server." +def main(): + output = sys.stdin.read() - key=$(echo "$json" \ - | python3 -c 'import json,sys;o=json.load(sys.stdin);print(o["key"])' \ - 2>/dev/null) - [[ -z "$key" ]] && _die "Unable to parse the server response." + if not output: + print("\033[31mError: No input received from stdin.\033[0m") + sys.exit(1) - echo "${PASTE_URL}/${key}" -} + url = create_snippet(output) -usage() { - printf "Usage: ${0} [OPTION]... + print("\033[32mURL: {}\033[0m".format(url)) -Read from input stream and paste the data to the YunoHost -Haste server. - -For example, to paste the output of the YunoHost diagnosis, you -can simply execute the following: - yunohost diagnosis show | ${0} - -It will return the URL where you can access the pasted data. - -Options: - -h, --help show this help message and exit -" -} - -main() { - # parse options - while (( ${#} )); do - case "${1}" in - --help|-h) - usage - exit 0 - ;; - *) - echo "Unknown parameter detected: ${1}" >&2 - echo >&2 - usage >&2 - exit 1 - ;; - esac - - shift 1 - done - - # check input stream - read -t 0 || { - echo -e "Invalid usage: No input is provided.\n" >&2 - usage - exit 1 - } - - paste_data "$(cat)" -} - -check_dependencies - -main "${@}" +if __name__ == "__main__": + main() From 81f269fc29ccd68fc3a79c482386e6ce9d7363e7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 19:35:17 +0200 Subject: [PATCH 91/93] Fix funky no_unsubscribe dyndns stuff in test_domains.py ... --- src/tests/test_domains.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index b7625ff7c..c5c1ab7ae 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -4,7 +4,6 @@ import time import random from moulinette.core import MoulinetteError -from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.domain import ( @@ -41,7 +40,7 @@ def setup_function(function): for domain in domains: if (domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]) and domain != TEST_DYNDNS_DOMAIN: # Clean domains not used for testing - domain_remove(domain, no_unsubscribe=is_yunohost_dyndns_domain(domain)) + domain_remove(domain) elif domain in TEST_DOMAINS: # Reset settings if any os.system(f"rm -rf {DOMAIN_SETTINGS_DIR}/{domain}.yml") From 7c1c147a74e5592f5e312419b0594bb477f18f9c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2023 15:46:35 +0200 Subject: [PATCH 92/93] quality: we don't really care about linter for the tests/ folder ... --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 49c78959d..c38df434b 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ deps = py39-black-{run,check}: black py39-mypy: mypy >= 0.900 commands = - py39-lint: flake8 src doc maintenance tests --ignore E402,E501,E203,W503,E741 --exclude src/vendor + py39-lint: flake8 src doc maintenance tests --ignore E402,E501,E203,W503,E741 --exclude src/tests,src/vendor py39-invalidcode: flake8 src bin maintenance --exclude src/tests,src/vendor --select F,E722,W605 py39-black-check: black --check --diff bin src doc maintenance tests py39-black-run: black bin src doc maintenance tests From e695c89ad05c8fdf618ef5c761d93dc25805d377 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2023 15:51:19 +0200 Subject: [PATCH 93/93] Typo in i18n key --- src/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index dca4e9c77..c3fa80d3a 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -237,7 +237,7 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): timeout=30, ) except Exception as e: - raise YunohostError("dyndns_unregistration_failed", error=str(e)) + raise YunohostError("dyndns_unsubscribe_failed", error=str(e)) if r.status_code == 200: # Deletion was successful for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key"):