diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index 379b597a7..4ee20a720 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -44,10 +44,10 @@ server { ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; - {% if domain_cert_ca != "Self-signed" %} + {% if domain_cert_ca != "selfsigned" %} more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload"; {% endif %} - {% if domain_cert_ca == "Let's Encrypt" %} + {% if domain_cert_ca == "letsencrypt" %} # OCSP settings ssl_stapling on; ssl_stapling_verify on; @@ -99,10 +99,10 @@ server { ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; - {% if domain_cert_ca != "Self-signed" %} + {% if domain_cert_ca != "selfsigned" %} more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload"; {% endif %} - {% if domain_cert_ca == "Let's Encrypt" %} + {% if domain_cert_ca == "letsencrypt" %} # OCSP settings ssl_stapling on; ssl_stapling_verify on; diff --git a/debian/changelog b/debian/changelog index 32c42fb9c..f6fbe6eba 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (11.0.9.14) stable; urgency=low + + - [fix] dns: confusion on XMPP CNAME records for nohost.me & co domains (f6057d25) + - [fix] helper ynh_get_ram: LANG= isn't enough to get en_US output, gotta use LC_ALL (e51cdd98) + + -- Alexandre Aubin Wed, 07 Sep 2022 13:08:31 +0200 + yunohost (11.0.9.13) stable; urgency=low - [fix] defaultapp: domain may not exist in app_map dict output (efe0e601) @@ -10,7 +17,6 @@ yunohost (11.0.9.13) stable; urgency=low -- Alexandre Aubin Sat, 03 Sep 2022 23:27:56 +0200 - yunohost (11.0.9.12) stable; urgency=low - [fix] postinstall: check all partitions (not only physical ones) ([#1497](https://github.com/YunoHost/yunohost/pull/1497)) diff --git a/helpers/config b/helpers/config index c1f8bca32..fce215a30 100644 --- a/helpers/config +++ b/helpers/config @@ -285,6 +285,18 @@ ynh_app_config_apply() { _ynh_app_config_apply } +ynh_app_action_run() { + local runner="run__$1" + # Get value from getter if exists + if type -t "$runner" 2>/dev/null | grep -q '^function$' 2>/dev/null; then + $runner + #ynh_return "result:" + #ynh_return "$(echo "${result}" | sed 's/^/ /g')" + else + ynh_die "No handler defined in app's script for action $1. If you are the maintainer of this app, you should define '$runner'" + fi +} + ynh_app_config_run() { declare -Ag old=() declare -Ag changed=() @@ -309,5 +321,7 @@ ynh_app_config_run() { ynh_app_config_apply ynh_script_progression --message="Configuration of $app completed" --last ;; + *) + ynh_app_action_run $1 esac } diff --git a/helpers/hardware b/helpers/hardware index 337630fa8..3ccf7ffe8 100644 --- a/helpers/hardware +++ b/helpers/hardware @@ -30,8 +30,8 @@ ynh_get_ram() { ram=0 # Use the total amount of ram elif [ $free -eq 1 ]; then - local free_ram=$(LANG=C vmstat --stats --unit M | grep "free memory" | awk '{print $1}') - local free_swap=$(LANG=C vmstat --stats --unit M | grep "free swap" | awk '{print $1}') + local free_ram=$(LC_ALL=C vmstat --stats --unit M | grep "free memory" | awk '{print $1}') + local free_swap=$(LC_ALL=C vmstat --stats --unit M | grep "free swap" | awk '{print $1}') local free_ram_swap=$((free_ram + free_swap)) # Use the total amount of free ram @@ -44,8 +44,8 @@ ynh_get_ram() { ram=$free_swap fi elif [ $total -eq 1 ]; then - local total_ram=$(LANG=C vmstat --stats --unit M | grep "total memory" | awk '{print $1}') - local total_swap=$(LANG=C vmstat --stats --unit M | grep "total swap" | awk '{print $1}') + local total_ram=$(LC_ALL=C vmstat --stats --unit M | grep "total memory" | awk '{print $1}') + local total_swap=$(LC_ALL=C vmstat --stats --unit M | grep "total swap" | awk '{print $1}') local total_ram_swap=$((total_ram + total_swap)) local ram=$total_ram_swap diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index aab617c2c..ac9326834 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -220,7 +220,10 @@ do_post_regen() { systemctl restart ntp } [[ ! "$regen_conf_files" =~ "nftables.service.d/ynh-override.conf" ]] || systemctl daemon-reload - [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || systemctl daemon-reload + [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || { + systemctl daemon-reload + systemctl restart systemd-logind + } [[ ! "$regen_conf_files" =~ "yunohost-firewall.service" ]] || systemctl daemon-reload [[ ! "$regen_conf_files" =~ "yunohost-api.service" ]] || systemctl daemon-reload diff --git a/locales/ar.json b/locales/ar.json index bb25123da..604065c70 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -30,7 +30,7 @@ "backup_method_copy_finished": "إنتهت عملية النسخ الإحتياطي", "backup_nothings_done": "ليس هناك أي شيء للحفظ", "backup_output_directory_required": "يتوجب عليك تحديد مجلد لتلقي النسخ الإحتياطية", - "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق {domain}", + "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق '{domain}'", "certmanager_cert_install_success_selfsigned": "نجحت عملية تثبيت الشهادة الموقعة ذاتيا الخاصة بالنطاق '{domain}'", "certmanager_cert_renew_success": "نجحت عملية تجديد شهادة Let's Encrypt الخاصة باسم النطاق '{domain}'", "certmanager_cert_signing_failed": "فشل إجراء توقيع الشهادة الجديدة", @@ -63,7 +63,7 @@ "service_disabled": "لن يتم إطلاق خدمة '{service}' أثناء بداية تشغيل النظام.", "service_enabled": "تم تنشيط خدمة '{service}'", "service_removed": "تمت إزالة خدمة '{service}'", - "service_started": "تم إطلاق تشغيل خدمة '{service}'", + "service_started": "تم إطلاق تشغيل خدمة '{service}'", "service_stopped": "تمّ إيقاف خدمة '{service}'", "system_upgraded": "تمت عملية ترقية النظام", "unlimit": "دون تحديد الحصة", @@ -159,5 +159,6 @@ "diagnosis_description_basesystem": "النظام الأساسي", "global_settings_setting_admin_strength": "قوة الكلمة السرية الإدارية", "global_settings_setting_user_strength": "قوة الكلمة السرية للمستخدم", - "field_invalid": "الحقل غير صحيح : '{}'" -} \ No newline at end of file + "field_invalid": "الحقل غير صحيح : '{}'", + "diagnosis_ignored_issues": "(+ {nb_ignored} مشاكل تم تجاهلها)" +} diff --git a/locales/en.json b/locales/en.json index 74b62408e..82a26a418 100644 --- a/locales/en.json +++ b/locales/en.json @@ -14,7 +14,7 @@ "app_already_up_to_date": "{app} is already up-to-date", "app_argument_choice_invalid": "Pick a valid value for argument '{name}': '{value}' is not among the available choices ({choices})", "app_argument_invalid": "Pick a valid value for the argument '{name}': {error}", - "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reason", + "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reasons", "app_argument_required": "Argument '{name}' is required", "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain}{path}'), nothing to do.", "app_change_url_no_script": "The app '{app_name}' doesn't support URL modification yet. Maybe you should upgrade it.", @@ -40,12 +40,12 @@ "app_not_properly_removed": "{app} has not been properly removed", "app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps' upgrades have been cancelled: {apps}", "app_packaging_format_not_supported": "This app cannot be installed because its packaging format is not supported by your YunoHost version. You should probably consider upgrading your system.", - "app_remove_after_failed_install": "Removing the app following the installation failure...", + "app_remove_after_failed_install": "Removing the app after installation failure...", "app_removed": "{app} uninstalled", "app_requirements_checking": "Checking requirements for {app}...", "app_restore_failed": "Could not restore {app}: {error}", "app_restore_script_failed": "An error occured inside the app restore script", - "app_sources_fetch_failed": "Could not fetch sources files, is the URL correct?", + "app_sources_fetch_failed": "Could not fetch source files, is the URL correct?", "app_start_backup": "Collecting files to be backed up for {app}...", "app_start_install": "Installing {app}...", "app_start_remove": "Removing {app}...", @@ -80,7 +80,7 @@ "backup_applying_method_tar": "Creating the backup TAR archive...", "backup_archive_app_not_found": "Could not find {app} in the backup archive", "backup_archive_broken_link": "Could not access the backup archive (broken link to {path})", - "backup_archive_cant_retrieve_info_json": "Could not load infos for archive '{archive}'... The info.json cannot be retrieved (or is not a valid json).", + "backup_archive_cant_retrieve_info_json": "Could not load info for archive '{archive}'... The info.json file cannot be retrieved (or is not a valid json).", "backup_archive_corrupted": "It looks like the backup archive '{archive}' is corrupted : {error}", "backup_archive_name_exists": "A backup archive with this name already exists.", "backup_archive_name_unknown": "Unknown local backup archive named '{name}'", @@ -118,28 +118,34 @@ "backup_unable_to_organize_files": "Could not use the quick method to organize files in the archive", "backup_with_no_backup_script_for_app": "The app '{app}' has no backup script. Ignoring.", "backup_with_no_restore_script_for_app": "{app} has no restoration script, you will not be able to automatically restore the backup of this app.", - "certmanager_acme_not_configured_for_domain": "The ACME challenge cannot be ran for {domain} right now because its nginx conf lacks the corresponding code snippet... Please make sure that your nginx configuration is up to date using `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "The ACME challenge cannot be run for {domain} right now because its nginx conf lacks the corresponding code snippet... Please make sure that your nginx configuration is up to date using `yunohost tools regen-conf nginx --dry-run --with-diff`.", "certmanager_attempt_to_renew_nonLE_cert": "The certificate for the domain '{domain}' is not issued by Let's Encrypt. Cannot renew it automatically!", "certmanager_attempt_to_renew_valid_cert": "The certificate for the domain '{domain}' is not about to expire! (You may use --force if you know what you're doing)", "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain}! (Use --force to bypass)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain} (file: {file}), reason: {reason}", + "certmanager_cert_install_failed": "Let's Encrypt certificate install failed for {domains}", + "certmanager_cert_install_failed_selfsigned": "Self-signed certificate install failed for {domains}", "certmanager_cert_install_success": "Let's Encrypt certificate now installed for the domain '{domain}'", "certmanager_cert_install_success_selfsigned": "Self-signed certificate now installed for the domain '{domain}'", + "certmanager_cert_renew_failed": "Let's Encrypt certificate renew failed for {domains}", "certmanager_cert_renew_success": "Let's Encrypt certificate renewed for the domain '{domain}'", "certmanager_cert_signing_failed": "Could not sign the new certificate", "certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain} did not work...", "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain} is not self-signed. Are you sure you want to replace it? (Use '--force' to do so.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' is different from this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off those checks.)", - "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off those checks.)", - "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off those checks.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' are different to this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off these checks.)", + "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off these checks.)", + "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off these checks.)", "certmanager_hit_rate_limit": "Too many certificates already issued for this exact set of domains {domain} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", "certmanager_no_cert_file": "Could not read the certificate file for the domain {domain} (file: {file})", "certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file})", "certmanager_unable_to_parse_self_CA_name": "Could not parse name of self-signing authority (file: {file})", "certmanager_warning_subdomain_dns_record": "Subdomain '{subdomain}' does not resolve to the same IP address as '{domain}'. Some features will not be available until you fix this and regenerate the certificate.", + "config_action_failed": "Failed to run action '{action}': {error}", + "config_action_disabled": "Could not run action '{action}' since it is disabled, make sure to meet its constraints. help: {help}", "config_apply_failed": "Applying the new configuration failed: {error}", "config_cant_set_value_on_section": "You can't set a single value on an entire config section.", "config_forbidden_keyword": "The keyword '{keyword}' is reserved, you can't create or use a config panel with a question with this id.", + "config_forbidden_readonly_type": "The type '{type}' can't be set as readonly, use another type to render this value (relevant arg id: '{id}').", "config_no_panel": "No config panel found.", "config_unknown_filter_key": "The filter key '{filter_key}' is incorrect.", "config_validate_color": "Should be a valid RGB hexadecimal color", @@ -150,17 +156,17 @@ "config_version_not_supported": "Config panel versions '{version}' are not supported.", "confirm_app_install_danger": "DANGER! This app is known to be still experimental (if not explicitly not working)! You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system... If you are willing to take that risk anyway, type '{answers}'", "confirm_app_install_thirdparty": "DANGER! This app is not part of YunoHost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system... If you are willing to take that risk anyway, type '{answers}'", - "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ", + "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated into YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app}", "danger": "Danger:", "diagnosis_apps_allgood": "All installed apps respect basic packaging practices", "diagnosis_apps_bad_quality": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", "diagnosis_apps_broken": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", - "diagnosis_apps_deprecated_practices": "This app's installed version still uses some super-old deprecated packaging practices. You should really consider upgrading it.", + "diagnosis_apps_deprecated_practices": "This app's installed version still uses some very old, deprecated packaging practices. You should really consider upgrading it.", "diagnosis_apps_issue": "An issue was found for app {app}", - "diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and got removed, you should consider uninstalling this app as it won't receive upgrade, and may compromise the integrity and security of your system.", + "diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and was removed, you should consider uninstalling this app as it won't receive upgrades and may compromise the integrity and security of your system.", "diagnosis_apps_outdated_ynh_requirement": "This app's installed version only requires yunohost >= 2.x or 3.x, which tends to indicate that it's not up to date with recommended packaging practices and helpers. You should really consider upgrading it.", - "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage from installing packages from backports, because it's likely to create unstabilities or conflicts on your system.", + "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage installing packages from backports, because it's likely to create unstabilities or conflicts on your system.", "diagnosis_basesystem_hardware": "Server hardware architecture is {virt} {arch}", "diagnosis_basesystem_hardware_model": "Server model is {model}", "diagnosis_basesystem_host": "Server is running Debian {debian_version}", @@ -188,7 +194,7 @@ "diagnosis_dns_discrepancy": "The following DNS record does not seem to follow the recommended configuration:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", "diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})", "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_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 dyndns update --force.", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", @@ -212,14 +218,14 @@ "diagnosis_http_hairpinning_issue": "Your local network does not seem to have hairpinning enabled.", "diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at https://yunohost.org/dns_local_network", "diagnosis_http_nginx_conf_not_up_to_date": "This domain's nginx configuration appears to have been modified manually, and prevents YunoHost from diagnosing if it's reachable on HTTP.", - "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference with the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok, apply the changes with yunohost tools regen-conf nginx --force.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference from the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok with it, apply the changes with yunohost tools regen-conf nginx --force.", "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.", "diagnosis_http_partially_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network in IPv{failed}, though it works in IPv{passed}.", "diagnosis_http_special_use_tld": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to be exposed outside the local network.", - "diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", + "diagnosis_http_timeout": "Timed-out while trying to contact your server from the outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", "diagnosis_http_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network.", "diagnosis_ignored_issues": "(+ {nb_ignored} ignored issue(s))", - "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests ?", + "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests?", "diagnosis_ip_broken_resolvconf": "Domain name resolution seems to be broken on your server, which seems related to /etc/resolv.conf not pointing to 127.0.0.1.", "diagnosis_ip_connected_ipv4": "The server is connected to the Internet through IPv4!", "diagnosis_ip_connected_ipv6": "The server is connected to the Internet through IPv6!", @@ -235,26 +241,26 @@ "diagnosis_mail_blacklist_listed_by": "Your IP or domain {item} is blacklisted on {blacklist_name}", "diagnosis_mail_blacklist_ok": "The IPs and domains used by this server do not appear to be blacklisted", "diagnosis_mail_blacklist_reason": "The blacklist reason is: {reason}", - "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixed it, feel free to ask for your IP or domaine to be removed on {blacklist_website}", + "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixing it, feel free to ask for your IP or domain to be removed on {blacklist_website}", "diagnosis_mail_ehlo_bad_answer": "A non-SMTP service answered on port 25 on IPv{ipversion}", - "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an other machine answering instead of your server.", - "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from outside in IPv{ipversion}.", + "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an another machine answering instead of your server.", + "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from the outside in IPv{ipversion}.", "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}", "diagnosis_mail_ehlo_ok": "The SMTP mail server is reachable from the outside and therefore is able to receive emails!", "diagnosis_mail_ehlo_unreachable": "The SMTP mail server is unreachable from the outside on IPv{ipversion}. It won't be able to receive emails.", "diagnosis_mail_ehlo_unreachable_details": "Could not open a connection on port 25 to your server in IPv{ipversion}. It appears to be unreachable.
1. The most common cause for this issue is that port 25 is not correctly forwarded to your server.
2. You should also make sure that service postfix is running.
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", "diagnosis_mail_ehlo_wrong": "A different SMTP mail server answers on IPv{ipversion}. Your server will probably not be able to receive emails.", "diagnosis_mail_ehlo_wrong_details": "The EHLO received by the remote diagnoser in IPv{ipversion} is different from your server's domain.
Received EHLO: {wrong_ehlo}
Expected: {right_ehlo}
The most common cause for this issue is that port 25 is not correctly forwarded to your server. Alternatively, make sure that no firewall or reverse-proxy is interfering.", - "diagnosis_mail_fcrdns_different_from_ehlo_domain": "The reverse DNS is not correctly configured in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Reverse DNS is not correctly configured for IPv{ipversion}. Some emails may fail to get delivered or be flagged as spam.", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Current reverse DNS: {rdns_domain}
Expected value: {ehlo_domain}", - "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.", + "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or be flagged as spam.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If you are experiencing issues because of this, consider the following solutions:
- Some ISP provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- Or it's possible to switch to a different provider", "diagnosis_mail_fcrdns_nok_alternatives_6": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If your reverse DNS is correctly configured for IPv4, you can try disabling the use of IPv6 when sending emails by running yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Note: this last solution means that you won't be able to send or receive emails from the few IPv6-only servers out there.", - "diagnosis_mail_fcrdns_nok_details": "You should first try to configure the reverse DNS with {ehlo_domain} in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).", + "diagnosis_mail_fcrdns_nok_details": "You should first try to configure reverse DNS with {ehlo_domain} in your internet router interface or your hosting provider interface. (Some hosting providers may require you to send them a support ticket for this).", "diagnosis_mail_fcrdns_ok": "Your reverse DNS is correctly configured!", "diagnosis_mail_outgoing_port_25_blocked": "The SMTP mail server cannot send emails to other servers because outgoing port 25 is blocked in IPv{ipversion}.", - "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider", + "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting providers may require you to send them a support ticket for this).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass these kinds of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider", "diagnosis_mail_outgoing_port_25_ok": "The SMTP mail server is able to send emails (outgoing port 25 is not blocked).", "diagnosis_mail_queue_ok": "{nb_pending} pending emails in the mail queues", "diagnosis_mail_queue_too_big": "Too many pending emails in mail queue ({nb_pending} emails)", @@ -268,20 +274,20 @@ "diagnosis_ports_could_not_diagnose_details": "Error: {error}", "diagnosis_ports_forwarding_tip": "To fix this issue, you most probably need to configure port forwarding on your internet router as described in https://yunohost.org/isp_box_config", "diagnosis_ports_needed_by": "Exposing this port is needed for {category} features (service {service})", - "diagnosis_ports_ok": "Port {port} is reachable from outside.", - "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from outside in IPv{failed}.", - "diagnosis_ports_unreachable": "Port {port} is not reachable from outside.", - "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process that ate up to much memory. Summary of the processes killed:\n{kills_summary}", + "diagnosis_ports_ok": "Port {port} is reachable from the outside.", + "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from the outside in IPv{failed}.", + "diagnosis_ports_unreachable": "Port {port} is not reachable from the outside.", + "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process consuming too much memory. Summary of the processes killed:\n{kills_summary}", "diagnosis_ram_low": "The system has {available} ({available_percent}%) RAM available (out of {total}). Be careful.", "diagnosis_ram_ok": "The system still has {available} ({available_percent}%) RAM available out of {total}.", "diagnosis_ram_verylow": "The system has only {available} ({available_percent}%) RAM available! (out of {total})", - "diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!", + "diagnosis_regenconf_allgood": "All configuration files are in line with the recommended configuration!", "diagnosis_regenconf_manually_modified": "Configuration file {file} appears to have been manually modified.", "diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with yunohost tools regen-conf {category} --dry-run --with-diff and force the reset to the recommended configuration with yunohost tools regen-conf {category} --force", "diagnosis_rootfstotalspace_critical": "The root filesystem only has a total of {space} which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16 GB for the root filesystem.", "diagnosis_rootfstotalspace_warning": "The root filesystem only has a total of {space}. This may be okay, but be careful because ultimately you may run out of disk space quickly... It's recommended to have at least 16 GB for the root filesystem.", - "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown criticial security vulnerability", - "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more infos.", + "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown critical security vulnerability", + "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more info.", "diagnosis_services_bad_status": "Service {service} is {status} :(", "diagnosis_services_bad_status_tip": "You can try to restart the service, and if it doesn't work, have a look at the service logs in the webadmin (from the command line, you can do this with yunohost service restart {service} and yunohost service log {service}).", "diagnosis_services_conf_broken": "Configuration is broken for service {service}!", @@ -292,11 +298,11 @@ "diagnosis_swap_none": "The system has no swap at all. You should consider adding at least {recommended} of swap to avoid situations where the system runs out of memory.", "diagnosis_swap_notsomuch": "The system has only {total} swap. You should consider having at least {recommended} to avoid situations where the system runs out of memory.", "diagnosis_swap_ok": "The system has {total} of swap!", - "diagnosis_swap_tip": "Please be careful and aware that if the server is hosting swap on an SD card or SSD storage, it may drastically reduce the life expectancy of the device`.", + "diagnosis_swap_tip": "Please be careful and aware that if the server is hosting swap on an SD card or SSD storage, it may drastically reduce the life expectancy of the device.", "diagnosis_unknown_categories": "The following categories are unknown: {categories}", "disk_space_not_sufficient_install": "There is not enough disk space left to install this application", "disk_space_not_sufficient_update": "There is not enough disk space left to update this application", - "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated in YunoHost.", + "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated into YunoHost.", "domain_cannot_remove_main": "You cannot remove '{domain}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n '; here is the list of candidate domains: {other_domains}", "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add ', then set is as the main domain using 'yunohost domain main-domain -n ' and then you can remove the domain '{domain}' using 'yunohost domain remove {domain}'.'", "domain_cert_gen_failed": "Could not generate certificate", @@ -313,6 +319,20 @@ "domain_config_mail_in": "Incoming emails", "domain_config_mail_out": "Outgoing emails", "domain_config_xmpp": "Instant messaging (XMPP)", + "domain_config_acme_eligible": "ACME eligibility", + "domain_config_acme_eligible_explain": "This domain doesn't seem ready for a Let's Encrypt certificate. Please check your DNS configuration and HTTP server reachability. The 'DNS records' and 'Web' section in the diagnosis page can help you understand what is misconfigured.", + "domain_config_cert_install": "Install Let's Encrypt certificate", + "domain_config_cert_issuer": "Certification authority", + "domain_config_cert_no_checks": "Ignore diagnosis checks", + "domain_config_cert_renew": "Renew Let's Encrypt certificate", + "domain_config_cert_renew_help":"Certificate will be automatically renewed during the last 15 days of validity. You can manually renew it if you want to. (Not recommended).", + "domain_config_cert_summary": "Certificate status", + "domain_config_cert_summary_expired": "CRITICAL: Current certificate is not valid! HTTPS won't work at all!", + "domain_config_cert_summary_selfsigned": "WARNING: Current certificate is self-signed. Browsers will display a spooky warning to new visitors!", + "domain_config_cert_summary_abouttoexpire": "Current certificate is about to expire. It should soon be renewed automatically.", + "domain_config_cert_summary_ok": "Okay, current certificate looks good!", + "domain_config_cert_summary_letsencrypt": "Great! You're using a valid Let's Encrypt certificate!", + "domain_config_cert_validity": "Validity", "domain_created": "Domain created", "domain_creation_failed": "Unable to create domain {domain}: {error}", "domain_deleted": "Domain deleted", @@ -358,7 +378,6 @@ "dyndns_registered": "DynDNS domain registered", "dyndns_registration_failed": "Could not register DynDNS domain: {error}", "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...", "field_invalid": "Invalid field '{}'", "file_does_not_exist": "The file {path} does not exist.", @@ -537,8 +556,8 @@ "migrations_need_to_accept_disclaimer": "To run the migration {id}, your must accept the following disclaimer:\n---\n{disclaimer}\n---\nIf you accept to run the migration, please re-run the command with the option '--accept-disclaimer'.", "migrations_no_migrations_to_run": "No migrations to run", "migrations_no_such_migration": "There is no migration called '{id}'", - "migrations_not_pending_cant_skip": "Those migrations are not pending, so cannot be skipped: {ids}", - "migrations_pending_cant_rerun": "Those migrations are still pending, so cannot be run again: {ids}", + "migrations_not_pending_cant_skip": "These migrations are not pending, so cannot be skipped: {ids}", + "migrations_pending_cant_rerun": "These migrations are still pending, so cannot be run again: {ids}", "migrations_running_forward": "Running migration {id}...", "migrations_skip_migration": "Skipping migration {id}...", "migrations_success_forward": "Migration {id} completed", diff --git a/locales/sk.json b/locales/sk.json index 18a4bf8bf..939f28836 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -233,5 +233,8 @@ "diagnosis_ip_no_ipv4": "Na serveri nefunguje spojenie cez protokol IPv4.", "diagnosis_ip_no_ipv6": "Na serveri nefunguje spojenie cez protokol IPv6.", "diagnosis_ip_not_connected_at_all": "Zdá sa, že tento server nie je vôbec pripojený k internetu!?", - "diagnosis_ip_weird_resolvconf": "Zdá sa, že preklad názvov domén funguje, ale podľa všetkého používate vlastný súbor /etc/resolv.conf." -} \ No newline at end of file + "diagnosis_ip_weird_resolvconf": "Zdá sa, že preklad názvov domén funguje, ale podľa všetkého používate vlastný súbor /etc/resolv.conf.", + "root_password_desynchronized": "Heslo pre správu bolo zmenené, ale YunoHost nedokázal túto zmenu premietnuť do hesla používateľa root!", + "main_domain_changed": "Hlavná doména bola zmenená", + "user_updated": "Informácie o používateľovi boli zmenené" +} diff --git a/locales/te.json b/locales/te.json index ca871c2ae..63d7feb25 100644 --- a/locales/te.json +++ b/locales/te.json @@ -14,5 +14,34 @@ "app_action_broke_system": "ఈ చర్య ఈ ముఖ్యమైన సేవలను విచ్ఛిన్నం చేసినట్లుగా కనిపిస్తోంది: {services}", "app_action_cannot_be_ran_because_required_services_down": "ఈ చర్యను అమలు చేయడానికి ఈ అవసరమైన సేవలు అమలు చేయబడాలి: {services}. కొనసాగడం కొరకు వాటిని పునఃప్రారంభించడానికి ప్రయత్నించండి (మరియు అవి ఎందుకు పనిచేయడం లేదో పరిశోధించవచ్చు).", "app_argument_choice_invalid": "ఆర్గ్యుమెంట్ '{name}' కొరకు చెల్లుబాటు అయ్యే వైల్యూ ఎంచుకోండి: '{value}' అనేది లభ్యం అవుతున్న ఎంపికల్లో ({choices}) లేదు", - "app_argument_password_no_default": "పాస్వర్డ్ ఆర్గ్యుమెంట్ '{name}'ని పార్సింగ్ చేసేటప్పుడు దోషం: భద్రతా కారణం కొరకు పాస్వర్డ్ ఆర్గ్యుమెంట్ డిఫాల్ట్ విలువను కలిగి ఉండరాదు" -} \ No newline at end of file + "app_argument_password_no_default": "పాస్వర్డ్ ఆర్గ్యుమెంట్ '{name}'ని పార్సింగ్ చేసేటప్పుడు దోషం: భద్రతా కారణం కొరకు పాస్వర్డ్ ఆర్గ్యుమెంట్ డిఫాల్ట్ విలువను కలిగి ఉండరాదు", + "app_extraction_failed": "ఇన్‌స్టాలేషన్ ఫైల్‌లను సంగ్రహించడం సాధ్యపడలేదు", + "app_id_invalid": "చెల్లని యాప్ ID", + "app_install_failed": "{app}ని ఇన్‌స్టాల్ చేయడం సాధ్యపడలేదు: {error}", + "app_install_script_failed": "యాప్ ఇన్‌స్టాలేషన్ స్క్రిప్ట్‌లో లోపం సంభవించింది", + "app_manifest_install_ask_domain": "ఈ యాప్‌ను ఇన్‌స్టాల్ చేయాల్సిన డొమైన్‌ను ఎంచుకోండి", + "app_manifest_install_ask_password": "ఈ యాప్‌కు అడ్మినిస్ట్రేషన్ పాస్‌వర్డ్‌ను ఎంచుకోండి", + "app_not_installed": "ఇన్‌స్టాల్ చేసిన యాప్‌ల జాబితాలో {app}ని కనుగొనడం సాధ్యపడలేదు: {all apps}", + "app_removed": "{app} అన్‌ఇన్‌స్టాల్ చేయబడింది", + "app_restore_failed": "{app}: {error}ని పునరుద్ధరించడం సాధ్యపడలేదు", + "app_start_backup": "{app} కోసం బ్యాకప్ చేయాల్సిన ఫైల్‌లను సేకరిస్తోంది...", + "app_start_install": "{app}ని ఇన్‌స్టాల్ చేస్తోంది...", + "app_start_restore": "{app}ని పునరుద్ధరిస్తోంది...", + "app_unknown": "తెలియని యాప్", + "app_upgrade_failed": "అప్‌గ్రేడ్ చేయడం సాధ్యపడలేదు {app}: {error}", + "app_manifest_install_ask_admin": "ఈ యాప్ కోసం నిర్వాహక వినియోగదారుని ఎంచుకోండి", + "app_argument_required": "ఆర్గ్యుమెంట్ '{name}' అవసరం", + "app_change_url_success": "{app} URL ఇప్పుడు {domain}{path}", + "app_config_unable_to_apply": "config ప్యానెల్ values దరఖాస్తు చేయడంలో విఫలమయ్యాము.", + "app_install_files_invalid": "ఈ ఫైల్‌లను ఇన్‌స్టాల్ చేయడం సాధ్యం కాదు", + "app_manifest_install_ask_is_public": "అనామక సందర్శకులకు ఈ యాప్ బహిర్గతం కావాలా?", + "app_not_correctly_installed": "{app} తప్పుగా ఇన్‌స్టాల్ చేయబడినట్లుగా ఉంది", + "app_not_properly_removed": "{app} సరిగ్గా తీసివేయబడలేదు", + "app_remove_after_failed_install": "ఇన్‌స్టాలేషన్ విఫలమైనందున యాప్‌ని తీసివేస్తోంది...", + "app_requirements_checking": "{app} కోసం అవసరమైన ప్యాకేజీలను తనిఖీ చేస్తోంది...", + "app_restore_script_failed": "యాప్ పునరుద్ధరణ స్క్రిప్ట్‌లో లోపం సంభవించింది", + "app_sources_fetch_failed": "మూలాధార ఫైల్‌లను పొందడం సాధ్యపడలేదు, URL సరైనదేనా?", + "app_start_remove": "{app}ని తీసివేస్తోంది...", + "app_upgrade_app_name": "ఇప్పుడు {app}ని అప్‌గ్రేడ్ చేస్తోంది...", + "app_config_unable_to_read": "కాన్ఫిగరేషన్ ప్యానెల్ విలువలను చదవడంలో విఫలమైంది." +} diff --git a/locales/tr.json b/locales/tr.json index 6c881eec7..6dd03c57e 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -1,3 +1,12 @@ { - "password_too_simple_1": "Şifre en az 8 karakter uzunluğunda olmalı" -} \ No newline at end of file + "password_too_simple_1": "Şifre en az 8 karakter uzunluğunda olmalı", + "action_invalid": "Geçersiz işlem '{action}'", + "admin_password": "Yönetici şifresi", + "admin_password_change_failed": "Şifre değiştirme başarısız oldu", + "admin_password_changed": "Yönetici şifresi değişti", + "admin_password_too_long": "Lütfen 127 karakterden kısa bir şifre seçin", + "already_up_to_date": "Yapılacak yeni bir şey yok. Her şey zaten güncel.", + "app_action_broke_system": "Bu işlem bazı hizmetleri bozmuş olabilir: {services}", + "good_practices_about_user_password": "Şimdi yeni bir kullanıcı şifresi tanımlamak üzeresiniz. Parola en az 8 karakter uzunluğunda olmalıdır - ancak daha uzun bir parola (yani bir parola) ve/veya çeşitli karakterler (büyük harf, küçük harf, rakamlar ve özel karakterler) daha iyidir.", + "aborting": "İptal ediliyor." +} diff --git a/maintenance/make_changelog.sh b/maintenance/make_changelog.sh index 89087eba3..a73b5061b 100644 --- a/maintenance/make_changelog.sh +++ b/maintenance/make_changelog.sh @@ -7,8 +7,6 @@ EMAIL=$(git config --global --get user.email) LAST_RELEASE=$(git tag --list 'debian/11.*' --sort="v:refname" | tail -n 1) -echo $LAST_RELEASE - echo "$REPO ($VERSION) $RELEASE; urgency=low" echo "" diff --git a/share/actionsmap.yml b/share/actionsmap.yml index c54c0972c..78271b4cc 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -552,6 +552,7 @@ domain: ### domain_url_available() url-available: + hide_in_help: True action_help: Check availability of a web path api: GET /domain//urlavailable arguments: @@ -562,6 +563,20 @@ domain: path: help: The path to check (e.g. /coffee) + ### domain_action_run() + action-run: + hide_in_help: True + action_help: Run domain action + api: PUT /domain//actions/ + arguments: + domain: + help: Domain name + action: + help: action id + -a: + full: --args + help: Serialized arguments for action (i.e. "foo=bar&lorem=ipsum") + subcategories: config: @@ -571,7 +586,9 @@ domain: ### domain_config_get() get: action_help: Display a domain configuration - api: GET /domains//config + api: + - GET /domains//config + - GET /domains//config/ arguments: domain: help: Domain name @@ -590,7 +607,7 @@ domain: ### domain_config_set() set: action_help: Apply a new configuration - api: PUT /domains//config + api: PUT /domains//config/ arguments: domain: help: Domain name @@ -868,6 +885,7 @@ app: ### app_register_url() register-url: + hide_in_help: True action_help: Book/register a web path for a given app arguments: app: @@ -880,6 +898,7 @@ app: ### app_makedefault() makedefault: + hide_in_help: True action_help: Redirect domain root to an app api: PUT /apps//default arguments: @@ -941,7 +960,9 @@ app: ### app_config_get() get: action_help: Display an app configuration - api: GET /apps//config-panel + api: + - GET /apps//config + - GET /apps//config/ arguments: app: help: App name @@ -960,7 +981,7 @@ app: ### app_config_set() set: action_help: Apply a new configuration - api: PUT /apps//config + api: PUT /apps//config/ arguments: app: help: App name @@ -1065,6 +1086,7 @@ backup: ### backup_download() download: + hide_in_help: True action_help: (API only) Request to download the file api: GET /backups//download arguments: @@ -1678,6 +1700,7 @@ hook: ### hook_info() info: + hide_in_help: True action_help: Get information about a given hook arguments: action: @@ -1707,6 +1730,7 @@ hook: ### hook_callback() callback: + hide_in_help: True action_help: Execute all scripts binded to an action arguments: action: @@ -1729,6 +1753,7 @@ hook: ### hook_exec() exec: + hide_in_help: True action_help: Execute hook from a file with arguments arguments: path: diff --git a/share/config_domain.toml b/share/config_domain.toml index 65e755365..28c394cf1 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -16,7 +16,7 @@ i18n = "domain_config" type = "app" filter = "is_webapp" default = "_none" - + [feature.mail] #services = ['postfix', 'dovecot'] @@ -28,17 +28,17 @@ i18n = "domain_config" [feature.mail.mail_out] type = "boolean" default = 1 - + [feature.mail.mail_in] type = "boolean" default = 1 - + #[feature.mail.backup_mx] #type = "tags" #default = [] #pattern.regexp = '^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$' #pattern.error = "pattern_error" - + [feature.xmpp] [feature.xmpp.xmpp] @@ -46,7 +46,7 @@ i18n = "domain_config" default = 0 [dns] - + [dns.registrar] optional = true @@ -58,3 +58,56 @@ i18n = "domain_config" # type = "number" # min = 0 # default = 3600 + + +[cert] + + [cert.status] + name = "Status" + + [cert.status.cert_summary] + type = "alert" + # Automatically filled by DomainConfigPanel + + [cert.status.cert_validity] + type = "number" + readonly = true + # Automatically filled by DomainConfigPanel + + [cert.cert] + name = "Manage" + + [cert.cert.cert_issuer] + type = "string" + visible = false + # Automatically filled by DomainConfigPanel + + [cert.cert.acme_eligible] + type = "boolean" + visible = false + # Automatically filled by DomainConfigPanel + + [cert.cert.acme_eligible_explain] + type = "alert" + style = "warning" + visible = "acme_eligible == false" + + [cert.cert.cert_no_checks] + ask = "Ignore diagnosis checks" + type = "boolean" + default = false + visible = "acme_eligible == false" + + [cert.cert.cert_install] + type = "button" + icon = "star" + style = "success" + visible = "issuer != 'letsencrypt'" + enabled = "acme_eligible || cert_no_checks" + + [cert.cert.cert_renew] + type = "button" + icon = "refresh" + style = "warning" + visible = "issuer == 'letsencrypt'" + enabled = "acme_eligible || cert_no_checks" diff --git a/src/app.py b/src/app.py index 1b34e0106..9af21df7f 100644 --- a/src/app.py +++ b/src/app.py @@ -1551,89 +1551,16 @@ def app_change_label(app, new_label): def app_action_list(app): - logger.warning(m18n.n("experimental_feature")) - # this will take care of checking if the app is installed - app_info_dict = app_info(app) - - return { - "app": app, - "app_name": app_info_dict["name"], - "actions": _get_app_actions(app), - } + return AppConfigPanel(app).list_actions() @is_unit_operation() -def app_action_run(operation_logger, app, action, args=None): - logger.warning(m18n.n("experimental_feature")) +def app_action_run( + operation_logger, app, action, args=None, args_file=None +): - from yunohost.hook import hook_exec - - # will raise if action doesn't exist - actions = app_action_list(app)["actions"] - actions = {x["id"]: x for x in actions} - - if action not in actions: - available_actions = (", ".join(actions.keys()),) - raise YunohostValidationError( - f"action '{action}' not available for app '{app}', available actions are: {available_actions}", - raw_msg=True, - ) - - operation_logger.start() - - action_declaration = actions[action] - - # Retrieve arguments list for install script - raw_questions = actions[action].get("arguments", {}) - questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) - args = { - question.name: question.value - for question in questions - if question.value is not None - } - - tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) - - env_dict = _make_environment_for_app_script( - app, args=args, args_prefix="ACTION_", workdir=tmp_workdir_for_app, action=action - ) - - _, action_script = tempfile.mkstemp(dir=tmp_workdir_for_app) - - with open(action_script, "w") as script: - script.write(action_declaration["command"]) - - if action_declaration.get("cwd"): - cwd = action_declaration["cwd"].replace("$app", app) - else: - cwd = tmp_workdir_for_app - - try: - retcode = hook_exec( - action_script, - env=env_dict, - chdir=cwd, - user=action_declaration.get("user", "root"), - )[0] - # Calling hook_exec could fail miserably, or get - # manually interrupted (by mistake or because script was stuck) - # In that case we still want to delete the tmp work dir - except (KeyboardInterrupt, EOFError, Exception): - retcode = -1 - import traceback - - logger.error(m18n.n("unexpected_error", error="\n" + traceback.format_exc())) - finally: - shutil.rmtree(tmp_workdir_for_app) - - if retcode not in action_declaration.get("accepted_return_codes", [0]): - msg = f"Error while executing action '{action}' of app '{app}': return code {retcode}" - operation_logger.error(msg) - raise YunohostError(msg, raw_msg=True) - - operation_logger.success() - return logger.success("Action successed!") + return AppConfigPanel(app).run_action(action, args=args, args_file=args_file, operation_logger=operation_logger) def app_config_get(app, key="", full=False, export=False): @@ -1677,6 +1604,10 @@ class AppConfigPanel(ConfigPanel): def _load_current_values(self): self.values = self._call_config_script("show") + def _run_action(self, action): + env = {key: str(value) for key, value in self.new_values.items()} + self._call_config_script(action, env=env) + def _apply(self): env = {key: str(value) for key, value in self.new_values.items()} return_content = self._call_config_script("apply", env=env) @@ -1730,8 +1661,10 @@ ynh_app_config_run $1 if ret != 0: if action == "show": raise YunohostError("app_config_unable_to_read") - else: + elif action == "apply": raise YunohostError("app_config_unable_to_apply") + else: + raise YunohostError("app_action_failed", action=action) return values @@ -1740,58 +1673,6 @@ def _get_app_actions(app_id): actions_toml_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.toml") actions_json_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.json") - # sample data to get an idea of what is going on - # this toml extract: - # - - # [restart_service] - # name = "Restart service" - # command = "echo pouet $YNH_ACTION_SERVICE" - # user = "root" # optional - # cwd = "/" # optional - # accepted_return_codes = [0, 1, 2, 3] # optional - # description.en = "a dummy stupid exemple or restarting a service" - # - # [restart_service.arguments.service] - # type = "string", - # ask.en = "service to restart" - # example = "nginx" - # - # will be parsed into this: - # - # OrderedDict([(u'restart_service', - # OrderedDict([(u'name', u'Restart service'), - # (u'command', u'echo pouet $YNH_ACTION_SERVICE'), - # (u'user', u'root'), - # (u'cwd', u'/'), - # (u'accepted_return_codes', [0, 1, 2, 3]), - # (u'description', - # OrderedDict([(u'en', - # u'a dummy stupid exemple or restarting a service')])), - # (u'arguments', - # OrderedDict([(u'service', - # OrderedDict([(u'type', u'string'), - # (u'ask', - # OrderedDict([(u'en', - # u'service to restart')])), - # (u'example', - # u'nginx')]))]))])), - # - # - # and needs to be converted into this: - # - # [{u'accepted_return_codes': [0, 1, 2, 3], - # u'arguments': [{u'ask': {u'en': u'service to restart'}, - # u'example': u'nginx', - # u'name': u'service', - # u'type': u'string'}], - # u'command': u'echo pouet $YNH_ACTION_SERVICE', - # u'cwd': u'/', - # u'description': {u'en': u'a dummy stupid exemple or restarting a service'}, - # u'id': u'restart_service', - # u'name': u'Restart service', - # u'user': u'root'}] - if os.path.exists(actions_toml_path): toml_actions = toml.load(open(actions_toml_path, "r"), _dict=OrderedDict) diff --git a/src/certificate.py b/src/certificate.py index 30d1587b8..076a12980 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -33,6 +33,7 @@ from datetime import datetime from moulinette import m18n from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, chown, chmod +from moulinette.utils.process import check_output from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate from yunohost.utils.error import YunohostError, YunohostValidationError @@ -93,8 +94,6 @@ def certificate_status(domains, full=False): if not full: del status["subject"] del status["CA_name"] - status["CA_type"] = status["CA_type"]["verbose"] - status["summary"] = status["summary"]["verbose"] if full: try: @@ -129,6 +128,7 @@ def certificate_install(domain_list, force=False, no_checks=False, self_signed=F def _certificate_install_selfsigned(domain_list, force=False): + failed_cert_install = [] for domain in domain_list: operation_logger = OperationLogger( @@ -152,7 +152,7 @@ def _certificate_install_selfsigned(domain_list, force=False): if not force and os.path.isfile(current_cert_file): status = _get_status(domain) - if status["summary"]["code"] in ("good", "great"): + if status["style"] == "success": raise YunohostValidationError( "certmanager_attempt_to_replace_valid_cert", domain=domain ) @@ -214,7 +214,7 @@ def _certificate_install_selfsigned(domain_list, force=False): if ( status - and status["CA_type"]["code"] == "self-signed" + and status["CA_type"] == "selfsigned" and status["validity"] > 3648 ): logger.success( @@ -223,9 +223,17 @@ def _certificate_install_selfsigned(domain_list, force=False): operation_logger.success() else: msg = f"Installation of self-signed certificate installation for {domain} failed !" + failed_cert_install.append(domain) logger.error(msg) + logger.error(status) operation_logger.error(msg) + if failed_cert_install: + raise YunohostError( + "certmanager_cert_install_failed_selfsigned", + domains=",".join(failed_cert_install) + ) + def _certificate_install_letsencrypt(domains, force=False, no_checks=False): from yunohost.domain import domain_list, _assert_domain_exists @@ -239,7 +247,7 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): for domain in domain_list()["domains"]: status = _get_status(domain) - if status["CA_type"]["code"] != "self-signed": + if status["CA_type"] != "selfsigned": continue domains.append(domain) @@ -251,12 +259,13 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): # Is it self-signed? status = _get_status(domain) - if not force and status["CA_type"]["code"] != "self-signed": + if not force and status["CA_type"] != "selfsigned": raise YunohostValidationError( "certmanager_domain_cert_not_selfsigned", domain=domain ) # Actual install steps + failed_cert_install = [] for domain in domains: if not no_checks: @@ -285,11 +294,18 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): logger.error( f"Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain {domain}." ) + failed_cert_install.append(domain) else: logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) operation_logger.success() + if failed_cert_install: + raise YunohostError( + "certmanager_cert_install_failed", + domains=",".join(failed_cert_install) + ) + def certificate_renew(domains, force=False, no_checks=False, email=False): """ @@ -312,7 +328,7 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): # Does it have a Let's Encrypt cert? status = _get_status(domain) - if status["CA_type"]["code"] != "lets-encrypt": + if status["CA_type"] != "letsencrypt": continue # Does it expire soon? @@ -347,7 +363,7 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): ) # Does it have a Let's Encrypt cert? - if status["CA_type"]["code"] != "lets-encrypt": + if status["CA_type"] != "letsencrypt": raise YunohostValidationError( "certmanager_attempt_to_renew_nonLE_cert", domain=domain ) @@ -359,6 +375,7 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): ) # Actual renew steps + failed_cert_install = [] for domain in domains: if not no_checks: @@ -400,6 +417,8 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): logger.error(stack.getvalue()) logger.error(str(e)) + failed_cert_install.append(domain) + if email: logger.error("Sending email with details to root ...") _email_renewing_failed(domain, msg + "\n" + str(e), stack.getvalue()) @@ -407,6 +426,11 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) operation_logger.success() + if failed_cert_install: + raise YunohostError( + "certmanager_cert_renew_failed", + domains=",".join(failed_cert_install) + ) # # Back-end stuff # @@ -535,9 +559,9 @@ def _fetch_and_enable_new_certificate(domain, no_checks=False): _enable_certificate(domain, new_cert_folder) # Check the status of the certificate is now good - status_summary = _get_status(domain)["summary"] + status_style = _get_status(domain)["style"] - if status_summary["code"] != "great": + if status_style != "success": raise YunohostError( "certmanager_certificate_fetching_or_enabling_failed", domain=domain ) @@ -631,59 +655,42 @@ def _get_status(domain): ) days_remaining = (valid_up_to - datetime.utcnow()).days - if cert_issuer in ["yunohost.org"] + yunohost.domain.domain_list()["domains"]: - CA_type = { - "code": "self-signed", - "verbose": "Self-signed", - } + self_signed_issuers = ["yunohost.org"] + yunohost.domain.domain_list()["domains"] + # FIXME: is the .ca.cnf one actually used anywhere ? x_x + conf = os.path.join(SSL_DIR, "openssl.ca.cnf") + if os.path.exists(conf): + self_signed_issuers.append(check_output(f"grep commonName_default {conf}").split()[-1]) + conf = os.path.join(SSL_DIR, "openssl.cnf") + if os.path.exists(conf): + self_signed_issuers.append(check_output(f"grep commonName_default {conf}").split()[-1]) + + if cert_issuer in self_signed_issuers: + CA_type = "selfsigned" elif organization_name == "Let's Encrypt": - CA_type = { - "code": "lets-encrypt", - "verbose": "Let's Encrypt", - } - + CA_type = "letsencrypt" else: - CA_type = { - "code": "other-unknown", - "verbose": "Other / Unknown", - } + CA_type = "other" if days_remaining <= 0: - status_summary = { - "code": "critical", - "verbose": "CRITICAL", - } - - elif CA_type["code"] in ("self-signed", "fake-lets-encrypt"): - status_summary = { - "code": "warning", - "verbose": "WARNING", - } - + style = "danger" + summary = "expired" + elif CA_type == "selfsigned": + style = "warning" + summary = "selfsigned" elif days_remaining < VALIDITY_LIMIT: - status_summary = { - "code": "attention", - "verbose": "About to expire", - } - - elif CA_type["code"] == "other-unknown": - status_summary = { - "code": "good", - "verbose": "Good", - } - - elif CA_type["code"] == "lets-encrypt": - status_summary = { - "code": "great", - "verbose": "Great!", - } - + style = "warning" + summary = "abouttoexpire" + elif CA_type == "other": + style = "success" + summary = "ok" + elif CA_type == "letsencrypt": + style = "success" + summary = "letsencrypt" else: - status_summary = { - "code": "unknown", - "verbose": "Unknown?", - } + # shouldnt happen, because CA_type can be only selfsigned, letsencrypt, or other + style = "" + summary = "wat" return { "domain": domain, @@ -691,7 +698,8 @@ def _get_status(domain): "CA_name": cert_issuer, "CA_type": CA_type, "validity": days_remaining, - "summary": status_summary, + "style": style, + "summary": summary, } @@ -903,6 +911,4 @@ def _name_self_CA(): def _tail(n, file_path): - from moulinette.utils.process import check_output - return check_output(f"tail -n {n} '{file_path}'") diff --git a/src/domain.py b/src/domain.py index d8668e7b9..14b28940a 100644 --- a/src/domain.py +++ b/src/domain.py @@ -498,6 +498,24 @@ class DomainConfigPanel(ConfigPanel): self.registar_id = toml["dns"]["registrar"]["registrar"]["value"] del toml["dns"]["registrar"]["registrar"]["value"] + # Cert stuff + if not filter_key or filter_key[0] == "cert": + + from yunohost.certificate import certificate_status + status = certificate_status([self.entity], full=True)["certificates"][self.entity] + + toml["cert"]["status"]["cert_summary"]["style"] = status["style"] + + # i18n: domain_config_cert_summary_expired + # i18n: domain_config_cert_summary_selfsigned + # i18n: domain_config_cert_summary_abouttoexpire + # i18n: domain_config_cert_summary_ok + # i18n: domain_config_cert_summary_letsencrypt + toml["cert"]["status"]["cert_summary"]["ask"] = m18n.n(f"domain_config_cert_summary_{status['summary']}") + + # FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ... + self.cert_status = status + return toml def _load_current_values(self): @@ -510,6 +528,28 @@ class DomainConfigPanel(ConfigPanel): if not filter_key or filter_key[0] == "dns": self.values["registrar"] = self.registar_id + # FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ... + if not filter_key or filter_key[0] == "cert": + self.values["cert_validity"] = self.cert_status["validity"] + self.values["cert_issuer"] = self.cert_status["CA_type"] + self.values["acme_eligible"] = self.cert_status["ACME_eligible"] + self.values["summary"] = self.cert_status["summary"] + + +def domain_action_run(domain, action, args=None): + + import urllib.parse + + if action == "cert.cert.cert_install": + from yunohost.certificate import certificate_install as action_func + elif action == "cert.cert.cert_renew": + from yunohost.certificate import certificate_renew as action_func + + args = dict(urllib.parse.parse_qsl(args or "", keep_blank_values=True)) + no_checks = args["cert_no_checks"] in ("y", "yes", "on", "1") + + action_func([domain], force=True, no_checks=no_checks) + def _get_domain_settings(domain: str) -> dict: diff --git a/src/tools.py b/src/tools.py index ccc2b4a32..09574c36e 100644 --- a/src/tools.py +++ b/src/tools.py @@ -191,6 +191,9 @@ def tools_postinstall( # If this is a nohost.me/noho.st, actually check for availability if not ignore_dyndns and is_yunohost_dyndns_domain(domain): + + available = None + # Check if the domain is available... try: available = _dyndns_available(domain) diff --git a/src/utils/config.py b/src/utils/config.py index 57a33606e..a13f37f1b 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -49,6 +49,7 @@ from yunohost.log import OperationLogger logger = getActionLogger("yunohost.config") CONFIG_PANEL_VERSION_SUPPORTED = 1.0 + # Those js-like evaluate functions are used to eval safely visible attributes # The goal is to evaluate in the same way than js simple-evaluate # https://github.com/shepherdwind/simple-evaluate @@ -273,6 +274,10 @@ class ConfigPanel: logger.debug(f"Formating result in '{mode}' mode") result = {} for panel, section, option in self._iterate(): + + if section["is_action_section"] and mode != "full": + continue + key = f"{panel['id']}.{section['id']}.{option['id']}" if mode == "export": result[option["id"]] = option.get("current_value") @@ -311,6 +316,82 @@ class ConfigPanel: else: return result + def list_actions(self): + + actions = {} + + # FIXME : meh, loading the entire config panel is again going to cause + # stupid issues for domain (e.g loading registrar stuff when willing to just list available actions ...) + self.filter_key = "" + self._get_config_panel() + for panel, section, option in self._iterate(): + if option["type"] == "button": + key = f"{panel['id']}.{section['id']}.{option['id']}" + actions[key] = _value_for_locale(option["ask"]) + + return actions + + def run_action( + self, action=None, args=None, args_file=None, operation_logger=None + ): + # + # FIXME : this stuff looks a lot like set() ... + # + + self.filter_key = ".".join(action.split(".")[:2]) + action_id = action.split(".")[2] + + # Read config panel toml + self._get_config_panel() + + # FIXME: should also check that there's indeed a key called action + if not self.config: + raise YunohostValidationError(f"No action named {action}", raw_msg=True) + + # Import and parse pre-answered options + logger.debug("Import and parse pre-answered options") + self._parse_pre_answered(args, None, args_file) + + # Read or get values and hydrate the config + self._load_current_values() + self._hydrate() + Question.operation_logger = operation_logger + self._ask(action=action_id) + + # FIXME: here, we could want to check constrains on + # the action's visibility / requirements wrt to the answer to questions ... + + if operation_logger: + operation_logger.start() + + try: + self._run_action(action_id) + except YunohostError: + raise + # Script got manually interrupted ... + # N.B. : KeyboardInterrupt does not inherit from Exception + except (KeyboardInterrupt, EOFError): + error = m18n.n("operation_interrupted") + logger.error(m18n.n("config_action_failed", action=action, error=error)) + raise + # Something wrong happened in Yunohost's code (most probably hook_exec) + except Exception: + import traceback + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error(m18n.n("config_action_failed", action=action, error=error)) + raise + finally: + # Delete files uploaded from API + # FIXME : this is currently done in the context of config panels, + # but could also happen in the context of app install ... (or anywhere else + # where we may parse args etc...) + FileQuestion.clean_upload_dirs() + + # FIXME: i18n + logger.success(f"Action {action_id} successful") + operation_logger.success() + def set( self, key=None, value=None, args=None, args_file=None, operation_logger=None ): @@ -417,6 +498,7 @@ class ConfigPanel: "name": "", "services": [], "optional": True, + "is_action_section": False, }, }, "options": { @@ -443,6 +525,9 @@ class ConfigPanel: "accept", "redact", "filter", + "readonly", + "enabled", + # "confirm", # TODO: to ask confirmation before running an action ], "defaults": {}, }, @@ -485,6 +570,9 @@ class ConfigPanel: elif level == "sections": 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["type"] == "button": + out["is_action_section"] = True out.setdefault(sublevel, []).append(subnode) # Key/value are a property else: @@ -525,18 +613,39 @@ 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 def _hydrate(self): # Hydrating config panel with current value - for _, _, option in self._iterate(): + for _, section, option in self._iterate(): if option["id"] not in self.values: - allowed_empty_types = ["alert", "display_text", "markdown", "file"] - if ( + + allowed_empty_types = ["alert", "display_text", "markdown", "file", "button"] + + if section["is_action_section"] and option.get("default") is not None: + self.values[option["id"]] = option["default"] + elif ( option["type"] in allowed_empty_types or option.get("bind") == "null" ): @@ -554,13 +663,16 @@ class ConfigPanel: return self.values - def _ask(self): + def _ask(self, action=None): logger.debug("Ask unanswered question and prevalidate data") if "i18n" in self.config: for panel, section, option in self._iterate(): if "ask" not in option: option["ask"] = m18n.n(self.config["i18n"] + "_" + option["id"]) + # auto add i18n help text if present in locales + if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + '_help'): + option["help"] = m18n.n(self.config["i18n"] + "_" + option["id"] + '_help') def display_header(message): """CLI panel/section header display""" @@ -568,13 +680,33 @@ class ConfigPanel: Moulinette.display(colorize(message, "purple")) for panel, section, obj in self._iterate(["panel", "section"]): - if panel == obj: - name = _value_for_locale(panel["name"]) - display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}") + + if section and section.get("visible") and not evaluate_simple_js_expression( + section["visible"], context=self.new_values + ): + continue + + # Ugly hack to skip action section ... except when when explicitly running actions + if not action: + if section and section["is_action_section"]: + continue + + if panel == obj: + name = _value_for_locale(panel["name"]) + display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}") + else: + name = _value_for_locale(section["name"]) + if name: + display_header(f"\n# {name}") + elif section: + # filter action section options in case of multiple buttons + section["options"] = [ + option for option in section["options"] + if option.get("type", "string") != "button" or option["id"] == action + ] + + if panel == obj: continue - name = _value_for_locale(section["name"]) - if name: - display_header(f"\n# {name}") # Check and ask unanswered questions prefilled_answers = self.args.copy() @@ -594,8 +726,6 @@ class ConfigPanel: } ) - self.errors = None - def _get_default_values(self): return { option["id"]: option["default"] @@ -702,6 +832,7 @@ class Question: self.default = question.get("default", None) self.optional = question.get("optional", False) self.visible = question.get("visible", None) + 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) @@ -762,8 +893,10 @@ class Question: # 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 getattr(self, "readonly", False): + 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) @@ -823,7 +956,12 @@ class Question: text_for_user_input_in_cli = _value_for_locale(self.ask) - if self.choices: + 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]}" + return text_for_user_input_in_cli + 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...) @@ -909,7 +1047,7 @@ class DateQuestion(StringQuestion): class TimeQuestion(StringQuestion): pattern = { - "regexp": r"^(1[12]|0?\d):[0-5]\d$", + "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", "error": "config_validate_time", # i18n: config_validate_time } @@ -923,6 +1061,7 @@ class ColorQuestion(StringQuestion): class TagsQuestion(Question): argument_type = "tags" + default_value = "" @staticmethod def humanize(value, option={}): @@ -1094,7 +1233,8 @@ class BooleanQuestion(Question): def _format_text_for_user_input_in_cli(self): text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() - text_for_user_input_in_cli += " [yes | no]" + if not self.readonly: + text_for_user_input_in_cli += " [yes | no]" return text_for_user_input_in_cli @@ -1264,7 +1404,6 @@ class NumberQuestion(Question): class DisplayTextQuestion(Question): argument_type = "display_text" - readonly = True def __init__( self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} @@ -1272,6 +1411,7 @@ class DisplayTextQuestion(Question): super().__init__(question, context, hooks) self.optional = True + self.readonly = True self.style = question.get( "style", "info" if question["type"] == "alert" else "" ) @@ -1351,6 +1491,17 @@ class FileQuestion(Question): return self.value +class ButtonQuestion(Question): + argument_type = "button" + enabled = None + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + self.enabled = question.get("enabled", None) + + ARGUMENTS_TYPE_PARSERS = { "string": StringQuestion, "text": StringQuestion, @@ -1374,6 +1525,7 @@ ARGUMENTS_TYPE_PARSERS = { "markdown": DisplayTextQuestion, "file": FileQuestion, "app": AppQuestion, + "button": ButtonQuestion, } @@ -1416,6 +1568,19 @@ def ask_questions_and_parse_answers( question_class = ARGUMENTS_TYPE_PARSERS[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 # type: ignore + or evaluate_simple_js_expression(question.enabled, context=context) # 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)