diff --git a/.gitignore b/.gitignore index eae46b4c5..91b5b56e4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,10 @@ src/locales # Test src/tests/apps + +# Tmp/local doc stuff +doc/bash-completion.sh +doc/bash_completion.d +doc/openapi.js +doc/openapi.json +doc/swagger diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 59179f7a7..528d8f5aa 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -14,7 +14,7 @@ generate-helpers-doc: - cd doc - python3 generate_helper_doc.py - hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo - - cp helpers.md doc_repo/pages/04.contribute/04.packaging_apps/11.helpers/packaging_apps_helpers.md + - cp helpers.md doc_repo/pages/06.contribute/10.packaging_apps/11.helpers/packaging_apps_helpers.md - cd doc_repo # replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ? - hub checkout -b "${CI_COMMIT_REF_NAME}" diff --git a/.gitlab/ci/install.gitlab-ci.yml b/.gitlab/ci/install.gitlab-ci.yml index 335e07eb6..ecdfecfcd 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 --force-diskspace + - yunohost tools postinstall -d domain.tld -u syssa -F 'Syssa Mine' -p the_password --ignore-dyndns --force-diskspace diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index 27b9b4913..804940aa2 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -1,7 +1,7 @@ .install_debs: &install_debs - 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 - - pip3 install -U mock pip pytest pytest-cov pytest-mock pytest-sugar requests-mock tox ansi2html black jinja2 + - pip3 install -U mock pip pytest pytest-cov pytest-mock pytest-sugar requests-mock tox ansi2html black jinja2 "packaging<22" .test-stage: stage: test @@ -34,7 +34,7 @@ full-tests: PYTEST_ADDOPTS: "--color=yes" before_script: - *install_debs - - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace + - yunohost tools postinstall -d domain.tld -u syssa -F 'Syssa Mine' -p the_password --ignore-dyndns --force-diskspace script: - python3 -m pytest --cov=yunohost tests/ src/tests/ src/diagnosers/ --junitxml=report.xml - cd tests @@ -125,6 +125,15 @@ test-app-config: - src/app.py - src/utils/config.py +test-app-resources: + extends: .test-stage + script: + - python3 -m pytest src/tests/test_app_resources.py + only: + changes: + - src/app.py + - src/utils/resources.py + test-changeurl: extends: .test-stage script: diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md deleted file mode 100644 index 0a9ac7527..000000000 --- a/CONTRIBUTORS.md +++ /dev/null @@ -1,101 +0,0 @@ -YunoHost core contributors -========================== - -YunoHost is built and maintained by the YunoHost project community. -Everyone is encouraged to submit issues and changes, and to contribute in other ways -- see https://yunohost.org/contribute to find out how. - --- - -Initial YunoHost core was built by Kload & beudbeud, for YunoHost v2. - -Most of code was written by Kload and jerome, with help of numerous contributors. - -Translation is made by a bunch of lovely people all over the world. - -We would like to thank anyone who ever helped the YunoHost project <3 - - -YunoHost core Contributors --------------------------- - -- Jérôme Lebleu -- Kload -- Laurent 'Bram' Peuch -- Julien 'ju' Malik -- opi -- Aleks -- Adrien 'beudbeud' Beudin -- M5oul -- Valentin 'zamentur' / 'ljf' Grimaud -- Jocelyn Delalande -- infertux -- Taziden -- ZeHiro -- Josue-T -- nahoj -- a1ex -- JimboJoe -- vetetix -- jellium -- Sebastien 'sebian' Badia -- lmangani -- Julien Vaubourg -- thardev -- zimo2001 - - -YunoHost core Translators -------------------------- - -If you want to help translation, please visit https://translate.yunohost.org/projects/yunohost/yunohost/ - - -### Dutch - -- DUBWiSE -- Jeroen Keerl -- marut - -### English - -- Bugsbane -- rokaz - -### French - -- aoz roon -- Genma -- Jean-Baptiste Holcroft -- Jean P. -- Jérôme Lebleu -- Lapineige -- paddy - - -### German - -- david.bartke -- Fabian Gruber -- Felix Bartels -- Jeroen Keerl -- martin kistner -- Philip Gatzka - -### Hindi - -- Anmol - -### Italian - -- bricabrac -- Thomas Bille - -### Portuguese - -- Deleted User -- Trollken - -### Spanish - -- Juanu - diff --git a/README.md b/README.md index 969651eee..5d37b2af1 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,24 @@ Webadmin ([Yunohost-Admin](https://github.com/YunoHost/yunohost-admin)) | Single ## License As [other components of YunoHost](https://yunohost.org/#/faq_en), this repository is licensed under GNU AGPL v3. + +## They support us <3 + +We are thankful for our sponsors providing us with infrastructure and grants! + +
+

+ + + +

+

+ + + + + +

+
+ +This project was funded through the [NGI0 PET](https://nlnet.nl/PET) Fund, a fund established by NLnet with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825310. If you're interested, [check out how to apply in this video](https://media.ccc.de/v/36c3-10795-ngi_zero_a_treasure_trove_of_it_innovation)! diff --git a/bin/yunohost b/bin/yunohost index 8cebdee8e..3f985e6e7 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -1,5 +1,4 @@ #! /usr/bin/python3 -# -*- coding: utf-8 -*- import os import sys @@ -25,6 +24,9 @@ def _parse_cli_args(): parser.add_argument( "--quiet", action="store_true", default=False, help="Don't produce any output" ) + parser.add_argument( + "--version", action="store_true", default=False, help="Display YunoHost packages versions (alias to 'yunohost tools versions')" + ) parser.add_argument( "--timeout", type=int, @@ -51,6 +53,7 @@ def _parse_cli_args(): # Stupid PATH management because sometimes (e.g. some cron job) PATH is only /usr/bin:/bin ... + default_path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" if os.environ["PATH"] != default_path: os.environ["PATH"] = default_path + ":" + os.environ["PATH"] @@ -67,6 +70,9 @@ if __name__ == "__main__": parser, opts, args = _parse_cli_args() + if opts.version: + args = ["tools", "versions"] + # Execute the action yunohost.cli( debug=opts.debug, diff --git a/bin/yunohost-api b/bin/yunohost-api index 8cf9d4f26..9f4d5eb26 100755 --- a/bin/yunohost-api +++ b/bin/yunohost-api @@ -1,5 +1,4 @@ #! /usr/bin/python3 -# -*- coding: utf-8 -*- import argparse import yunohost diff --git a/bin/yunopaste b/bin/yunopaste index 679f13544..edf8d55c8 100755 --- a/bin/yunopaste +++ b/bin/yunopaste @@ -19,7 +19,7 @@ paste_data() { [[ -z "$json" ]] && _die "Unable to post the data to the server." key=$(echo "$json" \ - | python -c 'import json,sys;o=json.load(sys.stdin);print o["key"]' \ + | 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." diff --git a/bin/yunoprompt b/bin/yunoprompt index 8062ab06e..3ab510d2a 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -56,7 +56,7 @@ EOF echo "$LOGO_AND_FINGERPRINTS" > /etc/issue -if [[ ! -f /etc/yunohost/installed ]] +if ! groups | grep -q all_users && [[ ! -f /etc/yunohost/installed ]] then chvt 2 diff --git a/conf/dovecot/dovecot.sieve b/conf/dovecot/dovecot.sieve index 639c28303..bf4754529 100644 --- a/conf/dovecot/dovecot.sieve +++ b/conf/dovecot/dovecot.sieve @@ -1,4 +1,4 @@ require "fileinto"; - if header :contains "X-Spam-Flag" "YES" { + if header :contains "X-Spam-Flag" "Yes" { fileinto "Junk"; } diff --git a/conf/fail2ban/postfix-sasl.conf b/conf/fail2ban/postfix-sasl.conf new file mode 100644 index 000000000..a9f470782 --- /dev/null +++ b/conf/fail2ban/postfix-sasl.conf @@ -0,0 +1,6 @@ +# Fail2Ban filter for postfix authentication failures +[INCLUDES] +before = common.conf +[Definition] +_daemon = postfix/smtpd +failregex = ^%(__prefix_line)swarning: [-._\w]+\[\]: SASL (?:LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5) authentication failed(: [ A-Za-z0-9+/]*={0,2})?\s*$ diff --git a/conf/fail2ban/yunohost-jails.conf b/conf/fail2ban/yunohost-jails.conf index 1cf1a1966..911f9cd85 100644 --- a/conf/fail2ban/yunohost-jails.conf +++ b/conf/fail2ban/yunohost-jails.conf @@ -8,6 +8,13 @@ enabled = true [postfix] enabled = true +[sasl] +enabled = true +port = smtp +filter = postfix-sasl +logpath = /var/log/mail.log +maxretry = 5 + [dovecot] enabled = true diff --git a/conf/nginx/plain/yunohost_http_errors.conf.inc b/conf/nginx/plain/yunohost_http_errors.conf.inc new file mode 100644 index 000000000..76f1015f3 --- /dev/null +++ b/conf/nginx/plain/yunohost_http_errors.conf.inc @@ -0,0 +1,7 @@ +error_page 502 /502.html; + +location = /502.html { + + root /usr/share/yunohost/html/; + +} diff --git a/conf/nginx/security.conf.inc b/conf/nginx/security.conf.inc index a35bb566e..fe853155b 100644 --- a/conf/nginx/security.conf.inc +++ b/conf/nginx/security.conf.inc @@ -29,7 +29,6 @@ ssl_dhparam /usr/share/yunohost/ffdhe2048.pem; more_set_headers "Content-Security-Policy : upgrade-insecure-requests; default-src https: data: blob: ; object-src https: data: 'unsafe-inline'; style-src https: data: 'unsafe-inline' ; script-src https: data: 'unsafe-inline' 'unsafe-eval'"; {% else %} more_set_headers "Content-Security-Policy : upgrade-insecure-requests"; -more_set_headers "Content-Security-Policy-Report-Only : default-src https: wss: data: blob: ; object-src https: data: 'unsafe-inline'; style-src https: data: 'unsafe-inline' ; script-src https: data: 'unsafe-inline' 'unsafe-eval'"; {% endif %} more_set_headers "X-Content-Type-Options : nosniff"; more_set_headers "X-XSS-Protection : 1; mode=block"; diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index 379b597a7..40f85b328 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -6,7 +6,7 @@ map $http_upgrade $connection_upgrade { server { listen 80; listen [::]:80; - server_name {{ domain }} xmpp-upload.{{ domain }}; + server_name {{ domain }}{% if xmpp_enabled != "True" %} xmpp-upload.{{ domain }} muc.{{ domain }}{% endif %}; access_by_lua_file /usr/share/ssowat/access.lua; @@ -16,9 +16,11 @@ server { alias /tmp/.well-known/ynh-diagnosis/; } + {% if mail_enabled == "True" %} location ^~ '/.well-known/autoconfig/mail/' { alias /var/www/.well-known/{{ domain }}/autoconfig/mail/; } + {% endif %} {# Note that this != "False" is meant to be failure-safe, in the case the redrect_to_https would happen to contain empty string or whatever value. We absolutely don't want to disable the HTTPS redirect *except* when it's explicitly being asked to be disabled. #} {% if redirect_to_https != "False" %} @@ -30,6 +32,8 @@ server { include /etc/nginx/conf.d/{{ domain }}.d/*.conf; {% endif %} + include /etc/nginx/conf.d/yunohost_http_errors.conf.inc; + access_log /var/log/nginx/{{ domain }}-access.log; error_log /var/log/nginx/{{ domain }}-error.log; } @@ -44,10 +48,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; @@ -56,9 +60,11 @@ server { resolver_timeout 5s; {% endif %} + {% if mail_enabled == "True" %} location ^~ '/.well-known/autoconfig/mail/' { alias /var/www/.well-known/{{ domain }}/autoconfig/mail/; } + {% endif %} access_by_lua_file /usr/share/ssowat/access.lua; @@ -67,11 +73,13 @@ server { include /etc/nginx/conf.d/yunohost_sso.conf.inc; include /etc/nginx/conf.d/yunohost_admin.conf.inc; include /etc/nginx/conf.d/yunohost_api.conf.inc; + include /etc/nginx/conf.d/yunohost_http_errors.conf.inc; access_log /var/log/nginx/{{ domain }}-access.log; error_log /var/log/nginx/{{ domain }}-error.log; } +{% if xmpp_enabled == "True" %} # vhost dedicated to XMPP http_upload server { listen 443 ssl http2; @@ -99,10 +107,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; @@ -114,3 +122,4 @@ server { access_log /var/log/nginx/xmpp-upload.{{ domain }}-access.log; error_log /var/log/nginx/xmpp-upload.{{ domain }}-error.log; } +{% endif %} diff --git a/conf/nginx/yunohost_admin.conf.inc b/conf/nginx/yunohost_admin.conf.inc index b5eff7a5e..84c49d30b 100644 --- a/conf/nginx/yunohost_admin.conf.inc +++ b/conf/nginx/yunohost_admin.conf.inc @@ -19,6 +19,10 @@ location /yunohost/admin/ { more_set_headers "Cache-Control: no-store, no-cache, must-revalidate"; } + location /yunohost/admin/applogos/ { + alias /usr/share/yunohost/applogos/; + } + more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; connect-src 'self' https://paste.yunohost.org wss://$host; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; object-src 'none'; img-src 'self' data:;"; more_set_headers "Content-Security-Policy-Report-Only:"; } diff --git a/conf/postfix/main.cf b/conf/postfix/main.cf index 3e53714d0..19b40aefb 100644 --- a/conf/postfix/main.cf +++ b/conf/postfix/main.cf @@ -81,7 +81,7 @@ alias_maps = hash:/etc/aliases alias_database = hash:/etc/aliases mydomain = {{ main_domain }} mydestination = localhost -{% if relay_host == "" %} +{% if relay_enabled != "True" %} relayhost = {% else %} relayhost = [{{ relay_host }}]:{{ relay_port }} @@ -102,7 +102,7 @@ message_size_limit = 35914708 virtual_mailbox_domains = ldap:/etc/postfix/ldap-domains.cf virtual_mailbox_maps = ldap:/etc/postfix/ldap-accounts.cf virtual_mailbox_base = -virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf +virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf,ldap:/etc/postfix/ldap-groups.cf virtual_alias_domains = virtual_minimum_uid = 100 virtual_uid_maps = static:vmail @@ -198,7 +198,7 @@ smtpd_client_recipient_rate_limit=150 # and after to send spam disable_vrfy_command = yes -{% if relay_user != "" %} +{% if relay_enabled == "True" %} # Relay email through an other smtp account # enable SASL authentication smtp_sasl_auth_enable = yes diff --git a/conf/postfix/plain/ldap-groups.cf b/conf/postfix/plain/ldap-groups.cf new file mode 100644 index 000000000..dbf768641 --- /dev/null +++ b/conf/postfix/plain/ldap-groups.cf @@ -0,0 +1,9 @@ +server_host = localhost +server_port = 389 +search_base = dc=yunohost,dc=org +query_filter = (&(objectClass=groupOfNamesYnh)(mail=%s)) +exclude_internal = yes +search_timeout = 30 +scope = sub +result_attribute = memberUid, mail +terminal_result_attribute = memberUid diff --git a/conf/rspamd/rspamd.sieve b/conf/rspamd/rspamd.sieve index 38943eefa..56a30c3c1 100644 --- a/conf/rspamd/rspamd.sieve +++ b/conf/rspamd/rspamd.sieve @@ -1,4 +1,4 @@ require ["fileinto"]; -if header :is "X-Spam" "yes" { +if header :is "X-Spam" "Yes" { fileinto "Junk"; } diff --git a/conf/slapd/config.ldif b/conf/slapd/config.ldif index e1fe3b1b5..1037e8bed 100644 --- a/conf/slapd/config.ldif +++ b/conf/slapd/config.ldif @@ -10,7 +10,7 @@ # Config database customization: # 1. Edit this file as you want. # 2. Apply your modifications. For this just run this following command in a shell: -# $ /usr/share/yunohost/hooks/conf_regen/06-slapd apply_config +# $ /usr/share/yunohost/hooks/conf_regen/06-slapd post true # # Note that if you customize this file, YunoHost's regen-conf will NOT # overwrite this file. But that also means that you should be careful about @@ -130,7 +130,6 @@ olcSuffix: dc=yunohost,dc=org # admin entry below # These access lines apply to database #1 only olcAccess: {0}to attrs=userPassword,shadowLastChange - by dn.base="cn=admin,dc=yunohost,dc=org" write by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write by anonymous auth by self write @@ -140,7 +139,6 @@ olcAccess: {0}to attrs=userPassword,shadowLastChange # owning it if they are authenticated. # Others should be able to see it. olcAccess: {1}to attrs=cn,gecos,givenName,mail,maildrop,displayName,sn - by dn.base="cn=admin,dc=yunohost,dc=org" write by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write by self write by * read @@ -160,9 +158,8 @@ olcAccess: {2}to dn.base="" # The admin dn has full write access, everyone else # can read everything. olcAccess: {3}to * - by dn.base="cn=admin,dc=yunohost,dc=org" write by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write - by group/groupOfNames/member.exact="cn=admin,ou=groups,dc=yunohost,dc=org" write + by group/groupOfNames/member.exact="cn=admins,ou=groups,dc=yunohost,dc=org" write by * read # olcAddContentAcl: FALSE diff --git a/conf/slapd/db_init.ldif b/conf/slapd/db_init.ldif index be0181dfe..8703afb85 100644 --- a/conf/slapd/db_init.ldif +++ b/conf/slapd/db_init.ldif @@ -5,15 +5,6 @@ objectClass: organization o: yunohost.org dc: yunohost -dn: cn=admin,ou=sudo,dc=yunohost,dc=org -cn: admin -objectClass: sudoRole -objectClass: top -sudoCommand: ALL -sudoUser: admin -sudoOption: !authenticate -sudoHost: ALL - dn: ou=users,dc=yunohost,dc=org objectClass: organizationalUnit objectClass: top @@ -39,27 +30,23 @@ objectClass: organizationalUnit objectClass: top ou: groups +dn: cn=admins,ou=sudo,dc=yunohost,dc=org +cn: admins +objectClass: sudoRole +objectClass: top +sudoCommand: ALL +sudoUser: %admins +sudoHost: ALL + dn: ou=sudo,dc=yunohost,dc=org objectClass: organizationalUnit objectClass: top ou: sudo -dn: cn=admin,dc=yunohost,dc=org -objectClass: organizationalRole -objectClass: posixAccount -objectClass: simpleSecurityObject -cn: admin -uid: admin -uidNumber: 1007 -gidNumber: 1007 -homeDirectory: /home/admin -loginShell: /bin/bash -userPassword: yunohost - dn: cn=admins,ou=groups,dc=yunohost,dc=org objectClass: posixGroup objectClass: top -memberUid: admin +objectClass: groupOfNamesYnh gidNumber: 4001 cn: admins diff --git a/conf/slapd/mailserver.ldif b/conf/slapd/mailserver.ldif index 849d1d9e1..09f5c64cc 100644 --- a/conf/slapd/mailserver.ldif +++ b/conf/slapd/mailserver.ldif @@ -89,4 +89,7 @@ olcObjectClasses: ( 1.3.6.1.4.1.40328.1.1.2.3 NAME 'mailGroup' SUP top AUXILIARY DESC 'Mail Group' MUST ( mail ) + MAY ( + mailalias $ maildrop + ) ) diff --git a/conf/ssh/sshd_config b/conf/ssh/sshd_config index b6d4111ee..eaa0c7380 100644 --- a/conf/ssh/sshd_config +++ b/conf/ssh/sshd_config @@ -3,7 +3,7 @@ Protocol 2 # PLEASE: if you wish to change the ssh port properly in YunoHost, use this command: -# yunohost settings set security.ssh.port -v +# yunohost settings set security.ssh.ssh_port -v Port {{ port }} {% if ipv6_enabled == "true" %}ListenAddress ::{% endif %} @@ -56,7 +56,7 @@ ChallengeResponseAuthentication no UsePAM yes # PLEASE: if you wish to force everybody to authenticate using ssh keys, run this command: -# yunohost settings set security.ssh.password_authentication -v no +# yunohost settings set security.ssh.ssh_password_authentication -v no {% if password_authentication == "False" %} PasswordAuthentication no {% else %} diff --git a/debian/changelog b/debian/changelog index 6a945f739..24a1969ed 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,265 @@ +yunohost (11.1.2) testing; urgency=low + + - apps: Various fixes/improvements for appsv2, mostly related to webadmin integration ([#1526](https://github.com/yunohost/yunohost/pull/1526)) + - domains/regenconf: propagate mail/xmpp enable/disable toggle to actual system configs ([#1541](https://github.com/yunohost/yunohost/pull/1541)) + - settings: Add a virtual setting to enable passwordless sudo for admins (75cb3cb2) + - settings: Add a global setting to choose SSOwat's theme ([#1545](https://github.com/yunohost/yunohost/pull/1545)) + - certs: Improve trick to identify certs as self-signed (c38aba74) + - certs: be more resilient when mail cant be sent to root for some reason .. (d7ee1c23) + - certs/postfix: propagate postfix SNI stuff when renewing certificates (31794008) + - certs/xmpp: add to domain's certificate the alt subdomain muc ([#1163](https://github.com/yunohost/yunohost/pull/1163)) + - conf/ldap: fix issue where sudo doesn't work because sudo-ldap doesn't create /etc/sudo-ldap.conf :/ (d2417c33) + - configpanels: fix custom getter ([#1546](https://github.com/yunohost/yunohost/pull/1546)) + - configpanels: fix inconsistent return format for boolean, sometimes 1/0, sometimes True/False -> force normalization of values when calling get() for a single setting from a config panel (47b9b8b5) + - postfix/fail2ban: Add postfix SASL login failure to a fail2ban jail ([#1552](https://github.com/yunohost/yunohost/pull/1552)) + - mail: Fix flag case sensitivity in dovecot and rspamd sieve filter ([#1450](https://github.com/yunohost/yunohost/pull/1450)) + - misc: Don't disable avahi-daemon by force in conf_regen ([#1555](https://github.com/yunohost/yunohost/pull/1555)) + - misc: Fix yunopaste ([#1558](https://github.com/yunohost/yunohost/pull/1558)) + - misc: Don't take lock for read/GET operations (#1554) (0ac8e66a) + - i18n: Translations updated for Basque, French, Galician, Portuguese, Slovak, Spanish, Ukrainian + + Thanks to all contributors <3 ! (axolotle, DDATAA, Fabian Wilkens, Gabriel, José M, Jose Riha, ljf, Luis H. Porras, ppr, quiwy, Rafael Fontenelle, selfhoster1312, Tymofii-Lytvynenko, xabirequejo, Xavier Brochard) + + -- Alexandre Aubin Fri, 06 Jan 2023 00:12:53 +0100 + +yunohost (11.1.1.2) testing; urgency=low + + - group mailalias: the ldap class is in fact mailGroup, not mailAccount -_- (1cb5e43e) + + -- Alexandre Aubin Sat, 03 Dec 2022 15:57:22 +0100 + +yunohost (11.1.1.1) testing; urgency=low + + - Fix again the legacy patch for yunohost user create @_@ (46d6fab0) + + -- Alexandre Aubin Sat, 03 Dec 2022 14:13:09 +0100 + +yunohost (11.1.1) testing; urgency=low + + - groups: add mail-aliases management (#1539) (0f9d9388) + - apps: Allow apps to be installed on a path sharing a common base, eg /foo and /foo2 (#1537) (ae594111) + - admins/ldap: re-allow member of the admins group to edit ldap db (4f5cc166) + - nginx: Add 502 custom error page (#1530) (5063e128) + - misc/nodejs: Upgrade n to version 9.0.1 ([#1528](https://github.com/yunohost/yunohost/pull/1528)) + - misc/update: add --allow-releaseinfo-change option to apt update to prevent the classic nightmare when debian changes from stable to oldstable (ac6d6871) + - misc/dns: Add Webgo as Registrar to support it via LexiconAdd Webgo as Registrar (#1529) (c50f3771) + - misc/debug: Improve dpkg_is_broken instruction to also mention dpkg --audit (a772153b) + - misc/regeconf: fix yunohost hook incorectly tweaking mdns.yml ownership (9bd98162) + - misc/helpers: fix docker-image-extract helper (#1532) + - misc/yunoprompt: don't display postinstall tip to members of all_users group (because they can't check if /etc/yunohost/installed exists, but if they're member of the all_users group, then postinstall was already done) (4aaa8896) + - misc/diagnosis: make the dnsrecord diagnoser not complain about the damn 128 vs 0 stuff in CAA records (70a8225b) + - misc/settings: fix output format for 'yunohost settings list' (70bf38ce) + - misc/helpers: Better error message when psql is not there for database_exists (#992) (f49c121b) + - misc/multimedia: fix edgecase where setfacl crashes because of broken symlinks (94f21ea2) + - misc/legacy: auto-patch yunohost user create syntax in app scripts to use --fullname instead (d254fb1b) + - [i18n] Translations updated for Arabic, Basque, Chinese (Simplified), Dutch, French, Galician, German, Spanish, Ukrainian + + Thanks to all contributors <3 ! (André Koot, Augustin Trancart, Axolotle, ButterflyOfFire, Christian Wehrli, Éric Gaspar, José M, lee, mod242, quiwy, tituspijean, Tymofii-Lytvynenko, xabirequejo) + + -- Alexandre Aubin Fri, 02 Dec 2022 23:31:28 +0100 + +yunohost (11.1.0.2) testing; urgency=low + + - globalsettings: make sure to run migration 25 prior to the regenconf (f3750598) + - domaininfo: Some apps don't have path ([#1521](https://github.com/yunohost/yunohost/pull/1521)) + - Add sponsors to the README ([#1522](https://github.com/yunohost/yunohost/pull/1522)) + - postfix: fix relay conf not triggered because new setting system now returns '1' and not 'True' (cd43c8bd) + - postfix: fix permission issue preventing to properly create sasl_passwd.db (5394790f) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Félix Piédallu, Florian Masy, ppr, Tagada) + + -- Alexandre Aubin Fri, 04 Nov 2022 13:13:40 +0100 + +yunohost (11.1.0.1) testing; urgency=low + + - Bump version after propagating hotfix on 11.0.10.2 + + -- Alexandre Aubin Thu, 27 Oct 2022 15:46:26 +0200 + +yunohost (11.1.0) testing; urgency=low + + - apps: New 'v2' packaging format ([#1289](https://github.com/yunohost/yunohost/pull/1289)) + - helpers: Upgrade n to version 9.0.0 ([#1477](https://github.com/yunohost/yunohost/pull/1477)) + - helpers: Support extracting source from docker images in ynh_setup_source ([#1505](https://github.com/yunohost/yunohost/pull/1505)) + - configpanels: Refactor global settings the new config panel framework ([#1459](https://github.com/yunohost/yunohost/pull/1459)) + - configpanels: Add support for actions (= button widget) and apply it to domain cert management ([#1436](https://github.com/YunoHost/yunohost/pull/1436)) + - admin: Drop the 'admin' user, have 'admins' be a group of Yunohost users instead ([#1408](https://github.com/yunohost/yunohost/pull/1408)) + - admin: Implement a new 'virtual global setting' to change root password from the global setting config panel ([#1515](https://github.com/yunohost/yunohost/pull/1515)) + - domains: Be able to "list" domain as a tree structure + add new 'domain_info' API endpoint ([#1434](https://github.com/yunohost/yunohost/pull/1434)) + - users: Encourage to define a single 'full display name' instead of separate 'firstname/lastname' ([#1516](https://github.com/yunohost/yunohost/pull/1516)) + - security: Improve most used password check list ([#1517](https://github.com/yunohost/yunohost/pull/1517)) + - i18n: Translations updated for Slovak + + Thanks to all contributors <3 ! (axolotle, Dante, Jose Riha, Tagadda, yalh76) + + -- Alexandre Aubin Tue, 25 Oct 2022 17:57:29 +0200 + +yunohost (11.0.10.2) stable; urgency=low + + - Add another trick to autorestart yunohost-api at the end of the upgrade when ran from the api itself... (6f640c08) + + -- Alexandre Aubin Thu, 27 Oct 2022 15:46:26 +0200 + +yunohost (11.0.10.1) stable; urgency=low + + - self-upgrade: fix yunohost-api restart which was not triggered @_@ (472e9250) + + -- Alexandre Aubin Mon, 17 Oct 2022 23:56:37 +0200 + +yunohost (11.0.10) stable; urgency=low + + - configpanels: fix nested bind statements (0252a6fd) + - ynh_setup_source: Add option to fully replace the destination dir ([#1509](https://github.com/yunohost/yunohost/pull/1509)) + - tools_update: also yield a boolean to easily know if there's a major yunohost upgrade pending + list of pending migrations (cf change in webadmin to encourage people to check the release note on the forum before yoloupgrading) (86e45f9c) + - diagnosis: add reports when apt is configured with the 'testing' channel for yunohost, or with the 'stable' codename for debian (0adff31d) + - [i18n] Translations updated for French, Slovak + + Thanks to all contributors <3 ! (Dante, Jose Riha, ppr, yalh76) + + -- Alexandre Aubin Mon, 17 Oct 2022 16:56:47 +0200 + +yunohost (11.0.9.15) stable; urgency=low + + - [fix] Lidswitch if no reboot ([#1506](https://github.com/yunohost/yunohost/pull/1506)) + - [fix] postinstall: edge case where var would get undefined.. (b7bea608) + - [fix] backup: Try to fix again the infamous issue where from_yunohost_version gets filled with 'BASH_XTRACEFD' (14fb1cfd) + - [fix] Various english wording improvements ([#1507](https://github.com/yunohost/yunohost/pull/1507)) + - [i18n] Translations updated for Arabic, Slovak, Telugu, Turkish + + Thanks to all contributors <3 ! (Alice Kile, ButterflyOfFire, Jose Riha, ljf (zamentur), marty hiatt, Sedat Albayrak) + + -- Alexandre Aubin Fri, 30 Sep 2022 16:24:59 +0200 + +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) + - [fix] regenconf: fix a stupid issue with slapcat displaying an error message because grep -q breaks the pipe (503b9031) + - [fix] regenconf: add a timeout to curl inside dnsmasq regenconf to prevent being stuck too long when no network on the machine (b77e8114) + - [fix] ynh_delete_file_checksum with non-existing option in helpers/config ([#1501](https://github.com/YunoHost/yunohost/pull/1501)) + - [i18n] Translations updated for Basque, Galician, Slovak + + Thanks to all contributors <3 ! (José M, Jose Riha, tituspijean, xabirequejo) + + -- 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)) + - [i18n] Translations updated for Basque, French, Indonesian, Italian, Slovak + + Thanks to all contributors <3 ! (Salamandar) + + -- Alexandre Aubin Sun, 28 Aug 2022 14:50:38 +0200 + +yunohost (11.0.9.11) stable; urgency=low + + = Merge with Buster branch + - [fix] diagnosis: fix inaccurate message (ae92a0b8) + - [fix] logrotate helpers: getopts miserably explodes if 'legacy_args' is inconsistent with 'args_array' ... (530bf04a) + - [i18n] Translations updated for Basque, French, Indonesian, Italian, Slovak + + Thanks to all contributors <3 ! (Jose Riha, Leandro Noferini, liimee, Stephan Klein, xabirequejo) + + -- Alexandre Aubin Fri, 26 Aug 2022 16:32:19 +0200 + +yunohost (11.0.9.9) stable; urgency=low + + - Sync with Buster branch + - [fix] php7.3->7.4: autopatch nginx configs during restore (18e041c4) + + -- Alexandre Aubin Fri, 19 Aug 2022 20:50:52 +0200 + +yunohost (11.0.9.7) stable; urgency=low + + - [fix] logorate helper: was broken because wrong index é_è (efa80304) + - [i18n] Translations updated for French, Galician, Ukrainian + + Thanks to all contributors <3 ! (Éric Gaspar, José M, Tymofii-Lytvynenko) + + -- Alexandre Aubin Wed, 17 Aug 2022 19:24:11 +0200 + +yunohost (11.0.9.6) stable; urgency=low + + - Sync with Buster branch + - [fix] helpers: logrotate shitty inconsistent handling of 'supposedly legacy' --non-append option ... (8d1c75e7) + - [fix] apps: Better handling of super shitty edge case where an app settings.yml is empty for some unexpected mystic reason ... (9eb123f8) + + -- Alexandre Aubin Wed, 17 Aug 2022 01:26:28 +0200 + +yunohost (11.0.9.5) stable; urgency=low + + - Propagate fixes in buster->bullseye migration + - [fix] venv rebuild: synapse's folder is named matrix-synapse (c8031ace) + + -- Alexandre Aubin Sun, 14 Aug 2022 18:22:30 +0200 + +yunohost (11.0.9.3) stable; urgency=low + + - [fix] postgresql 11->13: Epic typo / missing import (3cb1a41a) + - [i18n] Translations updated for Basque, French, Galician + + Thanks to all contributors <3 ! (Éric Gaspar, José M, Kay0u, punkrockgirl) + + -- Alexandre Aubin Sat, 13 Aug 2022 22:37:05 +0200 + +yunohost (11.0.9.2) stable; urgency=low + + - [fix] venv rebuild: fix yunohost app force upgrade command (5d90971b) + - [fix] apt helpers: simplify ynh_remove_app_dependencies, we don't need to care about removing php-fpm services from yunohost, because 'yunohost service' now dynamically check what relevant phpX.Y-fpm service exist on the system (64e35815) + - [enh] diagnosis: add complains if some app installed are still requiring only yunohost 3.x (31aacb33) + - [fix] venv rebuild: migration should have an empty disclaimer when in auto mode (d2a6dcd4) + - [fix] postgresql 11->13 migration: skip if no yunohost app depend on postgresql (d161da03) + + Thanks to all contributors <3 ! (Éric Gaspar, ljf) + + -- Alexandre Aubin Sat, 13 Aug 2022 20:08:27 +0200 + +yunohost (11.0.9.1) stable; urgency=low + + - [fix] venv rebuild: /opt may not exist ... + + -- Alexandre Aubin Thu, 11 Aug 2022 16:00:40 +0200 + +yunohost (11.0.9) stable; urgency=low + + - [fix] services: Skip php 7.3 which is most likely dead after buster->bullseye migration because users get spooked (51804925) + - [enh] bullseye: add a migration process to automatically attempt to rebuild venvs (3b8e49dc) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Éric Gaspar, Kayou, ljf, theo-is-taken) + + -- Alexandre Aubin Sun, 07 Aug 2022 23:27:41 +0200 + +yunohost (11.0.8.1) testing; urgency=low + + - Fix tests é_è (7fa67b2b) + + -- Alexandre Aubin Sun, 07 Aug 2022 12:41:28 +0200 + +yunohost (11.0.8) testing; urgency=low + + - [fix] helpers: escape username in ynh_user_exists ([#1469](https://github.com/YunoHost/yunohost/pull/1469)) + - [fix] helpers: in nginx helpers, do not change the nginx template conf, replace #sub_path_only and #root_path_only after ynh_add_config, otherwise it breaks the change_url script (30e926f9) + - [fix] helpers: fix arg parsing in ynh_install_apps ([#1480](https://github.com/YunoHost/yunohost/pull/1480)) + - [fix] postinstall: be able to redo postinstall when the 128+ chars + password error is raised ([#1476](https://github.com/YunoHost/yunohost/pull/1476)) + - [fix] regenconf dhclient/resolvconf: fix weird typo, probably meant 'search' (like in our rpi-image tweaking) (9d39a2c0) + - [fix] configpanels: remove debug message because it floods the regenconf logs (f6cd35d9) + - [fix] configpanels: don't restrict choices if there's no choices specified ([#1478](https://github.com/YunoHost/yunohost/pull/1478) + - [i18n] Translations updated for Arabic, German, Slovak, Telugu + + Thanks to all contributors <3 ! (Alice Kile, ButterflyOfFire, Éric Gaspar, Gregor, Jose Riha, Kay0u, ljf, Meta Meta, tituspijean, Valentin von Guttenberg, yalh76) + + -- Alexandre Aubin Sun, 07 Aug 2022 11:26:54 +0200 + yunohost (11.0.7) testing; urgency=low - [fix] Allow lime2 to upgrade even if kernel is hold ([#1452](https://github.com/YunoHost/yunohost/pull/1452)) @@ -102,6 +364,80 @@ yunohost (11.0.2) testing; urgency=low -- Alexandre Aubin Wed, 19 Jan 2022 20:52:39 +0100 +yunohost (4.4.2.14) stable; urgency=low + + - bullseye migration: remove derpy OVH repo... (76014920) + - bullseye migration: improve autofix procedure for the libc6 hell (02b3a138) + + -- Alexandre Aubin Sat, 03 Sep 2022 23:19:08 +0200 + +yunohost (4.4.2.13) stable; urgency=low + + - [fix] bullseye migration: a few annoying issues related to Sury (b5fabc87) + + -- Alexandre Aubin Mon, 29 Aug 2022 15:40:03 +0200 + +yunohost (4.4.2.12) stable; urgency=low + + - bullseye migration: add trick to automagically find the likely log of a previously failed migration to ease support (f5d94509) + + -- Alexandre Aubin Fri, 26 Aug 2022 19:22:30 +0200 + +yunohost (4.4.2.10) stable; urgency=low + + - bullseye migration: add proper explanations and advices after the damn 'The distribution is not Buster' message ... (6a594d0e) + + -- Alexandre Aubin Mon, 22 Aug 2022 10:28:50 +0200 + +yunohost (4.4.2.9) stable; urgency=low + + - apt helper: fix edge case with equivs package being flagged hold because of buster->bullseye migration (b306df2c) + - bullseye migration: fix check about free space in /boot/ ... (a2d4abc1) + + -- Alexandre Aubin Thu, 18 Aug 2022 19:24:47 +0200 + +yunohost (4.4.2.7) stable; urgency=low + + - upgrades: ignore boring insserv warnings during apt commands (87f0eff9) + - bullseye migration: higher treshold for low space detection in /boot/ because some people still experience the issue on 4.4.2.6 (d283c900) + + -- Alexandre Aubin Wed, 17 Aug 2022 01:21:36 +0200 + +yunohost (4.4.2.6) stable; urgency=low + + - [fix] bullseye migration: trash pip freeze stderr because it's confusing users ... (e68fc821) + - [fix] bullseye migration: add a check that there's at least 70MB available in /boot ... (02fcbd97) + - [fix] bullseye migration: better detection mechanism for the libc6 / libgcc hell issue (633a1fbf) + + -- Alexandre Aubin Sun, 14 Aug 2022 18:18:13 +0200 + +yunohost (4.4.2.3) stable; urgency=low + + - [fix] bullseye migration: add fix for stupid dnsmasq not picking new init script (origin/dev, origin/HEAD, dev) + - [fix] bullseye migration: add the patch for the build-essential / libc6-dev / libgcc-8-dev hell ... + - [fix] bullseye migration: add critical fix for RPi failing to get network on reboot + - [fix] bullseye migration: add ffsync to deprecated apps (77c2f5dc) + + -- Alexandre Aubin Sat, 13 Aug 2022 20:06:00 +0200 + +yunohost (4.4.2.1) stable; urgency=low + + - [fix] bullseye migration: /opt may not exist ... (5fd74577) + + -- Alexandre Aubin Thu, 11 Aug 2022 15:56:16 +0200 + +yunohost (4.4.2) stable; urgency=low + + - Release as stable + - [fix] bullseye migration: /etc/apt/sources.list may not exist (b928dd12) + - [fix] bullseye migration: Allow lime2 to upgrade even if kernel is hold (#1452) + - [fix] bullseye migration: Save python apps venv in a requirements file, in order to regenerate it in a follow-up migration ([#1479](https://github.com/YunoHost/yunohost/pull/1479)) + - [fix] bullseye migration: tweak message to prepare for stable release (80015a72) + + Thanks to all contributors <3 ! (ljf, theo-is-taken) + + -- Alexandre Aubin Tue, 09 Aug 2022 16:59:15 +0200 + yunohost (4.4.1) testing; urgency=low - [fix] php helpers: prevent epic catastrophies when the app changes php version (31d3719b) diff --git a/debian/control b/debian/control index 0760e2cde..facedbff2 100644 --- a/debian/control +++ b/debian/control @@ -27,7 +27,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , rspamd, opendkim-tools, postsrsd, procmail, mailutils , redis-server , acl - , git, curl, wget, cron, unzip, jq, bc, at + , git, curl, wget, cron, unzip, jq, bc, at, procps , lsb-release, haveged, fake-hwclock, equivs, lsof, whois Recommends: yunohost-admin , ntp, inetutils-ping | iputils-ping diff --git a/debian/postinst b/debian/postinst index e93845e88..9fb9b9977 100644 --- a/debian/postinst +++ b/debian/postinst @@ -38,6 +38,24 @@ do_configure() { systemctl restart yunohost-api else echo "(Delaying the restart of yunohost-api, this should automatically happen after the end of this upgrade)" + cat << EOF | at -M now >/dev/null 2>&1 +# Wait for apt / dpkg / yunohost to not be up anymore, hence the upgrade finished + +while pgrep -x apt || pgrep -x apt-get || pgrep dpkg || test -e /var/run/moulinette_yunohost.lock; +do + sleep 3 +done + +# Restart yunohost-api, though only if it wasnt already restarted by something else in the last 60 secs + +API_START_TIMESTAMP="\$(date --date="\$(systemctl show yunohost-api | grep ExecMainStartTimestamp= | awk -F= '{print \$2}')" +%s)" + +if [ "\$(( \$(date +%s) - \$API_START_TIMESTAMP ))" -ge 60 ]; +then + echo "restart" >> /var/log/testalex + systemctl restart yunohost-api +fi +EOF fi fi } diff --git a/doc/api.html b/doc/api.html new file mode 100644 index 000000000..502d1247f --- /dev/null +++ b/doc/api.html @@ -0,0 +1,42 @@ + + + + + + Swagger UI + + + + + + +
+ + + + + + + diff --git a/doc/generate_api_doc.py b/doc/generate_api_doc.py new file mode 100644 index 000000000..939dd90bd --- /dev/null +++ b/doc/generate_api_doc.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" License + Copyright (C) 2013 YunoHost + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses +""" + +""" + Generate JSON specification files API +""" +import os +import sys +import yaml +import json +import requests + +def main(): + """ + """ + with open('../share/actionsmap.yml') as f: + action_map = yaml.safe_load(f) + + try: + with open('/etc/yunohost/current_host', 'r') as f: + domain = f.readline().rstrip() + except IOError: + domain = requests.get('http://ip.yunohost.org').text + with open('../debian/changelog') as f: + top_changelog = f.readline() + api_version = top_changelog[top_changelog.find("(")+1:top_changelog.find(")")] + + csrf = { + 'name': 'X-Requested-With', + 'in': 'header', + 'required': True, + 'schema': { + 'type': 'string', + 'default': 'Swagger API' + } + + } + + resource_list = { + 'openapi': '3.0.3', + 'info': { + 'title': 'YunoHost API', + 'description': 'This is the YunoHost API used on all YunoHost instances. This API is essentially used by YunoHost Webadmin.', + 'version': api_version, + + }, + 'servers': [ + { + 'url': "https://{domain}/yunohost/api", + 'variables': { + 'domain': { + 'default': 'demo.yunohost.org', + 'description': 'Your yunohost domain' + } + } + } + ], + 'tags': [ + { + 'name': 'public', + 'description': 'Public route' + } + ], + 'paths': { + '/login': { + 'post': { + 'tags': ['public'], + 'summary': 'Logs in and returns the authentication cookie', + 'parameters': [csrf], + 'requestBody': { + 'required': True, + 'content': { + 'multipart/form-data': { + 'schema': { + 'type': 'object', + 'properties': { + 'credentials': { + 'type': 'string', + 'format': 'password' + } + }, + 'required': [ + 'credentials' + ] + } + } + } + }, + 'security': [], + 'responses': { + '200': { + 'description': 'Successfully login', + 'headers': { + 'Set-Cookie': { + 'schema': { + 'type': 'string' + } + } + } + } + } + } + }, + '/installed': { + 'get': { + 'tags': ['public'], + 'summary': 'Test if the API is working', + 'parameters': [], + 'security': [], + 'responses': { + '200': { + 'description': 'Successfully working', + } + } + } + } + }, + } + + + def convert_categories(categories, parent_category=""): + for category, category_params in categories.items(): + if parent_category: + category = f"{parent_category} {category}" + if 'subcategory_help' in category_params: + category_params['category_help'] = category_params['subcategory_help'] + + if 'category_help' not in category_params: + category_params['category_help'] = '' + resource_list['tags'].append({ + 'name': category, + 'description': category_params['category_help'] + }) + + + for action, action_params in category_params['actions'].items(): + if 'action_help' not in action_params: + action_params['action_help'] = '' + if 'api' not in action_params: + continue + if not isinstance(action_params['api'], list): + action_params['api'] = [action_params['api']] + + for i, api in enumerate(action_params['api']): + print(api) + method, path = api.split(' ') + method = method.lower() + key_param = '' + if '{' in path: + key_param = path[path.find("{")+1:path.find("}")] + resource_list['paths'].setdefault(path, {}) + + notes = '' + + operationId = f"{category}_{action}" + if i > 0: + operationId += f"_{i}" + operation = { + 'tags': [category], + 'operationId': operationId, + 'summary': action_params['action_help'], + 'description': notes, + 'responses': { + '200': { + 'description': 'successful operation' + } + } + } + if action_params.get('deprecated'): + operation['deprecated'] = True + + operation['parameters'] = [] + if method == 'post': + operation['parameters'] = [csrf] + + if 'arguments' in action_params: + if method in ['put', 'post', 'patch']: + operation['requestBody'] = { + 'required': True, + 'content': { + 'multipart/form-data': { + 'schema': { + 'type': 'object', + 'properties': { + }, + 'required': [] + } + } + } + } + for arg_name, arg_params in action_params['arguments'].items(): + if 'help' not in arg_params: + arg_params['help'] = '' + param_type = 'query' + allow_multiple = False + required = True + allowable_values = None + name = str(arg_name).replace('-', '_') + if name[0] == '_': + required = False + if 'full' in arg_params: + name = arg_params['full'][2:] + else: + name = name[2:] + name = name.replace('-', '_') + + if 'choices' in arg_params: + allowable_values = arg_params['choices'] + _type = 'string' + if 'type' in arg_params: + types = { + 'open': 'file', + 'int': 'int' + } + _type = types[arg_params['type']] + if 'action' in arg_params and arg_params['action'] == 'store_true': + _type = 'boolean' + + if 'nargs' in arg_params: + if arg_params['nargs'] == '*': + allow_multiple = True + required = False + _type = 'array' + if arg_params['nargs'] == '+': + allow_multiple = True + required = True + _type = 'array' + if arg_params['nargs'] == '?': + allow_multiple = False + required = False + else: + allow_multiple = False + + + if name == key_param: + param_type = 'path' + required = True + allow_multiple = False + + if method in ['put', 'post', 'patch']: + schema = operation['requestBody']['content']['multipart/form-data']['schema'] + schema['properties'][name] = { + 'type': _type, + 'description': arg_params['help'] + } + if required: + schema['required'].append(name) + prop_schema = schema['properties'][name] + else: + parameters = { + 'name': name, + 'in': param_type, + 'description': arg_params['help'], + 'required': required, + 'schema': { + 'type': _type, + }, + 'explode': allow_multiple + } + prop_schema = parameters['schema'] + operation['parameters'].append(parameters) + + if allowable_values is not None: + prop_schema['enum'] = allowable_values + if 'default' in arg_params: + prop_schema['default'] = arg_params['default'] + if arg_params.get('metavar') == 'PASSWORD': + prop_schema['format'] = 'password' + if arg_params.get('metavar') == 'MAIL': + prop_schema['format'] = 'mail' + # Those lines seems to slow swagger ui too much + #if 'pattern' in arg_params.get('extra', {}): + # prop_schema['pattern'] = arg_params['extra']['pattern'][0] + + + + resource_list['paths'][path][method.lower()] = operation + + # Includes subcategories + if 'subcategories' in category_params: + convert_categories(category_params['subcategories'], category) + + del action_map['_global'] + convert_categories(action_map) + + openapi_json = json.dumps(resource_list) + # Save the OpenAPI json + with open(os.getcwd() + '/openapi.json', 'w') as f: + f.write(openapi_json) + + openapi_js = f"var openapiJSON = {openapi_json}" + with open(os.getcwd() + '/openapi.js', 'w') as f: + f.write(openapi_js) + + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index 371e8899b..525482596 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -221,6 +221,9 @@ def main(): helpers = [] for helper_file in helper_files: + if not os.path.isfile(helper_file): + continue + category_name = os.path.basename(helper_file) print("Parsing %s ..." % category_name) p = Parser(helper_file) diff --git a/doc/generate_resource_doc.py b/doc/generate_resource_doc.py new file mode 100644 index 000000000..1e16a76d9 --- /dev/null +++ b/doc/generate_resource_doc.py @@ -0,0 +1,12 @@ +from yunohost.utils.resources import AppResourceClassesByType + +resources = sorted(AppResourceClassesByType.values(), key=lambda r: r.priority) + +for klass in resources: + + doc = klass.__doc__.replace("\n ", "\n") + + print("") + print(f"## {klass.type.replace('_', ' ').title()}") + print("") + print(doc) diff --git a/helpers/apps b/helpers/apps index 0faad863c..85b74de15 100644 --- a/helpers/apps +++ b/helpers/apps @@ -31,8 +31,11 @@ ynh_install_apps() { if ! yunohost app list --output-as json --quiet | jq -e --arg id $one_app '.apps[] | select(.id == $id)' >/dev/null then # Retrieve the arguments of the app (part after ?) - local one_argument=$(cut -d "?" -f2- <<< "$one_app_and_its_args") - [ ! -z "$one_argument" ] && one_argument="--args $one_argument" + local one_argument="" + if [[ "$one_app_and_its_args" == *"?"* ]]; then + one_argument=$(cut -d "?" -f2- <<< "$one_app_and_its_args") + one_argument="--args $one_argument" + fi # Install the app with its arguments yunohost app install $one_app $one_argument diff --git a/helpers/apt b/helpers/apt index 5ddcba381..8caf9f3dc 100644 --- a/helpers/apt +++ b/helpers/apt @@ -227,9 +227,8 @@ ynh_install_app_dependencies() { # Add a comma for each space between packages. But not add a comma if the space separate a version specification. (See below) dependencies="$(echo "$dependencies" | sed 's/\([^\<=\>]\)\ \([^(]\)/\1, \2/g')" local dependencies=${dependencies//|/ | } - local manifest_path="$YNH_APP_BASEDIR/manifest.json" - local version=$(jq -r '.version' "$manifest_path") + local version=$(ynh_read_manifest --manifest_key="version") if [ -z "${version}" ] || [ "$version" == "null" ]; then version="1.0" fi @@ -361,17 +360,14 @@ ynh_remove_app_dependencies() { current_dependencies=${current_dependencies// | /|} fi - ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. - - # Check if this app used a specific php version ... in which case we check - # if the corresponding php-fpm is still there. Otherwise, we remove the - # service from yunohost as well - - local specific_php_version=$(echo $current_dependencies | tr '-' ' ' | grep -o -E "\" | sed 's/php//g' | sort | uniq) - [[ "$specific_php_version" != "$YNH_DEFAULT_PHP_VERSION" ]] || specific_php_version="" - if [[ -n "$specific_php_version" ]] && ! ynh_package_is_installed --package="php${specific_php_version}-fpm"; then - yunohost service remove php${specific_php_version}-fpm + # Edge case where the app dep may be on hold, + # cf https://forum.yunohost.org/t/migration-error-cause-of-ffsync/20675/4 + if apt-mark showhold | grep -q -w ${dep_app}-ynh-deps + then + apt-mark unhold ${dep_app}-ynh-deps fi + + ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. } # Install packages from an extra repository properly. diff --git a/helpers/backup b/helpers/backup index 01b51d5a1..22737ff86 100644 --- a/helpers/backup +++ b/helpers/backup @@ -285,6 +285,14 @@ ynh_restore_file() { else mv "$archive_path" "${dest_path}" fi + + # Boring hack for nginx conf file mapped to php7.3 + # Note that there's no need to patch the fpm config because most php apps + # will call "ynh_add_fpm_config" during restore, effectively recreating the file from scratch + if [[ "${dest_path}" == "/etc/nginx/conf.d/"* ]] && grep 'php7.3.*sock' "${dest_path}" + then + sed -i 's/php7.3/php7.4/g' "${dest_path}" + fi } # Calculate and store a file checksum into the app settings diff --git a/helpers/config b/helpers/config index 9c7272b85..77f118c5f 100644 --- a/helpers/config +++ b/helpers/config @@ -22,7 +22,7 @@ _ynh_app_config_get_one() { if [[ "$bind" == "settings" ]]; then ynh_die --message="File '${short_setting}' can't be stored in settings" fi - old[$short_setting]="$(ls "$(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" 2>/dev/null || echo YNH_NULL)" + old[$short_setting]="$(ls "$(echo $bind | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" 2>/dev/null || echo YNH_NULL)" file_hash[$short_setting]="true" # Get multiline text from settings or from a full file @@ -32,7 +32,7 @@ _ynh_app_config_get_one() { elif [[ "$bind" == *":"* ]]; then ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" else - old[$short_setting]="$(cat $(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2>/dev/null || echo YNH_NULL)" + old[$short_setting]="$(cat $(echo $bind | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2>/dev/null || echo YNH_NULL)" fi # Get value from a kind of key/value file @@ -47,7 +47,7 @@ _ynh_app_config_get_one() { bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" fi - local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key_}" --after="${bind_after}")" fi @@ -73,11 +73,11 @@ _ynh_app_config_apply_one() { if [[ "$bind" == "settings" ]]; then ynh_die --message="File '${short_setting}' can't be stored in settings" fi - local bind_file="$(echo "$bind" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + local bind_file="$(echo "$bind" | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" if [[ "${!short_setting}" == "" ]]; then ynh_backup_if_checksum_is_different --file="$bind_file" ynh_secure_remove --file="$bind_file" - ynh_delete_file_checksum --file="$bind_file" --update_only + ynh_delete_file_checksum --file="$bind_file" ynh_print_info --message="File '$bind_file' removed" else ynh_backup_if_checksum_is_different --file="$bind_file" @@ -98,7 +98,7 @@ _ynh_app_config_apply_one() { if [[ "$bind" == *":"* ]]; then ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" fi - local bind_file="$(echo "$bind" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + local bind_file="$(echo "$bind" | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" ynh_backup_if_checksum_is_different --file="$bind_file" echo "${!short_setting}" >"$bind_file" ynh_store_file_checksum --file="$bind_file" --update_only @@ -113,7 +113,7 @@ _ynh_app_config_apply_one() { bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" fi - local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" ynh_backup_if_checksum_is_different --file="$bind_file" ynh_write_var_in_file --file="${bind_file}" --key="${bind_key_}" --value="${!short_setting}" --after="${bind_after}" @@ -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/fail2ban b/helpers/fail2ban index 21177fa8d..31f55b312 100644 --- a/helpers/fail2ban +++ b/helpers/fail2ban @@ -99,8 +99,8 @@ ignoreregex = " >$YNH_APP_BASEDIR/conf/f2b_filter.conf fi - ynh_add_config --template="$YNH_APP_BASEDIR/conf/f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf" - ynh_add_config --template="$YNH_APP_BASEDIR/conf/f2b_filter.conf" --destination="/etc/fail2ban/filter.d/$app.conf" + ynh_add_config --template="f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf" + ynh_add_config --template="f2b_filter.conf" --destination="/etc/fail2ban/filter.d/$app.conf" ynh_systemd_action --service_name=fail2ban --action=reload --line_match="(Started|Reloaded) Fail2Ban Service" --log_path=systemd 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/helpers/logrotate b/helpers/logrotate index 6f9726beb..45f66d443 100644 --- a/helpers/logrotate +++ b/helpers/logrotate @@ -16,10 +16,27 @@ # Requires YunoHost version 2.6.4 or higher. # Requires YunoHost version 3.2.0 or higher for the argument `--specific_user` ynh_use_logrotate() { + + # Stupid patch to remplace --non-append by --nonappend + # Because for some reason --non-append was supposed to be legacy + # (why is it legacy ? Idk maybe because getopts cant parse args with - in their names..) + # but there was no good communication about this, and now --non-append + # is still the most-used option, yet it was parsed with batshit stupid code + # So instead this loops over the positional args, and replace --non-append + # with --nonappend so it's transperent for the rest of the function... + local all_args=( ${@} ) + for I in $(seq 0 $(($# - 1))) + do + if [[ "${all_args[$I]}" == "--non-append" ]] + then + all_args[$I]="--nonappend" + fi + done + set -- "${all_args[@]}" + # Declare an array to define the options of this helper. - local legacy_args=lnuya - local -A args_array=([l]=logfile= [n]=nonappend [u]=specific_user= [y]=non [a]=append) - # [y]=non [a]=append are only for legacy purpose, to not fail on the old option '--non-append' + local legacy_args=lnu + local -A args_array=([l]=logfile= [n]=nonappend [u]=specific_user=) local logfile local nonappend local specific_user @@ -30,14 +47,6 @@ ynh_use_logrotate() { specific_user="${specific_user:-}" # LEGACY CODE - PRE GETOPTS - if [ $# -gt 0 ] && [ "$1" == "--non-append" ]; then - nonappend=1 - # Destroy this argument for the next command. - shift - elif [ $# -gt 1 ] && [ "$2" == "--non-append" ]; then - nonappend=1 - fi - if [ $# -gt 0 ] && [ "$(echo ${1:0:1})" != "-" ]; then # If the given logfile parameter already exists as a file, or if it ends up with ".log", # we just want to manage a single file diff --git a/helpers/multimedia b/helpers/multimedia index abeb9ed2c..05479a84a 100644 --- a/helpers/multimedia +++ b/helpers/multimedia @@ -48,7 +48,7 @@ ynh_multimedia_build_main_dir() { # Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers. setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY" # Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl. - setfacl -RL -m m::rwx "$MEDIA_DIRECTORY" + setfacl -RL -m m::rwx "$MEDIA_DIRECTORY" || true } # Add a directory in yunohost.multimedia diff --git a/helpers/nginx b/helpers/nginx index e69e06bf1..9512f8d23 100644 --- a/helpers/nginx +++ b/helpers/nginx @@ -20,13 +20,15 @@ ynh_add_nginx_config() { local finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" + ynh_add_config --template="nginx.conf" --destination="$finalnginxconf" + if [ "${path_url:-}" != "/" ]; then - ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="$YNH_APP_BASEDIR/conf/nginx.conf" + ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="$finalnginxconf" else - ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="$YNH_APP_BASEDIR/conf/nginx.conf" + ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="$finalnginxconf" fi - ynh_add_config --template="$YNH_APP_BASEDIR/conf/nginx.conf" --destination="$finalnginxconf" + ynh_store_file_checksum --file="$finalnginxconf" ynh_systemd_action --service_name=nginx --action=reload } diff --git a/helpers/nodejs b/helpers/nodejs index 42c25e51f..b692bfc70 100644 --- a/helpers/nodejs +++ b/helpers/nodejs @@ -1,7 +1,7 @@ #!/bin/bash -n_version=8.2.0 -n_checksum=75efd9e583836f3e6cc6d793df1501462fdceeb3460d5a2dbba99993997383b9 +n_version=9.0.1 +n_checksum=ad305e8ee9111aa5b08e6dbde23f01109401ad2d25deecacd880b3f9ea45702b n_install_dir="/opt/node_n" node_version_path="$n_install_dir/n/versions/node" # N_PREFIX is the directory of n, it needs to be loaded as a environment variable. diff --git a/helpers/php b/helpers/php index 05e0939c8..6119c4870 100644 --- a/helpers/php +++ b/helpers/php @@ -192,7 +192,7 @@ pm.process_idle_timeout = 10s if [ -e "$YNH_APP_BASEDIR/conf/php-fpm.ini" ]; then ynh_print_warn --message="Packagers ! Please do not use a separate php ini file, merge your directives in the pool file instead." - ynh_add_config --template="$YNH_APP_BASEDIR/conf/php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini" + ynh_add_config --template="php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini" fi if [ $dedicated_service -eq 1 ]; then @@ -206,7 +206,7 @@ syslog.ident = php-fpm-__APP__ include = __FINALPHPCONF__ " >$YNH_APP_BASEDIR/conf/php-fpm-$app.conf - ynh_add_config --template="$YNH_APP_BASEDIR/conf/php-fpm-$app.conf" --destination="$globalphpconf" + ynh_add_config --template="php-fpm-$app.conf" --destination="$globalphpconf" # Create a config for a dedicated PHP-FPM service for the app echo "[Unit] @@ -474,9 +474,9 @@ YNH_COMPOSER_VERSION=${YNH_COMPOSER_VERSION:-$YNH_DEFAULT_COMPOSER_VERSION} # Execute a command with Composer # -# usage: ynh_composer_exec [--phpversion=phpversion] [--workdir=$final_path] --commands="commands" +# usage: ynh_composer_exec [--phpversion=phpversion] [--workdir=$install_dir] --commands="commands" # | arg: -v, --phpversion - PHP version to use with composer -# | arg: -w, --workdir - The directory from where the command will be executed. Default $final_path. +# | arg: -w, --workdir - The directory from where the command will be executed. Default $install_dir or $final_path # | arg: -c, --commands - Commands to execute. # # Requires YunoHost version 4.2 or higher. @@ -489,7 +489,7 @@ ynh_composer_exec() { local commands # Manage arguments with getopts ynh_handle_getopts_args "$@" - workdir="${workdir:-$final_path}" + workdir="${workdir:-${install_dir:-$final_path}}" phpversion="${phpversion:-$YNH_PHP_VERSION}" COMPOSER_HOME="$workdir/.composer" COMPOSER_MEMORY_LIMIT=-1 \ diff --git a/helpers/postgresql b/helpers/postgresql index 92a70a166..796a36214 100644 --- a/helpers/postgresql +++ b/helpers/postgresql @@ -195,7 +195,13 @@ ynh_psql_database_exists() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - if ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database"; then + # if psql is not there, we cannot check the db + # though it could exists. + if ! command -v psql + then + ynh_print_err -m "PostgreSQL is not installed, impossible to check for db existence." + return 1 + elif ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database"; then return 1 else return 0 diff --git a/helpers/systemd b/helpers/systemd index 270b0144d..06551d2b3 100644 --- a/helpers/systemd +++ b/helpers/systemd @@ -23,7 +23,7 @@ ynh_add_systemd_config() { service="${service:-$app}" template="${template:-systemd.service}" - ynh_add_config --template="$YNH_APP_BASEDIR/conf/$template" --destination="/etc/systemd/system/$service.service" + ynh_add_config --template="$template" --destination="/etc/systemd/system/$service.service" systemctl enable $service --quiet systemctl daemon-reload diff --git a/helpers/utils b/helpers/utils index 60cbedb5c..344493ff3 100644 --- a/helpers/utils +++ b/helpers/utils @@ -61,12 +61,19 @@ ynh_abort_if_errors() { trap ynh_exit_properly EXIT # Capturing exit signals on shell script } +# When running an app script with packaging format >= 2, auto-enable ynh_abort_if_errors except for remove script +if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} ge 2 && [[ ${YNH_APP_ACTION} != "remove" ]] +then + ynh_abort_if_errors +fi + # Download, check integrity, uncompress and patch the source from app.src # -# usage: ynh_setup_source --dest_dir=dest_dir [--source_id=source_id] [--keep="file1 file2"] -# | arg: -d, --dest_dir= - Directory where to setup sources -# | arg: -s, --source_id= - Name of the source, defaults to `app` -# | arg: -k, --keep= - Space-separated list of files/folders that will be backup/restored in $dest_dir, such as a config file you don't want to overwrite. For example 'conf.json secrets.json logs/' +# usage: ynh_setup_source --dest_dir=dest_dir [--source_id=source_id] [--keep="file1 file2"] [--full_replace] +# | arg: -d, --dest_dir= - Directory where to setup sources +# | arg: -s, --source_id= - Name of the source, defaults to `app` +# | arg: -k, --keep= - Space-separated list of files/folders that will be backup/restored in $dest_dir, such as a config file you don't want to overwrite. For example 'conf.json secrets.json logs/' +# | arg: -r, --full_replace= - Remove previous sources before installing new sources # # This helper will read `conf/${source_id}.src`, download and install the sources. # @@ -86,6 +93,8 @@ ynh_abort_if_errors() { # # (Optional) If it set as false don't extract the source. Default: true # # (Useful to get a debian package or a python wheel.) # SOURCE_EXTRACT=(true|false) +# # (Optionnal) Name of the plateform. Default: "linux/$YNH_ARCH" +# SOURCE_PLATFORM=linux/arm64/v8 # ``` # # The helper will: @@ -102,14 +111,16 @@ ynh_abort_if_errors() { ynh_setup_source() { # Declare an array to define the options of this helper. local legacy_args=dsk - local -A args_array=([d]=dest_dir= [s]=source_id= [k]=keep=) + local -A args_array=([d]=dest_dir= [s]=source_id= [k]=keep= [r]=full_replace=) local dest_dir local source_id local keep + local full_replace # Manage arguments with getopts ynh_handle_getopts_args "$@" source_id="${source_id:-app}" keep="${keep:-}" + full_replace="${full_replace:-0}" local src_file_path="$YNH_APP_BASEDIR/conf/${source_id}.src" @@ -119,9 +130,10 @@ ynh_setup_source() { local src_sum=$(grep 'SOURCE_SUM=' "$src_file_path" | cut --delimiter='=' --fields=2-) local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$src_file_path" | cut --delimiter='=' --fields=2-) local src_format=$(grep 'SOURCE_FORMAT=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_extract=$(grep 'SOURCE_EXTRACT=' "$src_file_path" | cut --delimiter='=' --fields=2-) local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$src_file_path" | cut --delimiter='=' --fields=2-) local src_filename=$(grep 'SOURCE_FILENAME=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_extract=$(grep 'SOURCE_EXTRACT=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_plateform=$(grep 'SOURCE_PLATFORM=' "$src_file_path" | cut --delimiter='=' --fields=2-) # Default value src_sumprg=${src_sumprg:-sha256sum} @@ -139,7 +151,9 @@ ynh_setup_source() { mkdir -p /var/cache/yunohost/download/${YNH_APP_ID}/ src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${src_filename}" - if test -e "$local_src"; then + if [ "$src_format" = "docker" ]; then + src_plateform="${src_plateform:-"linux/$YNH_ARCH"}" + elif test -e "$local_src"; then cp $local_src $src_filename else [ -n "$src_url" ] || ynh_die "Couldn't parse SOURCE_URL from $src_file_path ?" @@ -151,12 +165,11 @@ ynh_setup_source() { # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) out=$(wget --tries 3 --no-dns-cache --timeout 900 --no-verbose --output-document=$src_filename $src_url 2>&1) \ || ynh_die --message="$out" + # Check the control sum + echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status \ + || ynh_die --message="Corrupt source" fi - # Check the control sum - echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status \ - || ynh_die --message="Corrupt source" - # Keep files to be backup/restored at the end of the helper # Assuming $dest_dir already exists rm -rf /var/cache/yunohost/files_to_keep_during_setup_source/ @@ -172,15 +185,24 @@ ynh_setup_source() { done fi + if [ "$full_replace" -eq 1 ]; then + ynh_secure_remove --file="$dest_dir" + fi + # Extract source into the app dir mkdir --parents "$dest_dir" + if [ -n "${install_dir:-}" ] && [ "$dest_dir" == "$install_dir" ]; then + _ynh_apply_default_permissions $dest_dir + fi if [ -n "${final_path:-}" ] && [ "$dest_dir" == "$final_path" ]; then _ynh_apply_default_permissions $dest_dir fi if ! "$src_extract"; then mv $src_filename $dest_dir + elif [ "$src_format" = "docker" ]; then + /usr/share/yunohost/helpers.d/vendor/docker-image-extract/docker-image-extract -p $src_plateform -o $dest_dir $src_url 2>&1 elif [ "$src_format" = "zip" ]; then # Zip format # Using of a temp directory, because unzip doesn't manage --strip-components @@ -311,7 +333,7 @@ ynh_local_curl() { # | arg: -d, --destination= - Destination of the config file # # examples: -# ynh_add_config --template=".env" --destination="$final_path/.env" use the template file "../conf/.env" +# ynh_add_config --template=".env" --destination="$install_dir/.env" use the template file "../conf/.env" # ynh_add_config --template="/etc/nginx/sites-available/default" --destination="etc/nginx/sites-available/mydomain.conf" # # The template can be by default the name of a file in the conf directory @@ -425,8 +447,10 @@ ynh_replace_vars() { ynh_replace_string --match_string="__NAMETOCHANGE__" --replace_string="$app" --target_file="$file" ynh_replace_string --match_string="__USER__" --replace_string="$app" --target_file="$file" fi + # Legacy if test -n "${final_path:-}"; then ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$file" + ynh_replace_string --match_string="__INSTALL_DIR__" --replace_string="$final_path" --target_file="$file" fi if test -n "${YNH_PHP_VERSION:-}"; then ynh_replace_string --match_string="__PHPVERSION__" --replace_string="$YNH_PHP_VERSION" --target_file="$file" @@ -513,7 +537,7 @@ ynh_read_var_in_file() { # Get the line number after which we search for the variable local line_number=1 if [[ -n "$after" ]]; then - line_number=$(grep -n $after $file | cut -d: -f1) + line_number=$(grep -m1 -n $after $file | cut -d: -f1) if [[ -z "$line_number" ]]; then set -o xtrace # set -x return 1 @@ -591,7 +615,7 @@ ynh_write_var_in_file() { # Get the line number after which we search for the variable local line_number=1 if [[ -n "$after" ]]; then - line_number=$(grep -n $after $file | cut -d: -f1) + line_number=$(grep -m1 -n $after $file | cut -d: -f1) if [[ -z "$line_number" ]]; then set -o xtrace # set -x return 1 @@ -758,12 +782,25 @@ ynh_read_manifest() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - if [ ! -e "$manifest" ]; then + if [ ! -e "${manifest:-}" ]; then # If the manifest isn't found, try the common place for backup and restore script. - manifest="$YNH_APP_BASEDIR/manifest.json" + if [ -e "$YNH_APP_BASEDIR/manifest.json" ] + then + manifest="$YNH_APP_BASEDIR/manifest.json" + elif [ -e "$YNH_APP_BASEDIR/manifest.toml" ] + then + manifest="$YNH_APP_BASEDIR/manifest.toml" + else + ynh_die --message "No manifest found !?" + fi fi - jq ".$manifest_key" "$manifest" --raw-output + if echo "$manifest" | grep -q '\.json$' + then + jq ".$manifest_key" "$manifest" --raw-output + else + cat "$manifest" | python3 -c 'import json, toml, sys; print(json.dumps(toml.load(sys.stdin)))' | jq ".$manifest_key" --raw-output + fi } # Read the upstream version from the manifest or `$YNH_APP_MANIFEST_VERSION` @@ -907,9 +944,9 @@ ynh_compare_current_package_version() { _ynh_apply_default_permissions() { local target=$1 - local ynh_requirement=$(jq -r '.requirements.yunohost' $YNH_APP_BASEDIR/manifest.json | tr -d '>= ') + local ynh_requirement=$(ynh_read_manifest --manifest_key="requirements.yunohost" | tr -d '<>= ') - if [ -z "$ynh_requirement" ] || [ "$ynh_requirement" == "null" ] || dpkg --compare-versions $ynh_requirement ge 4.2; then + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} ge 2 || [ -z "$ynh_requirement" ] || [ "$ynh_requirement" == "null" ] || dpkg --compare-versions $ynh_requirement ge 4.2; then chmod o-rwx $target chmod g-w $target chown -R root:root $target @@ -925,3 +962,7 @@ _ynh_apply_default_permissions() { chown root:root $target fi } + +int_to_bool() { + sed -e 's/^1$/True/g' -e 's/^0$/False/g' +} diff --git a/helpers/vendor/docker-image-extract/LICENSE b/helpers/vendor/docker-image-extract/LICENSE new file mode 100644 index 000000000..82579b059 --- /dev/null +++ b/helpers/vendor/docker-image-extract/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Emmanuel Frecon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/helpers/vendor/docker-image-extract/README.md b/helpers/vendor/docker-image-extract/README.md new file mode 100644 index 000000000..6f6cb5074 --- /dev/null +++ b/helpers/vendor/docker-image-extract/README.md @@ -0,0 +1 @@ +This is taken from https://github.com/efrecon/docker-image-extract diff --git a/helpers/vendor/docker-image-extract/docker-image-extract b/helpers/vendor/docker-image-extract/docker-image-extract new file mode 100755 index 000000000..4842a8e04 --- /dev/null +++ b/helpers/vendor/docker-image-extract/docker-image-extract @@ -0,0 +1,262 @@ +#!/bin/sh +# +# This script pulls and extracts all files from an image in Docker Hub. +# +# Copyright (c) 2020-2022, Jeremy Lin +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +PLATFORM_DEFAULT="linux/amd64" +PLATFORM="${PLATFORM_DEFAULT}" +OUT_DIR="./output" + +usage() { + echo "This script pulls and extracts all files from an image in Docker Hub." + echo + echo "$0 [OPTIONS...] IMAGE[:REF]" + echo + echo "IMAGE can be a community user image (like 'some-user/some-image') or a" + echo "Docker official image (like 'hello-world', which contains no '/')." + echo + echo "REF is either a tag name or a full SHA-256 image digest (with a 'sha256:' prefix)." + echo "The default ref is the 'latest' tag." + echo + echo "Options:" + echo + echo " -p PLATFORM Pull image for the specified platform (default: ${PLATFORM})" + echo " For a given image on Docker Hub, the 'Tags' tab lists the" + echo " platforms supported for that image." + echo " -o OUT_DIR Extract image to the specified output dir (default: ${OUT_DIR})" + echo " -h Show help with usage examples" +} + +usage_detailed() { + usage + echo + echo "Examples:" + echo + echo "# Pull and extract all files in the 'hello-world' image tagged 'latest'." + echo "\$ $0 hello-world:latest" + echo + echo "# Same as above; ref defaults to the 'latest' tag." + echo "\$ $0 hello-world" + echo + echo "# Pull the 'hello-world' image for the 'linux/arm64/v8' platform." + echo "\$ $0 -p linux/arm64/v8 hello-world" + echo + echo "# Pull an image by digest." + echo "\$ $0 hello-world:sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042" +} + +if [ $# -eq 0 ]; then + usage_detailed + exit 0 +fi + +while getopts ':ho:p:' opt; do + case $opt in + o) + OUT_DIR="${OPTARG}" + ;; + p) + PLATFORM="${OPTARG}" + ;; + h) + usage_detailed + exit 0 + ;; + \?) + echo "ERROR: Invalid option '-$OPTARG'." + echo + usage + exit 1 + ;; + \:) echo "ERROR: Argument required for option '-$OPTARG'." + echo + usage + exit 1 + ;; + esac +done +shift $(($OPTIND - 1)) + +if [ $# -eq 0 ]; then + echo "ERROR: Image to pull must be specified." + echo + usage + exit 1 +fi + +have_curl() { + command -v curl >/dev/null +} + +have_wget() { + command -v wget >/dev/null +} + +if ! have_curl && ! have_wget; then + echo "This script requires either curl or wget." + exit 1 +fi + +image_spec="$1" +image="${image_spec%%:*}" +if [ "${image#*/}" = "${image}" ]; then + # Docker official images are in the 'library' namespace. + image="library/${image}" +fi +ref="${image_spec#*:}" +if [ "${ref}" = "${image_spec}" ]; then + echo "Defaulting ref to tag 'latest'..." + ref=latest +fi + +# Split platform (OS/arch/variant) into separate variables. +# A platform specifier doesn't always include the `variant` component. +OLD_IFS="${IFS}" +IFS=/ read -r OS ARCH VARIANT <":"" (assumes key/val won't contain double quotes). + # The colon may have whitespace on either side. + grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]\+\"" | + # Extract just by deleting the last '"', and then greedily deleting + # everything up to '"'. + sed -e 's/"$//' -e 's/.*"//' +} + +# Fetch a URL to stdout. Up to two header arguments may be specified: +# +# fetch [name1: value1] [name2: value2] +# +fetch() { + if have_curl; then + if [ $# -eq 2 ]; then + set -- -H "$2" "$1" + elif [ $# -eq 3 ]; then + set -- -H "$2" -H "$3" "$1" + fi + curl -sSL "$@" + else + if [ $# -eq 2 ]; then + set -- --header "$2" "$1" + elif [ $# -eq 3 ]; then + set -- --header "$2" --header "$3" "$1" + fi + wget -qO- "$@" + fi +} + +# https://docs.docker.com/docker-hub/api/latest/#tag/repositories +manifest_list_url="https://hub.docker.com/v2/repositories/${image}/tags/${ref}" + +# If we're pulling the image for the default platform, or the ref is already +# a SHA-256 image digest, then we don't need to look up anything. +if [ "${PLATFORM}" = "${PLATFORM_DEFAULT}" ] || [ -z "${ref##sha256:*}" ]; then + digest="${ref}" +else + echo "Getting multi-arch manifest list..." + digest=$(fetch "${manifest_list_url}" | + # Break up the single-line JSON output into separate lines by adding + # newlines before and after the chars '[', ']', '{', and '}'. + sed -e 's/\([][{}]\)/\n\1\n/g' | + # Extract the "images":[...] list. + sed -n '/"images":/,/]/ p' | + # Each image's details are now on a separate line, e.g. + # "architecture":"arm64","features":"","variant":"v8","digest":"sha256:054c85801c4cb41511b176eb0bf13a2c4bbd41611ddd70594ec3315e88813524","os":"linux","os_features":"","os_version":null,"size":828724,"status":"active","last_pulled":"2022-09-02T22:46:48.240632Z","last_pushed":"2022-09-02T00:42:45.69226Z" + # The image details are interspersed with lines of stray punctuation, + # so grep for an arbitrary string that must be in these lines. + grep architecture | + # Search for an image that matches the platform. + while read -r image; do + # Arch is probably most likely to be unique, so check that first. + arch="$(echo ${image} | extract 'architecture')" + if [ "${arch}" != "${ARCH}" ]; then continue; fi + + os="$(echo ${image} | extract 'os')" + if [ "${os}" != "${OS}" ]; then continue; fi + + variant="$(echo ${image} | extract 'variant')" + if [ "${variant}" = "${VARIANT}" ]; then + echo ${image} | extract 'digest' + break + fi + done) +fi + +if [ -n "${digest}" ]; then + echo "Platform ${PLATFORM} resolved to '${digest}'..." +else + echo "No image digest found. Verify that the image, ref, and platform are valid." + exit 1 +fi + +# https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate +api_token_url="https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" + +# https://github.com/docker/distribution/blob/master/docs/spec/api.md#pulling-an-image-manifest +manifest_url="https://registry-1.docker.io/v2/${image}/manifests/${digest}" + +# https://github.com/docker/distribution/blob/master/docs/spec/api.md#pulling-a-layer +blobs_base_url="https://registry-1.docker.io/v2/${image}/blobs" + +echo "Getting API token..." +token=$(fetch "${api_token_url}" | extract 'token') +auth_header="Authorization: Bearer $token" +v2_header="Accept: application/vnd.docker.distribution.manifest.v2+json" + +echo "Getting image manifest for $image:$ref..." +layers=$(fetch "${manifest_url}" "${auth_header}" "${v2_header}" | + # Extract `digest` values only after the `layers` section appears. + sed -n '/"layers":/,$ p' | + extract 'digest') + +if [ -z "${layers}" ]; then + echo "No layers returned. Verify that the image and ref are valid." + exit 1 +fi + +mkdir -p "${OUT_DIR}" + +for layer in $layers; do + hash="${layer#sha256:}" + echo "Fetching and extracting layer ${hash}..." + fetch "${blobs_base_url}/${layer}" "${auth_header}" | gzip -d | tar -C "${OUT_DIR}" -xf - + # Ref: https://github.com/moby/moby/blob/master/image/spec/v1.2.md#creating-an-image-filesystem-changeset + # https://github.com/moby/moby/blob/master/pkg/archive/whiteouts.go + # Search for "whiteout" files to indicate files deleted in this layer. + OLD_IFS="${IFS}" + find "${OUT_DIR}" -name '.wh.*' | while IFS= read -r f; do + dir="${f%/*}" + wh_file="${f##*/}" + file="${wh_file#.wh.}" + # Delete both the whiteout file and the whited-out file. + rm -rf "${dir}/${wh_file}" "${dir}/${file}" + done + IFS="${OLD_IFS}" +done + +echo "Image contents extracted into ${OUT_DIR}." diff --git a/hooks/backup/20-conf_ynh_settings b/hooks/backup/20-conf_ynh_settings index 76ab0aaca..0820978e7 100644 --- a/hooks/backup/20-conf_ynh_settings +++ b/hooks/backup/20-conf_ynh_settings @@ -13,6 +13,6 @@ backup_dir="${1}/conf/ynh" ynh_backup "/etc/yunohost/firewall.yml" "${backup_dir}/firewall.yml" ynh_backup "/etc/yunohost/current_host" "${backup_dir}/current_host" [ ! -d "/etc/yunohost/domains" ] || ynh_backup "/etc/yunohost/domains" "${backup_dir}/domains" -[ ! -e "/etc/yunohost/settings.json" ] || ynh_backup "/etc/yunohost/settings.json" "${backup_dir}/settings.json" +[ ! -e "/etc/yunohost/settings.yml" ] || ynh_backup "/etc/yunohost/settings.yml" "${backup_dir}/settings.yml" [ ! -d "/etc/yunohost/dyndns" ] || ynh_backup "/etc/yunohost/dyndns" "${backup_dir}/dyndns" [ ! -d "/etc/dkim" ] || ynh_backup "/etc/dkim" "${backup_dir}/dkim" diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 55accc4f4..a3fd13687 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -42,7 +42,7 @@ do_init_regen() { # Backup folders mkdir -p /home/yunohost.backup/archives chmod 750 /home/yunohost.backup/archives - chown root:root /home/yunohost.backup/archives # This is later changed to admin:root once admin user exists + chown root:root /home/yunohost.backup/archives # This is later changed to root:admins once the admins group exists # Empty ssowat json persistent conf echo "{}" >'/etc/ssowat/conf.json.persistent' @@ -169,12 +169,11 @@ do_post_regen() { # Enfore permissions # ###################### - chmod 750 /home/admin - chmod 750 /home/yunohost.backup - chmod 750 /home/yunohost.backup/archives + chmod 770 /home/yunohost.backup + chmod 770 /home/yunohost.backup/archives chmod 700 /var/cache/yunohost - chown admin:root /home/yunohost.backup - chown admin:root /home/yunohost.backup/archives + chown root:admins /home/yunohost.backup + chown root:admins /home/yunohost.backup/archives chown root:root /var/cache/yunohost # NB: x permission for 'others' is important for ssl-cert (and maybe mdns), otherwise slapd will fail to start because can't access the certs @@ -204,7 +203,7 @@ do_post_regen() { mkdir -p /etc/yunohost/domains # Misc configuration / state files - chown root:root $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null) + chown root:root $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null | grep -vw mdns.yml) chmod 600 $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null) # Apps folder, custom hooks folder @@ -222,7 +221,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/hooks/conf_regen/03-ssh b/hooks/conf_regen/03-ssh index 9a7f5ce4d..d0351b4e5 100755 --- a/hooks/conf_regen/03-ssh +++ b/hooks/conf_regen/03-ssh @@ -14,15 +14,10 @@ do_pre_regen() { ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null || true) - # Support legacy setting (this setting might be disabled by a user during a migration) - if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then - ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null || true)" - fi - # Support different strategy for security configurations - export compatibility="$(yunohost settings get 'security.ssh.compatibility')" - export port="$(yunohost settings get 'security.ssh.port')" - export password_authentication="$(yunohost settings get 'security.ssh.password_authentication')" + export compatibility="$(yunohost settings get 'security.ssh.ssh_compatibility')" + export port="$(yunohost settings get 'security.ssh.ssh_port')" + export password_authentication="$(yunohost settings get 'security.ssh.ssh_password_authentication' | int_to_bool)" export ssh_keys export ipv6_enabled ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config" diff --git a/hooks/conf_regen/06-slapd b/hooks/conf_regen/06-slapd index 616b383ec..9ba61863b 100755 --- a/hooks/conf_regen/06-slapd +++ b/hooks/conf_regen/06-slapd @@ -58,14 +58,6 @@ EOF nscd -i passwd || true systemctl restart slapd - - # We don't use mkhomedir_helper because 'admin' may not be recognized - # when this script is ran in a chroot (e.g. ISO install) - # We also refer to admin as uid 1007 for the same reason - if [ ! -d /home/admin ]; then - cp -r /etc/skel /home/admin - chown -R 1007:1007 /home/admin - fi } _regenerate_slapd_conf() { @@ -131,6 +123,10 @@ do_post_regen() { chown -R openldap:openldap /etc/ldap/schema/ chown -R openldap:openldap /etc/ldap/slapd.d/ + # Fix weird scenarios where /etc/sudo-ldap.conf doesn't exists (yet is supposed to be + # created by the sudo-ldap package) : https://github.com/YunoHost/issues/issues/2091 + [ -e /etc/sudo-ldap.conf ] || ln -s /etc/ldap/ldap.conf /etc/sudo-ldap.conf + # If we changed the systemd ynh-override conf if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/slapd.service.d/ynh-override.conf$"; then systemctl daemon-reload @@ -139,7 +135,7 @@ do_post_regen() { fi # For some reason, old setups don't have the admins group defined... - if ! slapcat | grep -q 'cn=admins,ou=groups,dc=yunohost,dc=org'; then + if ! slapcat -H "ldap:///cn=admins,ou=groups,dc=yunohost,dc=org" | grep -q 'cn=admins,ou=groups,dc=yunohost,dc=org'; then slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org <<< \ "dn: cn=admins,ou=groups,dc=yunohost,dc=org cn: admins @@ -172,22 +168,6 @@ objectClass: top" echo "Reloading slapd" systemctl force-reload slapd - - # on slow hardware/vm this regen conf would exit before the admin user that - # is stored in ldap is available because ldap seems to slow to restart - # so we'll wait either until we are able to log as admin or until a timeout - # is reached - # we need to do this because the next hooks executed after this one during - # postinstall requires to run as admin thus breaking postinstall on slow - # hardware which mean yunohost can't be correctly installed on those hardware - # and this sucks - # wait a maximum time of 5 minutes - # yes, force-reload behave like a restart - number_of_wait=0 - while ! su admin -c '' && ((number_of_wait < 60)); do - sleep 5 - ((number_of_wait += 1)) - done } do_$1_regen ${@:2} diff --git a/hooks/conf_regen/12-metronome b/hooks/conf_regen/12-metronome index 220d18d58..cad8d3805 100755 --- a/hooks/conf_regen/12-metronome +++ b/hooks/conf_regen/12-metronome @@ -26,8 +26,14 @@ do_pre_regen() { | sed "s/{{ main_domain }}/${main_domain}/g" \ >"${metronome_dir}/metronome.cfg.lua" - # add domain conf files + # Trick such that old conf files are flagged as to remove for domain in $YNH_DOMAINS; do + touch "${metronome_conf_dir}/${domain}.cfg.lua" + done + + # add domain conf files + domain_list="$(yunohost domain list --features xmpp --output-as json | jq -r ".domains[]")" + for domain in $domain_list; do cat domain.tpl.cfg.lua \ | sed "s/{{ domain }}/${domain}/g" \ >"${metronome_conf_dir}/${domain}.cfg.lua" diff --git a/hooks/conf_regen/15-nginx b/hooks/conf_regen/15-nginx index c1d943681..28d9e90fb 100755 --- a/hooks/conf_regen/15-nginx +++ b/hooks/conf_regen/15-nginx @@ -56,8 +56,8 @@ do_pre_regen() { # install / update plain conf files cp plain/* "$nginx_conf_dir" # remove the panel overlay if this is specified in settings - panel_overlay=$(yunohost settings get 'ssowat.panel_overlay.enabled') - if [ "$panel_overlay" == "false" ] || [ "$panel_overlay" == "False" ]; then + panel_overlay=$(yunohost settings get 'misc.portal.ssowat_panel_overlay_enabled' | int_to_bool) + if [ "$panel_overlay" == "False" ]; then echo "#" >"${nginx_conf_dir}/yunohost_panel.conf.inc" fi @@ -65,14 +65,16 @@ do_pre_regen() { main_domain=$(cat /etc/yunohost/current_host) # Support different strategy for security configurations - export redirect_to_https="$(yunohost settings get 'security.nginx.redirect_to_https')" - export compatibility="$(yunohost settings get 'security.nginx.compatibility')" - export experimental="$(yunohost settings get 'security.experimental.enabled')" + export redirect_to_https="$(yunohost settings get 'security.nginx.nginx_redirect_to_https' | int_to_bool)" + export compatibility="$(yunohost settings get 'security.nginx.nginx_compatibility')" + export experimental="$(yunohost settings get 'security.experimental.security_experimental_enabled' | int_to_bool)" ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc" cert_status=$(yunohost domain cert status --json) # add domain conf files + xmpp_domain_list="$(yunohost domain list --features xmpp --output-as json | jq -r ".domains[]")" + mail_domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]")" for domain in $YNH_DOMAINS; do domain_conf_dir="${nginx_conf_dir}/${domain}.d" mkdir -p "$domain_conf_dir" @@ -84,17 +86,32 @@ do_pre_regen() { export domain_cert_ca=$(echo $cert_status \ | jq ".certificates.\"$domain\".CA_type" \ | tr -d '"') + if echo "$xmpp_domain_list" | grep -q "^$domain$" + then + export xmpp_enabled="True" + else + export xmpp_enabled="False" + fi + if echo "$mail_domain_list" | grep -q "^$domain$" + then + export mail_enabled="True" + else + export mail_enabled="False" + fi ynh_render_template "server.tpl.conf" "${nginx_conf_dir}/${domain}.conf" - ynh_render_template "autoconfig.tpl.xml" "${mail_autoconfig_dir}/config-v1.1.xml" + if [ $mail_enabled == "True" ] + then + ynh_render_template "autoconfig.tpl.xml" "${mail_autoconfig_dir}/config-v1.1.xml" + fi touch "${domain_conf_dir}/yunohost_local.conf" # Clean legacy conf files done - export webadmin_allowlist_enabled=$(yunohost settings get security.webadmin.allowlist.enabled) + export webadmin_allowlist_enabled=$(yunohost settings get security.webadmin.webadmin_allowlist_enabled | int_to_bool) if [ "$webadmin_allowlist_enabled" == "True" ]; then - export webadmin_allowlist=$(yunohost settings get security.webadmin.allowlist) + export webadmin_allowlist=$(yunohost settings get security.webadmin.webadmin_allowlist) fi ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc" ynh_render_template "yunohost_api.conf.inc" "${nginx_conf_dir}/yunohost_api.conf.inc" diff --git a/hooks/conf_regen/19-postfix b/hooks/conf_regen/19-postfix index 177ea23e9..3a2aead5d 100755 --- a/hooks/conf_regen/19-postfix +++ b/hooks/conf_regen/19-postfix @@ -22,17 +22,19 @@ do_pre_regen() { main_domain=$(cat /etc/yunohost/current_host) # Support different strategy for security configurations - export compatibility="$(yunohost settings get 'security.postfix.compatibility')" + export compatibility="$(yunohost settings get 'security.postfix.postfix_compatibility')" # Add possibility to specify a relay # Could be useful with some isp with no 25 port open or more complex setup export relay_port="" export relay_user="" - export relay_host="$(yunohost settings get 'smtp.relay.host')" - if [ -n "${relay_host}" ]; then - relay_port="$(yunohost settings get 'smtp.relay.port')" - relay_user="$(yunohost settings get 'smtp.relay.user')" - relay_password="$(yunohost settings get 'smtp.relay.password')" + export relay_host="" + export relay_enabled="$(yunohost settings get 'email.smtp.smtp_relay_enabled' | int_to_bool)" + if [ "${relay_enabled}" == "True" ]; then + relay_host="$(yunohost settings get 'email.smtp.smtp_relay_host')" + relay_port="$(yunohost settings get 'email.smtp.smtp_relay_port')" + relay_user="$(yunohost settings get 'email.smtp.smtp_relay_user')" + relay_password="$(yunohost settings get 'email.smtp.smtp_relay_password')" # Avoid to display "Relay account paswword" to other users touch ${postfix_dir}/sasl_passwd @@ -44,17 +46,17 @@ do_pre_regen() { cat <<<"[${relay_host}]:${relay_port} ${relay_user}:${relay_password}" >${postfix_dir}/sasl_passwd fi export main_domain - export domain_list="$YNH_DOMAINS" + export domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')" ynh_render_template "main.cf" "${postfix_dir}/main.cf" ynh_render_template "sni" "${postfix_dir}/sni" cat postsrsd \ | sed "s/{{ main_domain }}/${main_domain}/g" \ - | sed "s/{{ domain_list }}/${YNH_DOMAINS}/g" \ + | sed "s/{{ domain_list }}/${domain_list}/g" \ >"${default_dir}/postsrsd" # adapt it for IPv4-only hosts - ipv6="$(yunohost settings get 'smtp.allow_ipv6')" + ipv6="$(yunohost settings get 'email.smtp.smtp_allow_ipv6' | int_to_bool)" if [ "$ipv6" == "False" ] || [ ! -f /proc/net/if_inet6 ]; then sed -i \ 's/ \[::ffff:127.0.0.0\]\/104 \[::1\]\/128//g' \ @@ -68,6 +70,8 @@ do_pre_regen() { do_post_regen() { regen_conf_files=$1 + chown postfix /etc/postfix + if [ -e /etc/postfix/sasl_passwd ]; then chmod 750 /etc/postfix/sasl_passwd* chown postfix:root /etc/postfix/sasl_passwd* diff --git a/hooks/conf_regen/25-dovecot b/hooks/conf_regen/25-dovecot index 37c73b6d8..adbb7761e 100755 --- a/hooks/conf_regen/25-dovecot +++ b/hooks/conf_regen/25-dovecot @@ -16,9 +16,9 @@ do_pre_regen() { cp dovecot-ldap.conf "${dovecot_dir}/dovecot-ldap.conf" cp dovecot.sieve "${dovecot_dir}/global_script/dovecot.sieve" - export pop3_enabled="$(yunohost settings get 'pop3.enabled')" + export pop3_enabled="$(yunohost settings get 'email.pop3.pop3_enabled')" export main_domain=$(cat /etc/yunohost/current_host) - export domain_list="$YNH_DOMAINS" + export domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')" ynh_render_template "dovecot.conf" "${dovecot_dir}/dovecot.conf" diff --git a/hooks/conf_regen/31-rspamd b/hooks/conf_regen/31-rspamd index 536aec7c2..6807ce0cd 100755 --- a/hooks/conf_regen/31-rspamd +++ b/hooks/conf_regen/31-rspamd @@ -26,7 +26,8 @@ do_post_regen() { chown _rspamd /etc/dkim # create DKIM key for domains - for domain in $YNH_DOMAINS; do + domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')" + for domain in $domain_list; do domain_key="/etc/dkim/${domain}.mail.key" [ ! -f "$domain_key" ] && { # We use a 1024 bit size because nsupdate doesn't seem to be able to diff --git a/hooks/conf_regen/37-mdns b/hooks/conf_regen/37-mdns index 3a877970b..bd813e588 100755 --- a/hooks/conf_regen/37-mdns +++ b/hooks/conf_regen/37-mdns @@ -53,9 +53,6 @@ do_post_regen() { systemctl daemon-reload fi - systemctl disable avahi-daemon.socket --quiet --now 2>/dev/null || true - systemctl disable avahi-daemon --quiet --now 2>/dev/null || true - # Legacy stuff to enable the new yunomdns service on legacy systems if [[ -e /etc/avahi/avahi-daemon.conf ]] && grep -q 'yunohost' /etc/avahi/avahi-daemon.conf; then systemctl enable yunomdns --now --quiet diff --git a/hooks/conf_regen/43-dnsmasq b/hooks/conf_regen/43-dnsmasq index ec53d75bc..648a128c2 100755 --- a/hooks/conf_regen/43-dnsmasq +++ b/hooks/conf_regen/43-dnsmasq @@ -21,9 +21,9 @@ do_pre_regen() { cat plain/resolv.dnsmasq.conf | grep "^nameserver" | shuf >${pending_dir}/etc/resolv.dnsmasq.conf # retrieve variables - ipv4=$(curl -s -4 https://ip.yunohost.org 2>/dev/null || true) + ipv4=$(curl --max-time 10 -s -4 https://ip.yunohost.org 2>/dev/null || true) ynh_validate_ip4 "$ipv4" || ipv4='127.0.0.1' - ipv6=$(curl -s -6 https://ip6.yunohost.org 2>/dev/null || true) + ipv6=$(curl --max-time 10 -s -6 https://ip6.yunohost.org 2>/dev/null || true) ynh_validate_ip6 "$ipv6" || ipv6='' interfaces="$(ip -j addr show | jq -r '[.[].ifname]|join(" ")')" wireless_interfaces="lo" @@ -73,7 +73,7 @@ do_post_regen() { grep -q '^supersede domain-name "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede domain-name "";' >>/etc/dhcp/dhclient.conf grep -q '^supersede domain-search "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede domain-search "";' >>/etc/dhcp/dhclient.conf - grep -q '^supersede name "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede name "";' >>/etc/dhcp/dhclient.conf + grep -q '^supersede search "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede search "";' >>/etc/dhcp/dhclient.conf systemctl restart resolvconf fi diff --git a/hooks/conf_regen/52-fail2ban b/hooks/conf_regen/52-fail2ban index 8129e977d..d463892c7 100755 --- a/hooks/conf_regen/52-fail2ban +++ b/hooks/conf_regen/52-fail2ban @@ -14,9 +14,10 @@ do_pre_regen() { mkdir -p "${fail2ban_dir}/jail.d" cp yunohost.conf "${fail2ban_dir}/filter.d/yunohost.conf" + cp postfix-sasl.conf "${fail2ban_dir}/filter.d/postfix-sasl.conf" cp jail.conf "${fail2ban_dir}/jail.conf" - export ssh_port="$(yunohost settings get 'security.ssh.port')" + export ssh_port="$(yunohost settings get 'security.ssh.ssh_port')" ynh_render_template "yunohost-jails.conf" "${fail2ban_dir}/jail.d/yunohost-jails.conf" } diff --git a/hooks/restore/20-conf_ynh_settings b/hooks/restore/20-conf_ynh_settings index 2d731bd54..aba2b7a46 100644 --- a/hooks/restore/20-conf_ynh_settings +++ b/hooks/restore/20-conf_ynh_settings @@ -3,6 +3,6 @@ backup_dir="$1/conf/ynh" cp -a "${backup_dir}/current_host" /etc/yunohost/current_host cp -a "${backup_dir}/firewall.yml" /etc/yunohost/firewall.yml [ ! -d "${backup_dir}/domains" ] || cp -a "${backup_dir}/domains" /etc/yunohost/domains -[ ! -e "${backup_dir}/settings.json" ] || cp -a "${backup_dir}/settings.json" "/etc/yunohost/settings.json" +[ ! -e "${backup_dir}/settings.yml" ] || cp -a "${backup_dir}/settings.yml" "/etc/yunohost/settings.yml" [ ! -d "${backup_dir}/dyndns" ] || cp -raT "${backup_dir}/dyndns" "/etc/yunohost/dyndns" [ ! -d "${backup_dir}/dkim" ] || cp -raT "${backup_dir}/dkim" "/etc/dkim" diff --git a/locales/ar.json b/locales/ar.json index c440e442f..673176cdf 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -1,8 +1,6 @@ { "action_invalid": "إجراء غير صالح '{action}'", "admin_password": "كلمة السر الإدارية", - "admin_password_change_failed": "لا يمكن تعديل الكلمة السرية", - "admin_password_changed": "عُدلت كلمة السر الإدارية", "app_already_installed": "{app} تم تنصيبه مِن قبل", "app_already_up_to_date": "{app} حديثٌ", "app_argument_required": "المُعامِل '{name}' مطلوب", @@ -15,28 +13,26 @@ "app_requirements_checking": "جار فحص الحزم اللازمة لـ {app}…", "app_sources_fetch_failed": "تعذرت عملية جلب مصادر الملفات", "app_unknown": "برنامج مجهول", - "app_upgrade_app_name": "جارٍ تحديث تطبيق {app}…", + "app_upgrade_app_name": "جارٍ تحديث {app}…", "app_upgrade_failed": "تعذرت عملية ترقية {app}", "app_upgrade_some_app_failed": "تعذرت عملية ترقية بعض التطبيقات", "app_upgraded": "تم تحديث التطبيق {app}", - "ask_firstname": "الإسم", - "ask_lastname": "اللقب", "ask_main_domain": "النطاق الرئيسي", "ask_new_admin_password": "كلمة السر الإدارية الجديدة", "ask_password": "كلمة السر", - "backup_applying_method_copy": "جارٍ نسخ كافة الملفات إلى النسخة الإحتياطية …", + "backup_applying_method_copy": "جارٍ نسخ كافة الملفات المراد نسخها احتياطيا …", "backup_applying_method_tar": "جارٍ إنشاء ملف TAR للنسخة الاحتياطية…", "backup_created": "تم إنشاء النسخة الإحتياطية", "backup_method_copy_finished": "إنتهت عملية النسخ الإحتياطي", "backup_nothings_done": "ليس هناك أي شيء للحفظ", "backup_output_directory_required": "يتوجب عليك تحديد مجلد لتلقي النسخ الإحتياطية", - "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق {domain} !", - "certmanager_cert_install_success_selfsigned": "نجحت عملية تثبيت الشهادة الموقعة ذاتيا الخاصة بالنطاق {domain}", - "certmanager_cert_renew_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": "فشل إجراء توقيع الشهادة الجديدة", "certmanager_no_cert_file": "تعذرت عملية قراءة شهادة نطاق {domain} (الملف : {file})", "domain_created": "تم إنشاء النطاق", - "domain_creation_failed": "تعذرت عملية إنشاء النطاق", + "domain_creation_failed": "تعذرت عملية إنشاء النطاق {domain}: {error}", "domain_deleted": "تم حذف النطاق", "domain_exists": "اسم النطاق موجود سلفًا", "domains_available": "النطاقات المتوفرة :", @@ -45,7 +41,7 @@ "dyndns_ip_updated": "لقد تم تحديث عنوان الإيبي الخاص بك على نظام أسماء النطاقات الديناميكي", "dyndns_key_generating": "عملية توليد مفتاح نظام أسماء النطاقات جارية. يمكن للعملية أن تستغرق بعضا من الوقت…", "dyndns_key_not_found": "لم يتم العثور على مفتاح DNS الخاص باسم النطاق هذا", - "extracting": "عملية فك الضغط جارية …", + "extracting": "عملية فك الضغط جارية…", "installation_complete": "إكتملت عملية التنصيب", "main_domain_change_failed": "تعذّر تغيير النطاق الأساسي", "main_domain_changed": "تم تغيير النطاق الأساسي", @@ -63,7 +59,7 @@ "service_disabled": "لن يتم إطلاق خدمة '{service}' أثناء بداية تشغيل النظام.", "service_enabled": "تم تنشيط خدمة '{service}'", "service_removed": "تمت إزالة خدمة '{service}'", - "service_started": "تم إطلاق تشغيل خدمة '{service}'", + "service_started": "تم إطلاق تشغيل خدمة '{service}'", "service_stopped": "تمّ إيقاف خدمة '{service}'", "system_upgraded": "تمت عملية ترقية النظام", "unlimit": "دون تحديد الحصة", @@ -77,7 +73,7 @@ "user_unknown": "المستخدم {user} مجهول", "user_update_failed": "لا يمكن تحديث المستخدم", "user_updated": "تم تحديث المستخدم", - "yunohost_installing": "عملية تنصيب يونوهوست جارية …", + "yunohost_installing": "عملية تنصيب واي يونوهوست جارية …", "yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب أو هو مثبت حاليا بشكل خاطئ. قم بتنفيذ الأمر 'yunohost tools postinstall'", "migrations_list_conflict_pending_done": "لا يمكنك استخدام --previous و --done معًا على نفس سطر الأوامر.", "service_description_metronome": "يُدير حسابات الدردشة الفورية XMPP", @@ -112,28 +108,24 @@ "service_description_rspamd": "يقوم بتصفية البريد المزعج و إدارة ميزات أخرى للبريد", "service_description_yunohost-firewall": "يُدير فتح وإغلاق منافذ الاتصال إلى الخدمات", "aborting": "إلغاء.", - "admin_password_too_long": "يرجى اختيار كلمة سرية أقصر مِن 127 حرف", "app_not_upgraded": "", - "app_start_install": "جارٍ تثبيت التطبيق {app}…", - "app_start_remove": "جارٍ حذف التطبيق {app}…", - "app_start_restore": "جارٍ استرجاع التطبيق {app}…", + "app_start_install": "جارٍ تثبيت {app}…", + "app_start_remove": "جارٍ حذف {app}…", + "app_start_restore": "جارٍ استرجاع {app}…", "app_upgrade_several_apps": "سوف يتم تحديث التطبيقات التالية: {apps}", "ask_new_domain": "نطاق جديد", "ask_new_path": "مسار جديد", - "global_settings_setting_security_password_admin_strength": "قوة الكلمة السرية الإدارية", - "global_settings_setting_security_password_user_strength": "قوة الكلمة السرية للمستخدم", "password_too_simple_1": "يجب أن يكون طول الكلمة السرية على الأقل 8 حروف", "already_up_to_date": "كل شيء على ما يرام. ليس هناك ما يتطلّب تحديثًا.", "service_description_slapd": "يخزّن المستخدمين والنطاقات والمعلومات المتعلقة بها", "service_reloaded": "تم إعادة تشغيل خدمة '{service}'", "service_restarted": "تم إعادة تشغيل خدمة '{service}'", - "group_unknown": "الفريق {group} مجهول", + "group_unknown": "الفريق '{group}' مجهول", "group_deletion_failed": "فشلت عملية حذف الفريق '{group}': {error}", "group_deleted": "تم حذف الفريق '{group}'", "group_created": "تم إنشاء الفريق '{group}'", "dyndns_could_not_check_available": "لا يمكن التحقق مِن أنّ {domain} متوفر على {provider}.", "backup_mount_archive_for_restore": "جارٍ تهيئة النسخة الاحتياطية للاسترجاع…", - "root_password_replaced_by_admin_password": "لقد تم استبدال كلمة سر الجذر root بالكلمة الإدارية لـ admin.", "app_action_broke_system": "يبدو أنّ هذا الإجراء أدّى إلى تحطيم هذه الخدمات المهمة: {services}", "diagnosis_basesystem_host": "هذا الخادم يُشغّل ديبيان {debian_version}", "diagnosis_basesystem_kernel": "هذا الخادم يُشغّل نواة لينكس {kernel_version}", @@ -145,7 +137,7 @@ "diagnosis_ip_not_connected_at_all": "يبدو أنّ الخادم غير مُتّصل بتاتا بالإنترنت!؟", "app_install_failed": "لا يمكن تنصيب {app}: {error}", "apps_already_up_to_date": "كافة التطبيقات مُحدّثة", - "app_remove_after_failed_install": "جارٍ حذف التطبيق بعدما فشل تنصيبها…", + "app_remove_after_failed_install": "جارٍ حذف التطبيق بعدما فشل تنصيبه…", "apps_catalog_updating": "جارٍ تحديث فهرس التطبيقات…", "apps_catalog_update_success": "تم تحديث فهرس التطبيقات!", "diagnosis_domain_expiration_error": "ستنتهي مدة صلاحية بعض النطاقات في القريب العاجل!", @@ -158,5 +150,48 @@ "diagnosis_description_services": "حالة الخدمات", "diagnosis_description_dnsrecords": "تسجيلات خدمة DNS", "diagnosis_description_ip": "الإتصال بالإنترنت", - "diagnosis_description_basesystem": "النظام الأساسي" + "diagnosis_description_basesystem": "النظام الأساسي", + "global_settings_setting_admin_strength": "قوة الكلمة السرية الإدارية", + "global_settings_setting_user_strength": "قوة الكلمة السرية للمستخدم", + "field_invalid": "الحقل غير صحيح : '{}'", + "diagnosis_ignored_issues": "(+ {nb_ignored} مشاكل تم تجاهلها)", + "domain_config_mail_in": "البريد الوارد", + "domain_config_mail_out": "البريد الخارج", + "domain_unknown": "النطاق '{domain}' مجهول", + "disk_space_not_sufficient_install": "ليس هناك مساحة كافية لتنصيب هذا التطبيق", + "diagnosis_unknown_categories": "الفئات التالية غير معروفة: {categories}", + "ask_fullname": "الاسم الكامل (اللقب والاسم)", + "diagnosis_ports_unreachable": "المنفذ {port} غير متاح الوصول إليه مِن الخارج.", + "domain_config_api_protocol": "بروتوكول API", + "domain_config_auth_application_secret": "المفتاح السري للتطبيق", + "domain_config_auth_consumer_key": "مفتاح المستخدِم", + "domain_config_auth_entrypoint": "نقطة الدخول API", + "domain_config_auth_key": "مفتاح التوثيق", + "domain_config_cert_issuer": "الهيئة الموثِّقة", + "domain_config_cert_renew": "تجديد شهادة Let's Encrypt", + "domain_config_cert_summary": "حالة الشهادة", + "domain_config_cert_summary_ok": "حسنًا، يبدو أنّ الشهادة جيدة!", + "domain_config_cert_validity": "مدة الصلاحية", + "domain_config_xmpp": "المراسَلة الفورية (XMPP)", + "global_settings_setting_root_password": "كلمة السر الجديدة لـ root", + "global_settings_setting_root_password_confirm": "كلمة السر الجديدة لـ root (تأكيد)", + "global_settings_setting_security_experimental_enabled": "ميزات أمان تجريبية", + "global_settings_setting_ssh_password_authentication": "الاستيثاق بكلمة سرية", + "global_settings_setting_ssh_port": "منفذ SSH", + "global_settings_setting_webadmin_allowlist": "قائمة عناوين الإيبي المسموح لها النفاذ إلى واجهة الويب الإدارية", + "ask_admin_username": "اسم المستخدِم للمدير", + "backup_archive_open_failed": "تعذر فتح النسخة الاحتياطية", + "diagnosis_mail_queue_unavailable_details": "خطأ: {error}", + "diagnosis_services_bad_status": "خدمة {service} {status} :(", + "diagnosis_services_running": "خدمة {service} شغّالة!", + "domain_config_cert_install": "تنصيب شهادة Let's Encrypt", + "domain_config_cert_summary_letsencrypt": "هنيئا! إنّك تستخدم الآن شهادة Let's Encrypt صالحة!", + "domain_config_default_app": "التطبيق الافتراضي", + "domain_dns_push_success": "تم تحديث ادخالات سِجِلات نظام أسماء النطاقات!", + "dyndns_unavailable": "النطاق '{domain}' غير متوفر.", + "global_settings_setting_pop3_enabled": "تفعيل POP3", + "diagnosis_ports_ok": "المنفذ {port} مفتوح ومتاح الوصول إليه مِن الخارج.", + "global_settings_setting_smtp_allow_ipv6": "سماح IPv6", + "disk_space_not_sufficient_update": "ليس هناك مساحة كافية لتحديث هذا التطبيق", + "domain_cert_gen_failed": "لا يمكن إعادة توليد الشهادة" } diff --git a/locales/ca.json b/locales/ca.json index 57ee0234c..808354264 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -1,8 +1,6 @@ { "action_invalid": "Acció '{action}' invàlida", "admin_password": "Contrasenya d'administració", - "admin_password_change_failed": "No es pot canviar la contrasenya", - "admin_password_changed": "S'ha canviat la contrasenya d'administració", "app_already_installed": "{app} ja està instal·lada", "app_already_installed_cant_change_url": "Aquesta aplicació ja està instal·lada. La URL no és pot canviar únicament amb aquesta funció. Mireu a `app changeurl` si està disponible.", "app_already_up_to_date": "{app} ja està actualitzada", @@ -22,7 +20,6 @@ "app_not_properly_removed": "{app} no s'ha pogut suprimir correctament", "app_removed": "{app} ha estat suprimida", "app_requirements_checking": "Verificació dels paquets requerits per {app}...", - "app_requirements_unmeet": "No es compleixen els requeriments per {app}, el paquet {pkgname} ({version}) ha de ser {spec}", "app_sources_fetch_failed": "No s'han pogut carregar els fitxers font, l'URL és correcta?", "app_unknown": "Aplicació desconeguda", "app_unsupported_remote_type": "El tipus remot utilitzat per l'aplicació no està suportat", @@ -30,8 +27,6 @@ "app_upgrade_failed": "No s'ha pogut actualitzar {app}: {error}", "app_upgrade_some_app_failed": "No s'han pogut actualitzar algunes aplicacions", "app_upgraded": "S'ha actualitzat {app}", - "ask_firstname": "Nom", - "ask_lastname": "Cognom", "ask_main_domain": "Domini principal", "ask_new_admin_password": "Nova contrasenya d'administrador", "ask_password": "Contrasenya", @@ -113,7 +108,6 @@ "confirm_app_install_danger": "PERILL! Aquesta aplicació encara és experimental (si no és que no funciona directament)! No hauríeu d'instal·lar-la a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema... Si accepteu el risc, escriviu «{answers}»", "confirm_app_install_thirdparty": "PERILL! Aquesta aplicació no es part del catàleg d'aplicacions de YunoHost. La instal·lació d'aplicacions de terceres parts pot comprometre la integritat i seguretat del seu sistema. No hauríeu d'instal·lar-ne a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema… Si accepteu el risc, escriviu «{answers}»", "custom_app_url_required": "Heu de especificar una URL per actualitzar la vostra aplicació personalitzada {app}", - "admin_password_too_long": "Trieu una contrasenya de menys de 127 caràcters", "dpkg_is_broken": "No es pot fer això en aquest instant perquè dpkg/APT (els gestors de paquets del sistema) sembla estar mal configurat... Podeu intentar solucionar-ho connectant-vos per SSH i executant «sudo apt install --fix-broken» i/o «sudo dpkg --configure -a».", "domain_cannot_remove_main": "No es pot eliminar «{domain}» ja que és el domini principal, primer s'ha d'establir un nou domini principal utilitzant «yunohost domain main-domain -n »; aquí hi ha una llista dels possibles dominis: {other_domains}", "domain_cert_gen_failed": "No s'ha pogut generar el certificat", @@ -142,26 +136,11 @@ "dyndns_domain_not_provided": "El proveïdor de DynDNS {provider} no pot oferir el domini {domain}.", "dyndns_unavailable": "El domini {domain} no està disponible.", "extracting": "Extracció en curs...", - "experimental_feature": "Atenció: Aquesta funcionalitat és experimental i no es considera estable, no s'ha d'utilitzar a excepció de saber el que esteu fent.", "field_invalid": "Camp incorrecte « {} »", "file_does_not_exist": "El camí {path} no existeix.", "firewall_reload_failed": "No s'ha pogut tornar a carregar el tallafocs", "firewall_reloaded": "S'ha tornat a carregar el tallafocs", "firewall_rules_cmd_failed": "Han fallat algunes comandes per aplicar regles del tallafocs. Més informació en el registre.", - "global_settings_bad_choice_for_enum": "Opció pel paràmetre {setting} incorrecta, s'ha rebut «{choice}», però les opcions disponibles són: {available_choices}", - "global_settings_bad_type_for_setting": "El tipus del paràmetre {setting} és incorrecte. S'ha rebut {received_type}, però s'esperava {expected_type}", - "global_settings_cant_open_settings": "No s'ha pogut obrir el fitxer de configuració, raó: {reason}", - "global_settings_cant_serialize_settings": "No s'ha pogut serialitzar les dades de configuració, raó: {reason}", - "global_settings_cant_write_settings": "No s'ha pogut escriure el fitxer de configuració, raó: {reason}", - "global_settings_key_doesnt_exists": "La clau « {settings_key} » no existeix en la configuració global, podeu veure totes les claus disponibles executant « yunohost settings list »", - "global_settings_reset_success": "S'ha fet una còpia de seguretat de la configuració anterior a {path}", - "global_settings_setting_security_nginx_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor web NGINX. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", - "global_settings_setting_security_password_admin_strength": "Robustesa de la contrasenya d'administrador", - "global_settings_setting_security_password_user_strength": "Robustesa de la contrasenya de l'usuari", - "global_settings_setting_security_ssh_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor SSH. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", - "global_settings_unknown_setting_from_settings_file": "Clau de configuració desconeguda: «{setting_key}», refusada i guardada a /etc/yunohost/settings-unknown.json", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permetre la clau d'hoste DSA (obsolet) per la configuració del servei SSH", - "global_settings_unknown_type": "Situació inesperada, la configuració {setting} sembla tenir el tipus {unknown_type} però no és un tipus reconegut pel sistema.", "good_practices_about_admin_password": "Esteu a punt de definir una nova contrasenya d'administrador. La contrasenya ha de tenir un mínim de 8 caràcters; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).", "hook_exec_failed": "No s'ha pogut executar el script: {path}", "hook_exec_not_terminated": "El script no s'ha acabat correctament: {path}", @@ -207,7 +186,6 @@ "log_tools_reboot": "Reinicia el servidor", "already_up_to_date": "No hi ha res a fer. Tot està actualitzat.", "dpkg_lock_not_available": "No es pot utilitzar aquesta comanda en aquest moment ja que sembla que un altre programa està utilitzant el lock de dpkg (el gestor de paquets del sistema)", - "global_settings_setting_security_postfix_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor Postfix. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", "mail_alias_remove_failed": "No s'han pogut eliminar els àlies del correu «{mail}»", "mail_domain_unknown": "El domini «{domain}» de l'adreça de correu no és vàlid. Utilitzeu un domini administrat per aquest servidor.", "mail_forward_remove_failed": "No s'han pogut eliminar el reenviament de correu «{mail}»", @@ -266,7 +244,6 @@ "restore_running_hooks": "Execució dels hooks de restauració...", "restore_system_part_failed": "No s'ha pogut restaurar la part «{part}» del sistema", "root_password_desynchronized": "S'ha canviat la contrasenya d'administració, però YunoHost no ha pogut propagar-ho cap a la contrasenya root!", - "root_password_replaced_by_admin_password": "La contrasenya root s'ha substituït per la contrasenya d'administració.", "server_shutdown": "S'aturarà el servidor", "server_shutdown_confirm": "S'aturarà el servidor immediatament, n'esteu segur? [{answers}]", "server_reboot": "Es reiniciarà el servidor", @@ -471,7 +448,6 @@ "diagnosis_services_running": "El servei {service} s'està executant!", "diagnosis_services_conf_broken": "La configuració pel servei {service} està trencada!", "diagnosis_ports_needed_by": "És necessari exposar aquest port per a les funcions {category} (servei {service})", - "global_settings_setting_pop3_enabled": "Activa el protocol POP3 per al servidor de correu", "log_app_action_run": "Executa l'acció de l'aplicació «{}»", "diagnosis_never_ran_yet": "Sembla que el servidor s'ha configurat recentment i encara no hi cap informe de diagnòstic per mostrar. S'ha d'executar un diagnòstic complet primer, ja sigui des de la pàgina web d'administració o utilitzant la comanda «yunohost diagnosis run» al terminal.", "diagnosis_description_web": "Web", @@ -506,7 +482,6 @@ "diagnosis_http_hairpinning_issue": "Sembla que la vostra xarxa no té el hairpinning activat.", "diagnosis_http_nginx_conf_not_up_to_date": "La configuració NGINX d'aquest domini sembla que ha estat modificada manualment, i no deixa que YunoHost diagnostiqui si és accessible amb HTTP.", "diagnosis_http_nginx_conf_not_up_to_date_details": "Per arreglar el problema, mireu les diferències amb la línia d'ordres utilitzant yunohost tools regen-conf nginx --dry-run --with-diff i si els canvis us semblen bé els podeu fer efectius utilitzant yunohost tools regen-conf nginx --force.", - "global_settings_setting_smtp_allow_ipv6": "Permet l'ús de IPv6 per rebre i enviar correus electrònics", "diagnosis_mail_ehlo_unreachable_details": "No s'ha pogut establir una connexió amb el vostre servidor en el port 25 amb IPv{ipversion}. Sembla que el servidor no és accessible.
1. La causa més comú per aquest problema és que el port 25 no està correctament redireccionat cap al vostre servidor.
2. També us hauríeu d'assegurar que el servei postfix estigui funcionant.
3. En configuracions més complexes: assegureu-vos que que no hi hagi cap tallafoc ni reverse-proxy interferint.", "diagnosis_mail_ehlo_wrong_details": "El EHLO rebut pel servidor de diagnòstic remot amb IPv{ipversion} és diferent al domini del vostre servidor.
EHLO rebut: {wrong_ehlo}
Esperat: {right_ehlo}
La causa més habitual d'aquest problema és que el port 25 no està correctament reenviat cap al vostre servidor. També podeu comprovar que no hi hagi un tallafocs o un reverse-proxy interferint.", "diagnosis_mail_fcrdns_dns_missing": "No hi ha cap DNS invers definit per IPv{ipversion}. Alguns correus electrònics poden no entregar-se o poden ser marcats com a correu brossa.", @@ -533,8 +508,6 @@ "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.", "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.", "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", "show_tile_cant_be_enabled_for_url_not_defined": "No podeu activar «show_title» ara, perquè primer s'ha de definir una URL per al permís «{permission}»", @@ -568,5 +541,13 @@ "diagnosis_sshd_config_inconsistent": "Sembla que el port SSH s'ha modificat manualment a /etc/ssh/sshd_config. Des de YunoHost 4.2, hi ha un nou paràmetre global «security.ssh.port» per evitar modificar manualment la configuració.", "diagnosis_sshd_config_insecure": "Sembla que la configuració SSH s'ha modificat manualment, i no es segura ha que no conté la directiva «AllowGroups» o «AllowUsers» per limitar l'accés a usuaris autoritzats.", "backup_create_size_estimation": "L'arxiu tindrà aproximadament {size} de dades.", - "app_restore_script_failed": "S'ha produït un error en el script de restauració de l'aplicació" + "app_restore_script_failed": "S'ha produït un error en el script de restauració de l'aplicació", + "global_settings_setting_backup_compress_tar_archives_help": "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_nginx_compatibility_help": "Solució de compromís entre compatibilitat i seguretat pel servidor web NGINX. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", + "global_settings_setting_admin_strength": "Robustesa de la contrasenya d'administrador", + "global_settings_setting_user_strength": "Robustesa de la contrasenya de l'usuari", + "global_settings_setting_postfix_compatibility_help": "Solució de compromís entre compatibilitat i seguretat pel servidor Postfix. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", + "global_settings_setting_ssh_compatibility_help": "Solució de compromís entre compatibilitat i seguretat pel servidor SSH. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", + "global_settings_setting_smtp_allow_ipv6_help": "Permet l'ús de IPv6 per rebre i enviar correus electrònics", + "global_settings_setting_smtp_relay_enabled_help": "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." } \ No newline at end of file diff --git a/locales/cs.json b/locales/cs.json index 47262064e..680d54743 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -2,9 +2,6 @@ "password_too_simple_1": "Heslo musí být aspoň 8 znaků dlouhé", "app_already_installed": "{app} je již nainstalován/a", "already_up_to_date": "Neprovedena žádná akce. Vše je již aktuální.", - "admin_password_too_long": "Zvolte prosím heslo kratší než 127 znaků", - "admin_password_changed": "Administrační heslo bylo změněno", - "admin_password_change_failed": "Nebylo možné změnit heslo", "admin_password": "Administrační heslo", "additional_urls_already_removed": "Další URL '{url}' již bylo odebráno u oprávnění '{permission}'", "additional_urls_already_added": "Další URL '{url}' již bylo přidáno pro oprávnění '{permission}'", @@ -49,19 +46,16 @@ "group_already_exist": "Skupina {group} již existuje", "good_practices_about_user_password": "Nyní zvolte nové heslo uživatele. Heslo by mělo být minimálně 8 znaků dlouhé, avšak je dobrou taktikou jej mít delší (např. použít více slov) a použít kombinaci znaků (velké, malé, čísla a speciální znaky).", "good_practices_about_admin_password": "Nyní zvolte nové administrační heslo. Heslo by mělo být minimálně 8 znaků dlouhé, avšak je dobrou taktikou jej mít delší (např. použít více slov) a použít kombinaci znaků (velké, malé, čísla a speciílní znaky).", - "global_settings_unknown_type": "Neočekávaná situace, nastavení {setting} deklaruje typ {unknown_type} ale toto není systémem podporováno.", - "global_settings_setting_backup_compress_tar_archives": "Komprimovat nové zálohy (.tar.gz) namísto nekomprimovaných (.tar). Poznámka: povolení této volby znamená objemově menší soubory záloh, avšak zálohování bude trvat déle a bude více zatěžovat CPU.", "global_settings_setting_smtp_relay_password": "SMTP relay heslo uživatele/hostitele", "global_settings_setting_smtp_relay_user": "SMTP relay uživatelské jméno/účet", "global_settings_setting_smtp_relay_port": "SMTP relay port", - "global_settings_setting_smtp_relay_host": "Použít SMTP relay hostitele pro odesílání emailů místo této YunoHost instance. Užitečné v různých situacích: port 25 je blokován vaším ISP nebo VPS poskytovatelem, IP adresa je na blacklistu (např. DUHL), nemůžete nastavit reverzní DNS záznam nebo tento server není přímo připojen do internetu a vy chcete použít jiný server k odesílání emailů.", - "global_settings_setting_smtp_allow_ipv6": "Povolit použití IPv6 pro příjem a odesílání emailů", "global_settings_setting_ssowat_panel_overlay_enabled": "Povolit SSOwat překryvný panel", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Povolit použití (zastaralého) DSA klíče hostitele pro konfiguraci SSH služby", - "global_settings_unknown_setting_from_settings_file": "Neznámý klíč v nastavení: '{setting_key}', zrušte jej a uložte v /etc/yunohost/settings-unknown.json", - "global_settings_setting_security_ssh_port": "SSH port", - "global_settings_setting_security_postfix_compatibility": "Kompromis mezi kompatibilitou a bezpečností Postfix serveru. Ovlivní šifry a další související bezpečnostní nastavení", - "global_settings_setting_security_ssh_compatibility": "Kompromis mezi kompatibilitou a bezpečností SSH serveru. Ovlivní šifry a další související bezpečnostní nastavení", - "global_settings_setting_security_password_user_strength": "Síla uživatelského hesla", - "global_settings_setting_security_password_admin_strength": "Síla administračního hesla" + "global_settings_setting_backup_compress_tar_archives_help": "Komprimovat nové zálohy (.tar.gz) namísto nekomprimovaných (.tar). Poznámka: povolení této volby znamená objemově menší soubory záloh, avšak zálohování bude trvat déle a bude více zatěžovat CPU.", + "global_settings_setting_admin_strength": "Síla administračního hesla", + "global_settings_setting_user_strength": "Síla uživatelského hesla", + "global_settings_setting_postfix_compatibility_help": "Kompromis mezi kompatibilitou a bezpečností Postfix serveru. Ovlivní šifry a další související bezpečnostní nastavení", + "global_settings_setting_ssh_compatibility_help": "Kompromis mezi kompatibilitou a bezpečností SSH serveru. Ovlivní šifry a další související bezpečnostní nastavení", + "global_settings_setting_ssh_port": "SSH port", + "global_settings_setting_smtp_allow_ipv6_help": "Povolit použití IPv6 pro příjem a odesílání emailů", + "global_settings_setting_smtp_relay_enabled_help": "Použít SMTP relay hostitele pro odesílání emailů místo této YunoHost instance. Užitečné v různých situacích: port 25 je blokován vaším ISP nebo VPS poskytovatelem, IP adresa je na blacklistu (např. DUHL), nemůžete nastavit reverzní DNS záznam nebo tento server není přímo připojen do internetu a vy chcete použít jiný server k odesílání emailů." } \ No newline at end of file diff --git a/locales/de.json b/locales/de.json index 824616abd..4a1db2961 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,14 +1,12 @@ { "action_invalid": "Ungültige Aktion '{action}'", "admin_password": "Administrator-Passwort", - "admin_password_change_failed": "Ändern des Passworts nicht möglich", - "admin_password_changed": "Das Administrator-Kennwort wurde geändert", "app_already_installed": "{app} ist schon installiert", "app_argument_choice_invalid": "Wähle einen gültigen Wert für das Argument '{name}': '{value}' ist nicht unter den verfügbaren Auswahlmöglichkeiten ({choices})", "app_argument_invalid": "Wähle einen gültigen Wert für das Argument '{name}': {error}", "app_argument_required": "Argument '{name}' wird benötigt", "app_extraction_failed": "Installationsdateien konnten nicht entpackt werden", - "app_id_invalid": "Falsche App-ID", + "app_id_invalid": "Falsche Applikations-ID", "app_install_files_invalid": "Diese Dateien können nicht installiert werden", "app_not_installed": "{app} konnte nicht in der Liste installierter Apps gefunden werden: {all_apps}", "app_removed": "{app} wurde entfernt", @@ -16,8 +14,6 @@ "app_unknown": "Unbekannte App", "app_upgrade_failed": "{app} konnte nicht aktualisiert werden: {error}", "app_upgraded": "{app} aktualisiert", - "ask_firstname": "Vorname", - "ask_lastname": "Nachname", "ask_main_domain": "Hauptdomain", "ask_new_admin_password": "Neues Verwaltungskennwort", "ask_password": "Passwort", @@ -71,12 +67,12 @@ "mail_forward_remove_failed": "Die Weiterleitungs-E-Mail '{mail}' konnte nicht gelöscht werden", "main_domain_change_failed": "Die Hauptdomain konnte nicht geändert werden", "main_domain_changed": "Die Hauptdomain wurde geändert", - "pattern_backup_archive_name": "Muss ein gültiger Dateiname mit maximal 30 alphanumerischen sowie -_. Zeichen sein", + "pattern_backup_archive_name": "Es muss ein gültiger Dateiname mit maximal 30 Zeichen sein, nur alphanumerische Zeichen und -_.", "pattern_domain": "Muss ein gültiger Domainname sein (z.B. meine-domain.org)", - "pattern_email": "Muss eine gültige E-Mail-Adresse ohne '+' Symbol sein (z.B. someone@example.com)", + "pattern_email": "Es muss sich um eine gültige E-Mail-Adresse handeln, ohne '+'-Symbol (z. B. name@domäne.de)", "pattern_firstname": "Muss ein gültiger Vorname sein", "pattern_lastname": "Muss ein gültiger Nachname sein", - "pattern_mailbox_quota": "Muss eine Größe mit b/k/M/G/T Suffix, oder 0 zum deaktivieren sein", + "pattern_mailbox_quota": "Es muss eine Größe mit dem Suffix b/k/M/G/T sein oder 0 um kein Kontingent zu haben", "pattern_password": "Muss mindestens drei Zeichen lang sein", "pattern_port_or_range": "Muss ein valider Port (z.B. 0-65535) oder ein Bereich (z.B. 100:200) sein", "pattern_username": "Darf nur aus klein geschriebenen alphanumerischen Zeichen und Unterstrichen bestehen", @@ -86,7 +82,7 @@ "restore_cleaning_failed": "Das temporäre Dateiverzeichnis für Systemrestaurierung konnte nicht gelöscht werden", "restore_complete": "Vollständig wiederhergestellt", "restore_confirm_yunohost_installed": "Möchtest du die Wiederherstellung wirklich starten? [{answers}]", - "restore_failed": "Das System konnte nicht wiederhergestellt werden", + "restore_failed": "System konnte nicht wiederhergestellt werden", "restore_hook_unavailable": "Das Wiederherstellungsskript für '{part}' steht weder in deinem System noch im Archiv zur Verfügung", "restore_nothings_done": "Nichts wurde wiederhergestellt", "restore_running_app_script": "App '{app}' wird wiederhergestellt...", @@ -97,21 +93,21 @@ "service_already_stopped": "Der Dienst '{service}' wurde bereits gestoppt", "service_cmd_exec_failed": "Der Befehl '{command}' konnte nicht ausgeführt werden", "service_disable_failed": "Der Start des Dienstes '{service}' beim Hochfahren konnte nicht verhindert werden.\n\nKürzlich erstellte Logs des Dienstes: {logs}", - "service_disabled": "Der Dienst '{service}' wird beim Hochfahren des Systems nicht mehr gestartet werden.", + "service_disabled": "Der Dienst '{service}' wird beim Systemstart nicht mehr gestartet.", "service_enable_failed": "Der Dienst '{service}' konnte beim Hochfahren nicht gestartet werden.\n\nKürzlich erstellte Logs des Dienstes: {logs}", "service_enabled": "Der Dienst '{service}' wird nun beim Hochfahren des Systems automatisch gestartet.", "service_remove_failed": "Konnte den Dienst '{service}' nicht entfernen", "service_removed": "Der Dienst '{service}' wurde erfolgreich entfernt", "service_start_failed": "Der Dienst '{service}' konnte nicht gestartet werden\n\nKürzlich erstellte Logs des Dienstes: {logs}", "service_started": "Der Dienst '{service}' wurde erfolgreich gestartet", - "service_stop_failed": "Der Dienst '{service}' kann nicht gestoppt werden\n\nAktuelle Service-Logs: {logs}", + "service_stop_failed": "Der Dienst '{service}' kann nicht beendet werden\n\nLetzte Dienstprotokolle:{logs}", "service_stopped": "Der Dienst '{service}' wurde erfolgreich beendet", "service_unknown": "Unbekannter Dienst '{service}'", - "ssowat_conf_generated": "Konfiguration von SSOwat neu erstellt", + "ssowat_conf_generated": "SSOwat-Konfiguration neu generiert", "system_upgraded": "System aktualisiert", "system_username_exists": "Der Anmeldename existiert bereits in der Liste der System-Konten", "unbackup_app": "'{app}' wird nicht gespeichert werden", - "unexpected_error": "Etwas Unerwartetes ist passiert: {error}", + "unexpected_error": "Ein unerwarteter Fehler ist aufgetreten {error}", "unlimit": "Kein Kontingent", "unrestore_app": "{app} wird nicht wiederhergestellt werden", "updating_apt_cache": "Die Liste der verfügbaren Pakete wird aktualisiert…", @@ -132,13 +128,12 @@ "yunohost_already_installed": "YunoHost ist bereits installiert", "yunohost_configured": "YunoHost ist nun konfiguriert", "yunohost_installing": "YunoHost wird installiert...", - "yunohost_not_installed": "YunoHost ist nicht oder nur unvollständig installiert worden. Bitte 'yunohost tools postinstall' ausführen", + "yunohost_not_installed": "YunoHost ist nicht oder unvollständig installiert worden. Bitte 'yunohost tools postinstall' ausführen", "app_not_properly_removed": "{app} wurde nicht ordnungsgemäß entfernt", - "not_enough_disk_space": "Nicht genügend Speicherplatz auf '{path}' frei", + "not_enough_disk_space": "Nicht genügend freier Speicherplatz unter '{path}'", "backup_creation_failed": "Konnte Backup-Archiv nicht erstellen", "app_not_correctly_installed": "{app} scheint nicht korrekt installiert zu sein", - "app_requirements_checking": "Überprüfe notwendige Pakete für {app}...", - "app_requirements_unmeet": "Anforderungen für {app} werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein", + "app_requirements_checking": "Überprüfe Voraussetzungen für {app}...", "app_unsupported_remote_type": "Für die App wurde ein nicht unterstützer Steuerungstyp verwendet", "backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path})", "domains_available": "Verfügbare Domains:", @@ -150,8 +145,8 @@ "certmanager_certificate_fetching_or_enabling_failed": "Die Aktivierung des neuen Zertifikats für die {domain} ist fehlgeschlagen...", "certmanager_attempt_to_renew_nonLE_cert": "Das Zertifikat der Domain '{domain}' wurde nicht von Let's Encrypt ausgestellt. Es kann nicht automatisch erneuert werden!", "certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain} läuft nicht in Kürze ab! (Benutze --force um diese Nachricht zu umgehen)", - "certmanager_domain_http_not_working": "Es scheint, als ob die Domäne {domain} nicht über HTTP erreicht werden kann. Bitte überprüfe, ob deine DNS- und nginx-Konfiguration in Ordnung ist. (Wenn du weißt, was du tust, nutze '--no-checks' um die Überprüfung zu überspringen.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "Der DNS-A-Eintrag der Domain {domain} unterscheidet sich von dieser Server-IP. Für weitere Informationen überprüfe bitte die 'DNS records' (basic) Kategorie in der Diagnose. Wenn du kürzlich deinen A-Eintrag verändert hast, warte bitte etwas, damit die Änderungen wirksam werden (Du kannst die DNS-Propagation mittels Website überprüfen) (Wenn du weißt, was du tust, kannst du '--no-checks' benutzen, um diese Überprüfung zu überspringen.)", + "certmanager_domain_http_not_working": "Es scheint, als ob die Domäne '{domain}' über HTTP nicht erreichbar ist. Bitte schauen Sie sich die 'Web'-Kategorie in der Diagnose an für weitere Informationen. (Wenn Sie wissen, was Sie tun, nutzen Sie '--no-checks' um die Überprüfung zu deaktivieren.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Die DNS-Einträge der Domäne '{domain}' unterscheiden sich von der IP dieses Servers. Für weitere Informationen überprüfen Sie bitte die Kategorie \"DNS-Einträge\" (basic) in der Diagnose. Wenn Sie kürzlich Ihren A-Eintrag verändert haben, warten Sie bitte ein wenig, bis die Änderungen wirksam werden (es gibt Online-Checks für die DNS-Propagation). (Wenn Sie wissen, was Sie tun, können Sie '--no-checks' verwenden, um diese Überprüfung zu überspringen.)", "certmanager_cannot_read_cert": "Es ist ein Fehler aufgetreten, als es versucht wurde das aktuelle Zertifikat für die Domain {domain} zu öffnen (Datei: {file}), Grund: {reason}", "certmanager_cert_install_success_selfsigned": "Das selbstsignierte Zertifikat für die Domäne '{domain}' wurde erfolgreich installiert", "certmanager_cert_install_success": "Let's-Encrypt-Zertifikat für die Domäne {domain} ist jetzt installiert", @@ -161,7 +156,7 @@ "certmanager_no_cert_file": "Die Zertifikatsdatei für die Domain {domain} (Datei: {file}) konnte nicht gelesen werden", "domain_cannot_remove_main": "Die Domäne '{domain}' konnten nicht entfernt werden, weil es die Haupt-Domäne ist. Du musst zuerst eine andere Domäne zur Haupt-Domäne machen. Dies ist über den Befehl 'yunohost domain main-domain -n ' möglich. Hier ist eine Liste möglicher Domänen: {other_domains}", "certmanager_self_ca_conf_file_not_found": "Die Konfigurationsdatei der Zertifizierungsstelle für selbstsignierte Zertifikate wurde nicht gefunden (Datei {file})", - "certmanager_acme_not_configured_for_domain": "Die ACME Challenge kann im Moment nicht für {domain} ausgeführt werden, weil in deiner nginx-Konfiguration das entsprechende Code-Snippet fehlt... Bitte stelle sicher, dass deine nginx-Konfiguration mit 'yunohost tools regen-conf nginx --dry-run --with-diff' auf dem neuesten Stand ist.", + "certmanager_acme_not_configured_for_domain": "Die ACME-Challenge für {domain} kann momentan nicht ausgeführt werden, weil in Ihrer nginx-Konfiguration das entsprechende Code-Snippet fehlt... Bitte stellen Sie sicher, dass Ihre nginx-Konfiguration mit 'yunohost tools regen-conf nginx --dry-run --with-diff' auf dem neuesten Stand ist.", "certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht aufgelöst werden (Datei: {file})", "domain_hostname_failed": "Neuer Hostname wurde nicht gesetzt. Das kann zukünftige Probleme verursachen (es kann auch sein, dass es funktioniert).", "app_already_installed_cant_change_url": "Diese Applikation ist bereits installiert. Die URL kann durch diese Funktion nicht modifiziert werden. Überprüfe ob `app changeurl` verfügbar ist.", @@ -176,17 +171,14 @@ "backup_archive_system_part_not_available": "Der System-Teil '{part}' ist in diesem Backup nicht enthalten", "backup_archive_writing_error": "Die Dateien '{source} (im Ordner '{dest}') konnten nicht in das komprimierte Archiv-Backup '{archive}' hinzugefügt werden", "app_change_url_success": "{app} URL ist nun {domain}{path}", - "global_settings_bad_type_for_setting": "Falscher Typ der Einstellung {setting}. Empfangen: {received_type}, aber erwarteter Typ: {expected_type}", - "global_settings_bad_choice_for_enum": "Wert des Einstellungsparameters {setting} ungültig. Du hast '{choice}' eingegeben. Aber nur folgende Werte sind gültig: {available_choices}", "file_does_not_exist": "Die Datei {path} existiert nicht.", - "experimental_feature": "Warnung: Der Maintainer hat diese Funktion als experimentell gekennzeichnet. Sie ist nicht stabil. Du solltest sie nur verwenden, wenn du weißt, was du tust.", "dyndns_domain_not_provided": "Der DynDNS-Anbieter {provider} kann die Domäne(n) {domain} nicht bereitstellen.", "dyndns_could_not_check_available": "Konnte nicht überprüfen, ob {domain} auf {provider} verfügbar ist.", "domain_dns_conf_is_just_a_recommendation": "Dieser Befehl zeigt dir die *empfohlene* Konfiguration. Er konfiguriert *nicht* das DNS für dich. Es liegt in deiner Verantwortung, die DNS-Zone bei deinem DNS-Registrar nach dieser Empfehlung zu konfigurieren.", "dpkg_lock_not_available": "Dieser Befehl kann momentan nicht ausgeführt werden, da anscheinend ein anderes Programm die Sperre von dpkg (dem Systempaket-Manager) verwendet", - "confirm_app_install_thirdparty": "WARNUNG! Diese Applikation ist nicht Teil des YunoHost-Applikationskatalogs. Das Installieren von Drittanbieterapplikationen könnte die Sicherheit und Integrität deines Systems beeinträchtigen. Du solltest wahrscheinlich NICHT fortfahren, es sei denn, du weißt, was du tust. Es wird KEINE UNTERSTÜTZUNG angeboten, wenn die Applikation nicht funktionieren oder dein System beschädigen sollte... Wenn du das Risiko trotzdem eingehen möchrst, tippe '{answers}'", + "confirm_app_install_thirdparty": "Warnung! Diese Applikation ist nicht Teil des App-Katalogs von YunoHost. Die Installation von Drittanbieter Applikationen kann die Integrität und Sicherheit Ihres Systems gefährden. Sie sollten sie NICHT installieren, wenn Sie nicht wissen, was Sie tun. Es wird KEIN SUPPORT geleistet, wenn diese Applikation nicht funktioniert oder Ihr System beschädigt! Wenn Sie dieses Risiko trotzdem eingehen wollen, geben Sie '{answers}' ein", "confirm_app_install_danger": "WARNUNG! Diese Applikation ist noch experimentell (wenn nicht sogar ausdrücklich nicht funktionsfähig)! Du solltest sie wahrscheinlich NICHT installieren, es sei denn, du weißt, was du tust. Es wird keine Unterstützung angeboten, falls diese Applikation nicht funktionieren oder dein System beschädigen sollte... Falls du bereit bist, dieses Risiko einzugehen, tippe '{answers}'", - "confirm_app_install_warning": "Warnung: Diese Applikation funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers}] ", + "confirm_app_install_warning": "Warnung: Diese Applikation funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single-Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers}] ", "backup_with_no_restore_script_for_app": "{app} hat kein Wiederherstellungsskript. Das Backup dieser App kann nicht automatisch wiederhergestellt werden.", "backup_with_no_backup_script_for_app": "Die App {app} hat kein Sicherungsskript. Ignoriere es.", "backup_unable_to_organize_files": "Dateien im Archiv konnten nicht mit der schnellen Methode organisiert werden", @@ -217,11 +209,9 @@ "aborting": "Breche ab.", "app_action_cannot_be_ran_because_required_services_down": "Diese erforderlichen Dienste sollten zur Durchführung dieser Aktion laufen: {services}. Versuche, sie neu zu starten, um fortzufahren (und möglicherweise zu untersuchen, warum sie nicht verfügbar sind).", "already_up_to_date": "Nichts zu tun. Alles ist bereits auf dem neusten Stand.", - "admin_password_too_long": "Bitte ein Passwort kürzer als 127 Zeichen wählen", "app_action_broke_system": "Diese Aktion scheint diese wichtigen Dienste unterbrochen zu haben: {services}", "apps_already_up_to_date": "Alle Apps sind bereits aktuell", "backup_copying_to_organize_the_archive": "Kopieren von {size} MB, um das Archiv zu organisieren", - "global_settings_setting_security_ssh_compatibility": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den SSH-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", "group_deleted": "Gruppe '{group}' gelöscht", "group_deletion_failed": "Konnte Gruppe '{group}' nicht löschen: {error}", "dyndns_provider_unreachable": "DynDNS-Anbieter {provider} kann nicht erreicht werden: Entweder ist dein YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.", @@ -232,33 +222,21 @@ "group_update_failed": "Kann Gruppe '{group}' nicht aktualisieren: {error}", "log_does_exists": "Es gibt kein Operationsprotokoll mit dem Namen'{log}', verwende 'yunohost log list', um alle verfügbaren Operationsprotokolle anzuzeigen", "log_operation_unit_unclosed_properly": "Die Operationseinheit wurde nicht richtig geschlossen", - "global_settings_setting_security_postfix_compatibility": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Postfix-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", - "global_settings_unknown_type": "Unerwartete Situation, die Einstellung {setting} scheint den Typ {unknown_type} zu haben, ist aber kein vom System unterstützter Typ.", "dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo apt install --fix-broken` sowie/oder `sudo dpkg --configure -a` ausführst.", - "global_settings_unknown_setting_from_settings_file": "Unbekannter Schlüssel in den Einstellungen: '{setting_key}', verwerfen und speichern in /etc/yunohost/settings-unknown.json", "log_link_to_log": "Vollständiges Log dieser Operation: '{desc}'", "log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwende den Befehl 'yunohost log show {name}'", - "global_settings_setting_security_nginx_compatibility": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Webserver NGINX. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Erlaubt die Verwendung eines (veralteten) DSA-Hostkeys für die SSH-Daemon-Konfiguration", "log_app_remove": "Entferne die Applikation '{}'", - "global_settings_cant_open_settings": "Einstellungsdatei konnte nicht geöffnet werden, Grund: {reason}", - "global_settings_cant_write_settings": "Einstellungsdatei konnte nicht gespeichert werden, Grund: {reason}", "log_app_install": "Installiere die Applikation '{}'", - "global_settings_reset_success": "Frühere Einstellungen werden nun auf {path} gesichert", "log_app_upgrade": "Upgrade der Applikation '{}'", "good_practices_about_admin_password": "Du bist nun dabei, ein neues Administratorpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - es ist jedoch empfehlenswert, ein längeres Passwort (z.B. eine Passphrase) und/oder verschiedene Arten von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", "log_corrupted_md_file": "Die mit Protokollen verknüpfte YAML-Metadatendatei ist beschädigt: '{md_file}\nFehler: {error}''", - "global_settings_cant_serialize_settings": "Einstellungsdaten konnten nicht serialisiert werden, Grund: {reason}", "log_help_to_get_failed_log": "Der Vorgang'{desc}' konnte nicht abgeschlossen werden. Bitte teile das vollständige Protokoll dieser Operation mit dem Befehl 'yunohost log share {name}', um Hilfe zu erhalten", "backup_no_uncompress_archive_dir": "Dieses unkomprimierte Archivverzeichnis gibt es nicht", "log_app_change_url": "Ändere die URL der Applikation '{}'", - "global_settings_setting_security_password_user_strength": "Stärke des Anmeldepassworts", "good_practices_about_user_password": "Du bist nun dabei, ein neues Nutzerpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - es ist jedoch empfehlenswert, ein längeres Passwort (z.B. eine Passphrase) und/oder verschiedene Arten von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", "log_link_to_failed_log": "Der Vorgang konnte nicht abgeschlossen werden '{desc}'. Bitte gib das vollständige Protokoll dieser Operation mit Klicken Sie hier an, um Hilfe zu erhalten", "backup_cant_mount_uncompress_archive": "Das unkomprimierte Archiv konnte nicht als schreibgeschützt gemountet werden", "backup_csv_addition_failed": "Es konnten keine Dateien zur Sicherung in die CSV-Datei hinzugefügt werden", - "global_settings_setting_security_password_admin_strength": "Stärke des Admin-Passworts", - "global_settings_key_doesnt_exists": "Der Schlüssel'{settings_key}' existiert nicht in den globalen Einstellungen, du kannst alle verfügbaren Schlüssel sehen, indem du 'yunohost settings list' ausführst", "log_app_makedefault": "Mache '{}' zur Standard-Applikation", "hook_json_return_error": "Konnte die Rückkehr vom Einsprungpunkt {path} nicht lesen. Fehler: {msg}. Unformatierter Inhalt: {raw_content}", "app_full_domain_unavailable": "Es tut uns leid, aber diese Applikation erfordert die Installation auf einer eigenen Domain, aber einige andere Applikationen sind bereits auf der Domäne'{domain}' installiert. Eine mögliche Lösung ist das Hinzufügen und Verwenden einer Subdomain, die dieser Applikation zugeordnet ist.", @@ -289,7 +267,7 @@ "diagnosis_cache_still_valid": "(Cache noch gültig für {category} Diagnose. Es wird keine neue Diagnose durchgeführt!)", "diagnosis_cant_run_because_of_dep": "Kann Diagnose für {category} nicht ausführen während wichtige Probleme zu {dep} noch nicht behoben sind.", "diagnosis_found_errors_and_warnings": "Habe {errors} erhebliche(s) Problem(e) (und {warnings} Warnung(en)) in Verbindung mit {category} gefunden!", - "diagnosis_ip_broken_dnsresolution": "Domänen-Namens-Auflösung scheint aus einem bestimmten Grund nicht zu funktionieren... Blockiert eine Firewall die DNS Anfragen?", + "diagnosis_ip_broken_dnsresolution": "Domänennamen-Auflösung scheint aus einem bestimmten Grund nicht zu funktionieren... Blockiert vielleicht eine Firewall die DNS-Anfragen?", "diagnosis_ip_broken_resolvconf": "Domänen-Namensauflösung scheint nicht zu funktionieren, was daran liegen könnte, dass in /etc/resolv.conf kein Eintrag auf 127.0.0.1 zeigt.", "diagnosis_ip_weird_resolvconf_details": "Die Datei /etc/resolv.conf muss ein Symlink auf /etc/resolvconf/run/resolv.conf sein, welcher auf 127.0.0.1 (dnsmasq) zeigt. Falls du die DNS-Resolver manuell konfigurieren möchtest, bearbeite bitte /etc/resolv.dnsmasq.conf.", "diagnosis_dns_good_conf": "DNS Einträge korrekt konfiguriert für die Domäne {domain} (Kategorie {category})", @@ -303,7 +281,7 @@ "backup_archive_corrupted": "Das Backup-Archiv '{archive}' scheint beschädigt: {error}", "backup_archive_cant_retrieve_info_json": "Die Informationen für das Archiv '{archive}' konnten nicht geladen werden... Die Datei info.json wurde nicht gefunden (oder ist kein gültiges json).", "app_packaging_format_not_supported": "Diese App kann nicht installiert werden da das Paketformat nicht von der YunoHost-Version unterstützt wird. Am besten solltest du dein System aktualisieren.", - "certmanager_domain_not_diagnosed_yet": "Für die Domain {domain} gibt es noch keine Diagnose-Resultate. Bitte widerhole die Diagnose für die Kategorien 'DNS records' und 'Web' im Diagnose-Bereich um zu überprüfen ob die Domain für Let's Encrypt bereit ist. (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen.)", + "certmanager_domain_not_diagnosed_yet": "Für die Domäne {domain} gibt es noch keine Diagnose-Resultate. Bitte wiederholen Sie die Diagnose für die Kategorien 'DNS-Einträge' und 'Web' im Diagnose-Bereich um zu überprüfen ob die Domäne für Let's Encrypt bereit ist. (Wenn Sie wissen was Sie tun, können Sie --no-checks benutzen, um diese Überprüfung zu überspringen.)", "mail_unavailable": "Diese E-Mail Adresse ist reserviert und wird dem ersten Konto automatisch zugewiesen", "diagnosis_services_conf_broken": "Die Konfiguration für den Dienst {service} ist fehlerhaft!", "diagnosis_services_running": "Dienst {service} läuft!", @@ -313,25 +291,25 @@ "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_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_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}", "diagnosis_dns_bad_conf": "Einige DNS-Einträge für die Domäne {domain} fehlen oder sind nicht korrekt (Kategorie {category})", "diagnosis_ip_local": "Lokale IP: {local}", "diagnosis_ip_global": "Globale IP: {global}", - "diagnosis_ip_no_ipv6_tip": "Die Verwendung von IPv6 ist nicht Voraussetzung für das Funktionieren deines Servers, trägt aber zur Gesundheit des Internet als Ganzes bei. IPv6 sollte normalerweise automatisch von deinem Server oder deinem Provider konfiguriert werden, sofern verfügbar. Andernfalls musst du einige Dinge manuell konfigurieren. Weitere Informationen findest du hier: https://yunohost.org/#/ipv6. Wenn du IPv6 nicht aktivieren kannst oder dir das zu technisch ist, kannst du diese Warnung gefahrlos ignorieren.", + "diagnosis_ip_no_ipv6_tip": "Ein funktionierendes IPv6 ist für den Betrieb Ihres Servers nicht zwingend erforderlich, aber es ist besser für das Funktionieren des Internets als Ganzes. IPv6 sollte normalerweise automatisch vom System oder Ihrem Provider konfiguriert werden, wenn es verfügbar ist. Andernfalls müssen Sie möglicherweise einige Dinge manuell konfigurieren, wie in der Dokumentation hier beschrieben: https://yunohost.org/#/ipv6. Wenn Sie IPv6 nicht aktivieren können oder wenn es Ihnen zu technisch erscheint, können Sie diese Warnung auch getrost ignorieren.", "diagnosis_services_bad_status_tip": "Du kannst versuchen, den Dienst neu zu starten, und wenn das nicht funktioniert, schaue dir die (Dienst-)Logs in der Verwaltung an (In der Kommandozeile kannst du dies mit yunohost service restart {service} und yunohost service log {service} tun).", "diagnosis_services_bad_status": "Der Dienst {service} ist {status} :(", "diagnosis_diskusage_verylow": "Der Speicher {mountpoint} (auf Gerät {device}) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von ingesamt {total}). Du solltest ernsthaft in Betracht ziehen, etwas Seicherplatz frei zu machen!", "diagnosis_http_ok": "Die Domäne {domain} ist über HTTP von außerhalb des lokalen Netzwerks erreichbar.", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es dir nicht gestatten, den ausgehenden Port 25 zu öffnen, da diese sich nicht um die Netzneutralität kümmern.
- Einige davon bieten als Alternative an, ein Mailserver-Relay zu verwenden, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine, die Privatsphäre berücksichtigende, Alternative ist die Verwendung eines VPN *mit einer dedizierten öffentlichen IP* um solche Einschränkungen zu umgehen. Schaue unter https://yunohost.org/#/vpn_advantage nach.
- Du kannst auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln", - "diagnosis_http_timeout": "Wartezeit wurde beim Versuch, von außen eine Verbindung zum Server aufzubauen, überschritten. Er scheint nicht erreichbar zu sein.
1. Die häufigste Ursache für dieses Problem ist daß der Port 80 (und 433) nicht richtig zu deinem Server weitergeleitet werden.
2. Du solltest auch sicherstellen, daß der Dienst nginx läuft.
3. In komplexeren Umgebungen: Stelle sicher, daß keine Firewall oder Reverse-Proxy stört.", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es Ihnen nicht gestatten, den ausgehenden Port 25 zu öffnen, weil Ihnen die Netzneutralität nichts bedeutet.
- Einige davon bieten als Alternative an, ein Mailserver-Relay zu verwenden, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine Alternative, welche die Privatsphäre berücksichtigt, wäre die Verwendung eines VPN *mit einer öffentlichen dedizierten IP* um solche Einschränkungen zu umgehen. Schauen Sie unter https://yunohost.org/#/vpn_advantage nach.
- Sie können auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln", + "diagnosis_http_timeout": "Wartezeit wurde beim Versuch überschritten, von Aussen eine Verbindung zu Ihrem Server aufzubauen. Er scheint nicht erreichbar zu sein.
1. Die häufigste Ursache für dieses Problem ist, dass die Ports 80 und 433 nicht richtig zu Ihrem Server weitergeleitet werden.
2. Sie sollten zudem sicherstellen, dass der Dienst nginx läuft.
3. In komplexeren Umgebungen: Stellen Sie sicher, dass keine Firewall oder Reverse-Proxy stört .", "service_reloaded_or_restarted": "Der Dienst '{service}' wurde erfolgreich neu geladen oder gestartet", "service_restarted": "Der Dienst '{service}' wurde neu gestartet", "certmanager_warning_subdomain_dns_record": "Die Subdomäne \"{subdomain}\" löst nicht zur gleichen IP Adresse auf wie \"{domain}\". Einige Funktionen sind nicht verfügbar bis du dies behebst und die Zertifikate neu erzeugst.", - "diagnosis_ports_ok": "Port {port} ist von außen erreichbar.", + "diagnosis_ports_ok": "Port {port} ist von Aussen erreichbar.", "diagnosis_ram_verylow": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total})", - "diagnosis_mail_outgoing_port_25_blocked_details": "Du solltest zuerst versuchen den ausgehenden Port 25 auf deiner Router-Konfigurationsoberfläche oder deiner Hosting-Anbieter-Konfigurationsoberfläche zu öffnen. (Bei einigen Hosting-Anbietern kann es sein, daß sie verlangen, daß man dafür ein Support-Ticket sendet).", + "diagnosis_mail_outgoing_port_25_blocked_details": "Sie sollten zuerst versuchen, den ausgehenden Port 25 in Ihrer Router-Konfigurationsoberfläche oder in der Konfigurationsoberfläche Ihres Hosting-Anbieters zu öffnen. (Bei einigen Hosting-Anbietern kann es sein, dass man von Ihnen verlangt, dass Sie dafür ein Support-Ticket erstellen).", "diagnosis_mail_ehlo_ok": "Der SMTP-Server ist von von außen erreichbar und darum auch in der Lage E-Mails zu empfangen!", "diagnosis_mail_ehlo_bad_answer": "Ein nicht-SMTP-Dienst antwortete auf Port 25 per IPv{ipversion}", "diagnosis_swap_notsomuch": "Das System hat nur {total} Swap. Du solltest dir überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.", @@ -346,7 +324,7 @@ "diagnosis_domain_expiration_not_found_details": "Die WHOIS-Informationen für die Domäne {domain} scheinen keine Informationen über das Ablaufdatum zu enthalten. Stimmt das?", "diagnosis_domain_expiration_warning": "Einige Domänen werden bald ablaufen!", "diagnosis_diskusage_ok": "Der Speicher {mountpoint} (auf Gerät {device}) hat immer noch {free} ({free_percent}%) freien Speicherplatz übrig(von insgesamt {total})!", - "diagnosis_ram_ok": "Das System hat immer noch {available} ({available_percent}%) RAM zu Verfügung von {total}.", + "diagnosis_ram_ok": "Das System hat noch {available} ({available_percent}%) RAM von {total} zur Verfügung.", "diagnosis_swap_none": "Das System hat gar keinen Swap. Du solltest überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.", "diagnosis_mail_ehlo_unreachable_details": "Konnte keine Verbindung zu deinem Server auf dem Port 25 herzustellen über IPv{ipversion}. Er scheint nicht erreichbar zu sein.
1. Das häufigste Problem ist, dass der Port 25 nicht richtig zu deinem Server weitergeleitet ist.
2. Du solltest auch sicherstellen, dass der Postfix-Dienst läuft.
3. In komplexeren Umgebungen: Stelle sicher, daß keine Firewall oder Reverse-Proxy stört.", "diagnosis_mail_ehlo_wrong": "Ein anderer SMTP-Server antwortet auf IPv{ipversion}. Dein Server wird wahrscheinlich nicht in der Lage sein, E-Mails zu empfangen.", @@ -358,13 +336,13 @@ "log_letsencrypt_cert_renew": "Erneuern des Let's Encrypt-Zeritifikates von '{}'", "log_selfsigned_cert_install": "Das selbstsignierte Zertifikat auf der Domäne '{}' installieren", "log_letsencrypt_cert_install": "Das Let’s Encrypt auf der Domäne '{}' installieren", - "diagnosis_mail_fcrdns_nok_details": "Du solltest zuerst versuchen, in deiner Internet-Router-Oberfläche oder in deiner Hosting-Anbieter-Oberfläche den Reverse-DNS-Eintrag mit {ehlo_domain}zu konfigurieren. (Gewisse Hosting-Anbieter können dafür möglicherweise verlangen, dass du dafür ein Support-Ticket erstellst).", - "diagnosis_mail_fcrdns_dns_missing": "Es wurde kein Reverse-DNS-Eintrag definiert für IPv{ipversion}. Einige E-Mails könnten möglicherweise zurückgewiesen oder als Spam markiert werden.", + "diagnosis_mail_fcrdns_nok_details": "Sie sollten zuerst versuchen, auf Ihrer Internet-Router-Oberfläche, in Ihrer Internet-Box oder auf Ihrer Hosting-Anbieter-Oberfläche den Reverse-DNS-Eintrag mit {ehlo_domain}zu konfigurieren. (Gewisse Hosting-Anbieter können möglicherweise verlangen, dass Sie dafür ein Support-Ticket erstellen).", + "diagnosis_mail_fcrdns_dns_missing": "Kein Reverse-DNS-Eintrag ist definiert für IPv{ipversion}. Einige E-Mails könnten eventuell nicht zugestellt oder als Spam markiert werden.", "diagnosis_mail_fcrdns_ok": "Dein Reverse-DNS-Eintrag ist korrekt konfiguriert!", "diagnosis_mail_ehlo_could_not_diagnose_details": "Fehler: {error}", - "diagnosis_mail_ehlo_could_not_diagnose": "Konnte nicht überprüfen, ob der Postfix-Mail-Server von aussen per IPv{ipversion} erreichbar ist.", + "diagnosis_mail_ehlo_could_not_diagnose": "Es war nicht möglich zu diagnostizieren, ob der Postfix-Mailserver von Aussen über IPv{ipversion} erreichbar ist.", "diagnosis_mail_ehlo_wrong_details": "Die vom Remote-Diagnose-Server per IPv{ipversion} empfangene EHLO weicht von der Domäne deines Servers ab.
Empfangene EHLO: {wrong_ehlo}
Erwartet: {right_ehlo}
Die geläufigste Ursache für dieses Problem ist, dass der Port 25 nicht korrekt auf deinem Server weitergeleitet wird. Du kannst zusätzlich auch prüfen, dass keine Firewall oder Reverse-Proxy stört.", - "diagnosis_mail_ehlo_bad_answer_details": "Das könnte daran liegen, dass anstelle deines Servers eine andere Maschine antwortet.", + "diagnosis_mail_ehlo_bad_answer_details": "Das könnte daran liegen, dass anstelle Ihres Servers ein anderes Gerät antwortet.", "ask_user_domain": "Domäne, welche für die E-Mail-Adresse und den XMPP-Account des Kontos verwendet werden soll", "app_manifest_install_ask_is_public": "Soll diese Applikation für Gäste sichtbar sein?", "app_manifest_install_ask_admin": "Wähle einen Administrator für diese Applikation", @@ -372,9 +350,9 @@ "diagnosis_mail_blacklist_listed_by": "Deine IP-Adresse oder Domäne {item} ist auf der Blacklist auf {blacklist_name}", "diagnosis_mail_blacklist_ok": "Die IP-Adressen und die Domänen, welche von diesem Server verwendet werden, scheinen nicht auf einer Blacklist zu sein", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Aktueller Reverse-DNS-Eintrag: {rdns_domain}
Erwarteter Wert: {ehlo_domain}", - "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Der Reverse-DNS-Eintrag für IPv{ipversion} ist nicht korrekt konfiguriert. Einige E-Mails könnten abgewiesen oder als Spam markiert werden.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Einige Provider werden es dir nicht erlauben, deinen Reverse-DNS-Eintrag zu konfigurieren (oder ihre Funktionalität könnte defekt sein ...). Falls du deinen Reverse-DNS-Eintrag für IPv4 korrekt konfiguiert ist, kannst du versuchen, die Verwendung von IPv6 für das Versenden von E-Mails auszuschalten, indem du den Befehl yunohost settings set smtp.allow_ipv6 -v off ausführst. Bemerkung: Die Folge dieser letzten Lösung ist, dass du mit Servern, welche ausschliesslich über IPv6 verfügen, keine E-Mails mehr versenden oder empfangen kannst.", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Einige Anbieter werden es dir nicht erlauben, deinen Reverse-DNS zu konfigurieren (oder deren Funktionalität ist defekt...). Falls du deswegen auf Probleme stoßen solltest, ziehe folgende Lösungen in Betracht:
- Manche ISPs stellen als Alternative die Benutzung eines Mail-Server-Relays zur Verfügung, was jedoch mit sich zieht, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine privatsphärenfreundlichere Alternative ist die Benutzung eines VPN *mit einer dedizierten öffentlichen IP* um Einschränkungen dieser Art zu umgehen. Schaue hier nach https://yunohost.org/#/vpn_advantage
- Schließlich ist es auch möglich zu einem anderen Anbieter zu wechseln", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Reverse-DNS-Eintrag ist nicht korrekt konfiguriert für IPv{ipversion}. Einige E-Mails könnten eventuell nicht zugestellt oder als Spam markiert werden.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Einige Provider werden es Ihnen vermutlich nicht erlauben, den Reverse-DNS-Eintrag zu konfigurieren (oder vielleicht ist diese Funktion beschädigt...). Falls Sie Ihren Reverse-DNS-Eintrag für IPv4 korrekt konfiguriert haben, können Sie versuchen, die Verwendung von IPv6 für das Versenden von E-Mails auszuschalten, indem Sie den Befehl yunohost settings set smtp.allow_ipv6 -v off ausführen. Bemerkung: Die Folge dieser letzten Lösung ist, dass Sie mit Servern, welche nur über IPv6 verfügen, keine E-Mails mehr versenden oder empfangen können.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Einige Anbieter werden es nicht zulassen, den Reverse-DNS zu konfigurieren (oder diese Funktion ist defekt...). Falls du deswegen auf Probleme stoßen solltest, ziehe folgende Lösungen in Betracht:
- Manche ISPs stellen als Alternative die Benutzung eines Mail-Server-Relays zur Verfügung, was jedoch mit sich zieht, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine privatsphärenfreundlichere Alternative ist die Benutzung eines VPN *mit einer dedizierten öffentlichen IP* um Einschränkungen dieser Art zu umgehen. Schaue hier nach https://yunohost.org/#/vpn_advantage
- Schließlich ist es auch möglich zu einem anderen Anbieter zu wechseln", "diagnosis_mail_queue_unavailable_details": "Fehler: {error}", "diagnosis_mail_queue_unavailable": "Die Anzahl der anstehenden Nachrichten in der Warteschlange kann nicht abgefragt werden", "diagnosis_mail_queue_ok": "{nb_pending} anstehende E-Mails in der Warteschlange", @@ -405,12 +383,12 @@ "diagnosis_package_installed_from_sury": "Einige System-Pakete sollten gedowngradet werden", "diagnosis_ports_forwarding_tip": "Um dieses Problem zu beheben, musst du höchstwahrscheinlich die Port-Weiterleitung auf deinem Internet-Router einrichten wie in https://yunohost.org/isp_box_config beschrieben", "diagnosis_regenconf_manually_modified_details": "Das ist wahrscheinlich OK wenn du weißt, was du tust! YunoHost wird in Zukunft diese Datei nicht mehr automatisch updaten... Aber sei bitte vorsichtig, da die zukünftigen Upgrades von YunoHost wichtige empfohlene Änderungen enthalten könnten. Wenn du möchtest, kannst du die Unterschiede mit yunohost tools regen-conf {category} --dry-run --with-diff inspizieren und mit yunohost tools regen-conf {category} --force auf das Zurücksetzen die empfohlene Konfiguration erzwingen", - "diagnosis_mail_blacklist_website": "Nachdem du herausgefunden hast, weshalb du auf die Blacklist gesetzt wurdest und dies behoben hast, zögere nicht, nachzufragen, ob deine IP-Adresse oder Ihre Domäne von auf {blacklist_website} entfernt wird", + "diagnosis_mail_blacklist_website": "Nachdem Sie herausgefunden haben, weshalb Sie auf die Blacklist gesetzt wurden und dies behoben haben, zögern Sie nicht, nachzufragen, ob Ihre IP oder Ihre Domäne von {blacklist_website} entfernt werden kann", "diagnosis_unknown_categories": "Folgende Kategorien sind unbekannt: {categories}", "diagnosis_http_hairpinning_issue": "In deinem lokalen Netzwerk scheint Hairpinning nicht aktiviert zu sein.", "diagnosis_ports_needed_by": "Diesen Port zu öffnen ist nötig, um die Funktionalität des Typs {category} (service {service}) zu gewährleisten", "diagnosis_mail_queue_too_big": "Zu viele anstehende Nachrichten in der Warteschlange ({nb_pending} emails)", - "diagnosis_package_installed_from_sury_details": "Einige Pakete wurden unbeabsichtigterweise aus einem Drittanbieter-Repository, genannt Sury, installiert. Das YunoHost-Team hat die Strategie, um diese Pakete zu handhaben, verbessert, aber es wird erwartet, dass einige Setups, welche PHP7.3-Applikationen installiert haben und immer noch auf Strech laufen, ein paar Inkonsistenzen aufweisen. Um diese Situation zu beheben, solltest du versuchen, den folgenden Befehl auszuführen: {cmd_to_fix}", + "diagnosis_package_installed_from_sury_details": "Einige Pakete wurden versehentlich von einem Drittanbieter-Repository namens Sury installiert. Das YunoHost-Team hat die Strategie für den Umgang mit diesen Paketen verbessert, aber es ist zu erwarten, dass einige Setups, die PHP7.3-Anwendungen installiert haben, während sie noch auf Stretch waren, einige verbleibende Inkonsistenzen aufweisen. Um diese Situation zu beheben, sollten Sie versuchen, den folgenden Befehl auszuführen: {cmd_to_fix}", "domain_cannot_add_xmpp_upload": "Eine hinzugefügte Domain darf nicht mit 'xmpp-upload.' beginnen. Dieser Name ist für das XMPP-Upload-Feature von YunoHost reserviert.", "group_cannot_be_deleted": "Die Gruppe {group} kann nicht manuell entfernt werden.", "group_cannot_edit_primary_group": "Die Gruppe '{group}' kann nicht manuell bearbeitet werden. Es ist die primäre Gruppe, welche dazu gedacht ist, nur ein spezifisches Konto zu enthalten.", @@ -422,9 +400,9 @@ "diagnosis_http_hairpinning_issue_details": "Das liegt wahrscheinlich an deinem Router. Dadurch können Personen von ausserhalb deines Netzwerkes, aber nicht von innerhalb deines lokalen Netzwerkes (wie wahrscheinlich du selbst), auf deinen Server zugreifen, wenn dazu die Domäne oder öffentliche IP verwendet wird. Du kannst das Problem eventuell beheben, indem du ein einen Blick auf https://yunohost.org/dns_local_network wirfst", "diagnosis_http_nginx_conf_not_up_to_date": "Die Konfiguration von Nginx scheint für diese Domäne manuell geändert worden zu sein. Dies hindert YunoHost daran festzustellen, ob es über HTTP erreichbar ist.", "diagnosis_http_bad_status_code": "Es sieht so aus als ob ein anderes Gerät (vielleicht dein Router/Modem) anstelle deines Servers antwortet.
1. Der häufigste Grund hierfür ist, dass Port 80 (und 443) nicht korrekt zu deinem Server weiterleiten.
2. Bei komplexeren Setups: prüfe ob deine Firewall oder Reverse-Proxy die Verbindung stören.", - "diagnosis_never_ran_yet": "Du hast kürzlich einen neuen YunoHost-Server installiert aber es gibt davon noch keinen Diagnosereport. Du solltest eine Diagnose anstossen. Du kannst das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwendest du dafür den Befehl 'yunohost diagnosis run'.", - "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, gebe in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein. Dieses Tool zeigt dir den Unterschied an. Wenn du damit einverstanden bist, kannst du mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", - "diagnosis_backports_in_sources_list": "Du hast anscheinend apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, du weißt, was du tust.", + "diagnosis_never_ran_yet": "Es sieht so aus, als wäre dieser Server erst kürzlich eingerichtet worden und es gibt noch keinen Diagnosebericht, der angezeigt werden könnte. Sie sollten zunächst eine vollständige Diagnose durchführen, entweder über die Web-Oberfläche oder mit \"yunohost diagnosis run\" von der Kommandozeile aus.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein, um die Unterschiede anzuzeigen. Wenn Sie damit einverstanden sind, können Sie mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", + "diagnosis_backports_in_sources_list": "Sie haben vermutlich apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie, was Sie tun.", "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}", "group_user_not_in_group": "Konto {user} ist nicht in der Gruppe {group}", "group_user_already_in_group": "Konto {user} ist bereits in der Gruppe {group}", @@ -436,22 +414,18 @@ "global_settings_setting_smtp_relay_password": "SMTP Relay Host Passwort", "global_settings_setting_smtp_relay_user": "SMTP Relay Benutzer Account", "global_settings_setting_smtp_relay_port": "SMTP Relay Port", - "global_settings_setting_smtp_allow_ipv6": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden", - "global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver", - "domain_cannot_remove_main_add_new_one": "Du kannst '{domain}' nicht entfernen, weil es die Haupt-Domäne und gleichzeitig deine einzige Domäne ist. Zuerst musst du eine andere Domäne hinzufügen, indem du 'yunohost domain add another-domain.com>' eingibst. Mache diese dann zu deiner Haupt-Domäne indem du 'yunohost domain main-domain -n ' eingibst. Nun kannst du die Domäne '{domain}' enfernen, indem du 'yunohost domain remove {domain}' eingibst.'", + "domain_cannot_remove_main_add_new_one": "Sie können '{domain}' nicht entfernen, da es die Hauptdomäne und Ihre einzige Domäne ist. Sie müssen zuerst eine andere Domäne mit 'yunohost domain add ' hinzufügen, dann als Hauptdomäne mit 'yunohost domain main-domain -n ' festlegen und dann können Sie die Domäne '{domain}' mit 'yunohost domain remove {domain}' entfernen'.'", "diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.", "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen.", - "global_settings_setting_smtp_relay_host": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn du in einer der folgenden Situationen bist: Dein ISP- oder VPS-Provider hat deinen Port 25 geblockt, eine deinen residentiellen IPs ist auf DUHL gelistet, du kannst keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und du möchtest einen anderen verwenden, um E-Mails zu versenden.", - "global_settings_setting_backup_compress_tar_archives": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen.", - "log_remove_on_failed_restore": "'{}' entfernen nach einer fehlerhaften Wiederherstellung aus einem Backup-Archiv", - "log_backup_restore_app": "'{}' aus einem Backup-Archiv wiederherstellen", - "log_backup_restore_system": "System aus einem Backup-Archiv wiederherstellen", + "log_remove_on_failed_restore": "Entfernen von '{}' nach einer fehlgeschlagenen Wiederherstellung aus einem Sicherungsarchiv", + "log_backup_restore_app": "Wiederherstellen von '{}' aus einem Sicherungsarchiv", + "log_backup_restore_system": "System aus einem Sicherungsarchiv wiederherstellen", "log_available_on_yunopaste": "Das Protokoll ist nun via {url} verfügbar", "log_app_action_run": "Führe Aktion der Applikation '{}' aus", "invalid_regex": "Ungültige Regex:'{regex}'", - "mailbox_disabled": "E-Mail für Konto {user} deaktiviert", - "log_tools_reboot": "Server neustarten", - "log_tools_shutdown": "Server ausschalten", + "mailbox_disabled": "E-Mail für Konto {user} ist deaktiviert", + "log_tools_reboot": "Starten Sie Ihren Server neu", + "log_tools_shutdown": "Ihren Server herunterfahren", "log_tools_upgrade": "Systempakete aktualisieren", "log_tools_postinstall": "Post-Installation des YunoHost-Servers durchführen", "log_tools_migrations_migrate_forward": "Migrationen durchführen", @@ -469,7 +443,7 @@ "log_permission_create": "Erstelle Berechtigung '{}'", "log_dyndns_update": "Die IP, die mit der YunoHost-Subdomain '{}' verbunden ist, aktualisieren", "log_dyndns_subscribe": "Für eine YunoHost-Subdomain registrieren '{}'", - "log_domain_remove": "Entfernen der Domäne '{}' aus der Systemkonfiguration", + "log_domain_remove": "Domäne '{}' aus der Systemkonfiguration entfernen", "log_domain_add": "Hinzufügen der Domäne '{}' zur Systemkonfiguration", "log_remove_on_failed_install": "Entfernen von '{}' nach einer fehlgeschlagenen Installation", "domain_remove_confirm_apps_removal": "Wenn du diese Domäne löschst, werden folgende Applikationen entfernt:\n{apps}\n\nBist du sicher? [{answers}]", @@ -536,7 +510,7 @@ "restore_extracting": "Packe die benötigten Dateien aus dem Archiv aus...", "restore_already_installed_apps": "Folgende Apps können nicht wiederhergestellt werden, weil sie schon installiert sind: {apps}", "regex_with_only_domain": "Du kannst regex nicht als Domain verwenden, sondern nur als Pfad", - "root_password_desynchronized": "Das Admin-Passwort wurde verändert, aber das Root-Passwort ist immer noch das alte!", + "root_password_desynchronized": "Das Admin-Passwort wurde geändert, aber YunoHost konnte dies nicht auf das Root-Passwort übertragen!", "regenconf_need_to_explicitly_specify_ssh": "Die SSH-Konfiguration wurde manuell modifiziert, aber du musst explizit die Kategorie 'SSH' mit --force spezifizieren, um die Änderungen tatsächlich anzuwenden.", "log_backup_create": "Erstelle ein Backup-Archiv", "diagnosis_sshd_config_inconsistent": "Es sieht aus, als ob der SSH-Port manuell geändert wurde in /etc/ssh/ssh_config. Seit YunoHost 4.2 ist eine neue globale Einstellung 'security.ssh.port' verfügbar um zu verhindern, dass die Konfiguration manuell verändert wird.", @@ -544,15 +518,14 @@ "backup_create_size_estimation": "Das Archiv wird etwa {size} an Daten enthalten.", "app_restore_script_failed": "Im Wiederherstellungsskript der Applikation ist ein Fehler aufgetreten", "app_restore_failed": "Konnte {app} nicht wiederherstellen: {error}", - "migration_ldap_rollback_success": "System-Rollback erfolgreich.", + "migration_ldap_rollback_success": "Das System wurde zurückgesetzt.", "migration_ldap_migration_failed_trying_to_rollback": "Migrieren war nicht möglich... Versuch, ein Rollback des Systems durchzuführen.", "migration_ldap_backup_before_migration": "Vor der eigentlichen Migration ein Backup der LDAP-Datenbank und der Applikations-Einstellungen erstellen.", "global_settings_setting_ssowat_panel_overlay_enabled": "Das SSOwat-Overlay-Panel aktivieren", - "global_settings_setting_security_ssh_port": "SSH-Port", "diagnosis_sshd_config_inconsistent_details": "Bitte führe yunohost settings set security.ssh.port -v YOUR_SSH_PORT aus, um den SSH-Port festzulegen, und prüfe yunohost tools regen-conf ssh --dry-run --with-diff und yunohost tools regen-conf ssh --force um deine Konfiguration auf die YunoHost-Empfehlung zurückzusetzen.", "regex_incompatible_with_tile": "/!\\ Packagers! Für Berechtigung '{permission}' ist show_tile auf 'true' gesetzt und deshalb kannst du keine regex-URL als Hauptdomäne setzen", - "permission_cant_add_to_all_users": "Die Berechtigung {permission} konnte nicht allen Konten gegeben werden.", - "migration_ldap_can_not_backup_before_migration": "Das System-Backup konnte nicht abgeschlossen werden, bevor die Migration fehlschlug. Fehler: {error}", + "permission_cant_add_to_all_users": "Die Berechtigung {permission} kann nicht für allen Konten hinzugefügt werden.", + "migration_ldap_can_not_backup_before_migration": "Die Sicherung des Systems konnte nicht abgeschlossen werden, bevor die Migration fehlschlug. Fehler: {error}", "service_description_fail2ban": "Schützt gegen Brute-Force-Angriffe und andere Angriffe aus dem Internet", "service_description_dovecot": "Ermöglicht es E-Mail-Clients auf Konten zuzugreifen (IMAP und POP3)", "service_description_dnsmasq": "Verarbeitet die Auflösung des Domainnamens (DNS)", @@ -567,21 +540,18 @@ "service_description_yunohost-firewall": "Verwaltet offene und geschlossene Ports zur Verbindung mit Diensten", "service_description_yunohost-api": "Verwaltet die Interaktionen zwischen der Weboberfläche von YunoHost und dem System", "service_description_ssh": "Ermöglicht die Verbindung zu deinem Server über ein Terminal (SSH-Protokoll)", - "server_reboot_confirm": "Der Server wird sofort heruntergefahren, bist du sicher? [{answers}]", + "server_reboot_confirm": "Der Server wird sofort neu gestartet. Sind Sie sicher? [{answers}]", "server_reboot": "Der Server wird neu gestartet", - "server_shutdown_confirm": "Der Server wird sofort heruntergefahren, bist du sicher? [{answers}]", + "server_shutdown_confirm": "Der Server wird sofort heruntergefahren, sind Sie sicher? [{answers}]", "server_shutdown": "Der Server wird heruntergefahren", - "root_password_replaced_by_admin_password": "Dein Root Passwort wurde durch dein Admin Passwort ersetzt.", "show_tile_cant_be_enabled_for_regex": "Du kannst 'show_tile' momentan nicht aktivieren, weil die URL für die Berechtigung '{permission}' ein regulärer Ausdruck ist", "show_tile_cant_be_enabled_for_url_not_defined": "Momentan kannst du 'show_tile' nicht aktivieren, weil du zuerst eine URL für die Berechtigung '{permission}' definieren musst", "this_action_broke_dpkg": "Diese Aktion hat unkonfigurierte Pakete verursacht, welche durch dpkg/apt (die Paketverwaltungen dieses Systems) zurückgelassen wurden... Du kannst versuchen dieses Problem zu lösen, indem du 'sudo apt install --fix-broken' und/oder 'sudo dpkg --configure -a' ausführst.", "update_apt_cache_failed": "Kann den Cache von APT (Debians Paketmanager) nicht aktualisieren. Hier ist ein Auszug aus den sources.list-Zeilen, die helfen könnten, das Problem zu identifizieren:\n{sourceslist}", - "unknown_main_domain_path": "Unbekannte:r Domain oder Pfad für '{app}'. Du musst eine Domain und einen Pfad setzen, um die URL für Berechtigungen zu setzen.", + "unknown_main_domain_path": "Unbekannte Domäne oder Pfad für '{app}'. Sie müssen eine Domäne und einen Pfad angeben, um eine URL für die Genehmigung angeben zu können.", "yunohost_postinstall_end_tip": "Post-install ist fertig! Um das Setup abzuschliessen, wird empfohlen:\n - ein erstes Konto über den Bereich 'Konto' im Adminbereich hinzuzufügen (oder mit 'yunohost user create ' in der Kommandezeile);\n - mögliche Fehler zu diagnostizieren über den Bereich 'Diagnose' im Adminbereich (oder mit 'yunohost diagnosis run' in der Kommandozeile;\n - Die Abschnitte 'Install YunoHost' und 'Geführte Tour' im Administratorenhandbuch zu lesen: https://yunohost.org/admindoc.", - "user_already_exists": "Konto '{user}' ist bereits vorhanden", + "user_already_exists": "Das Konto '{user}' ist bereits vorhanden", "update_apt_cache_warning": "Beim Versuch den Cache für APT (Debians Paketmanager) zu aktualisieren, ist etwas schief gelaufen. Hier ist ein Dump der Zeilen aus sources.list, die Ihnen vielleicht dabei helfen, das Problem zu identifizieren:\n{sourceslist}", - "global_settings_setting_security_webadmin_allowlist": "IP-Adressen, die auf die Verwaltungsseite zugreifen dürfen. Kommasepariert.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Erlaube nur bestimmten IP-Adressen den Zugriff auf die Verwaltungsseite.", "disk_space_not_sufficient_update": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu aktualisieren", "disk_space_not_sufficient_install": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu installieren", "danger": "Warnung:", @@ -598,11 +568,11 @@ "config_version_not_supported": "Konfigurationspanel Versionen '{version}' sind nicht unterstützt.", "diagnosis_apps_allgood": "Alle installierten Apps berücksichtigen die grundlegenden Paketierungspraktiken", "diagnosis_apps_broken": "Diese App ist im YunoHost-Applikationskatalog momentan als defekt gekennzeichnet. Es könnte sich dabei um einen vorübergehendes Problem handeln. Während der/die Betreuer:in versucht das Problem zu beheben, ist die Upgrade-Funktion für diese App gesperrt.", - "diagnosis_apps_not_in_app_catalog": "Diese App fehlt im Applikationskatalog von YunoHost oder wird in diesem nicht mehr angezeigt. Du solltest in Betracht ziehen, sie zu deinstallieren, weil sie keine Aktualisierungen mehr erhält und die Integrität und die Sicherheit deines Systems kompromittieren könnte.", - "diagnosis_apps_outdated_ynh_requirement": "Die installierte Version dieser App erfordert nur YunoHost >=2.x, was darauf hinweist, dass die App nicht nach aktuell empfohlenen Paketierungspraktiken und mit aktuellen Helpern erstellt worden ist. Du solltest wirklich in Betracht ziehen, sie zu aktualisieren.", + "diagnosis_apps_not_in_app_catalog": "Diese Applikation steht nicht im Applikationskatalog von YunoHost. Sie sollten in Betracht ziehen, sie zu deinstallieren, weil sie keine Aktualisierungen mehr erhält und die Integrität und die Sicherheit Ihres Systems kompromittieren könnte.", + "diagnosis_apps_outdated_ynh_requirement": "Die installierte Version dieser Applikation erfordert nur YunoHost >=2.x oder 3.x, was darauf hinweisen könnte, dass die Applikation nicht nach aktuell empfohlenen Paketierungspraktiken und mit aktuellen Helpern erstellt worden ist. Sie sollten wirklich in Betracht ziehen, sie zu aktualisieren.", "diagnosis_description_apps": "Applikationen", "config_cant_set_value_on_section": "Du kannst einen einzelnen Wert nicht auf einen gesamten Konfigurationsbereich anwenden.", - "diagnosis_apps_deprecated_practices": "Die installierte Version dieser App verwendet immer noch gewisse veraltete Paketierungspraktiken. Du solltest die App wirklich aktualisieren.", + "diagnosis_apps_deprecated_practices": "Die installierte Version dieser Applikation verwendet gewisse veraltete Paketierungspraktiken. Sie sollten sie wirklich aktualisieren.", "app_config_unable_to_apply": "Konnte die Werte des Konfigurations-Panels nicht anwenden.", "app_config_unable_to_read": "Konnte die Werte des Konfigurations-Panels nicht auslesen.", "config_unknown_filter_key": "Der Filterschlüssel '{filter_key}' ist inkorrekt.", @@ -618,8 +588,6 @@ "domain_unknown": "Domäne '{domain}' unbekannt", "ldap_server_is_down_restart_it": "Der LDAP-Dienst ist nicht erreichbar, versuche ihn neu zu starten...", "user_import_bad_file": "Deine CSV-Datei ist nicht korrekt formatiert und wird daher ignoriert, um einen möglichen Datenverlust zu vermeiden", - "global_settings_setting_security_experimental_enabled": "Aktiviere experimentelle Sicherheitsfunktionen (nur aktivieren, wenn Du weißt was Du tust!)", - "global_settings_setting_security_nginx_redirect_to_https": "HTTP-Anfragen standardmäßig auf HTTPs umleiten (NICHT AUSSCHALTEN, sofern Du nicht weißt was Du tust!)", "user_import_missing_columns": "Die folgenden Spalten fehlen: {columns}", "user_import_nothing_to_do": "Es muss kein Konto importiert werden", "user_import_partial_failed": "Der Import von Konten ist teilweise fehlgeschlagen", @@ -633,7 +601,7 @@ "user_import_failed": "Der Import von Konten ist komplett fehlgeschlagen", "domain_dns_push_failed_to_list": "Auflistung der aktuellen Einträge über die API des Registrars fehlgeschlagen: {error}", "domain_dns_pushing": "DNS-Einträge übertragen…", - "domain_dns_push_record_failed": "{action} für Eintrag {type}/{name} fehlgeschlagen: {error}", + "domain_dns_push_record_failed": "Fehler bei {action} Eintrag {type}/{name} : {error}", "domain_dns_push_success": "DNS-Einträge aktualisiert!", "domain_dns_push_failed": "Die Aktualisierung der DNS-Einträge ist leider gescheitert.", "domain_dns_push_partial_failure": "DNS-Einträge teilweise aktualisiert: einige Warnungen/Fehler wurden gemeldet.", @@ -648,10 +616,9 @@ "domain_config_auth_entrypoint": "API-Einstiegspunkt", "domain_config_auth_application_key": "Anwendungsschlüssel", "domain_config_auth_application_secret": "Geheimer Anwendungsschlüssel", - "domain_config_auth_consumer_key": "Consumer-Schlüssel", + "domain_config_auth_consumer_key": "Verbraucherschlüssel", "invalid_number_min": "Muss größer sein als {min}", "invalid_number_max": "Muss kleiner sein als {max}", - "invalid_password": "Ungültiges Passwort", "ldap_attribute_already_exists": "LDAP-Attribut '{attribute}' existiert bereits mit dem Wert '{value}'", "user_import_success": "Konten erfolgreich importiert", "domain_registrar_is_not_configured": "Der DNS-Registrar ist noch nicht für die Domäne '{domain}' konfiguriert.", @@ -666,23 +633,67 @@ "migration_0021_main_upgrade": "Starte Hauptupdate...", "migration_0021_still_on_buster_after_main_upgrade": "Irgendetwas ist während des Haupt-Upgrades schief gelaufen, das System scheint immer noch auf Debian Buster zu laufen", "migration_0021_yunohost_upgrade": "Start des YunoHost Kern-Upgrades...", - "migration_0021_not_buster": "Die aktuelle Debian-Distribution ist nicht Buster!", "migration_0021_not_enough_free_space": "Der freie Speicherplatz in /var/ ist ziemlich gering! Du solltest mindestens 1 GB frei haben, um diese Migration durchzuführen.", "migration_0021_system_not_fully_up_to_date": "Dein System ist nicht ganz aktuell. Bitte führe ein reguläres Upgrade durch, bevor du die Migration zu Bullseye durchführst.", "migration_0021_problematic_apps_warning": "Bitte beachte, dass die folgenden, möglicherweise problematischen installierten Anwendungen erkannt wurden. Es sieht so aus, als ob diese nicht aus dem YunoHost-Applikations-Katalog installiert wurden oder nicht als \"funktionierend\" gekennzeichnet sind. Es kann daher nicht garantiert werden, dass sie nach dem Upgrade noch funktionieren werden: {problematic_apps}", "migration_0021_modified_files": "Bitte beachte, dass die folgenden Dateien manuell geändert wurden und nach dem Update möglicherweise überschrieben werden: {manually_modified_files}", "migration_0021_cleaning_up": "Bereinigung von Cache und Paketen nicht mehr nötig...", "migration_0021_patch_yunohost_conflicts": "Patch anwenden, um das Konfliktproblem zu umgehen...", - "global_settings_setting_security_ssh_password_authentication": "Passwort-Authentifizierung für SSH zulassen", "migration_description_0021_migrate_to_bullseye": "Upgrade des Systems auf Debian Bullseye und YunoHost 11.x", "migration_0021_general_warning": "Bitte beachte, dass diese Migration ein heikler Vorgang ist. Das YunoHost-Team hat sein Bestes getan, um sie zu überprüfen und zu testen, aber die Migration könnte immer noch Teile des Systems oder seiner Anwendungen beschädigen.\n\nEs wird daher empfohlen,:\n - Führe eine Sicherung aller kritischen Daten oder Applikationen durch. Mehr Informationen unter https://yunohost.org/backup;\n - Habe Geduld, nachdem du die Migration gestartet hast: Je nach Internetverbindung und Hardware kann es bis zu ein paar Stunden dauern, bis alles aktualisiert ist.", - "tools_upgrade": "Upgrade Systempakete", + "tools_upgrade": "Aktualisieren von Systempaketen", "tools_upgrade_failed": "Pakete konnten nicht aktualisiert werden: {packages_list}", "domain_config_default_app": "Standard-Applikation", - "migration_0023_postgresql_11_not_installed": "PostgreSQL war auf deinem System nicht installiert. Es gibt nichts zu tun.", - "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 ist installiert, PostgreSQL 13 allerdings nicht? Mit deinem System scheint etwas seltsam zu sein :(...", - "migration_description_0022_php73_to_php74_pools": "Migriere php7.3-fpm 'pool' conf Dateien auf php7.4", - "migration_description_0023_postgresql_11_to_13": "Migriere Datenbanken von PostgreSQL 11 auf 13", + "migration_0023_postgresql_11_not_installed": "PostgreSQL wurde nicht auf Ihrem System installiert. Es ist nichts zu tun.", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 ist installiert, aber nicht PostgreSQL 13!? Irgendetwas Seltsames könnte auf Ihrem System passiert sein. :( ...", + "migration_description_0022_php73_to_php74_pools": "Migriere php7.3-fpm 'pool' Konfiguration nach php7.4", + "migration_description_0023_postgresql_11_to_13": "Migrieren von Datenbanken von PostgreSQL 11 nach 13", "service_description_postgresql": "Speichert Applikations-Daten (SQL Datenbank)", - "migration_0023_not_enough_space": "Stelle sicher, dass unter {path} genug Speicherplatz zur Verfügung steht, um die Migration auszuführen." + "migration_0023_not_enough_space": "Stelle sicher, dass unter {path} genug Speicherplatz zur Verfügung steht, um die Migration auszuführen.", + "global_settings_setting_backup_compress_tar_archives_help": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen.", + "global_settings_setting_security_experimental_enabled_help": "Aktiviere experimentelle Sicherheitsfunktionen (nur aktivieren, wenn Du weißt was Du tust!)", + "global_settings_setting_nginx_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Webserver NGINX. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", + "global_settings_setting_nginx_redirect_to_https_help": "HTTP-Anfragen standardmäßig auf HTTPs umleiten (NICHT AUSSCHALTEN, sofern Du nicht weißt was Du tust!)", + "global_settings_setting_admin_strength": "Stärke des Admin-Passworts", + "global_settings_setting_user_strength": "Stärke des Anmeldepassworts", + "global_settings_setting_postfix_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Postfix-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", + "global_settings_setting_ssh_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den SSH-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", + "global_settings_setting_ssh_password_authentication_help": "Passwort-Authentifizierung für SSH zulassen", + "global_settings_setting_ssh_port": "SSH-Port", + "global_settings_setting_webadmin_allowlist_help": "IP-Adressen, die auf die Verwaltungsseite zugreifen dürfen. Kommasepariert.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Erlaube nur bestimmten IP-Adressen den Zugriff auf die Verwaltungsseite.", + "global_settings_setting_smtp_allow_ipv6_help": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden", + "global_settings_setting_smtp_relay_enabled_help": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn du in einer der folgenden Situationen bist: Dein ISP- oder VPS-Provider hat deinen Port 25 geblockt, eine deinen residentiellen IPs ist auf DUHL gelistet, du kannst keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und du möchtest einen anderen verwenden, um E-Mails zu versenden.", + "admins": "Administratoren", + "all_users": "Alle YunoHost-Nutzer", + "app_action_failed": "Fehlgeschlagene Aktion {action} für Applikation {app}", + "app_manifest_install_ask_init_admin_permission": "Wer soll Zugriff auf die administrativen Funktionen für diese App erhalten? (Dies kann später wieder geändert werden)", + "app_manifest_install_ask_init_main_permission": "Wer soll Zugriff auf diese App erhalten? (Dies kann später wieder geändert werden)", + "ask_admin_fullname": "Vollständiger Name des Administrators", + "ask_admin_username": "Benutzername des Administrators", + "ask_fullname": "Vollständiger Name (Vorname und Nachname)", + "certmanager_cert_install_failed": "Installation des Let's Encrypt-Zertifikat fehlgeschlagen für {domains}", + "certmanager_cert_install_failed_selfsigned": "Installation des selbst-signierten Zertifikats fehlgeschlagen für {domains}", + "certmanager_cert_renew_failed": "Erneuern des Let's Encrypt-Zertifikat fehlgeschlagen für {domains}", + "config_action_disabled": "Konnte die Aktion '{action}' nicht durchführen, weil sie deaktiviert ist. Stellen Sie sicher, dass sie ihre Einschränkungen einhält. Hilfe: {help}", + "config_action_failed": "Ausführung der Aktion '{action}' fehlgeschlagen: {error}", + "config_forbidden_readonly_type": "Der Typ '{type}' kann nicht auf Nur-Lesen eingestellt werden. Verwenden Sie bitte einen anderen Typ, um diesen Wert zu generieren (relevante ID des Arguments: '{id}').", + "diagnosis_using_stable_codename": "apt (Paketmanager des Systems) ist gegenwärtig konfiguriert um die Pakete des Code-Namens 'stable' zu installieren, anstelle die des Code-Namen der aktuellen Debian-Version (bullseye).", + "domain_config_acme_eligible": "Geeignet für ACME", + "diagnosis_using_stable_codename_details": "Dies wird meistens durch eine fehlerhafte Konfiguration seitens des Hosting-Providers verursacht. Dies stellt eine Gefahr dar, weil sobald die nächste Debian-Version zum neuen 'stable' wird, wird apt alle System-Pakete aktualisieren wollen, ohne eine ordnungsgemässe Migration zu durchlaufen. Es wird sehr empfohlen dies zu berichtigen, indem Sie die Datei der apt-Quellen des Debian-Basis-Repositorys entsprechend anpassen indem Sie das stable-Keyword durch bullseye ersetzen. Die zugehörige Konfigurationsdatei sollte /etc/apt/sources.list oder eine Datei im Verzeichnis /etc/apt/sources.list.d/sein.", + "diagnosis_using_yunohost_testing": "apt (der Paketmanager des Systems) ist aktuell so konfiguriert, dass die 'testing'-Upgrades für YunoHost core installiert werden.", + "diagnosis_using_yunohost_testing_details": "Dies ist wahrscheinlich OK, wenn Sie wissen, was Sie tun. Aber beachten Sie bitte die Release-Notes bevor sie zukünftige YunoHost-Upgrades installieren! Wenn Sie die 'testing'-Upgrades deaktivieren möchten, sollten sie das testing-Schlüsselwort aus /etc/apt/sources.list.d/yunohost.list entfernen.", + "global_settings_setting_security_experimental_enabled": "Experimentelle Sicherheitsfunktionen", + "domain_config_acme_eligible_explain": "Es scheint, als ob diese Domäne nicht bereit ist für ein Let's Encrypt-Zertifikat. Bitte überprüfen Sie Ihre DNS-Konfiguration und ob Ihr Server über HTTP erreichbar ist . Die Abschnitte 'DNS-Einträge' und 'Web' auf der Diagnose-Seite können Ihnen dabei helfen, zu verstehen, was falsch konfiguriert ist.", + "domain_config_cert_install": "Installation des Let's Encrypt-Zertifikats", + "domain_config_cert_issuer": "Zertifizierungsstelle", + "domain_config_cert_no_checks": "Tests und andere Diagnose-Überprüfungen ignorieren", + "domain_config_cert_renew": "Erneuern des Let's Encrypt-Zertifikats", + "domain_config_cert_renew_help": "Das Zertifikat wird automatisch während den letzten 15 Tagen seiner Gültigkeit erneuert. Sie können es manuell erneuern, wenn Sie möchten. (nicht empfohlen).", + "domain_config_cert_summary": "Zertifikats-Status", + "visitors": "Besucher", + "domain_config_cert_summary_abouttoexpire": "Das aktuelle Zertifikat läuft bald ab. Es sollte bald automatisch erneuert werden.", + "domain_config_cert_summary_expired": "ACHTUNG: Das aktuelle Zertifikat ist nicht gültig! HTTPS wird gar nicht funktionieren!", + "domain_config_cert_summary_letsencrypt": "Toll! Sie benutzen ein gültiges Let's Encrypt-Zertifikat!", + "domain_config_cert_summary_ok": "Gut, das aktuelle Zertifikat sieht gut aus!" } diff --git a/locales/en.json b/locales/en.json index 432462708..21ffdfdc2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -4,18 +4,19 @@ "additional_urls_already_added": "Additionnal URL '{url}' already added in the additional URL for permission '{permission}'", "additional_urls_already_removed": "Additionnal URL '{url}' already removed in the additional URL for permission '{permission}'", "admin_password": "Administration password", - "admin_password_change_failed": "Unable to change password", - "admin_password_changed": "The administration password was changed", - "admin_password_too_long": "Please choose a password shorter than 127 characters", + "admins": "Admins", + "all_users": "All YunoHost users", "already_up_to_date": "Nothing to do. Everything is already up-to-date.", "app_action_broke_system": "This action seems to have broken these important services: {services}", "app_action_cannot_be_ran_because_required_services_down": "These required services should be running to run this action: {services}. Try restarting them to continue (and possibly investigate why they are down).", + "app_action_failed": "Failed to run action {action} for app {app}", "app_already_installed": "{app} is already installed", "app_already_installed_cant_change_url": "This app is already installed. The URL cannot be changed just by this function. Check in `app changeurl` if it's available.", "app_already_up_to_date": "{app} is already up-to-date", + "app_arch_not_supported": "This app can only be installed on architectures {', '.join(required)} but your server architecture is {current}", "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.", @@ -26,6 +27,7 @@ "app_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.", "app_id_invalid": "Invalid app ID", "app_install_failed": "Unable to install {app}: {error}", + "app_resource_failed": "Provisioning, deprovisioning, or updating resources for {app} failed: {error}", "app_install_files_invalid": "These files cannot be installed", "app_install_script_failed": "An error occurred inside the app installation script", "app_label_deprecated": "This command is deprecated! Please use the new command 'yunohost user permission update' to manage the app label.", @@ -33,21 +35,24 @@ "app_make_default_location_already_used": "Unable to make '{app}' the default app on the domain, '{domain}' is already in use by '{other_app}'", "app_manifest_install_ask_admin": "Choose an administrator user for this app", "app_manifest_install_ask_domain": "Choose the domain where this app should be installed", + "app_manifest_install_ask_init_admin_permission": "Who should have access to admin features for this app? (This can later be changed)", + "app_manifest_install_ask_init_main_permission": "Who should have access to this app? (This can later be changed)", "app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?", "app_manifest_install_ask_password": "Choose an administration password for this app", "app_manifest_install_ask_path": "Choose the URL path (after the domain) where this app should be installed", "app_not_correctly_installed": "{app} seems to be incorrectly installed", + "app_not_enough_disk": "This app requires {required} free space.", + "app_not_enough_ram": "This app requires {required} RAM to install/upgrade but only {current} is available right now.", "app_not_installed": "Could not find {app} in the list of installed apps: {all_apps}", "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 required packages for {app}...", - "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", + "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}...", @@ -60,14 +65,16 @@ "app_upgrade_several_apps": "The following apps will be upgraded: {apps}", "app_upgrade_some_app_failed": "Some apps could not be upgraded", "app_upgraded": "{app} upgraded", + "app_yunohost_version_not_supported": "This app requires YunoHost >= {required} but current installed version is {current}", "apps_already_up_to_date": "All apps are already up-to-date", "apps_catalog_failed_to_download": "Unable to download the {apps_catalog} app catalog: {error}", "apps_catalog_init_success": "App catalog system initialized!", "apps_catalog_obsolete_cache": "The app catalog cache is empty or obsolete.", "apps_catalog_update_success": "The application catalog has been updated!", "apps_catalog_updating": "Updating application catalog...", - "ask_firstname": "First name", - "ask_lastname": "Last name", + "ask_admin_fullname": "Admin full name", + "ask_admin_username": "Admin username", + "ask_fullname": "Full name", "ask_main_domain": "Main domain", "ask_new_admin_password": "New administration password", "ask_new_domain": "New domain", @@ -83,7 +90,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}'", @@ -121,28 +128,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_disabled": "Could not run action '{action}' since it is disabled, make sure to meet its constraints. help: {help}", + "config_action_failed": "Failed to run action '{action}': {error}", "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", @@ -153,17 +166,19 @@ "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}] ", + "confirm_app_insufficient_ram": "DANGER! This app requires {required} RAM to install/upgrade but only {current} is available right now. Even if this app could run, its installation/upgrade process requires a large amount of RAM so your server may freeze and fail miserably. If you are willing to take that risk anyway, type '{answers}'", + "confirm_notifications_read": "WARNING: You should check the app notifications above before continuing, there might be important stuff to know. [{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_outdated_ynh_requirement": "This app's installed version only requires yunohost >= 2.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_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 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}", @@ -191,7 +206,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 domain dns push DOMAIN --force.", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", @@ -215,14 +230,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!", @@ -238,26 +253,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 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_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 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)", @@ -271,38 +286,45 @@ "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}!", "diagnosis_services_running": "Service {service} is running!", - "diagnosis_sshd_config_inconsistent": "It looks like the SSH port was manually modified in /etc/ssh/sshd_config. Since YunoHost 4.2, a new global setting 'security.ssh.port' is available to avoid manually editing the configuration.", - "diagnosis_sshd_config_inconsistent_details": "Please run yunohost settings set security.ssh.port -v YOUR_SSH_PORT to define the SSH port, and check yunohost tools regen-conf ssh --dry-run --with-diff and yunohost tools regen-conf ssh --force to reset your conf to the YunoHost recommendation.", + "diagnosis_sshd_config_inconsistent": "It looks like the SSH port was manually modified in /etc/ssh/sshd_config. Since YunoHost 4.2, a new global setting 'security.ssh.ssh_port' is available to avoid manually editing the configuration.", + "diagnosis_sshd_config_inconsistent_details": "Please run yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT to define the SSH port, and check yunohost tools regen-conf ssh --dry-run --with-diff and yunohost tools regen-conf ssh --force to reset your conf to the YunoHost recommendation.", "diagnosis_sshd_config_insecure": "The SSH configuration appears to have been manually modified, and is insecure because it contains no 'AllowGroups' or 'AllowUsers' directive to limit access to authorized users.", "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}", + "diagnosis_using_stable_codename": "apt (the system's package manager) is currently configured to install packages from codename 'stable', instead of the codename of the current Debian version (bullseye).", + "diagnosis_using_stable_codename_details": "This is usually caused by incorrect configuration from your hosting provider. This is dangerous, because as soon as the next Debian version becomes the new 'stable', apt will want to upgrade all system packages without going through a proper migration procedure. It is recommended to fix this by editing the apt source for base Debian repository, and replace the stable keyword by bullseye. The corresponding configuration file should be /etc/apt/sources.list, or a file in /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing": "apt (the system's package manager) is currently configured to install any 'testing' upgrade for YunoHost core.", + "diagnosis_using_yunohost_testing_details": "This is probably OK if you know what you are doing, but pay attention to the release notes before installing YunoHost upgrades! If you want to disable 'testing' upgrades, you should remove the testing keyword from /etc/apt/sources.list.d/yunohost.list.", "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_add_muc_upload": "You cannot add domains starting with 'muc.'. This kind of name is reserved for the XMPP multi-users chat 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", + "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_api_protocol": "API protocol", "domain_config_auth_application_key": "Application key", "domain_config_auth_application_secret": "Application secret key", @@ -311,10 +333,21 @@ "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", + "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_abouttoexpire": "Current certificate is about to expire. It should soon be renewed automatically.", + "domain_config_cert_summary_expired": "CRITICAL: Current certificate is not valid! HTTPS won't work at all!", + "domain_config_cert_summary_letsencrypt": "Great! You're using a valid Let's Encrypt certificate!", + "domain_config_cert_summary_ok": "Okay, current certificate looks good!", + "domain_config_cert_summary_selfsigned": "WARNING: Current certificate is self-signed. Browsers will display a spooky warning to new visitors!", + "domain_config_cert_validity": "Validity", "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)", @@ -353,7 +386,7 @@ "domains_available": "Available domains:", "done": "Done", "downloading": "Downloading...", - "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.", + "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a` and/or `sudo dpkg --audit`.", "dpkg_lock_not_available": "This command can't be run right now because another program seems to be using the lock of dpkg (the system package manager)", "dyndns_could_not_check_available": "Could not check if {domain} is available on {provider}.", "dyndns_domain_not_provided": "DynDNS provider {provider} cannot provide domain {domain}.", @@ -372,42 +405,53 @@ "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...", "field_invalid": "Invalid field '{}'", "file_does_not_exist": "The file {path} does not exist.", "firewall_reload_failed": "Could not reload the firewall", "firewall_reloaded": "Firewall reloaded", "firewall_rules_cmd_failed": "Some firewall rule commands have failed. More info in log.", - "global_settings_bad_choice_for_enum": "Bad choice for setting {setting}, received '{choice}', but available choices are: {available_choices}", - "global_settings_bad_type_for_setting": "Bad type for setting {setting}, received {received_type}, expected {expected_type}", - "global_settings_cant_open_settings": "Could not open settings file, reason: {reason}", - "global_settings_cant_serialize_settings": "Could not serialize settings data, reason: {reason}", - "global_settings_cant_write_settings": "Could not save settings file, reason: {reason}", - "global_settings_key_doesnt_exists": "The key '{settings_key}' does not exist in the global settings, you can see all the available keys by running 'yunohost settings list'", - "global_settings_reset_success": "Previous settings now backed up to {path}", - "global_settings_setting_backup_compress_tar_archives": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", - "global_settings_setting_pop3_enabled": "Enable the POP3 protocol for the mail server", - "global_settings_setting_security_experimental_enabled": "Enable experimental security features (don't enable this if you don't know what you're doing!)", - "global_settings_setting_security_nginx_compatibility": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", - "global_settings_setting_security_nginx_redirect_to_https": "Redirect HTTP requests to HTTPs by default (DO NOT TURN OFF unless you really know what you're doing!)", - "global_settings_setting_security_password_admin_strength": "Admin password strength", - "global_settings_setting_security_password_user_strength": "User password strength", - "global_settings_setting_security_postfix_compatibility": "Compatibility vs. security tradeoff for the Postfix server. Affects the ciphers (and other security-related aspects)", - "global_settings_setting_security_ssh_compatibility": "Compatibility vs. security tradeoff for the SSH server. Affects the ciphers (and other security-related aspects)", - "global_settings_setting_security_ssh_password_authentication": "Allow password authentication for SSH", - "global_settings_setting_security_ssh_port": "SSH port", - "global_settings_setting_security_webadmin_allowlist": "IP adresses allowed to access the webadmin. Comma-separated.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Allow only some IPs to access the webadmin.", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Allow the use of (deprecated) DSA hostkey for the SSH daemon configuration", - "global_settings_setting_smtp_allow_ipv6": "Allow the use of IPv6 to receive and send mail", - "global_settings_setting_smtp_relay_host": "SMTP relay host to use in order to send mail instead of this yunohost instance. Useful if you are in one of this situation: your 25 port is blocked by your ISP or VPS provider, you have a residential IP listed on DUHL, you are not able to configure reverse DNS or this server is not directly exposed on the internet and you want use an other one to send mails.", - "global_settings_setting_smtp_relay_password": "SMTP relay host password", + "global_settings_reset_success": "Reset global settings", + "global_settings_setting_passwordless_sudo": "Allow admins to use 'sudo' without re-typing their passwords", + "global_settings_setting_admin_strength": "Admin password strength requirements", + "global_settings_setting_admin_strength_help": "These requirements are only enforced when initializing or changing the password", + "global_settings_setting_backup_compress_tar_archives": "Compress backups", + "global_settings_setting_backup_compress_tar_archives_help": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", + "global_settings_setting_nginx_compatibility": "NGINX Compatibility", + "global_settings_setting_nginx_compatibility_help": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", + "global_settings_setting_nginx_redirect_to_https": "Force HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "Redirect HTTP requests to HTTPs by default (DO NOT TURN OFF unless you really know what you're doing!)", + "global_settings_setting_pop3_enabled": "Enable POP3", + "global_settings_setting_pop3_enabled_help": "Enable the POP3 protocol for the mail server", + "global_settings_setting_postfix_compatibility": "Postfix Compatibility", + "global_settings_setting_postfix_compatibility_help": "Compatibility vs. security tradeoff for the Postfix server. Affects the ciphers (and other security-related aspects)", + "global_settings_setting_root_access_explain": "On Linux systems, 'root' is the absolute admin. In YunoHost context, direct 'root' SSH login is by default disable - except from the local network of the server. Members of the 'admins' group can use the sudo command to act as root from the command line. However, it can be helpful to have a (robust) root password to debug the system if for some reason regular admins can not login anymore.", + "global_settings_setting_root_password": "New root password", + "global_settings_setting_root_password_confirm": "New root password (confirm)", + "global_settings_setting_security_experimental_enabled": "Experimental security features", + "global_settings_setting_security_experimental_enabled_help": "Enable experimental security features (don't enable this if you don't know what you're doing!)", + "global_settings_setting_smtp_allow_ipv6": "Allow IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Allow the use of IPv6 to receive and send mail", + "global_settings_setting_smtp_relay_enabled": "Enable SMTP relay", + "global_settings_setting_smtp_relay_enabled_help": "Enable the SMTP relay to use in order to send mail instead of this yunohost instance. Useful if you are in one of this situation: your 25 port is blocked by your ISP or VPS provider, you have a residential IP listed on DUHL, you are not able to configure reverse DNS or this server is not directly exposed on the internet and you want use an other one to send mails.", + "global_settings_setting_smtp_relay_host": "SMTP relay host", + "global_settings_setting_smtp_relay_password": "SMTP relay password", "global_settings_setting_smtp_relay_port": "SMTP relay port", - "global_settings_setting_smtp_relay_user": "SMTP relay user account", - "global_settings_setting_ssowat_panel_overlay_enabled": "Enable SSOwat panel overlay", - "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key}', discard it and save it in /etc/yunohost/settings-unknown.json", - "global_settings_unknown_type": "Unexpected situation, the setting {setting} appears to have the type {unknown_type} but it is not a type supported by the system.", + "global_settings_setting_smtp_relay_user": "SMTP relay user", + "global_settings_setting_ssh_compatibility": "SSH Compatibility", + "global_settings_setting_ssh_compatibility_help": "Compatibility vs. security tradeoff for the SSH server. Affects the ciphers (and other security-related aspects). See https://infosec.mozilla.org/guidelines/openssh for more info.", + "global_settings_setting_ssh_password_authentication": "Password authentication", + "global_settings_setting_ssh_password_authentication_help": "Allow password authentication for SSH", + "global_settings_setting_ssh_port": "SSH port", + "global_settings_setting_ssowat_panel_overlay_enabled": "Enable the small 'YunoHost' portal shortcut square on apps", + "global_settings_setting_portal_theme": "Portal theme", + "global_settings_setting_portal_theme_help": "More info regarding creating custom portal themes at https://yunohost.org/theming", + "global_settings_setting_user_strength": "User password strength requirements", + "global_settings_setting_user_strength_help": "These requirements are only enforced when initializing or changing the password", + "global_settings_setting_webadmin_allowlist": "Webadmin IP allowlist", + "global_settings_setting_webadmin_allowlist_enabled": "Enable Webadmin IP allowlist", + "global_settings_setting_webadmin_allowlist_enabled_help": "Allow only some IPs to access the webadmin.", + "global_settings_setting_webadmin_allowlist_help": "IP adresses allowed to access the webadmin.", "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).", "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to a variation of characters (uppercase, lowercase, digits and special characters).", "group_already_exist": "Group {group} already exists", @@ -424,6 +468,8 @@ "group_unknown": "The group '{group}' is unknown", "group_update_failed": "Could not update the group '{group}': {error}", "group_updated": "Group '{group}' updated", + "group_update_aliases": "Updating aliases for group '{group}'", + "group_no_change": "Nothing to change for group '{group}'", "group_user_already_in_group": "User {user} is already in group {group}", "group_user_not_in_group": "User {user} is not in group {group}", "hook_exec_failed": "Could not run script: {path}", @@ -432,10 +478,10 @@ "hook_list_by_invalid": "This property can not be used to list hooks", "hook_name_unknown": "Unknown hook name '{name}'", "installation_complete": "Installation completed", + "invalid_credentials": "Invalid password or username", "invalid_number": "Must be a number", "invalid_number_max": "Must be lesser than {max}", "invalid_number_min": "Must be greater than {min}", - "invalid_password": "Invalid password", "invalid_regex": "Invalid regex:'{regex}'", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", @@ -476,7 +522,11 @@ "log_regen_conf": "Regenerate system configurations '{}'", "log_remove_on_failed_install": "Remove '{}' after a failed installation", "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", + "log_resource_snippet": "Provisioning/deprovisioning/updating a resource", "log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain", + "log_settings_reset": "Reset setting", + "log_settings_reset_all": "Reset all settings", + "log_settings_set": "Apply settings", "log_tools_migrations_migrate_forward": "Run migrations", "log_tools_postinstall": "Postinstall your YunoHost server", "log_tools_reboot": "Reboot your server", @@ -494,7 +544,7 @@ "mail_alias_remove_failed": "Could not remove e-mail alias '{mail}'", "mail_domain_unknown": "Invalid e-mail address for domain '{domain}'. Please, use a domain administrated by this server.", "mail_forward_remove_failed": "Could not remove e-mail forwarding '{mail}'", - "mail_unavailable": "This e-mail address is reserved and shall be automatically allocated to the very first user", + "mail_unavailable": "This e-mail address is reserved for the admins group", "mailbox_disabled": "E-mail turned off for user {user}", "mailbox_used_space_dovecot_down": "The Dovecot mailbox service needs to be up if you want to fetch used mailbox space", "main_domain_change_failed": "Unable to change the main domain", @@ -503,7 +553,7 @@ "migration_0021_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.", "migration_0021_main_upgrade": "Starting main upgrade...", "migration_0021_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}", - "migration_0021_not_buster": "The current Debian distribution is not Buster!", + "migration_0021_not_buster2": "The current Debian distribution is not Buster! If you already ran the Buster->Bullseye migration, then this error is symptomatic of the fact that the migration procedure was not 100% succesful (otherwise YunoHost would have flagged it as completed). It is recommended to investigate what happened with the support team, who will need the **full** log of the `migration, which can be found in Tools > Logs in the webadmin.", "migration_0021_not_enough_free_space": "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.", "migration_0021_patch_yunohost_conflicts": "Applying patch to workaround conflict issue...", "migration_0021_patching_sources_list": "Patching the sources.lists...", @@ -515,9 +565,18 @@ "migration_0023_not_enough_space": "Make sufficient space available in {path} to run the migration.", "migration_0023_postgresql_11_not_installed": "PostgreSQL was not installed on your system. Nothing to do.", "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 is installed, but not PostgreSQL 13!? Something weird might have happened on your system :(...", + "migration_0024_rebuild_python_venv_broken_app": "Skipping {app} because virtualenv can't easily be rebuilt for this app. Instead, you should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_disclaimer_base": "Following the upgrade to Debian Bullseye, some Python applications needs to be partially rebuilt to get converted to the new Python version shipped in Debian (in technical terms: what's called the 'virtualenv' needs to be recreated). In the meantime, those Python applications may not work. YunoHost can attempt to rebuild the virtualenv for some of those, as detailed below. For other apps, or if the rebuild attempt fails, you will need to manually force an upgrade for those apps.", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs can't be rebuilt automatically for those apps. You need to force an upgrade for those, which can be done from the command line with: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Rebuilding the virtualenv will be attempted for the following apps (NB: the operation may take some time!): {rebuild_apps}", + "migration_0024_rebuild_python_venv_failed": "Failed to rebuild the Python virtualenv for {app}. The app may not work as long as this is not resolved. You should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_in_progress": "Now attempting to rebuild the Python virtualenv for `{app}`", "migration_description_0021_migrate_to_bullseye": "Upgrade the system to Debian Bullseye and YunoHost 11.x", "migration_description_0022_php73_to_php74_pools": "Migrate php7.3-fpm 'pool' conf files to php7.4", "migration_description_0023_postgresql_11_to_13": "Migrate databases from PostgreSQL 11 to 13", + "migration_description_0024_rebuild_python_venv": "Repair Python app after bullseye migration", + "migration_description_0025_global_settings_to_configpanel": "Migrate legacy global settings nomenclature to the new, modern nomenclature", + "migration_description_0026_new_admins_group": "Migrate to the new 'multiple admins' system", "migration_ldap_backup_before_migration": "Creating a backup of LDAP database and apps settings prior to the actual migration.", "migration_ldap_can_not_backup_before_migration": "The backup of the system could not be completed before the migration failed. Error: {error}", "migration_ldap_migration_failed_trying_to_rollback": "Could not migrate... trying to roll back the system.", @@ -533,8 +592,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", @@ -542,7 +601,9 @@ "not_enough_disk_space": "Not enough free space on '{path}'", "operation_interrupted": "The operation was manually interrupted?", "other_available_options": "... and {n} other available options not shown", + "password_confirmation_not_the_same": "The password and its confirmation do not match", "password_listed": "This password is among the most used passwords in the world. Please choose something more unique.", + "password_too_long": "Please choose a password shorter than 127 characters", "password_too_simple_1": "The password needs to be at least 8 characters long", "password_too_simple_2": "The password needs to be at least 8 characters long and contain a digit, upper and lower characters", "password_too_simple_3": "The password needs to be at least 8 characters long and contain a digit, upper, lower and special characters", @@ -551,8 +612,9 @@ "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", "pattern_email": "Must be a valid e-mail address, without '+' symbol (e.g. someone@example.com)", "pattern_email_forward": "Must be a valid e-mail address, '+' symbol accepted (e.g. someone+tag@example.com)", - "pattern_firstname": "Must be a valid first name", - "pattern_lastname": "Must be a valid last name", + "pattern_firstname": "Must be a valid first name (at least 3 chars)", + "pattern_fullname": "Must be a valid full name (at least 3 chars)", + "pattern_lastname": "Must be a valid last name (at least 3 chars)", "pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to not have a quota", "pattern_password": "Must be at least 3 characters long", "pattern_password_app": "Sorry, passwords can not contain the following characters: {forbidden_chars}", @@ -595,6 +657,7 @@ "regenconf_would_be_updated": "The configuration would have been updated for category '{category}'", "regex_incompatible_with_tile": "/!\\ Packagers! Permission '{permission}' has show_tile set to 'true' and you therefore cannot define a regex URL as the main URL", "regex_with_only_domain": "You can't use a regex for domain, only for path", + "registrar_infos": "Registrar infos", "restore_already_installed_app": "An app with the ID '{app}' is already installed", "restore_already_installed_apps": "The following apps can't be restored because they are already installed: {apps}", "restore_backup_too_old": "This backup archive can not be restored because it comes from a too-old YunoHost version.", @@ -611,8 +674,8 @@ "restore_running_app_script": "Restoring the app '{app}'...", "restore_running_hooks": "Running restoration hooks...", "restore_system_part_failed": "Could not restore the '{part}' system part", + "root_password_changed": "root's password was changed", "root_password_desynchronized": "The admin password was changed, but YunoHost could not propagate this to the root password!", - "root_password_replaced_by_admin_password": "Your root password have been replaced by your admin password.", "server_reboot": "The server will reboot", "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers}]", "server_shutdown": "The server will shut down", @@ -693,9 +756,10 @@ "user_unknown": "Unknown user: {user}", "user_update_failed": "Could not update user {user}: {error}", "user_updated": "User info changed", + "visitors": "Visitors", "yunohost_already_installed": "YunoHost is already installed", "yunohost_configured": "YunoHost is now configured", "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." + "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." } diff --git a/locales/eo.json b/locales/eo.json index 8ac32d4ce..13c96499b 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -1,6 +1,4 @@ { - "admin_password_change_failed": "Ne povis ŝanĝi pasvorton", - "admin_password_changed": "La pasvorto de administrado estis ŝanĝita", "app_already_installed": "{app} estas jam instalita", "app_already_up_to_date": "{app} estas jam ĝisdata", "app_argument_required": "Parametro {name} estas bezonata", @@ -26,7 +24,6 @@ "service_disabled": "La servo '{service}' ne plu komenciĝos kiam sistemo ekos.", "action_invalid": "Nevalida ago « {action} »", "admin_password": "Pasvorto de la estro", - "admin_password_too_long": "Bonvolu elekti pasvorton pli mallonga ol 127 signoj", "already_up_to_date": "Nenio por fari. Ĉio estas jam ĝisdatigita.", "app_argument_choice_invalid": "Uzu unu el ĉi tiuj elektoj '{choices}' por la argumento '{name}' anstataŭ '{value}'", "app_argument_invalid": "Elektu validan valoron por la argumento '{name}': {error}", @@ -66,7 +63,6 @@ "app_upgrade_failed": "Ne povis ĝisdatigi {app}: {error}", "app_upgrade_several_apps": "La sekvaj apliko estos altgradigitaj: {apps}", "backup_archive_open_failed": "Ne povis malfermi la rezervan ar archiveivon", - "ask_lastname": "Familia nomo", "app_start_backup": "Kolekti dosierojn por esti subtenata por {app}...", "backup_archive_name_exists": "Rezerva arkivo kun ĉi tiu nomo jam ekzistas.", "backup_applying_method_tar": "Krei la rezervon TAR Arkivo...", @@ -86,8 +82,6 @@ "backup_applying_method_copy": "Kopii ĉiujn dosierojn por sekurigi...", "backup_couldnt_bind": "Ne povis ligi {src} al {dest}.", "ask_password": "Pasvorto", - "app_requirements_unmeet": "Postuloj ne estas renkontitaj por {app}, la pakaĵo {pkgname} ({version}) devas esti {spec}", - "ask_firstname": "Antaŭnomo", "backup_ask_for_copying_if_needed": "Ĉu vi volas realigi la sekurkopion uzante {size} MB provizore? (Ĉi tiu maniero estas uzata ĉar iuj dosieroj ne povus esti pretigitaj per pli efika metodo.)", "backup_mount_archive_for_restore": "Preparante arkivon por restarigo …", "backup_csv_creation_failed": "Ne povis krei la CSV-dosieron bezonatan por restarigo", @@ -105,7 +99,6 @@ "field_invalid": "Nevalida kampo '{}'", "log_app_makedefault": "Faru '{}' la defaŭlta apliko", "backup_system_part_failed": "Ne eblis sekurkopi la sistemon de '{part}'", - "global_settings_setting_security_postfix_compatibility": "Kongruo vs sekureca kompromiso por la Postfix-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", "group_unknown": "La grupo '{group}' estas nekonata", "mailbox_disabled": "Retpoŝto malŝaltita por uzanto {user}", "migrations_dependencies_not_satisfied": "Rulu ĉi tiujn migradojn: '{dependencies_id}', antaŭ migrado {id}.", @@ -169,10 +162,8 @@ "regenconf_file_manually_modified": "La agorddosiero '{conf}' estis modifita permane kaj ne estos ĝisdatigita", "regenconf_would_be_updated": "La agordo estus aktualigita por la kategorio '{category}'", "certmanager_cert_install_success_selfsigned": "Mem-subskribita atestilo nun instalita por la domajno '{domain}'", - "global_settings_unknown_setting_from_settings_file": "Nekonata ŝlosilo en agordoj: '{setting_key}', forĵetu ĝin kaj konservu ĝin en /etc/yunohost/settings-unknown.json", "regenconf_file_backed_up": "Agordodosiero '{conf}' estis rezervita al '{backup}'", "iptables_unavailable": "Vi ne povas ludi kun iptables ĉi tie. Vi estas en ujo aŭ via kerno ne subtenas ĝin", - "global_settings_cant_write_settings": "Ne eblis konservi agordojn, tial: {reason}", "service_added": "La servo '{service}' estis aldonita", "upnp_disabled": "UPnP malŝaltis", "service_started": "Servo '{service}' komenciĝis", @@ -196,7 +187,6 @@ "log_letsencrypt_cert_renew": "Renovigu '{}' Let's Encrypt atestilon", "backup_output_directory_required": "Vi devas provizi elirejan dosierujon por la sekurkopio", "log_link_to_log": "Plena ŝtipo de ĉi tiu operacio: '{desc} '", - "global_settings_cant_serialize_settings": "Ne eblis serialigi datumojn pri agordoj, motivo: {reason}", "backup_running_hooks": "Kurado de apogaj hokoj …", "unexpected_error": "Io neatendita iris malbone: {error}", "password_listed": "Ĉi tiu pasvorto estas inter la plej uzataj pasvortoj en la mondo. Bonvolu elekti ion pli unikan.", @@ -211,7 +201,6 @@ "pattern_mailbox_quota": "Devas esti grandeco kun la sufikso b/k/M/G/T aŭ 0 por ne havi kvoton", "user_deletion_failed": "Ne povis forigi uzanton {user}: {error}", "backup_with_no_backup_script_for_app": "La app '{app}' ne havas sekretan skripton. Ignorante.", - "global_settings_key_doesnt_exists": "La ŝlosilo '{settings_key}' ne ekzistas en la tutmondaj agordoj, vi povas vidi ĉiujn disponeblajn klavojn per uzado de 'yunohost settings list'", "dyndns_no_domain_registered": "Neniu domajno registrita ĉe DynDNS", "dyndns_could_not_check_available": "Ne povis kontroli ĉu {domain} haveblas sur {provider}.", "hook_exec_not_terminated": "Skripto ne finiĝis ĝuste: {path}", @@ -236,9 +225,6 @@ "restore_nothings_done": "Nenio estis restarigita", "log_tools_postinstall": "Afiŝu vian servilon YunoHost", "dyndns_unavailable": "La domajno '{domain}' ne haveblas.", - "experimental_feature": "Averto: Ĉi tiu funkcio estas eksperimenta kaj ne konsiderata stabila, vi ne uzu ĝin krom se vi scias kion vi faras.", - "root_password_replaced_by_admin_password": "Via radika pasvorto estis anstataŭigita per via administra pasvorto.", - "global_settings_setting_security_password_user_strength": "Uzanto pasvorta forto", "restore_may_be_not_enough_disk_space": "Via sistemo ne ŝajnas havi sufiĉe da spaco (libera: {free_space} B, necesa spaco: {needed_space} B, sekureca marĝeno: {margin} B)", "log_corrupted_md_file": "La YAD-metadata dosiero asociita kun protokoloj estas damaĝita: '{md_file}\nEraro: {error} '", "downloading": "Elŝutante …", @@ -264,7 +250,6 @@ "log_user_delete": "Forigi uzanton '{}'", "dyndns_ip_updated": "Ĝisdatigis vian IP sur DynDNS", "regenconf_up_to_date": "La agordo jam estas ĝisdatigita por kategorio '{category}'", - "global_settings_setting_security_ssh_compatibility": "Kongruo vs sekureca kompromiso por la SSH-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", "migrations_need_to_accept_disclaimer": "Por funkciigi la migradon {id}, via devas akcepti la sekvan malakcepton:\n---\n{disclaimer}\n---\nSe vi akceptas funkcii la migradon, bonvolu rekonduki la komandon kun la opcio '--accept-disclaimer'.", "regenconf_file_remove_failed": "Ne povis forigi la agordodosieron '{conf}'", "not_enough_disk_space": "Ne sufiĉe libera spaco sur '{path}'", @@ -277,7 +262,6 @@ "user_unknown": "Nekonata uzanto: {user}", "migrations_to_be_ran_manually": "Migrado {id} devas funkcii permane. Bonvolu iri al Iloj → Migradoj en la retpaĝa paĝo, aŭ kuri `yunohost tools migrations run`.", "certmanager_cert_renew_success": "Ni Ĉifru atestilon renovigitan por la domajno '{domain}'", - "global_settings_reset_success": "Antaŭaj agordoj nun estas rezervitaj al {path}", "pattern_domain": "Devas esti valida domajna nomo (t.e. mia-domino.org)", "dyndns_key_generating": "Generi DNS-ŝlosilon ... Eble daŭros iom da tempo.", "restore_running_app_script": "Restarigi la programon '{app}'…", @@ -295,7 +279,6 @@ "log_backup_restore_system": "Restarigi sistemon de rezerva arkivo", "log_app_change_url": "Ŝanĝu la URL de la apliko '{}'", "service_already_started": "La servo '{service}' jam funkcias", - "global_settings_setting_security_password_admin_strength": "Admin pasvorta forto", "service_reload_or_restart_failed": "Ne povis reŝargi aŭ rekomenci la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}", "migrations_list_conflict_pending_done": "Vi ne povas uzi ambaŭ '--previous' kaj '--done' samtempe.", "server_shutdown_confirm": "La servilo haltos tuj, ĉu vi certas? [{answers}]", @@ -310,10 +293,8 @@ "password_too_simple_4": "La pasvorto bezonas almenaŭ 12 signojn kaj enhavas ciferon, majuskle, pli malaltan kaj specialajn signojn", "regenconf_file_updated": "Agordodosiero '{conf}' ĝisdatigita", "log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log show {name}'", - "global_settings_setting_security_nginx_compatibility": "Kongruo vs sekureca kompromiso por la TTT-servilo NGINX. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", "restore_complete": "Restarigita", "hook_exec_failed": "Ne povis funkcii skripto: {path}", - "global_settings_cant_open_settings": "Ne eblis malfermi agordojn, tial: {reason}", "user_created": "Uzanto kreita", "certmanager_attempt_to_replace_valid_cert": "Vi provas anstataŭigi bonan kaj validan atestilon por domajno {domain}! (Uzu --forte pretervidi)", "regenconf_updated": "Agordo ĝisdatigita por '{category}'", @@ -331,14 +312,12 @@ "log_selfsigned_cert_install": "Instalu mem-subskribitan atestilon sur '{}' domajno", "log_tools_reboot": "Reklamu vian servilon", "certmanager_cert_install_success": "Ni Ĉifru atestilon nun instalitan por la domajno '{domain}'", - "global_settings_bad_choice_for_enum": "Malbona elekto por agordo {setting}, ricevita '{choice}', sed disponeblaj elektoj estas: {available_choices}", "server_shutdown": "La servilo haltos", "log_tools_migrations_migrate_forward": "Kuru migradoj", "regenconf_now_managed_by_yunohost": "La agorda dosiero '{conf}' nun estas administrata de YunoHost (kategorio {category}).", "server_reboot_confirm": "Ĉu la servilo rekomencos tuj, ĉu vi certas? [{answers}]", "log_app_install": "Instalu la aplikon '{}'", "service_description_dnsmasq": "Traktas rezolucion de domajna nomo (DNS)", - "global_settings_unknown_type": "Neatendita situacio, la agordo {setting} ŝajnas havi la tipon {unknown_type} sed ĝi ne estas tipo subtenata de la sistemo.", "domain_hostname_failed": "Ne povis agordi novan gastigilon. Ĉi tio eble kaŭzos problemon poste (eble bone).", "server_reboot": "La servilo rekomenciĝos", "regenconf_failed": "Ne povis regeneri la agordon por kategorio(j): {categories}", @@ -352,7 +331,6 @@ "log_domain_remove": "Forigi domon '{}' de agordo de sistemo", "hook_list_by_invalid": "Ĉi tiu posedaĵo ne povas esti uzata por listigi hokojn", "confirm_app_install_thirdparty": "Danĝero! Ĉi tiu apliko ne estas parto de la aplika katalogo de Yunohost. Instali triajn aplikojn povas kompromiti la integrecon kaj sekurecon de via sistemo. Vi probable ne devas instali ĝin krom se vi scias kion vi faras. NENIU SUBTENO estos provizita se ĉi tiu app ne funkcias aŭ rompas vian sistemon ... Se vi pretas riski ĉiuokaze, tajpu '{answers}'", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permesu uzon de (malaktuala) DSA-hostkey por la agordo de daemon SSH", "dyndns_domain_not_provided": "Provizanto DynDNS {provider} ne povas provizi domajnon {domain}.", "backup_unable_to_organize_files": "Ne povis uzi la rapidan metodon por organizi dosierojn en la ar archiveivo", "password_too_simple_2": "La pasvorto bezonas almenaŭ 8 signojn kaj enhavas ciferon, majusklojn kaj minusklojn", @@ -365,7 +343,6 @@ "domain_cannot_remove_main": "Vi ne povas forigi '{domain}' ĉar ĝi estas la ĉefa domajno, vi bezonas unue agordi alian domajnon kiel la ĉefan domajnon per uzado de 'yunohost domain main-domain -n ', jen la listo de kandidataj domajnoj. : {other_domains}", "service_reloaded_or_restarted": "La servo '{service}' estis reŝarĝita aŭ rekomencita", "log_domain_add": "Aldonu '{}' domajnon en sisteman agordon", - "global_settings_bad_type_for_setting": "Malbona tipo por agordo {setting}, ricevita {received_type}, atendata {expected_type}", "unlimit": "Neniu kvoto", "system_username_exists": "Uzantnomo jam ekzistas en la listo de uzantoj de sistemo", "firewall_reloaded": "Fajroŝirmilo reŝarĝis", @@ -454,7 +431,6 @@ "diagnosis_found_errors_and_warnings": "Trovis {errors} signifaj problemo (j) (kaj {warnings} averto) rilataj al {category}!", "diagnosis_diskusage_low": "Stokado {mountpoint} (sur aparato {device}) nur restas {free} ({free_percent}%) spaco restanta (el {total}). Estu zorgema.", "diagnosis_diskusage_ok": "Stokado {mountpoint} (sur aparato {device}) ankoraŭ restas {free} ({free_percent}%) spaco (el {total})!", - "global_settings_setting_pop3_enabled": "Ebligu la protokolon POP3 por la poŝta servilo", "diagnosis_unknown_categories": "La jenaj kategorioj estas nekonataj: {categories}", "diagnosis_services_running": "Servo {service} funkcias!", "diagnosis_ports_unreachable": "Haveno {port} ne atingeblas de ekstere.", @@ -516,7 +492,6 @@ "diagnosis_http_partially_unreachable": "Domajno {domain} ŝajnas neatingebla per HTTP de ekster la loka reto en IPv {failed}, kvankam ĝi funkcias en IPv {passed}.", "diagnosis_http_nginx_conf_not_up_to_date": "La nginx-agordo de ĉi tiu domajno ŝajnas esti modifita permane, kaj malhelpas YunoHost diagnozi ĉu ĝi atingeblas per HTTP.", "diagnosis_http_nginx_conf_not_up_to_date_details": "Por solvi la situacion, inspektu la diferencon per la komandlinio per yunohost tools regen-conf nginx --dry-run --with-diff kaj se vi aranĝas, apliku la ŝanĝojn per yunohost tools regen-conf nginx --force.", - "global_settings_setting_smtp_allow_ipv6": "Permesu la uzon de IPv6 por ricevi kaj sendi poŝton", "backup_archive_corrupted": "I aspektas kiel la rezerva arkivo '{archive}' estas koruptita: {error}", "backup_archive_cant_retrieve_info_json": "Ne povis ŝarĝi infos por arkivo '{archive}' ... la info.json ne povas esti reprenita (aŭ ne estas valida JSON).", "ask_user_domain": "Domajno uzi por la retpoŝta adreso de la uzanto kaj XMPP-konto", @@ -530,5 +505,11 @@ "app_label_deprecated": "Ĉi tiu komando estas malrekomendita! Bonvolu uzi la novan komandon 'yunohost user permission update' por administri la app etikedo.", "app_argument_password_no_default": "Eraro dum analiza pasvorta argumento '{name}': pasvorta argumento ne povas havi defaŭltan valoron por sekureca kialo", "additional_urls_already_removed": "Plia URL '{url}' jam forigita en la aldona URL por permeso '{permission}'", - "additional_urls_already_added": "Plia URL '{url}' jam aldonita en la aldona URL por permeso '{permission}'" + "additional_urls_already_added": "Plia URL '{url}' jam aldonita en la aldona URL por permeso '{permission}'", + "global_settings_setting_nginx_compatibility_help": "Kongruo vs sekureca kompromiso por la TTT-servilo NGINX. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", + "global_settings_setting_admin_strength": "Admin pasvorta forto", + "global_settings_setting_user_strength": "Uzanto pasvorta forto", + "global_settings_setting_postfix_compatibility_help": "Kongruo vs sekureca kompromiso por la Postfix-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", + "global_settings_setting_ssh_compatibility_help": "Kongruo vs sekureca kompromiso por la SSH-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", + "global_settings_setting_smtp_allow_ipv6_help": "Permesu la uzon de IPv6 por ricevi kaj sendi poŝton" } \ No newline at end of file diff --git a/locales/es.json b/locales/es.json index 93236189e..bfb111f26 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,8 +1,6 @@ { - "action_invalid": "Acción no válida '{action} 1'", + "action_invalid": "Acción inválida '{action}'", "admin_password": "Contraseña administrativa", - "admin_password_change_failed": "No se pudo cambiar la contraseña", - "admin_password_changed": "La contraseña de administración fue cambiada", "app_already_installed": "{app} ya está instalada", "app_argument_choice_invalid": "Elija un valor válido para el argumento '{name}': '{value}' no se encuentra entre las opciones disponibles ({choices})", "app_argument_invalid": "Elija un valor válido para el argumento «{name}»: {error}", @@ -15,14 +13,11 @@ "app_not_properly_removed": "La {app} 0 no ha sido desinstalada correctamente", "app_removed": "{app} Desinstalado", "app_requirements_checking": "Comprobando los paquetes necesarios para {app}…", - "app_requirements_unmeet": "No se cumplen los requisitos para {app}, el paquete {pkgname} ({version}) debe ser {spec}", "app_sources_fetch_failed": "No se pudieron obtener los archivos con el código fuente, ¿es el URL correcto?", "app_unknown": "Aplicación desconocida", "app_unsupported_remote_type": "Tipo remoto no soportado por la aplicación", "app_upgrade_failed": "No se pudo actualizar {app}: {error}", "app_upgraded": "Actualizado {app}", - "ask_firstname": "Nombre", - "ask_lastname": "Apellido", "ask_main_domain": "Dominio principal", "ask_new_admin_password": "Nueva contraseña administrativa", "ask_password": "Contraseña", @@ -150,7 +145,7 @@ "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio «{domain}» no ha sido emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", "certmanager_attempt_to_renew_valid_cert": "¡El certificado para el dominio «{domain}» no está a punto de expirar! (Puede usar --force si sabe lo que está haciendo)", "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain} a través de HTTP. Por favor compruebe en los diagnósticos la categoría 'Web'para más información. (Si sabe lo que está haciendo, utilice '--no-checks' para no realizar estas comprobaciones.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio '{domain}' es diferente de la IP de este servidor. Por favor comprueba los 'registros DNS' (básicos) la categoría de diagnósticos para mayor información. Si recientemente modificó su registro 'A', espere a que se propague (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabe lo que está haciendo, use '--no-checks' para desactivar esos cheques)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Los registros DNS para el dominio '{domain}' son diferentes para la IP de este servidor. Por favor comprueba la categoría de los 'registros DNS' (básicos) en la página de diagnóstico para mayor información. Si has modificado recientemente tu registro 'A', espera a que se propague (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabes lo que estás haciendo, usa '--no-checks' para desactivar estos marcadores)", "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain} (archivo: {file}), razón: {reason}", "certmanager_cert_install_success_selfsigned": "Instalado correctamente un certificado autofirmado para el dominio «{domain}»", "certmanager_cert_install_success": "Instalado correctamente un certificado de Let's Encrypt para el dominio «{domain}»", @@ -163,7 +158,7 @@ "certmanager_unable_to_parse_self_CA_name": "No se pudo procesar el nombre de la autoridad de autofirma (archivo: {file})", "domains_available": "Dominios disponibles:", "backup_archive_broken_link": "No se pudo acceder al archivo de respaldo (enlace roto a {path})", - "certmanager_acme_not_configured_for_domain": "El reto ACME no ha podido ser realizado para {domain} porque en su configuración de nginx falta el código correcto... Por favor, asegúrate que la configuración de nginx es correcta ejecutando en el terminal `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "El reto ACME no ha podido ser realizado para {domain} porque en su configuración de nginx falta el código correcto... Por favor, asegúrate que la configuración de nginx es correcta ejecutando en la terminal `yunohost tools regen-conf nginx --dry-run --with-diff`.", "domain_hostname_failed": "No se pudo establecer un nuevo nombre de anfitrión («hostname»). Esto podría causar problemas más tarde (no es seguro... podría ir bien).", "app_already_installed_cant_change_url": "Esta aplicación ya está instalada. La URL no se puede cambiar solo con esta función. Marque `app changeurl` si está disponible.", "app_change_url_identical_domains": "El antiguo y nuevo dominio/url_path son idénticos ('{domain}{path}'), no se realizarán cambios.", @@ -192,8 +187,7 @@ "backup_with_no_backup_script_for_app": "La aplicación «{app}» no tiene un guión de respaldo. Omitiendo.", "backup_with_no_restore_script_for_app": "«{app}» no tiene un script de restauración, no podá restaurar automáticamente la copia de seguridad de esta aplicación.", "dyndns_domain_not_provided": "El proveedor de DynDNS {provider} no puede proporcionar el dominio {domain}.", - "experimental_feature": "Aviso : esta funcionalidad es experimental y no se considera estable, no debería usarla a menos que sepa lo que está haciendo.", - "good_practices_about_user_password": "Ahora está a punto de definir una nueva contraseña de usuario. La contraseña debe tener al menos 8 caracteres, aunque es una buena práctica usar una contraseña más larga (es decir, una frase de contraseña) y / o una variación de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).", + "good_practices_about_user_password": "Está a punto de establecer una nueva contraseña de usuario. La contraseña debería de ser de al menos 8 caracteres, aunque es una buena práctica usar una contraseña más larga (es decir, una frase de paso) y/o usar varias clases de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).", "password_listed": "Esta contraseña se encuentra entre las contraseñas más utilizadas del mundo. Por favor, elija algo menos común y más robusto.", "password_too_simple_1": "La contraseña debe tener al menos 8 caracteres de longitud", "password_too_simple_2": "La contraseña debe ser de al menos 8 caracteres de longitud e incluir un número y caracteres en mayúsculas y minúsculas", @@ -225,7 +219,6 @@ "server_reboot": "El servidor se reiniciará", "server_shutdown_confirm": "El servidor se apagará inmediatamente ¿está seguro? [{answers}]", "server_shutdown": "El servidor se apagará", - "root_password_replaced_by_admin_password": "Su contraseña de root ha sido sustituida por su contraseña de administración.", "root_password_desynchronized": "La contraseña de administración ha sido cambiada pero ¡YunoHost no pudo propagar esto a la contraseña de root!", "restore_system_part_failed": "No se pudo restaurar la parte del sistema «{part}»", "restore_removing_tmp_dir_failed": "No se pudo eliminar un directorio temporal antiguo", @@ -319,21 +312,6 @@ "group_creation_failed": "No se pudo crear el grupo «{group}»: {error}", "group_created": "Creado el grupo «{group}»", "good_practices_about_admin_password": "Ahora está a punto de definir una nueva contraseña de usuario. La contraseña debe tener al menos 8 caracteres, aunque es una buena práctica usar una contraseña más larga (es decir, una frase de contraseña) y / o una variación de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).", - "global_settings_unknown_type": "Situación imprevista, la configuración {setting} parece tener el tipo {unknown_type} pero no es un tipo compatible con el sistema.", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permitir el uso de la llave (obsoleta) DSA para la configuración del demonio SSH", - "global_settings_unknown_setting_from_settings_file": "Clave desconocida en la configuración: «{setting_key}», desechada y guardada en /etc/yunohost/settings-unknown.json", - "global_settings_setting_security_postfix_compatibility": "Compromiso entre compatibilidad y seguridad para el servidor Postfix. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", - "global_settings_setting_security_ssh_compatibility": "Compromiso entre compatibilidad y seguridad para el servidor SSH. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", - "global_settings_setting_security_password_user_strength": "Seguridad de la contraseña de usuario", - "global_settings_setting_security_password_admin_strength": "Seguridad de la contraseña del administrador", - "global_settings_setting_security_nginx_compatibility": "Compromiso entre compatibilidad y seguridad para el servidor web NGINX. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", - "global_settings_reset_success": "Respaldada la configuración previa en {path}", - "global_settings_key_doesnt_exists": "La clave «{settings_key}» no existe en la configuración global, puede ver todas las claves disponibles ejecutando «yunohost settings list»", - "global_settings_cant_write_settings": "No se pudo guardar el archivo de configuración, motivo: {reason}", - "global_settings_cant_serialize_settings": "No se pudo seriar los datos de configuración, motivo: {reason}", - "global_settings_cant_open_settings": "No se pudo abrir el archivo de configuración, motivo: {reason}", - "global_settings_bad_type_for_setting": "Tipo erróneo para la configuración {setting}, obtuvo {received_type}, esperado {expected_type}", - "global_settings_bad_choice_for_enum": "Opción errónea para la configuración {setting}, obtuvo «{choice}» pero las opciones disponibles son: {available_choices}", "file_does_not_exist": "El archivo {path} no existe.", "dyndns_could_not_check_available": "No se pudo comprobar si {domain} está disponible en {provider}.", "domain_dns_conf_is_just_a_recommendation": "Este comando muestra la configuración *recomendada*. No configura las entradas DNS por ti. Es tu responsabilidad configurar la zona DNS en su registrador según esta recomendación.", @@ -361,7 +339,6 @@ "app_not_upgraded": "La aplicación '{failed_app}' no se pudo actualizar y, como consecuencia, se cancelaron las actualizaciones de las siguientes aplicaciones: {apps}", "app_action_cannot_be_ran_because_required_services_down": "Estos servicios necesarios deberían estar funcionando para ejecutar esta acción: {services}. Pruebe a reiniciarlos para continuar (y posiblemente investigar por qué están caídos).", "already_up_to_date": "Nada que hacer. Todo está actualizado.", - "admin_password_too_long": "Elija una contraseña de menos de 127 caracteres", "aborting": "Cancelando.", "app_action_broke_system": "Esta acción parece que ha roto estos servicios importantes: {services}", "operation_interrupted": "¿La operación fue interrumpida manualmente?", @@ -415,7 +392,7 @@ "diagnosis_ip_no_ipv4": "El servidor no cuenta con ipv4 funcional.", "diagnosis_ip_not_connected_at_all": "¿¡Está conectado el servidor a internet!?", "diagnosis_ip_broken_resolvconf": "La resolución de nombres de dominio parece no funcionar en tu servidor, lo que parece estar relacionado con que /etc/resolv.conf no apunta a 127.0.0.1.", - "diagnosis_dns_missing_record": "Según la configuración DNS recomendada, deberías añadir un registro DNS\ntipo: {type}\nnombre: {name}\nvalor: {value}", + "diagnosis_dns_missing_record": "Según la configuración DNS recomendada, deberías añadir un registro DNS con las informaciones siguientes. Tipo: {type}
Nombre: {name}
Valor: {value}", "diagnosis_diskusage_low": "El almacenamiento {mountpoint} (en el dispositivo {device}) solo tiene {free} ({free_percent}%) de espacio disponible (de {total}). Ten cuidado.", "diagnosis_services_bad_status_tip": "Puedes intentar reiniciar el servicio, y si no funciona, echar un vistazo a los logs del serviciode la administración web (desde la línea de comandos puedes hacerlo con yunohost service restart {service} y yunohost service log {service}).", "diagnosis_ip_connected_ipv6": "¡El servidor está conectado a internet a través de IPv6!", @@ -434,17 +411,17 @@ "diagnosis_services_running": "¡El servicio {service} está en ejecución!", "diagnosis_failed": "Error al obtener el resultado del diagnóstico para la categoría '{category}': {error}", "diagnosis_ip_connected_ipv4": "¡El servidor está conectado a internet a través de IPv4!", - "diagnosis_security_vulnerable_to_meltdown_details": "Para corregir esto, debieras actualizar y reiniciar tu sistema para cargar el nuevo kernel de Linux (o contacta tu proveedor si esto no funciona). Mas información en https://meltdownattack.com/ .", - "diagnosis_ram_verylow": "Al sistema le queda solamente {available} ({available_percent}%) de RAM! (De un total de {total})", + "diagnosis_security_vulnerable_to_meltdown_details": "Para corregir esto, debieras actualizar y reiniciar tu sistema para cargar el nuevo kernel de Linux (o contacta tu proveedor si esto no funciona). Más información en https://meltdownattack.com/ .", + "diagnosis_ram_verylow": "¡Al sistema le queda solamente {available} ({available_percent}%) de RAM! (De un total de {total})", "diagnosis_ram_low": "Al sistema le queda {available} ({available_percent}%) de RAM de un total de {total}. Cuidado.", - "diagnosis_ram_ok": "El sistema aun tiene {available} ({available_percent}%) de RAM de un total de {total}.", + "diagnosis_ram_ok": "El sistema aún tiene {available} ({available_percent}%) de RAM de un total de {total}.", "diagnosis_swap_none": "El sistema no tiene mas espacio de intercambio. Considera agregar por lo menos {recommended} de espacio de intercambio para evitar que el sistema se quede sin memoria.", "diagnosis_swap_notsomuch": "Al sistema le queda solamente {total} de espacio de intercambio. Considera agregar al menos {recommended} para evitar que el sistema se quede sin memoria.", "diagnosis_mail_outgoing_port_25_blocked": "El puerto de salida 25 parece estar bloqueado. Intenta desbloquearlo con el panel de configuración de tu proveedor de servicios de Internet (o proveedor de halbergue). Mientras tanto, el servidor no podrá enviar correos electrónicos a otros servidores.", - "diagnosis_regenconf_allgood": "Todos los archivos de configuración están en linea con la configuración recomendada!", + "diagnosis_regenconf_allgood": "¡Todos los archivos de configuración están en línea con la configuración recomendada!", "diagnosis_regenconf_manually_modified": "El archivo de configuración {file} parece que ha sido modificado manualmente.", "diagnosis_regenconf_manually_modified_details": "¡Esto probablemente esta BIEN si sabes lo que estás haciendo! YunoHost dejará de actualizar este fichero automáticamente... Pero ten en cuenta que las actualizaciones de YunoHost pueden contener importantes cambios que están recomendados. Si quieres puedes comprobar las diferencias mediante yunohost tools regen-conf {category} --dry-run --with-diff o puedes forzar el volver a las opciones recomendadas mediante el comando yunohost tools regen-conf {category} --force", - "diagnosis_security_vulnerable_to_meltdown": "Pareces vulnerable a el colapso de vulnerabilidad critica de seguridad", + "diagnosis_security_vulnerable_to_meltdown": "Pareces vulnerable al colapso de vulnerabilidad crítica de seguridad", "diagnosis_description_basesystem": "Sistema de base", "diagnosis_description_ip": "Conectividad a Internet", "diagnosis_description_dnsrecords": "Registro DNS", @@ -464,9 +441,8 @@ "log_domain_main_domain": "Hacer de '{}' el dominio principal", "log_app_action_run": "Inicializa la acción de la aplicación '{}'", "group_already_exist_on_system_but_removing_it": "El grupo {group} ya existe en los grupos del sistema, pero YunoHost lo suprimirá …", - "global_settings_setting_pop3_enabled": "Habilita el protocolo POP3 para el servidor de correo electrónico", "domain_cannot_remove_main_add_new_one": "No se puede remover '{domain}' porque es su principal y único dominio. Primero debe agregar un nuevo dominio con la linea de comando 'yunohost domain add ', entonces configurarlo como dominio principal con 'yunohost domain main-domain -n ' y finalmente borrar el dominio '{domain}' con 'yunohost domain remove {domain}'.'", - "diagnosis_never_ran_yet": "Este servidor todavía no tiene reportes de diagnostico. Puede iniciar un diagnostico completo desde la interface administrador web o con la linea de comando 'yunohost diagnosis run'.", + "diagnosis_never_ran_yet": "Este servidor todavía no tiene reportes de diagnostico. Puede iniciar un diagnostico completo desde la interface administrador web o con la linea de comando 'yunohost diagnosis run'.", "diagnosis_unknown_categories": "Las siguientes categorías están desconocidas: {categories}", "diagnosis_http_unreachable": "El dominio {domain} esta fuera de alcance desde internet y a través de HTTP.", "diagnosis_http_bad_status_code": "Parece que otra máquina (quizás el router de conexión a internet) haya respondido en vez de tu servidor.
1. La causa más común es que el puerto 80 (y el 443) no hayan sido redirigidos a tu servidor.
2. En situaciones más complejas: asegurate de que ni el cortafuegos ni el proxy inverso están interfiriendo.", @@ -479,11 +455,11 @@ "certmanager_warning_subdomain_dns_record": "El subdominio '{subdomain}' no se resuelve en la misma dirección IP que '{domain}'. Algunas funciones no estarán disponibles hasta que solucione esto y regenere el certificado.", "domain_cannot_add_xmpp_upload": "No puede agregar dominios que comiencen con 'xmpp-upload'. Este tipo de nombre está reservado para la función de carga XMPP integrada en YunoHost.", "yunohost_postinstall_end_tip": "¡La post-instalación completada! Para finalizar su configuración, por favor considere:\n - agregar un primer usuario a través de la sección 'Usuarios' del administrador web (o 'yunohost user create ' en la línea de comandos);\n - diagnosticar problemas potenciales a través de la sección 'Diagnóstico' del administrador web (o 'yunohost diagnosis run' en la línea de comandos);\n - leyendo las partes 'Finalizando su configuración' y 'Conociendo YunoHost' en la documentación del administrador: https://yunohost.org/admindoc.", - "diagnosis_dns_point_to_doc": "Por favor, consulta la documentación en https://yunohost.org/dns_config si necesitas ayuda para configurar los registros DNS.", + "diagnosis_dns_point_to_doc": "Por favor, consulta la documentación en https://yunohost.org/dns_config si necesitas ayuda para configurar los registros DNS.", "diagnosis_ip_global": "IP Global: {global}", "diagnosis_mail_outgoing_port_25_ok": "El servidor de email SMTP puede mandar emails (puerto saliente 25 no está bloqueado).", - "diagnosis_mail_outgoing_port_25_blocked_details": "Primeramente deberías intentar desbloquear el puerto de salida 25 en la interfaz de control de tu router o en la interfaz de tu provedor de hosting. (Algunos hosting pueden necesitar que les abras un ticket de soporte para esto).", - "diagnosis_swap_tip": "Por favor tenga cuidado y sepa que si el servidor contiene swap en una tarjeta SD o un disco duro de estado sólido, esto reducirá drásticamente la vida útil del dispositivo.", + "diagnosis_mail_outgoing_port_25_blocked_details": "Primeramente, deberías intentar desbloquear el puerto de salida 25 en la interfaz de control de tu router o en la interfaz de tu proveedor de hosting. (Algunos hostings pueden necesitar que les abras un ticket de soporte para esto).", + "diagnosis_swap_tip": "Por favor, tenga cuidado y sepa que si el servidor contiene swap en una tarjeta SD o un disco duro de estado sólido, esto reducirá drásticamente la vida útil del dispositivo.", "diagnosis_domain_expires_in": "{domain} expira en {days} días.", "diagnosis_domain_expiration_error": "¡Algunos dominios expirarán MUY PRONTO!", "diagnosis_domain_expiration_warning": "¡Algunos dominios expirarán pronto!", @@ -510,13 +486,10 @@ "app_label_deprecated": "Este comando está depreciado! Favor usar el nuevo comando 'yunohost user permission update' para administrar la etiqueta de app.", "app_argument_password_no_default": "Error al interpretar argumento de contraseña'{name}': El argumento de contraseña no puede tener un valor por defecto por razón de seguridad", "invalid_regex": "Regex no valido: «{regex}»", - "global_settings_setting_backup_compress_tar_archives": "Cuando se creen nuevas copias de respaldo, comprimir los archivos (.tar.gz) en lugar de descomprimir los archivos (.tar). N.B.: activar esta opción quiere decir que los archivos serán más pequeños pero que el proceso tardará más y utilizará más CPU.", "global_settings_setting_smtp_relay_password": "Clave de uso del SMTP", "global_settings_setting_smtp_relay_user": "Cuenta de uso de SMTP", "global_settings_setting_smtp_relay_port": "Puerto de envio / relay SMTP", - "global_settings_setting_smtp_relay_host": "El servidor relay de SMTP para enviar correo en lugar de esta instalación YunoHost. Útil si estás en una de estas situaciones: tu puerto 25 esta bloqueado por tu ISP o VPS, si estás en usado una IP marcada como residencial o DUHL, si no puedes configurar un DNS inverso o si el servidor no está directamente expuesto a internet y quieres utilizar otro servidor para enviar correos.", - "global_settings_setting_smtp_allow_ipv6": "Permitir el uso de IPv6 para enviar y recibir correo", - "diagnosis_processes_killed_by_oom_reaper": "Algunos procesos fueron terminados por el sistema recientemente porque se quedó sin memoria. Típicamente es sintoma de falta de memoria o de un proceso que se adjudicó demasiada memoria.
Resumen de los procesos terminados:
\n{kills_summary}", + "diagnosis_processes_killed_by_oom_reaper": "Algunos procesos fueron terminados por el sistema recientemente porque se quedó sin memoria. Típicamente, es síntoma de falta de memoria o de un proceso que se adjudicó demasiada memoria.
Resumen de los procesos terminados:
\n{kills_summary}", "diagnosis_http_nginx_conf_not_up_to_date_details": "Para arreglar este asunto, estudia las diferencias mediante el comando yunohost tools regen-conf nginx --dry-run --with-diff y si te parecen bien aplica los cambios mediante yunohost tools regen-conf nginx --force.", "diagnosis_http_nginx_conf_not_up_to_date": "Parece que la configuración nginx de este dominio haya sido modificada manualmente, esto no deja que YunoHost pueda diagnosticar si es accesible mediante HTTP.", "diagnosis_http_partially_unreachable": "El dominio {domain} parece que no es accesible mediante HTTP desde fuera de la red local mediante IPv{failed}, aunque si que funciona mediante IPv{passed}.", @@ -527,13 +500,13 @@ "diagnosis_mail_queue_unavailable_details": "Error: {error}", "diagnosis_mail_queue_unavailable": "No se ha podido consultar el número de correos electrónicos pendientes en la cola", "diagnosis_mail_queue_ok": "{nb_pending} correos esperando e la cola de correos electrónicos", - "diagnosis_mail_blacklist_website": "Cuando averigües y arregles el motivo por el que aprareces en la lista maligna, no dudes en solicitar que tu IP o dominio sea retirado de la {blacklist_website}", + "diagnosis_mail_blacklist_website": "Cuando averigües y arregles el motivo por el que apareces en la lista maligna, no dudes en solicitar que tu IP o dominio sea retirado de la {blacklist_website}", "diagnosis_mail_blacklist_reason": "El motivo de estar en la lista maligna es: {reason}", "diagnosis_mail_blacklist_listed_by": "Tu IP o dominio {item} está marcado como maligno en {blacklist_name}", "diagnosis_mail_blacklist_ok": "Las IP y los dominios utilizados en este servidor no parece que estén en ningún listado maligno (blacklist)", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "El DNS inverso actual es: {rdns_domain}
Valor esperado: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "La resolución de DNS inverso no está correctamente configurada mediante IPv{ipversion}. Algunos correos pueden fallar al ser enviados o pueden ser marcados como basura.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Algunos proveedores no permiten configurar el DNS inverso (o su funcionalidad puede estar rota...). Si tu DNS inverso está configurado correctamente para IPv4, puedes intentar deshabilitarlo para IPv6 cuando envies correos mediante el comando yunohost settings set smtp.allow_ipv6 -v off. Nota: esta solución quiere decir que no podrás enviar ni recibir correos con los pocos servidores que utilizan exclusivamente IPv6.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Algunos proveedores no permiten configurar el DNS inverso (o su funcionalidad puede estar rota...). Si tu DNS inverso está configurado correctamente para IPv4, puedes intentar deshabilitarlo para IPv6 cuando envies correos mediante el comando yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Nota: esta solución quiere decir que no podrás enviar ni recibir correos con los pocos servidores que utilizan exclusivamente IPv6.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Algunos proveedores no te permitirán que configures un DNS inverso (o puede que esta opción esté rota...). Si estás sufriendo problemas por este asunto, quizás te sirvan las siguientes soluciones:
- Algunos ISP proporcionan una alternativa mediante el uso de un relay de servidor de correo aunque esto implica que el relay podrá espiar tu tráfico de correo electrónico.
- Una solución amigable con la privacidad es utilizar una VPN con una *IP pública dedicada* para evitar este tipo de limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Quizás tu solución sea cambiar de proveedor de internet", "diagnosis_mail_fcrdns_nok_details": "Primero deberías intentar configurar el DNS inverso mediante {ehlo_domain} en la interfaz de internet de tu router o en la de tu proveedor de internet. (Algunos proveedores de internet en ocasiones necesitan que les solicites un ticket de soporte para ello).", "diagnosis_mail_fcrdns_dns_missing": "No hay definida ninguna DNS inversa mediante IPv{ipversion}. Algunos correos puede que fallen al enviarse o puede que se marquen como basura.", @@ -547,11 +520,11 @@ "diagnosis_mail_ehlo_unreachable_details": "No pudo abrirse la conexión en el puerto 25 de tu servidor mediante IPv{ipversion}. Parece que no se puede contactar.
1. La causa más común en estos casos suele ser que el puerto 25 no está correctamente redireccionado a tu servidor.
2. También deberías asegurarte que el servicio postfix está en marcha.
3. En casos más complejos: asegurate que no estén interfiriendo ni el firewall ni el reverse-proxy.", "diagnosis_mail_ehlo_unreachable": "El servidor de correo SMTP no puede contactarse desde el exterior mediante IPv{ipversion}. No puede recibir correos.", "diagnosis_mail_ehlo_ok": "¡El servidor de correo SMTP puede contactarse desde el exterior por lo que puede recibir correos!", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algunos proveedores de internet no le permitirán desbloquear el puerto 25 porque no les importa la Neutralidad de la Red.
- Algunos proporcionan una alternativa usando un relay como servidor de correo lo que implica que el relay podrá espiar tu trafico de correo.
- Una alternativa buena para la privacidad es utilizar una VPN *con una IP pública dedicada* para evitar estas limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Otra alternativa es cambiar de proveedor de inteernet a uno más amable con la Neutralidad de la Red", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algunos proveedores de internet no le permitirán desbloquear el puerto 25 porque no les importa la Neutralidad de la Red.
- Algunos proporcionan una alternativa usando un relay como servidor de correo lo que implica que el relay podrá espiar tu tráfico de correo.
- Una alternativa buena para la privacidad es utilizar una VPN *con una IP pública dedicada* para evitar estas limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Otra alternativa es cambiar de proveedor de internet a uno más amable con la Neutralidad de la Red", "diagnosis_backports_in_sources_list": "Parece que apt (el gestor de paquetes) está configurado para usar el repositorio backports. A menos que realmente sepas lo que estás haciendo, desaconsejamos absolutamente instalar paquetes desde backports, ya que pueden provocar comportamientos intestables o conflictos en el sistema.", "diagnosis_basesystem_hardware_model": "El modelo de servidor es {model}", - "additional_urls_already_removed": "La URL adicional «{url}» ya se ha eliminado para el permiso «{permission}»", - "additional_urls_already_added": "La URL adicional «{url}» ya se ha añadido para el permiso «{permission}»", + "additional_urls_already_removed": "La URL adicional '{url}' ya se ha eliminado para el permiso «{permission}»", + "additional_urls_already_added": "La URL adicional '{url}' ya se ha añadido para el permiso «{permission}»", "config_apply_failed": "Falló la aplicación de la nueva configuración: {error}", "app_restore_script_failed": "Ha ocurrido un error dentro del script de restauración de aplicaciones", "app_config_unable_to_apply": "No se pudieron aplicar los valores del panel configuración.", @@ -563,8 +536,8 @@ "domain_dns_push_partial_failure": "Entradas DNS actualizadas parcialmente: algunas advertencias/errores reportados.", "domain_unknown": "Dominio '{domain}' desconocido", "diagnosis_high_number_auth_failures": "Ultimamente ha habido un gran número de errores de autenticación. Asegúrate de que Fail2Ban está ejecutándose y correctamente configurado, o usa un puerto SSH personalizado como se explica en https://yunohost.org/security.", - "diagnosis_sshd_config_inconsistent": "Parece que el puerto SSH ha sido modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, hay un nuevo parámetro global 'security.ssh.port' disponible para evitar modificar manualmente la configuración.", - "diagnosis_sshd_config_inconsistent_details": "Por favor ejecuta yunohost settings set security.ssh.port -v TU_PUERTO_SSH para definir el puerto SSH, y comprueba yunohost tools regen-conf ssh --dry-run --with-diff y yunohost tools regen-conf ssh --force para resetear tu configuración a las recomendaciones de YunoHost.", + "diagnosis_sshd_config_inconsistent": "Parece que el puerto SSH ha sido modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, hay un nuevo parámetro global 'security.ssh.ssh_port' disponible para evitar modificar manualmente la configuración.", + "diagnosis_sshd_config_inconsistent_details": "Por favor ejecute yunohost settings set security.ssh.ssh_port -v TU_PUERTO_SSH para definir el puerto SSH, y compruebe las diferencias yunohost tools regen-conf ssh --dry-run --with-diff y yunohost tools regen-conf ssh --force para resetear tu configuración a las recomendaciones de YunoHost.", "config_forbidden_keyword": "'{keyword}' es una palabra reservada, no puedes crear ni usar un panel de configuración con una pregunta que use esta id.", "config_no_panel": "No se ha encontrado ningún panel de configuración.", "config_unknown_filter_key": "La clave de filtrado '{filter_key}' es incorrecta.", @@ -589,7 +562,7 @@ "diagnosis_apps_bad_quality": "Esta aplicación está etiquetada como defectuosa en el catálogo de aplicaciones YunoHost. Podría ser un problema temporal mientras las personas responsables corrigen el asunto. Mientras tanto, la actualización de esta aplicación está desactivada.", "diagnosis_apps_broken": "Esta aplicación está etiquetada como defectuosa en el catálogo de aplicaciones YunoHost. Podría ser un problema temporal mientras las personas responsables corrigen el asunto. Mientras tanto, la actualización de esta aplicación está desactivada.", "diagnosis_apps_deprecated_practices": "La versión instalada de esta aplicación usa aún prácticas de empaquetado obsoletas. Deberías actualizarla.", - "diagnosis_apps_outdated_ynh_requirement": "La versión instalada de esta aplicación solo necesita YunoHost >= 2.x, lo que indica que no está al día con la buena praxis de ayudas y empaquetado recomendadas. Deberías actualizarla.", + "diagnosis_apps_outdated_ynh_requirement": "La versión instalada de esta aplicación solo necesita YunoHost >= 2.x o 3.x, lo que indica que no está al día con la buena praxis de ayudas y empaquetado recomendadas. Deberías actualizarla.", "domain_dns_conf_special_use_tld": "Este dominio se basa en un dominio de primer nivel (TLD) de usos especiales como .local o .test y no debería tener entradas DNS reales.", "diagnosis_sshd_config_insecure": "Parece que la configuración SSH ha sido modificada manualmente, y es insegura porque no tiene ninguna instrucción 'AllowGroups' o 'AllowUsers' para limitar el acceso a los usuarios autorizados.", "domain_dns_push_not_applicable": "La configuración automática de los registros DNS no puede realizarse en el dominio {domain}. Deberìas configurar manualmente los registros DNS siguiendo la documentación.", @@ -600,7 +573,7 @@ "domain_dns_push_success": "¡Registros DNS actualizados!", "domain_dns_push_failed_to_authenticate": "No se pudo autenticar en la API del registrador para el dominio '{domain}'. ¿Lo más probable es que las credenciales sean incorrectas? (Error: {error})", "domain_dns_registrar_experimental": "Hasta ahora, la comunidad de YunoHost no ha probado ni revisado correctamente la interfaz con la API de **{registrar}**. El soporte es **muy experimental**. ¡Ten cuidado!", - "domain_dns_push_record_failed": "No se pudo {acción} registrar {tipo}/{nombre}: {error}", + "domain_dns_push_record_failed": "No se pudo {action} registrar {type}/{name}: {error}", "domain_config_features_disclaimer": "Hasta ahora, habilitar/deshabilitar las funciones de correo o XMPP solo afecta la configuración de DNS recomendada y automática, ¡no las configuraciones del sistema!", "domain_config_mail_in": "Correos entrantes", "domain_config_mail_out": "Correos salientes", @@ -618,13 +591,9 @@ "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_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.", "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.", - "global_settings_setting_security_ssh_port": "Puerto SSH", "invalid_number": "Debe ser un miembro", "ldap_server_is_down_restart_it": "El servicio LDAP está inactivo, intente reiniciarlo...", - "invalid_password": "Contraseña inválida", "permission_cant_add_to_all_users": "El permiso {permission} no se puede agregar a todos los usuarios.", "log_domain_dns_push": "Enviar registros DNS para el dominio '{}'", "log_user_import": "Importar usuarios", @@ -637,7 +606,6 @@ "migration_0021_main_upgrade": "Iniciando actualización principal...", "migration_0021_still_on_buster_after_main_upgrade": "Algo salió mal durante la actualización principal, el sistema parece estar todavía en Debian Buster", "migration_0021_yunohost_upgrade": "Iniciando la actualización principal de YunoHost...", - "migration_0021_not_buster": "¡La distribución actual de Debian no es Buster!", "migration_0021_not_enough_free_space": "¡El espacio libre es bastante bajo en /var/! Debe tener al menos 1 GB libre para ejecutar esta migración.", "migration_0021_system_not_fully_up_to_date": "Su sistema no está completamente actualizado. Realice una actualización regular antes de ejecutar la migración a Bullseye.", "migration_0021_general_warning": "Tenga en cuenta que esta migración es una operación delicada. El equipo de YunoHost hizo todo lo posible para revisarlo y probarlo, pero la migración aún podría romper partes del sistema o sus aplicaciones.\n\nPor lo tanto, se recomienda:\n - Realice una copia de seguridad de cualquier dato o aplicación crítica. Más información en https://yunohost.org/backup;\n - Sea paciente después de iniciar la migración: dependiendo de su conexión a Internet y hardware, puede tomar algunas horas para que todo se actualice.", @@ -645,7 +613,6 @@ "migration_0021_modified_files": "Tenga en cuenta que se encontró que los siguientes archivos se modificaron manualmente y podrían sobrescribirse después de la actualización: {manually_modified_files}", "invalid_number_min": "Debe ser mayor que {min}", "pattern_email_forward": "Debe ser una dirección de correo electrónico válida, se acepta el símbolo '+' (por ejemplo, alguien+etiqueta@ejemplo.com)", - "global_settings_setting_security_ssh_password_authentication": "Permitir autenticación de contraseña para SSH", "invalid_number_max": "Debe ser menor que {max}", "ldap_attribute_already_exists": "El atributo LDAP '{attribute}' ya existe con el valor '{value}'", "log_app_config_set": "Aplicar configuración a la aplicación '{}'", @@ -657,8 +624,6 @@ "ldap_server_down": "No se puede conectar con el servidor LDAP", "log_backup_create": "Crear un archivo de copia de seguridad", "migration_ldap_can_not_backup_before_migration": "La copia de seguridad del sistema no se pudo completar antes de que fallara la migración. Error: {error}", - "global_settings_setting_security_experimental_enabled": "Habilite las funciones de seguridad experimentales (¡no habilite esto si no sabe lo que está haciendo!)", - "global_settings_setting_security_webadmin_allowlist_enabled": "Permita que solo algunas IP accedan al administrador web.", "migration_ldap_migration_failed_trying_to_rollback": "No se pudo migrar... intentando revertir el sistema.", "migration_0023_not_enough_space": "Deje suficiente espacio disponible en {path} para ejecutar la migración.", "migration_0023_postgresql_11_not_installed": "PostgreSQL no estaba instalado en su sistema. Nada que hacer.", @@ -684,5 +649,35 @@ "service_description_yunomdns": "Le permite llegar a su servidor usando 'yunohost.local' en su red local", "show_tile_cant_be_enabled_for_regex": "No puede habilitar 'show_tile' en este momento porque la URL para el permiso '{permission}' es una expresión regular", "show_tile_cant_be_enabled_for_url_not_defined": "No puede habilitar 'show_tile' en este momento, porque primero debe definir una URL para el permiso '{permission}'", - "regex_incompatible_with_tile": "/!\\ Empaquetadores! El permiso '{permission}' tiene show_tile establecido en 'true' y, por lo tanto, no puede definir una URL de expresión regular como la URL principal" + "regex_incompatible_with_tile": "/!\\ Empaquetadores! El permiso '{permission}' tiene show_tile establecido en 'true' y, por lo tanto, no puede definir una URL de expresión regular como la URL principal", + "global_settings_setting_backup_compress_tar_archives_help": "Cuando se creen nuevas copias de respaldo, comprimir los archivos (.tar.gz) en lugar de descomprimir los archivos (.tar). N.B.: activar esta opción quiere decir que los archivos serán más pequeños pero que el proceso tardará más y utilizará más CPU.", + "global_settings_setting_security_experimental_enabled_help": "Habilite las funciones de seguridad experimentales (¡no habilite esto si no sabe lo que está haciendo!)", + "global_settings_setting_nginx_compatibility_help": "Compromiso entre compatibilidad y seguridad para el servidor web NGINX. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", + "global_settings_setting_nginx_redirect_to_https_help": "Redirija las solicitudes HTTP a HTTPs de forma predeterminada (¡NO LO DESACTIVE a menos que realmente sepa lo que está haciendo!)", + "global_settings_setting_admin_strength": "Seguridad de la contraseña del administrador", + "global_settings_setting_user_strength": "Seguridad de la contraseña de usuario", + "global_settings_setting_postfix_compatibility_help": "Compromiso entre compatibilidad y seguridad para el servidor Postfix. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", + "global_settings_setting_ssh_compatibility_help": "Compromiso entre compatibilidad y seguridad para el servidor SSH. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", + "global_settings_setting_ssh_password_authentication_help": "Permitir autenticación de contraseña para SSH", + "global_settings_setting_ssh_port": "Puerto SSH", + "global_settings_setting_webadmin_allowlist_help": "Direcciones IP permitidas para acceder al webadmin. Separado por comas.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permita que solo algunas IP accedan al administrador web.", + "global_settings_setting_smtp_allow_ipv6_help": "Permitir el uso de IPv6 para enviar y recibir correo", + "global_settings_setting_smtp_relay_enabled_help": "El servidor relay de SMTP para enviar correo en lugar de esta instalación YunoHost. Útil si estás en una de estas situaciones: tu puerto 25 esta bloqueado por tu ISP o VPS, si estás en usado una IP marcada como residencial o DUHL, si no puedes configurar un DNS inverso o si el servidor no está directamente expuesto a internet y quieres utilizar otro servidor para enviar correos.", + "admins": "Administradores", + "all_users": "Todos los usuarios de YunoHost", + "app_action_failed": "No se ha podido ejecutar la acción {action} para la aplicación {app}", + "app_manifest_install_ask_init_admin_permission": "¿Quién debe tener acceso a las funciones de administración de esta aplicación? (Esto puede cambiarse posteriormente)", + "app_manifest_install_ask_init_main_permission": "¿Quién debería tener acceso a esta aplicación? (Esto puede cambiarse posteriormente)", + "ask_admin_fullname": "Nombre completo del administrador", + "ask_admin_username": "Nombre de usuario del administrador", + "ask_fullname": "Nombre completo", + "certmanager_cert_install_failed": "La instalación del certificado Let's Encrypt a fallado para {domains}", + "certmanager_cert_install_failed_selfsigned": "La instalación del certificado autofirmado ha fallado para {domains}", + "certmanager_cert_renew_failed": "La renovación del certificado Let's Encrypt ha fallado para {domains}", + "config_action_disabled": "No se ha podido ejecutar la acción '{action}' porque está desactivada, asegúrese de cumplir sus restricciones. ayuda: {help}", + "config_action_failed": "Error al ejecutar la acción '{action}': {error}", + "config_forbidden_readonly_type": "El tipo '{type}' no puede establecerse como solo lectura, utilice otro tipo para representar este valor (arg id relevante: '{id}').", + "diagnosis_using_stable_codename": "apt (el gestor de paquetes del sistema) está configurado actualmente para instalar paquetes de nombre en clave 'estable', en lugar del nombre en clave de la versión actual de Debian (bullseye).", + "diagnosis_using_stable_codename_details": "Esto suele deberse a una configuración incorrecta de su proveedor de alojamiento. Esto es peligroso, porque tan pronto como la siguiente versión de Debian se convierta en la nueva 'estable', apt querrá actualizar todos los paquetes del sistema sin pasar por un procedimiento de migración adecuado. Se recomienda arreglar esto editando la fuente de apt para el repositorio base de Debian, y reemplazar la palabra clave stable por bullseye. El fichero de configuración correspondiente debería ser /etc/apt/sources.list, o un fichero en /etc/apt/sources.list.d/." } diff --git a/locales/eu.json b/locales/eu.json index 60676d287..47ff773da 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -1,25 +1,22 @@ { - "password_too_simple_1": "Pasahitzak gutxienez zortzi karaktere izan behar ditu", + "password_too_simple_1": "Pasahitzak 8 karaktere izan behar ditu gutxienez", "action_invalid": "'{action}' eragiketa baliogabea da", "aborting": "Bertan behera uzten.", - "admin_password_changed": "Administrazio-pasahitza aldatu da", - "admin_password_change_failed": "Ezinezkoa izan da pasahitza aldatzea", "additional_urls_already_added": "'{url}' URL gehigarria '{permission}' baimenerako gehitu da dagoeneko", "additional_urls_already_removed": "'{url}' URL gehigarriari '{permission}' baimena kendu zaio dagoeneko", "admin_password": "Administrazio-pasahitza", "diagnosis_ip_global": "IP orokorra: {global}", - "app_argument_password_no_default": "Errorea egon da '{name}' pasahitzaren argumentua ikuskatzean: pasahitzak ezin du balio hori izan segurtasuna dela-eta", + "app_argument_password_no_default": "Errorea egon da '{name}' pasahitzaren argumentua ikuskatzean: pasahitzak ezin du balio hori izan segurtasun arrazoiengatik", "app_extraction_failed": "Ezinezkoa izan da instalazio fitxategiak ateratzea", - "app_requirements_unmeet": "{app}(e)k behar dituen baldintzak ez dira betetzen, {pkgname} ({version}) paketea {spec} izan behar da", "backup_deleted": "Babeskopia ezabatuta", "app_argument_required": "'{name}' argumentua ezinbestekoa da", - "certmanager_acme_not_configured_for_domain": "Ezinezkoa da ACME azterketa {domain} domeinurako burutzea une honetan nginx ezarpenek ez dutelako beharrezko kodea… Egiaztatu nginx ezarpenak egunean daudela 'yunohost tools regen-conf nginx --dry-run --with-diff' komandoa exekutatuz.", - "certmanager_domain_dns_ip_differs_from_public_ip": "'{domain}' domeinurako DNS balioak ez datoz bat zerbitzariaren IParekin. Mesedez, egiaztatu 'DNS balioak' (oinarrizkoa) kategoria diagnostikoen atalean. A balioak duela gutxi aldatu badituzu, itxaron hedatu daitezen (badaude DNSen hedapena ikusteko erramintak interneten). (Zertan ari zeren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", - "confirm_app_install_thirdparty": "KONTUZ! Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Kanpoko aplikazioek sistemaren integritate eta segurtasuna arriskuan jarri dezakete. Ziur asko EZ zenuke instalatu beharko zertan ari zaren ez badakizu. Aplikazio hau ez badabil edo sistema kaltetzen badu EZ DA LAGUNTZARIK EMANGO… aurrera jarraitu nahi duzu hala ere? Aukeratu '{answers}'", + "certmanager_acme_not_configured_for_domain": "Une honetan ezinezkoa da ACME azterketa {domain} domeinurako burutzea nginx ezarpenek ez dutelako beharrezko kodea… Egiaztatu nginx ezarpenak egunean daudela 'yunohost tools regen-conf nginx --dry-run --with-diff' komandoa exekutatuz.", + "certmanager_domain_dns_ip_differs_from_public_ip": "'{domain}' domeinurako DNS balioak ez datoz bat zerbitzariaren IParekin. Egiaztatu 'DNS balioak' (oinarrizkoa) kategoria diagnostikoen atalean. 'A' balioak duela gutxi aldatu badituzu, itxaron hedatu daitezen (badaude DNSen hedapena ikusteko erramintak interneten). (Zertan ari zeren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", + "confirm_app_install_thirdparty": "KONTUZ! Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Kanpoko aplikazioek sistemaren integritate eta segurtasuna arriskuan jar dezakete. Ziur asko EZ zenuke instalatu beharko zertan ari zaren ez badakizu. Aplikazio hau ez badabil edo sistema kaltetzen badu EZ DA LAGUNTZARIK EMANGO… aurrera jarraitu nahi duzu hala ere? Hautatu '{answers}'", "app_start_remove": "{app} ezabatzen…", "diagnosis_http_hairpinning_issue_details": "Litekeena da erantzulea zure kable-modem / routerra izatea. Honen eraginez, saretik kanpo daudenek zerbitzaria arazorik gabe erabili ahal izango dute, baina sare lokalean bertan daudenek (ziur asko zure kasua) ezingo dute kanpoko IPa edo domeinu izena erabili zerbitzarira konektatzeko. Egoera hobetu edo guztiz konpontzeko, irakurri dokumentazioa", "diagnosis_http_special_use_tld": "{domain} domeinua top-level domain (TLD) motakoa da .local edo .test bezala eta ez du sare lokaletik kanpo eskuragarri zertan egon.", - "diagnosis_ip_weird_resolvconf_details": "/etc/resolv.conf fitxategia symlink bat izan beharko litzateke 127.0.0.1ra adi dagoen /etc/resolvconf/run/resolv.conf fitxategira (dnsmasq). DNS ebazleak eskuz konfiguratu nahi badituzu, mesedez aldatu /etc/resolv.dnsmasq.conf fitxategia.", + "diagnosis_ip_weird_resolvconf_details": "/etc/resolv.conf fitxategia symlink bat izan beharko litzateke 127.0.0.1ra adi dagoen /etc/resolvconf/run/resolv.conf fitxategira (dnsmasq). DNS ebazleak eskuz konfiguratu nahi badituzu, aldatu /etc/resolv.dnsmasq.conf fitxategia.", "diagnosis_ip_connected_ipv4": "Zerbitzaria IPv4 bidez dago internetera konektatuta!", "diagnosis_basesystem_ynh_inconsistent_versions": "YunoHost paketeen bertsioak ez datoz bat… ziur asko noizbait eguneraketa batek kale egin edo erabat amaitu ez zuelako.", "diagnosis_high_number_auth_failures": "Azken aldian kale egin duten saio-hasiera saiakera ugari egon dira. Egiaztatu fail2ban martxan dabilela eta egoki konfiguratuta dagoela, edo erabili beste ataka bat SSHrako dokumentazioan azaldu bezala.", @@ -28,7 +25,7 @@ "app_install_files_invalid": "Ezin dira fitxategi hauek instalatu", "diagnosis_description_ip": "Internet konexioa", "diagnosis_description_dnsrecords": "DNS erregistroak", - "app_label_deprecated": "Komando hau zaharkitua dago! Mesedez, erabili 'yunohost user permission update' komando berria aplikazioaren etiketa kudeatzeko.", + "app_label_deprecated": "Komando hau zaharkitua dago! Erabili 'yunohost user permission update' komando berria aplikazioaren etiketa kudeatzeko.", "confirm_app_install_danger": "KONTUZ! Aplikazio hau esperimentala da (edo ez dabil)! Ez zenuke instalatu beharko zertan ari zaren ez badakizu. Aplikazio hau ez badabil edo sistema kaltetzen badu, EZ DA LAGUNTZARIK EMANGO… aurrera jarraitu nahi al duzu hala ere? Aukeratu '{answers}'", "diagnosis_description_systemresources": "Sistemaren baliabideak", "backup_csv_addition_failed": "Ezinezkoa izan da fitxategiak CSV fitxategira kopiatzea", @@ -44,7 +41,6 @@ "diagnosis_ip_not_connected_at_all": "Badirudi zerbitzaria ez dagoela internetera konektatuta!?", "app_already_up_to_date": "{app} egunean da dagoeneko", "app_change_url_success": "{app} aplikazioaren URLa {domain}{path} da orain", - "admin_password_too_long": "Mesedez, aukeratu 127 karaktere baino laburragoa den pasahitz bat", "app_action_broke_system": "Eragiketa honek {services} zerbitzu garrantzitsua(k) hondatu d(it)uela dirudi", "diagnosis_basesystem_hardware_model": "Zerbitzariaren modeloa {model} da", "already_up_to_date": "Ez dago egiteko ezer. Guztia dago egunean.", @@ -84,8 +80,6 @@ "app_upgrade_failed": "Ezinezkoa izan da {app} eguneratzea: {error}", "app_upgrade_app_name": "Orain {app} eguneratzen…", "app_upgraded": "{app} eguneratu da", - "ask_firstname": "Izena", - "ask_lastname": "Abizena", "ask_main_domain": "Domeinu nagusia", "config_forbidden_keyword": "'{keyword}' etiketa sistemak bakarrik erabil dezake; ezin da ID hau daukan baliorik sortu edo erabili.", "config_unknown_filter_key": "'{filter_key}' filtroaren kakoa ez da zuzena.", @@ -118,12 +112,12 @@ "apps_catalog_updating": "Aplikazioen katalogoa eguneratzen…", "certmanager_cert_signing_failed": "Ezinezkoa izan da ziurtagiri berria sinatzea", "certmanager_cert_renew_success": "Let's Encrypt ziurtagiria berriztu da '{domain}' domeinurako", - "app_requirements_checking": "{app}(e)k behar dituen paketeak ikuskatzen…", + "app_requirements_checking": "{app}(e)k behar dituen betekizunak egiaztatzen…", "certmanager_unable_to_parse_self_CA_name": "Ezinezkoa izan da norberak sinatutako ziurtagiriaren izena prozesatzea (fitxategia: {file})", "app_remove_after_failed_install": "Aplikazioa ezabatzen instalatzerakoan errorea dela-eta…", "diagnosis_basesystem_ynh_single_version": "{package} bertsioa: {version} ({repo})", "diagnosis_failed_for_category": "'{category}' ataleko diagnostikoak kale egin du: {error}", - "diagnosis_cache_still_valid": "(Cachea oraindik baliogarria da {category} ataleko diagnosirako. Ez da berrabiaraziko!)", + "diagnosis_cache_still_valid": "(Katxea oraindik baliogarria da {category} ataleko diagnosirako. Ez da berrabiaraziko!)", "diagnosis_found_errors": "{category} atalari dago(z)kion {errors} arazo aurkitu d(ir)a!", "diagnosis_found_warnings": "{category} atalari dagokion eta hobetu daite(z)keen {warnings} abisu aurkitu d(ir)a.", "diagnosis_ip_connected_ipv6": "Zerbitzaria IPv6 bidez dago internetera konektatuta!", @@ -141,12 +135,12 @@ "diagnosis_http_unreachable": "Badirudi {domain} domeinua ez dagoela eskuragarri HTTP bidez sare lokaletik kanpo.", "apps_catalog_failed_to_download": "Ezinezkoa izan da {apps_catalog} aplikazioen zerrenda eskuratzea: {error}", "apps_catalog_init_success": "Abiarazi da aplikazioen katalogo sistema!", - "apps_catalog_obsolete_cache": "Aplikazioen katalogoaren cachea hutsik edo zaharkituta dago.", + "apps_catalog_obsolete_cache": "Aplikazioen katalogoaren katxea hutsik edo zaharkituta dago.", "diagnosis_description_mail": "Posta elektronikoa", "diagnosis_http_connection_error": "Arazoa konexioan: ezin izan da domeinu horretara konektatu, litekeena da eskuragarri ez egotea.", "diagnosis_description_web": "Weba", "diagnosis_display_tip": "Aurkitu diren arazoak ikusteko joan administrazio-atariko Diagnostikoak atalera, edo exekutatu 'yunohost diagnosis show --issues --human-readable' komandoak nahiago badituzu.", - "diagnosis_dns_point_to_doc": "Mesedez, irakurri dokumentazioa DNS erregistroekin laguntza behar baduzu.", + "diagnosis_dns_point_to_doc": "Irakurri dokumentazioa DNS erregistroekin laguntza behar baduzu.", "diagnosis_mail_ehlo_unreachable": "SMTP posta zerbitzaria ez dago eskuragarri IPv{ipversion}ko sare lokaletik kanpo eta, beraz, ez da posta elektronikoa jasotzeko gai.", "diagnosis_mail_ehlo_bad_answer_details": "Litekeena da zure zerbitzaria ez den beste gailu batek erantzun izana.", "diagnosis_mail_blacklist_listed_by": "Zure domeinua edo {item} IPa {blacklist_name} zerrenda beltzean ageri da", @@ -154,10 +148,10 @@ "diagnosis_http_could_not_diagnose_details": "Errorea: {error}", "diagnosis_http_hairpinning_issue": "Dirudienez zure sareak ez du hairpinninga gaituta.", "diagnosis_http_partially_unreachable": "Badirudi {domain} domeinua ezin dela bisitatu HTTP bidez IPv{failed} sare lokaletik kanpo, bai ordea IPv{passed} erabiliz.", - "backup_archive_cant_retrieve_info_json": "Ezinezkoa izan da '{archive}' fitxategiko informazioa eskuratzea… info.json ezin izan da eskuratu (edo ez da baliozko jsona).", + "backup_archive_cant_retrieve_info_json": "Ezinezkoa izan da '{archive}' fitxategiko informazioa eskuratzea… info.json fitxategia ezin izan da eskuratu (edo ez da baliozko jsona).", "diagnosis_domain_expiration_not_found": "Ezinezkoa izan da domeinu batzuen iraungitze data egiaztatzea", "diagnosis_domain_expiration_not_found_details": "Badirudi {domain} domeinuari buruzko WHOIS informazioak ez duela zehazten noiz iraungiko den.", - "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.)", + "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.", @@ -223,8 +217,8 @@ "certmanager_cert_install_success_selfsigned": "Norberak sinatutako ziurtagiria instalatu da '{domain}' domeinurako", "certmanager_domain_cert_not_selfsigned": "{domain} domeinurako ziurtagiria ez da norberak sinatutakoa. Ziur al zaude ordezkatu nahi duzula? (Erabili '--force' hori egiteko.)", "certmanager_certificate_fetching_or_enabling_failed": "{domain} domeinurako ziurtagiri berriak kale egin du…", - "certmanager_domain_http_not_working": "Ez dirudi {domain} domeinua HTTP bidez ikusgai dagoenik. Mesedez, egiaztatu 'Weba' atala diagnosien gunean informazio gehiagorako. (Zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", - "certmanager_hit_rate_limit": "{domain} domeinu-multzorako ziurtagiri gehiegi jaulki dira dagoeneko. Mesedez, saia saitez geroago. Ikus https://letsencrypt.org/docs/rate-limits/ xehetasun gehiagorako", + "certmanager_domain_http_not_working": "Ez dirudi {domain} domeinua HTTP bidez ikusgai dagoenik. Egiaztatu 'Weba' atala diagnosien gunean informazio gehiagorako. (Zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", + "certmanager_hit_rate_limit": "{domain} domeinu-multzorako ziurtagiri gehiegi jaulki dira dagoeneko. Saia saitez geroago. Ikus https://letsencrypt.org/docs/rate-limits/ xehetasun gehiagorako", "certmanager_no_cert_file": "Ezinezkoa izan da {domain} domeinurako ziurtagiri fitxategia irakurrtzea (fitxategia: {file})", "certmanager_self_ca_conf_file_not_found": "Ezinezkoa izan da konfigurazio-fitxategia aurkitzea norberak sinatutako ziurtagirirako (fitxategia: {file})", "confirm_app_install_warning": "Adi: litekeena da aplikazio hau ibiltzea baina ez dago YunoHostera egina. Ezaugarri batzuk, SSO edo babeskopia/lehengoratzea esaterako, desgaituta egon daitezke. Instalatu hala ere? [{answers}] ", @@ -242,8 +236,8 @@ "diagnosis_apps_broken": "Aplikazio hau YunoHosten aplikazioen katalogoan hondatuta dagoela ageri da. Agian behin-behineko kontua da arduradunak konpondu bitartean. Oraingoz, ezin da aplikazioa eguneratu.", "diagnosis_apps_deprecated_practices": "Instalatutako aplikazio honen bertsioak oraindik darabiltza zaharkitutako pakete-jarraibideak. Eguneratzea hausnartu beharko zenuke.", "diagnosis_apps_issue": "Arazo bat dago {app} aplikazioarekin", - "diagnosis_apps_not_in_app_catalog": "Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Iraganean egon bazen eta ezabatu izan balitz, desinstalatzea litzateke onena, ez baitu eguneraketarik jasoko eta sistemaren integritate eta segurtasuna arriskuan jarri lezakeelako.", - "diagnosis_apps_outdated_ynh_requirement": "Instalatutako aplikazio honen bertsioak yunohost >= 2.x baino ez du behar, eta horrek egungo pakete-jardunbideekin bat ez datorrela iradokitzen du. Eguneratzen saiatu beharko zinateke.", + "diagnosis_apps_not_in_app_catalog": "Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Iraganean egon bazen eta orain ez badago, desinstalatzea litzateke onena, ez baitu eguneraketarik jasoko eta sistemaren integritate eta segurtasuna arriskuan jar lezakeelako.", + "diagnosis_apps_outdated_ynh_requirement": "Instalatutako aplikazio honen bertsioak yunohost >= 2.x edo 3.x baino ez du behar, eta horrek eguneratua izan ez dela eta egungo pakete-jardunbideekin bat ez datorrela iradokitzen du. Eguneratzen saiatu beharko zinateke.", "diagnosis_description_apps": "Aplikazioak", "domain_dns_conf_special_use_tld": "Domeinu hau top-level domain (TLD) erabilera bereziko motakoa da .local edo .test bezala eta ez du DNS ezarpenik behar.", "log_permission_create": "Sortu '{}' baimena", @@ -253,10 +247,7 @@ "group_user_already_in_group": "{user} erabiltzailea {group} taldean dago dagoeneko", "firewall_reloaded": "Suebakia birkargatu da", "domain_unknown": "'{domain}' domeinua ezezaguna da", - "global_settings_cant_serialize_settings": "Ezinezkoa izan da konfikurazio-datuak serializatzea, zergatia: {reason}", - "global_settings_setting_security_nginx_redirect_to_https": "Birbideratu HTTP eskaerak HTTPSra (EZ ITZALI hau ez badakizu zertan ari zaren!)", "group_deleted": "'{group}' taldea ezabatu da", - "invalid_password": "Pasahitza ez da zuzena", "log_domain_main_domain": "Lehenetsi '{}' domeinua", "log_user_group_update": "Moldatu '{}' taldea", "dyndns_could_not_check_available": "Ezinezkoa izan da {domain} {provider}(e)n eskuragarri dagoen egiaztatzea.", @@ -280,22 +271,15 @@ "extracting": "Ateratzen…", "diagnosis_ports_unreachable": "{port}. ataka ez dago eskuragarri kanpotik.", "diagnosis_regenconf_manually_modified_details": "Ez dago arazorik zertan ari zaren baldin badakizu! YunoHostek fitxategi hau automatikoki eguneratzeari utziko dio… Baina kontuan izan YunoHosten eguneraketek aldaketa garrantzitsuak izan ditzaketela. Nahi izatekotan, desberdintasunak aztertu ditzakezu yunohost tools regen-conf {category} --dry-run --with-diff komandoa exekutatuz, eta gomendatutako konfiguraziora bueltatu yunohost tools regen-conf {category} --force erabiliz", - "experimental_feature": "Adi: Funtzio hau esperimentala eta ezegonkorra da, ez zenuke erabili beharko ez badakizu zertan ari zaren.", - "global_settings_cant_write_settings": "Ezinezkoa izan da konfigurazio fitxategia gordetzea, zergatia: {reason}", "dyndns_domain_not_provided": "{provider} DynDNS enpresak ezin du {domain} domeinua eskaini.", "firewall_reload_failed": "Ezinezkoa izan da suebakia birkargatzea", - "global_settings_setting_security_password_admin_strength": "Administrazio-pasahitzaren segurtasuna", "hook_name_unknown": "'{name}' 'hook' izen ezezaguna", "domain_deletion_failed": "Ezinezkoa izan da {domain} ezabatzea: {error}", - "global_settings_setting_security_nginx_compatibility": "Bateragarritasun eta segurtasun arteko gatazka NGINX web zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", "log_regen_conf": "Berregin '{}' sistemaren konfigurazioa", "dpkg_lock_not_available": "Ezin da komando hau une honetan exekutatu beste aplikazio batek dpkg (sistemaren paketeen kudeatzailea) blokeatuta duelako, erabiltzen ari baita", "group_created": "'{group}' taldea sortu da", - "global_settings_setting_security_password_user_strength": "Erabiltzaile-pasahitzaren segurtasuna", - "global_settings_setting_security_experimental_enabled": "Gaitu segurtasun funtzio esperimentalak (ez ezazu egin ez badakizu zertan ari zaren!)", - "good_practices_about_admin_password": "Administrazio-pasahitz berria ezartzear zaude. Pasahitzak zortzi karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", - "log_help_to_get_failed_log": "Ezin izan da '{desc}' eragiketa exekutatu. Mesedez, laguntza nahi baduzu partekatu eragiketa honen erregistro osoa 'yunohost log share {name}' komandoa erabiliz", - "global_settings_setting_security_webadmin_allowlist_enabled": "Baimendu IP zehatz batzuk bakarrik administrazio-atarian.", + "good_practices_about_admin_password": "Administrazio-pasahitz berria ezartzear zaude. Pasahitzak 8 karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", + "log_help_to_get_failed_log": "Ezin izan da '{desc}' eragiketa exekutatu. Laguntza nahi baduzu partekatu eragiketa honen erregistro osoa 'yunohost log share {name}' komandoa erabiliz", "group_unknown": "'{group}' taldea ezezaguna da", "group_updated": "'{group}' taldea eguneratu da", "group_update_failed": "Ezinezkoa izan da '{group}' taldea eguneratzea: {error}", @@ -304,10 +288,10 @@ "log_permission_delete": "Ezabatu '{}' baimena", "group_already_exist": "{group} taldea existitzen da dagoeneko", "group_user_not_in_group": "{user} erabiltzailea ez dago {group} taldean", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Operadore batzuek ez dute alderantzizko DNSa konfiguratzen uzten (edo funtzioa ez dabil…). IPv4rako alderantzizko DNSa zuzen konfiguratuta badago, IPv6 desgaitzen saia zaitezke posta elektronikoa bidaltzeko, yunohost settings set smtp.allow_ipv6 -v off exekutatuz. Adi: honek esan nahi du ez zarela gai izango IPv6 bakarrik darabilten zerbitzari apurren posta elektronikoa jasotzeko edo beraiei bidaltzeko.", - "diagnosis_sshd_config_inconsistent": "Dirudienez SSH ataka eskuz aldatu da /etc/ssh/sshd_config fitxategian. YunoHost 4.2tik aurrera 'security.ssh.port' izeneko ezarpen orokor bat dago konfigurazioa eskuz aldatzea ekiditeko.", - "diagnosis_sshd_config_inconsistent_details": "Mesedez, exekutatu yunohost settings set security.ssh.port -v YOUR_SSH_PORT SSH ataka zehazteko, egiaztatu yunohost tools regen-conf ssh --dry-run --with-diff erabiliz eta yunohost tools regen-conf ssh --force exekutatu gomendatutako konfiguraziora bueltatu nahi baduzu.", - "domain_dns_push_failed_to_authenticate": "Ezinezkoa izan da '{domain}' domeinurako APIa erabiliz erregistro-enpresan saioa hastea. Zuzenak al dira datuak? (Errorea: {error})", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Operadore batzuek ez dute alderantzizko DNSa konfiguratzen uzten (edo funtzioa ez dabil…). IPv4rako alderantzizko DNSa zuzen konfiguratuta badago, IPv6 desgaitzen saia zaitezke posta elektronikoa bidaltzeko, yunohost settings set email.smtp.smtp_allow_ipv6 -v off exekutatuz. Adi: honek esan nahi du ez zarela gai izango IPv6 bakarrik darabilten zerbitzari apurren posta elektronikoa jasotzeko edo beraiei bidaltzeko.", + "diagnosis_sshd_config_inconsistent": "Dirudienez SSH ataka eskuz aldatu da /etc/ssh/sshd_config fitxategian. YunoHost 4.2tik aurrera 'security.ssh.ssh_port' izeneko ezarpen orokor bat dago konfigurazioa eskuz aldatzea ekiditeko.", + "diagnosis_sshd_config_inconsistent_details": "Exekutatu yunohost settings set security.ssh.ssh_port -v SSH_ATAKA SSH ataka zehazteko, egiaztatu yunohost tools regen-conf ssh --dry-run --with-diff erabiliz eta yunohost tools regen-conf ssh --force exekutatu gomendatutako konfiguraziora bueltatu nahi baduzu.", + "domain_dns_push_failed_to_authenticate": "Ezinezkoa izan da '{domain}' domeinuko erregistro-enpresan APIa erabiliz saioa hastea. Ziurrenik datuak ez dira zuzenak. (Errorea: {error})", "domain_dns_pushing": "DNS ezarpenak bidaltzen…", "diagnosis_sshd_config_insecure": "Badirudi SSH konfigurazioa eskuz aldatu dela eta ez da segurua ez duelako 'AllowGroups' edo 'AllowUsers' baldintzarik jartzen fitxategien atzitzea oztopatzeko.", "disk_space_not_sufficient_update": "Ez dago aplikazio hau eguneratzeko nahikoa espaziorik", @@ -317,14 +301,13 @@ "domain_dns_push_success": "DNS ezarpenak eguneratu dira!", "domain_dns_push_failed": "DNS ezarpenen eguneratzeak kale egin du.", "domain_dns_push_partial_failure": "DNS ezarpenak erdipurdi eguneratu dira: jakinarazpen/errore batzuk egon dira.", - "global_settings_setting_smtp_relay_host": "YunoHosten ordez posta elektronikoa bidaltzeko SMTP relay helbidea. Erabilgarri izan daiteke egoera hauetan: operadore edo VPS enpresak 25. ataka blokeatzen badu, DUHLen zure etxeko IPa ageri bada, ezin baduzu alderantzizko DNSa ezarri edo zerbitzari hau ez badago zuzenean internetera konektatuta baina posta elektronikoa bidali nahi baduzu.", "group_deletion_failed": "Ezinezkoa izan da '{group}' taldea ezabatzea: {error}", "invalid_number_min": "{min} baino handiagoa izan behar da", "invalid_number_max": "{max} baino txikiagoa izan behar da", "diagnosis_services_bad_status": "{service} zerbitzua {status} dago :(", "diagnosis_ports_needed_by": "{category} funtzioetarako ezinbestekoa da ataka hau eskuragarri egotea ({service} zerbitzua)", "diagnosis_package_installed_from_sury": "Sistemaren pakete batzuen lehenagoko bertsioak beharko lirateke", - "global_settings_setting_smtp_relay_password": "SMTP relay helbideko pasahitza", + "global_settings_setting_smtp_relay_password": "SMTP relay pasahitza", "global_settings_setting_smtp_relay_port": "SMTP relay ataka", "domain_deleted": "Domeinua ezabatu da", "domain_dyndns_root_unknown": "Ez da ezagutzen DynDNSaren root domeinua", @@ -332,7 +315,7 @@ "domain_registrar_is_not_configured": "Oraindik ez da {domain} domeinurako erregistro-enpresa ezarri.", "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_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_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!", @@ -345,7 +328,6 @@ "domain_config_auth_application_key": "Aplikazioaren gakoa", "domain_config_auth_application_secret": "Aplikazioaren gako sekretua", "domain_config_auth_consumer_key": "Erabiltzailearen gakoa", - "global_settings_setting_smtp_allow_ipv6": "Baimendu IPv6 posta elektronikoa jaso eta bidaltzeko", "group_cannot_be_deleted": "{group} taldea ezin da eskuz ezabatu.", "log_domain_config_set": "Aldatu '{}' domeinuko ezarpenak", "log_domain_dns_push": "Bidali '{}' domeinuaren DNS ezarpenak", @@ -358,10 +340,7 @@ "domain_config_features_disclaimer": "Oraingoz, posta elektronikoa edo XMPP funtzioak gaitu/desgaitzeak DNS ezarpenei soilik eragiten die, ez sistemaren konfigurazioari!", "domain_config_mail_out": "Bidalitako mezuak", "domain_config_xmpp": "Bat-bateko mezularitza (XMPP)", - "global_settings_bad_choice_for_enum": "{setting} ezarpenerako aukera okerra. '{choice}' ezarri da baina hauek dira aukerak: {available_choices}", - "global_settings_setting_security_postfix_compatibility": "Bateragarritasun eta segurtasun arteko gatazka Postfix zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", - "global_settings_setting_security_ssh_compatibility": "Bateragarritasun eta segurtasun arteko gatazka SSH zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", - "good_practices_about_user_password": "Erabiltzaile-pasahitz berria ezartzear zaude. Pasahitzak zortzi karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", + "good_practices_about_user_password": "Erabiltzaile-pasahitz berria ezartzear zaude. Pasahitzak 8 karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", "group_cannot_edit_all_users": "'all_users' taldea ezin da eskuz moldatu. YunoHosten izena emanda dauden erabiltzaile guztiak barne dituen talde berezia da", "invalid_number": "Zenbaki bat izan behar da", "ldap_attribute_already_exists": "'{attribute}' LDAP funtzioa existitzen da dagoeneko eta '{value}' balioa dauka", @@ -378,9 +357,9 @@ "diagnosis_mail_queue_unavailable": "Ezinezkoa da ilaran zenbat posta elektroniko dauden kontsultatzea", "log_user_create": "Gehitu '{}' erabiltzailea", "group_cannot_edit_visitors": "'bisitariak' taldea ezin da eskuz moldatu. Saiorik hasi gabeko bisitariak barne hartzen dituen talde berezia da", - "diagnosis_ram_verylow": "RAM memoriaren {available} baino ez ditu erabilgarri sistemak; memoria guztiaren ({total}) %{available_percent}a bakarrik!", - "diagnosis_ram_low": "RAM memoriaren {available} ditu erabilgarri sistemak; memoria guztiaren ({total}) %{available_percent}a. Adi ibili.", - "diagnosis_ram_ok": "RAM memoriaren {available} ditu oraindik erabilgarri sistemak; memoria guztiaren ({total}) %{available_percent}a.", + "diagnosis_ram_verylow": "Sistemak RAM memoriaren {available} baino ez ditu erabilgarri; memoria guztiaren ({total}) %{available_percent}a bakarrik!", + "diagnosis_ram_low": "Sistemak RAM memoriaren {available} ditu erabilgarri; memoria guztiaren ({total}) %{available_percent}a. Adi ibili.", + "diagnosis_ram_ok": "Sistemak RAM memoriaren {available} ditu oraindik erabilgarri; memoria guztiaren ({total}) %{available_percent}a.", "diagnosis_swap_none": "Sistemak ez du swap-ik. Gutxienez {recommended} izaten saiatu beharko zinateke, sistema memoriarik gabe gera ez dadin.", "diagnosis_swap_ok": "Sistemak {total} swap dauzka!", "diagnosis_regenconf_allgood": "Konfigurazio-fitxategi guztiak bat datoz gomendatutako ezarpenekin!", @@ -399,7 +378,7 @@ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Oraingo alderantzizko DNSa: {rdns_domain}
Esperotako balioa: {ehlo_domain}", "diagnosis_mail_queue_too_big": "Mezu gehiegi posta elektronikoaren ilaran: ({nb_pending} mezu)", "diagnosis_ports_could_not_diagnose_details": "Errorea: {error}", - "diagnosis_swap_tip": "Mesedez, kontuan hartu zerbitzari honen swap memoria SD edo SSD euskarri batean gordetzeak euskarri horren bizi-iraupena izugarri laburtu dezakeela.", + "diagnosis_swap_tip": "Kontuan hartu zerbitzari honen swap memoria SD edo SSD euskarri batean gordetzeak euskarri horren bizi-iraupena izugarri laburtu dezakeela.", "invalid_regex": "'Regexa' ez da zuzena: '{regex}'", "group_creation_failed": "Ezinezkoa izan da '{group}' taldea sortzea: {error}", "log_user_permission_reset": "Berrezarri '{}' baimena", @@ -416,9 +395,6 @@ "diagnosis_ports_forwarding_tip": "Arazoa konpontzeko, litekeena da operadorearen routerrean ataken birbideraketa konfiguratu behar izatea, https://yunohost.org/isp_box_config-n agertzen den bezala", "domain_creation_failed": "Ezinezkoa izan da {domain} domeinua sortzea: {error}", "domains_available": "Erabilgarri dauden domeinuak:", - "global_settings_setting_pop3_enabled": "Gaitu POP3 protokoloa posta zerbitzarirako", - "global_settings_setting_security_ssh_port": "SSH ataka", - "global_settings_unknown_type": "Gertaera ezezaguna, {setting} ezarpenak {unknown_type} mota duela dirudi baina mota hori ez da sistemarekin bateragarria.", "group_already_exist_on_system": "{group} taldea existitzen da dagoeneko sistemaren taldeetan", "diagnosis_processes_killed_by_oom_reaper": "Memoria agortu eta sistemak prozesu batzuk amaituarazi behar izan ditu. Honek esan nahi du sistemak ez duela memoria nahikoa edo prozesuren batek memoria gehiegi behar duela. Amaituarazi d(ir)en prozesua(k):\n{kills_summary}", "hook_exec_not_terminated": "Aginduak ez du behar bezala amaitu: {path}", @@ -427,20 +403,16 @@ "log_remove_on_failed_restore": "Ezabatu '{}' babeskopia baten lehengoratzeak huts egin eta gero", "diagnosis_package_installed_from_sury_details": "Sury izena duen kanpoko biltegi batetik instalatu dira pakete batzuk, nahi gabe. YunoHosten taldeak hobekuntzak egin ditu pakete hauek kudeatzeko, baina litekeena da PHP7.3 aplikazioak Stretch sistema eragilean instalatu zituzten kasu batzuetan arazoak sortzea. Egoera hau konpontzeko, honako komando hau exekutatu beharko zenuke: {cmd_to_fix}", "log_help_to_get_log": "'{desc}' eragiketaren erregistroa ikusteko, exekutatu 'yunohost log show {name}'", - "dpkg_is_broken": "Ezin duzu une honetan egin dpkg/APT (sistemaren pakateen kudeatzaileak) hondatutako itxura dutelako… Arazoa konpontzeko SSH bidez konektatzen saia zaitezke eta ondoren exekutatu 'sudo apt install --fix-broken' edota 'sudo dpkg --configure -a'.", + "dpkg_is_broken": "Une honetan ezinezkoa da sistemaren dpkg/APT pakateen kudeatzaileek hondatutako itxura dutelako… Arazoa konpontzeko SSH bidez konektatzen saia zaitezke eta ondoren exekutatu 'sudo apt install --fix-broken' edota 'sudo dpkg --configure -a' edota 'sudo dpkg --audit'.", "domain_cannot_remove_main": "Ezin duzu '{domain}' ezabatu domeinu nagusia delako. Beste domeinu bat ezarri beharko duzu nagusi bezala 'yunohost domain main-domain -n ' erabiliz; honako hauek dituzu aukeran: {other_domains}", "domain_created": "Sortu da domeinua", "domain_dyndns_already_subscribed": "Dagoeneko izena eman duzu DynDNS domeinu batean", "domain_hostname_failed": "Ezinezkoa izan da hostname berria ezartzea. Honek arazoak ekar litzake etorkizunean (litekeena da ondo egotea).", - "domain_uninstall_app_first": "Honako aplikazio hauek domeinuan instalatuta daude:\n{apps}\n\nMesedez, desinstalatu 'yunohost app remove the_app_id' exekutatuz edo alda itzazu beste domeinu batera 'yunohost app change-url the_app_id' erabiliz domeinua ezabatu baino lehen", + "domain_uninstall_app_first": "Honako aplikazio hauek domeinuan instalatuta daude:\n{apps}\n\nDesinstalatu 'yunohost app remove the_app_id' exekutatuz edo alda itzazu beste domeinu batera 'yunohost app change-url the_app_id' erabiliz domeinua ezabatu baino lehen", "file_does_not_exist": "{path} fitxategia ez da existitzen.", "firewall_rules_cmd_failed": "Suebakiko arau batzuen exekuzioak huts egin du. Informazio gehiago erregistroetan.", "log_app_remove": "Ezabatu '{}' aplikazioa", - "global_settings_cant_open_settings": "Ezinezkoa izan da konfigurazio fitxategia irekitzea, zergatia: {reason}", - "global_settings_reset_success": "Lehengo ezarpenak {path}-n gorde dira", - "global_settings_unknown_setting_from_settings_file": "Gako ezezaguna ezarpenetan: '{setting_key}', baztertu eta gorde ezazu hemen: /etc/yunohost/settings-unknown.json", "domain_remove_confirm_apps_removal": "Domeinu hau ezabatzean aplikazio hauek desinstalatuko dira:\n{apps}\n\nZiur al zaude? [{answers}]", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Baimendu DSA gakoa (zaharkitua) SSH zerbitzuaren konfiguraziorako", "hook_list_by_invalid": "Aukera hau ezin da 'hook'ak zerrendatzeko erabili", "installation_complete": "Instalazioa amaitu da", "hook_exec_failed": "Ezinezkoa izan da agindua exekutatzea: {path}", @@ -452,7 +424,7 @@ "log_remove_on_failed_install": "Ezabatu '{}' instalazioak huts egin ondoren", "log_domain_add": "Gehitu '{}' domeinua sistemaren konfiguraziora", "log_dyndns_subscribe": "Eman izena YunoHosten '{}' azpidomeinuan", - "diagnosis_no_cache": "Oraindik ez dago '{category}' atalerako diagnostikoaren cacherik", + "diagnosis_no_cache": "Oraindik ez dago '{category}' atalerako diagnostikoaren katxerik", "diagnosis_mail_queue_ok": "Posta elektronikoaren ilaran zain dauden mezuak: {nb_pending}", "global_settings_setting_smtp_relay_user": "SMTP relay erabiltzailea", "domain_cert_gen_failed": "Ezinezkoa izan da ziurtagiria sortzea", @@ -465,21 +437,17 @@ "log_user_permission_update": "Eguneratu '{}' baimenerako sarbideak", "log_user_update": "Eguneratu '{}' erabiltzailearen informazioa", "dyndns_no_domain_registered": "Ez dago DynDNSrekin izena emandako domeinurik", - "global_settings_bad_type_for_setting": "{setting} ezarpenerako mota okerra. {received_type} ezarri da, {expected_type} espero zen", "diagnosis_mail_fcrdns_dns_missing": "Ez da alderantzizko DNSrik ezarri IPv{ipversion}rako. Litekeena da hartzaileak posta elektroniko batzuk jaso ezin izatea edo mezuok spam modura etiketatuak izatea.", "log_backup_create": "Sortu babeskopia fitxategia", - "global_settings_setting_backup_compress_tar_archives": "Babeskopia berriak sortzean, konprimitu fitxategiak (.tar.gz) konprimitu gabeko fitxategien (.tar) ordez. Aukera hau gaitzean babeskopiek espazio gutxiago beharko dute, baina hasierako prozesua luzeagoa izango da eta CPUari lan handiagoa eragingo dio.", - "global_settings_setting_security_webadmin_allowlist": "Administrazio-ataria bisita dezaketen IP helbideak, koma bidez bereiziak.", - "global_settings_key_doesnt_exists": "'{settings_key}' gakoa ez da existitzen konfigurazio orokorrean; erabilgarri dauden gakoak ikus ditzakezu 'yunohost settings list' exekutatuz", - "global_settings_setting_ssowat_panel_overlay_enabled": "Gaitu SSOwat paneleko \"overlay\"a", + "global_settings_setting_ssowat_panel_overlay_enabled": "Gaitu YunoHosten atarira daraman lasterbidea aplikazioetan", "log_backup_restore_system": "Lehengoratu sistema babeskopia fitxategi batetik", "log_domain_remove": "Ezabatu '{}' domeinua sistemaren ezarpenetatik", - "log_link_to_failed_log": "Ezinezkoa izan da '{desc}' eragiketa exekutatzea. Mesedez, laguntza nahi izanez gero, partekatu erakigeta honen erregistro osoa hemen sakatuz", + "log_link_to_failed_log": "Ezinezkoa izan da '{desc}' eragiketa exekutatzea. Laguntza nahi izanez gero, partekatu erakigeta honen erregistro osoa hemen sakatuz", "log_permission_url": "Eguneratu '{}' baimenari lotutako URLa", "log_user_group_create": "Sortu '{}' taldea", "permission_creation_failed": "Ezinezkoa izan da '{permission}' baimena sortzea: {error}", "permission_not_found": "Ez da '{permission}' baimena aurkitu", - "pattern_lastname": "Abizen horrek ez du balio", + "pattern_lastname": "Abizen horrek ez du balio (gutxienez hiru karaktere behar ditu)", "permission_deleted": "'{permission}' baimena ezabatu da", "service_disabled": "'{service}' zerbitzua ez da etorkizunean zerbitzaria abiaraztearekin batera exekutatuko.", "unexpected_error": "Ezusteko zerbaitek huts egin du: {error}", @@ -500,7 +468,7 @@ "user_import_success": "Erabiltzaileak arazorik gabe inportatu dira", "yunohost_already_installed": "YunoHost instalatuta dago dagoeneko", "migrations_success_forward": "{id} migrazioak amaitu du", - "migrations_to_be_ran_manually": "{id} migrazioa eskuz abiarazi behar da. Mesedez, joan Erramintak → Migrazioak atalera administrazio-atarian edo bestela exekutatu 'yunohost tools migrations run'.", + "migrations_to_be_ran_manually": "{id} migrazioa eskuz abiarazi behar da. Joan Tresnak → Migrazioak atalera administrazio-atarian edo bestela exekutatu 'yunohost tools migrations run'.", "permission_currently_allowed_for_all_users": "Baimen hau erabiltzaile guztiei esleitzen zaie eta baita beste talde batzuei ere. Litekeena da 'all users' baimena edo esleituta duten taldeei baimena kendu nahi izatea.", "permission_require_account": "'{permission}' baimena zerbitzarian kontua duten erabiltzaileentzat da eta, beraz, ezin da gaitu bisitarientzat.", "postinstall_low_rootfsspace": "'root' fitxategi-sistemak 10 GB edo espazio gutxiago dauka, kezkatzekoa dena! Litekeena da espaziorik gabe geratzea aurki! Gomendagarria da 'root' fitxategi-sistemak gutxienez 16 GB libre izatea. Jakinarazpen honen ondoren YunoHost instalatzen jarraitu nahi baduzu, berrabiarazi agindua '--force-diskspace' gehituz", @@ -517,8 +485,8 @@ "service_restart_failed": "Ezin izan da '{service}' zerbitzua berrabiarazi\n\nZerbitzuen azken erregistroak: {logs}", "service_restarted": "'{service}' zerbitzua berrabiarazi da", "service_start_failed": "Ezin izan da '{service}' zerbitzua abiarazi\n\nZerbitzuen azken erregistroak: {logs}", - "update_apt_cache_failed": "Ezin da APT Debian-en pakete kudeatzailearen cachea eguneratu. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", - "update_apt_cache_warning": "Zerbaitek huts egin du APT Debian-en pakete kudeatzailearen cachea eguneratzean. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", + "update_apt_cache_failed": "Ezin da APT Debian-en pakete kudeatzailearen katxea eguneratu. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", + "update_apt_cache_warning": "Zerbaitek huts egin du APT Debian-en pakete kudeatzailearen katxea eguneratzean. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", "user_created": "Erabiltzailea sortu da", "user_deletion_failed": "Ezin izan da '{user}' ezabatu: {error}", "permission_updated": "'{permission}' baimena moldatu da", @@ -532,7 +500,7 @@ "yunohost_installing": "YunoHost instalatzen…", "migrations_failed_to_load_migration": "Ezinezkoa izan da {id} migrazioa kargatzea: {error}", "migrations_must_provide_explicit_targets": "'--skip' edo '--force-rerun' aukerak erabiltzean jomuga zehatzak zehaztu behar dituzu", - "migrations_pending_cant_rerun": "Migrazio hauek exekutatzeke daude eta, beraz, ezin dira berriro abiarazi: {ids}", + "migrations_pending_cant_rerun": "Migrazio hauek oraindik ez dira exekutatu eta, beraz, ezin dira berriro abiarazi: {ids}", "regenconf_file_kept_back": "'{conf}' konfigurazio fitxategia regen-conf-ek ({category} atala) ezabatzekoa zen baina mantendu egin da.", "regenconf_file_removed": "'{conf}' konfigurazio fitxategia ezabatu da", "permission_already_allowed": "'{group} taldeak badauka dagoeneko '{permission}' baimena", @@ -560,7 +528,7 @@ "regenconf_file_manually_removed": "'{conf}' konfigurazio fitxategia eskuz ezabatu da eta ez da berriro sortuko", "regenconf_up_to_date": "Konfigurazioa egunean dago dagoeneko '{category}' atalerako", "migrations_no_such_migration": "Ez dago '{id}' izeneko migraziorik", - "migrations_not_pending_cant_skip": "Migrazio hauek ez daude exekutatzeke eta, beraz, ezin dira saihestu: {ids}", + "migrations_not_pending_cant_skip": "Migrazio hauek ez daude exekutatzeke eta, beraz, ez dago saihesteko aukerarik: {ids}", "regex_with_only_domain": "Ezin duzu regex domeinuetarako erabili; bideetarako bakarrik", "port_already_closed": "{port}. ataka itxita dago dagoeneko {ip_version} konexioetarako", "regenconf_file_copy_failed": "Ezinezkoa izan da '{new}' konfigurazio fitxategi berria '{conf}'-(e)n kopiatzea", @@ -571,18 +539,17 @@ "service_enable_failed": "Ezin izan da '{service}' zerbitzua sistema abiaraztearekin batera exekutatzea lortu.\n\nZerbitzuen erregistro berrienak: {logs}", "system_username_exists": "Erabiltzaile izena existitzen da dagoeneko sistemaren erabiltzaileen zerrendan", "user_already_exists": "'{user}' erabiltzailea existitzen da dagoeneko", - "mail_domain_unknown": "Ezinezkoa da posta elektroniko hori '{domain}' domeinurako erabiltzea. Mesedez, erabili zerbitzari honek kudeatzen duen domeinu bat.", + "mail_domain_unknown": "Ezinezkoa da posta elektroniko hori '{domain}' domeinurako erabiltzea. Erabili zerbitzari honek kudeatzen duen domeinu bat.", "migrations_list_conflict_pending_done": "Ezin dituzu '--previous' eta '--done' aldi berean erabili.", "migrations_loading_migration": "{id} migrazioa kargatzen…", "migrations_no_migrations_to_run": "Ez dago exekutatzeko migraziorik", - "password_listed": "Pasahitz hau munduan erabilienetarikoa da. Mesedez, aukeratu bereziagoa den beste bat.", - "password_too_simple_2": "Pasahitzak zortzi karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat eta txikiren bat izan behar ditu", - "pattern_firstname": "Izen horrek ez du balio", + "password_listed": "Pasahitz hau munduan erabilienetarikoa da. Aukeratu bereziagoa den beste bat.", + "password_too_simple_2": "Pasahitzak 8 karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat eta txikiren bat izan behar ditu", + "pattern_firstname": "Izen horrek ez du balio (gutxienez hiru karaktere behar ditu)", "pattern_password": "Gutxienez hiru karaktere izan behar ditu", "restore_failed": "Ezin izan da sistema lehengoratu", "restore_removing_tmp_dir_failed": "Ezinezkoa izan da behin-behineko direktorio zaharra ezabatzea", "restore_running_app_script": "'{app}' aplikazioa lehengoratzen…", - "root_password_replaced_by_admin_password": "Administrazio-pasahitzak root pasahitza ordezkatu du.", "service_description_fail2ban": "Internetetik datozen bortxaz egindako saiakerak eta bestelako erasoak ekiditen ditu", "service_description_ssh": "Zerbitzarira sare lokaletik kanpo konektatzea ahalbidetzen du (SSH protokoloa)", "service_description_yunohost-firewall": "Zerbitzuen konexiorako atakak ireki eta ixteko kudeatzailea da", @@ -600,13 +567,13 @@ "yunohost_configured": "YunoHost konfiguratuta dago", "service_description_yunomdns": "Sare lokalean zerbitzarira 'yunohost.local' erabiliz konektatzea ahalbidetzen du", "mail_alias_remove_failed": "Ezin izan da '{mail}' e-mail ezizena ezabatu", - "mail_unavailable": "Helbide elektroniko hau lehenengo erabiltzailearentzat gorde da eta hari ezarri zaio automatikoki", + "mail_unavailable": "Helbide elektroniko hau administratzaileen taldearentzat gorde da", "migration_ldap_backup_before_migration": "Sortu LDAP datubase eta aplikazioen ezarpenen babeskopia migrazioa abiarazi baino lehen.", "migration_ldap_can_not_backup_before_migration": "Sistemaren babeskopiak ez du amaitu migrazioak huts egin baino lehen. Errorea: {error}", "migrations_migration_has_failed": "{id} migrazioak ez du amaitu, geldiarazten. Errorea: {exception}", - "migrations_need_to_accept_disclaimer": "{id} migrazioa abiarazteko, ondorengo baldintzak onartu behar dituzu:\n---\n{disclaimer}\n---\nMigrazioa onartzen baduzu, mesedez berrabiarazi prozesua komandoan '--accept-disclaimer' aukera gehituz.", + "migrations_need_to_accept_disclaimer": "{id} migrazioa abiarazteko, ondorengo baldintzak onartu behar dituzu:\n---\n{disclaimer}\n---\nMigrazioa onartzen baduzu, berrabiarazi prozesua komandoan '--accept-disclaimer' aukera gehituz.", "not_enough_disk_space": "Ez dago nahikoa espazio librerik '{path}'-n", - "password_too_simple_3": "Pasahitzak zortzi karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat, txikiren bat eta karaktere bereziren bat izan behar ditu", + "password_too_simple_3": "Pasahitzak 8 karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat, txikiren bat eta karaktere bereziren bat izan behar ditu", "pattern_backup_archive_name": "Fitxategiaren izenak 30 karaktere izan ditzake gehienez, alfanumerikoak eta ._- baino ez", "pattern_domain": "Domeinu izen baliagarri bat izan behar da (adibidez: nire-domeinua.eus)", "pattern_mailbox_quota": "Tamainak b/k/M/G/T zehaztu behar du edo 0 mugarik ezarri nahi ez bada", @@ -619,8 +586,8 @@ "port_already_opened": "{port}. ataka dagoeneko irekita dago {ip_version} konexioetarako", "user_home_creation_failed": "Ezin izan da erabiltzailearentzat '{home}' direktorioa sortu", "user_unknown": "Erabiltzaile ezezaguna: {user}", - "yunohost_postinstall_end_tip": "Instalazio ondorengo prozesua amaitu da! Sistemaren konfigurazioa bukatzeko:\n- gehitu erabiltzaile bat administrazio-atariko 'Erabiltzaileak' atalean (edo 'yunohost user create ' komandoa erabiliz);\n- erabili 'Diagnostikoak' atala ohiko arazoei aurre hartzeko. Administrazio-atarian abiarazi edo 'yunohost diagnosis run' exekutatu;\n- irakurri 'Finalizing your setup' eta 'Getting to know YunoHost' atalak. Dokumentazioan aurki ditzakezu: https://yunohost.org/admindoc.", - "yunohost_not_installed": "YunoHost ez da zuzen instalatu. Mesedez, exekutatu 'yunohost tools postinstall'", + "yunohost_postinstall_end_tip": "Instalazio ondorengo prozesua amaitu da! Sistemaren konfigurazioa bukatzeko:\n- erabili 'Diagnostikoak' atala ohiko arazoei aurre hartzeko. Administrazio-atarian abiarazi edo 'yunohost diagnosis run' exekutatu;\n- irakurri 'Finalizing your setup' eta 'Getting to know YunoHost' atalak. Dokumentazioan aurki ditzakezu: https://yunohost.org/admindoc.", + "yunohost_not_installed": "YunoHost ez da zuzen instalatu. Exekutatu 'yunohost tools postinstall'", "unlimit": "Mugarik ez", "restore_already_installed_apps": "Ondorengo aplikazioak ezin dira lehengoratu dagoeneko instalatuta daudelako: {apps}", "password_too_simple_4": "Pasahitzak 12 karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat, txikiren bat eta karaktere bereziren bat izan behar ditu", @@ -670,19 +637,105 @@ "migration_0021_main_upgrade": "Eguneraketa nagusia abiarazten…", "migration_0021_still_on_buster_after_main_upgrade": "Zerbaitek huts egin du eguneraketa nagusian, badirudi sistemak oraindik darabilela Debian Buster", "migration_0021_yunohost_upgrade": "YunoHosten muineko eguneraketa abiarazten…", - "migration_0021_not_buster": "Uneko Debian ez da Buster!", "migration_0021_not_enough_free_space": "/var/-enerabilgarri dagoen espazioa oso txikia da! Guxtienez GB 1 izan beharko zenuke erabilgarri migrazioari ekiteko.", - "migration_0021_system_not_fully_up_to_date": "Sistema ez dago erabat egunean. Mesedez, egizu eguneraketa arrunt bat Bullseye-(e)rako migrazioa abiarazi baino lehen.", - "migration_0021_general_warning": "Mesedez, kontuan hartu migrazio hau konplexua dela. YunoHost taldeak ahalegin handia egin du probatzeko, baina hala ere migrazioak sistemaren zatiren bat edo aplikazioak apurt litzake.\n\nHorregatik, gomendagarria da:\n\t- Datu edo aplikazio garrantzitsuen babeskopia egitea. Informazio gehiago: https://yunohost.org/backup;\n\t- Ez izan presarik migrazioa abiaraztean: zure internet eta hardwarearen arabera ordu batzuk ere iraun lezake eguneraketa prozesuak.", - "migration_0021_modified_files": "Mesedez, kontuan hartu ondorengo fitxategiak eskuz moldatu omen direla eta eguneraketak berridatziko dituela: {manually_modified_files}", - "migration_0021_cleaning_up": "Cachea eta erabilgarriak ez diren paketeak garbitzen…", + "migration_0021_system_not_fully_up_to_date": "Sistema ez dago erabat egunean. Egizu eguneraketa arrunt bat Bullseye-(e)rako migrazioa abiarazi baino lehen.", + "migration_0021_general_warning": "Kontuan hartu migrazio hau konplexua dela. YunoHost taldeak ahalegin handia egin du probatzeko, baina hala ere migrazioak sistemaren zatiren bat edo aplikazioak apurt litzake.\n\nHorregatik, gomendagarria da:\n\t- Datu edo aplikazio garrantzitsuen babeskopia egitea. Informazio gehiago: https://yunohost.org/backup;\n\t- Ez izan presarik migrazioa abiaraztean: zure internet eta hardwarearen arabera ordu batzuk ere iraun lezake eguneraketa prozesuak.", + "migration_0021_modified_files": "Kontuan hartu ondorengo fitxategiak eskuz moldatu omen direla eta eguneraketak berridatziko dituela: {manually_modified_files}", + "migration_0021_cleaning_up": "Katxea eta erabilgarriak ez diren paketeak garbitzen…", "migration_0021_patch_yunohost_conflicts": "Arazo gatazkatsu bati adabakia jartzen…", "migration_description_0021_migrate_to_bullseye": "Eguneratu sistema Debian Bullseye eta Yunohost 11.x-ra", - "global_settings_setting_security_ssh_password_authentication": "Baimendu pasahitz bidezko autentikazioa SSHrako", - "migration_0021_problematic_apps_warning": "Mesedez, kontuan izan ziur asko gatazkatsuak izango diren odorengo aplikazioak aurkitu direla. Badirudi ez zirela YunoHost aplikazioen katalogotik instalatu, edo ez daude 'badabiltza' bezala etiketatuak. Ondorioz, ezin da bermatu eguneratu ondoren funtzionatzen jarraituko dutenik: {problematic_apps}", + "migration_0021_problematic_apps_warning": "Kontuan izan ziur asko gatazkatsuak izango diren odorengo aplikazioak aurkitu direla. Badirudi ez zirela YunoHost aplikazioen katalogotik instalatu, edo ez daude 'badabiltza' bezala etiketatuak. Ondorioz, ezin da bermatu eguneratu ondoren funtzionatzen jarraituko dutenik: {problematic_apps}", "migration_0023_not_enough_space": "{path}-en ez dago toki nahikorik migrazioa abiarazteko.", "migration_0023_postgresql_11_not_installed": "PostgreSQL ez zegoen zure isteman instalatuta. Ez dago egitekorik.", "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 dago instalatuta baina PostgreSQL 13 ez!? Zerbait arraroa gertatu omen zaio zure sistemari :( …", "migration_description_0022_php73_to_php74_pools": "Migratu php7.3-fpm 'pool' ezarpen-fitxategiak php7.4ra", - "migration_description_0023_postgresql_11_to_13": "Migratu datubaseak PostgreSQL 11tik 13ra" -} \ No newline at end of file + "migration_description_0023_postgresql_11_to_13": "Migratu datubaseak PostgreSQL 11tik 13ra", + "global_settings_setting_backup_compress_tar_archives_help": "Babeskopia berriak sortzean, konprimitu fitxategiak (.tar.gz) konprimitu gabeko fitxategien (.tar) ordez. Aukera hau gaitzean babeskopiek espazio gutxiago beharko dute, baina hasierako prozesua luzeagoa izango da eta CPUari lan handiagoa eragingo dio.", + "global_settings_setting_security_experimental_enabled_help": "Gaitu segurtasun funtzio esperimentalak (ez ezazu egin ez badakizu zertan ari zaren!)", + "global_settings_setting_nginx_compatibility_help": "Bateragarritasun eta segurtasun arteko gatazka NGINX web zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", + "global_settings_setting_nginx_redirect_to_https_help": "Birbideratu HTTP eskaerak HTTPSra (EZ ITZALI hau ez badakizu zertan ari zaren!)", + "global_settings_setting_admin_strength": "Administrazio-pasahitzaren segurtasuna", + "global_settings_setting_user_strength": "Erabiltzaile-pasahitzaren segurtasuna", + "global_settings_setting_postfix_compatibility_help": "Bateragarritasun eta segurtasun arteko gatazka Postfix zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", + "global_settings_setting_ssh_compatibility_help": "Bateragarritasun eta segurtasun arteko gatazka SSH zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", + "global_settings_setting_ssh_password_authentication_help": "Baimendu pasahitz bidezko autentikazioa SSHrako", + "global_settings_setting_ssh_port": "SSH ataka", + "global_settings_setting_webadmin_allowlist_help": "Administrazio-ataria bisita dezaketen IP helbideak, koma bidez bereiziak.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Baimendu IP zehatz batzuk bakarrik administrazio-atarian.", + "global_settings_setting_smtp_allow_ipv6_help": "Baimendu IPv6 posta elektronikoa jaso eta bidaltzeko", + "global_settings_setting_smtp_relay_enabled_help": "YunoHosten ordez posta elektronikoa bidaltzeko SMTP relay helbidea. Erabilgarri izan daiteke egoera hauetan: operadore edo VPS enpresak 25. ataka blokeatzen badu, DUHLen zure etxeko IPa ageri bada, ezin baduzu alderantzizko DNSa ezarri edo zerbitzari hau ez badago zuzenean internetera konektatuta baina posta elektronikoa bidali nahi baduzu.", + "migration_0024_rebuild_python_venv_broken_app": "{app} aplikazioari ez ikusiarena egin zaio ezin delako ingurune birtuala modu errazean birsortu. Horren ordez, aplikazioaren eguneraketa behartzen saia zaitezke `yunohost app upgrade --force {app}` arazoa konpontzeko.", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Ondorengo aplikazioen virtualenv-a birsortzeko saiakera egingo da (eragiketak luze jo dezake!): {rebuild_apps}", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenv-ak ezin dira birsortu aplikazio horientzat. Eguneraketa behartu behar duzu horientzat, ondorengo komandoa exekutatuz egin daiteke: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_0024_rebuild_python_venv_in_progress": "`{app}` aplikazioaren Python virtualenv-a birsortzeko lanetan", + "migration_0024_rebuild_python_venv_failed": "Kale egin du {app} aplikazioaren Python virtualenv-aren birsorkuntza saiakerak. Litekeena da aplikazioak ez funtzionatzea arazoa konpondu arte. Aplikazioaren eguneraketa behartu beharko zenuke ondorengo komandoarekin: `yunohost app upgrade --force {app}`.", + "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", + "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", + "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)", + "ask_admin_fullname": "Administratzailearen izen osoa", + "ask_admin_username": "Administratzailearen erabiltzaile-izena", + "ask_fullname": "Izen osoa", + "certmanager_cert_install_failed": "Let's Encrypt zirutagiriaren instalazioak huts egin du honako domeinu(eta)rako: {domains}", + "certmanager_cert_install_failed_selfsigned": "Norberak sinatutako zirutagiriaren instalazioak huts egin du honako domeinu(eta)rako: {domains}", + "certmanager_cert_renew_failed": "Let's Encrypt zirutagiriaren berrizteak huts egin du honako domeinu(eta)rako: {domains}", + "config_action_failed": "Ezin izan da '{action}' eragiketa exekutatu: {error}", + "domain_config_cert_summary_expired": "LARRIA: Uneko ziurtagiria ez da baliozkoa! HTTPS ezin da erabili!", + "domain_config_cert_summary_selfsigned": "ADI: Uneko zirutagiria norberak sinatutakoa da. Web-nabigatzaileek bisitariak izutuko dituen mezu bat erakutsiko dute!", + "global_settings_setting_postfix_compatibility": "Postfixekin bateragarritasuna", + "global_settings_setting_root_access_explain": "Linux sistemetan 'root' administratzaile gorena da. YunoHosten testuinguruan, zuzeneko 'root' SSH saioa ezgaituta dago defektuz, zerbitzariaren sare lokaletik ez bada. 'administrariak' taldeko kideek sudo komandoa erabili dezakete root bailitzan jarduteko terminalaren bidez. Hala ere lagungarri izan liteke root pasahitz (sendo) bat izatea sistema arazteko egoeraren batean administratzaile arruntek saiorik hasi ezin balute.", + "log_settings_reset": "Berrezarri ezarpenak", + "log_settings_reset_all": "Berrezarri ezarpen guztiak", + "root_password_changed": "root pasahitza aldatu da", + "visitors": "Bisitariak", + "global_settings_setting_security_experimental_enabled": "Segurtasun ezaugarri esperimentalak", + "registrar_infos": "Erregistro-enpresaren informazioa", + "global_settings_setting_pop3_enabled": "Gaitu POP3", + "global_settings_reset_success": "Berrezarri ezarpen globalak", + "global_settings_setting_backup_compress_tar_archives": "Konprimatu babeskopiak", + "config_forbidden_readonly_type": "'{type}' mota ezin da ezarri readonly bezala; beste mota bat erabili balio hau emateko (argudioaren ida: '{id}').", + "diagnosis_using_stable_codename": "apt (sistemaren pakete kudeatzailea) 'stable' (egonkorra) izen kodea duten paketeak instalatzeko ezarrita dago une honetan, eta ez uneko Debianen bertsioaren (bullseye) izen kodea.", + "diagnosis_using_yunohost_testing": "apt (sistemaren pakete kudeatzailea) YunoHosten muinerako 'testing' (proba) izen kodea duten paketeak instalatzeko ezarrita dago une honetan.", + "diagnosis_using_yunohost_testing_details": "Ez dago arazorik zertan ari zaren baldin badakizu, baina arretaz irakurri oharrak YunoHosten eguneraketak instalatu baino lehen! 'testing' (proba) bertsioak ezgaitu nahi badituzu, kendu testing gakoa /etc/apt/sources.list.d/yunohost.list fitxategitik.", + "global_settings_setting_smtp_allow_ipv6": "Baimendu IPv6", + "global_settings_setting_smtp_relay_host": "SMTP relay ostatatzailea", + "domain_config_acme_eligible": "ACME egokitasuna", + "domain_config_acme_eligible_explain": "Ez dirudi domeinu hau Let's Encrypt ziurtagirirako prest dagoenik. Egiaztatu DNS ezarpenak eta zerbitzariaren HTTP irisgarritasuna. Diagnostikoen orrialdeko 'DNS erregistroak' eta 'Web' atalek zer dagoen gaizki ulertzen lagun zaitzakete.", + "domain_config_cert_install": "Instalatu Let's Encrypt ziurtagiria", + "domain_config_cert_issuer": "Ziurtagiriaren jaulkitzailea", + "domain_config_cert_no_checks": "Muzin egin diagnostikoaren egiaztapenei", + "domain_config_cert_renew": "Berritu Let's Encrypt ziurtagiria", + "domain_config_cert_renew_help": "Ziurtagiria automatikoki berrituko da baliozkoa den azken 15 egunetan. Eskuz berritu dezakezu hala nahi baduzu. (Ez da gomendagarria).", + "domain_config_cert_summary": "Ziurtagiriaren egoera", + "domain_config_cert_summary_abouttoexpire": "Uneko ziurtagiria iraungitzear dago. Aurki berritu beharko litzateke automatikoki.", + "domain_config_cert_summary_letsencrypt": "Primeran! Baliozko Let's Encrypt zirutagiria erabiltzen ari zara!", + "domain_config_cert_summary_ok": "Ados, uneko ziurtagiriak itzura ona du!", + "domain_config_cert_validity": "Balizokotasuna", + "global_settings_setting_admin_strength_help": "Betekizun hauek pasahitza lehenbizikoz sortzerakoan edo aldatzerakoan baino ez dira bete behar", + "global_settings_setting_nginx_compatibility": "NGINXekin bateragarritasuna", + "global_settings_setting_nginx_redirect_to_https": "Behartu HTTPS", + "global_settings_setting_pop3_enabled_help": "Gaitu POP3 protokoloa eposta zerbitzarirako", + "global_settings_setting_root_password": "root pasahitz berria", + "global_settings_setting_root_password_confirm": "root pasahitz berria (egiaztatu)", + "global_settings_setting_smtp_relay_enabled": "Gaitu SMTP relay", + "global_settings_setting_ssh_compatibility": "SSH bateragarritasuna", + "global_settings_setting_ssh_password_authentication": "Pasahitz bidezko autentifikazioa", + "global_settings_setting_user_strength_help": "Betekizun hauek lehenbizikoz sortzerakoan edo pasahitza aldatzerakoan bete behar dira soilik", + "global_settings_setting_webadmin_allowlist": "Administrazio-atarira sartzeko baimendutako IPak", + "global_settings_setting_webadmin_allowlist_enabled": "Gaitu administrazio-ataria sartzeko baimendutako IPak", + "invalid_credentials": "Pasahitz edo erabiltzaile-izen baliogabea", + "log_resource_snippet": "Baliabide baten eguneraketa / eskuragarritasuna / eskuragarritasun eza", + "log_settings_set": "Aplikatu ezarpenak", + "migration_description_0025_global_settings_to_configpanel": "Migratu ezarpen globalen nomenklatura zaharra izendegi berri eta modernora", + "migration_description_0026_new_admins_group": "Migratu 'administrari bat baino gehiago' sistema berrira", + "password_confirmation_not_the_same": "Pasahitzak ez datoz bat", + "password_too_long": "Aukeratu 127 karaktere baino laburragoa den pasahitz bat", + "diagnosis_using_stable_codename_details": "Ostatatzaileak zerbait oker ezarri duenean gertatu ohi da hau. Arriskutsua da, Debianen datorren bertsioa 'estable' (egonkorra) bilakatzen denean, apt-ek sistemaren pakete guztiak bertsio-berritzen saiatuko da, beharrezko migrazio-prozedurarik burutu gabe. Debianen repositorioan apt iturria editatzen konpontzea da gomendioa, stable gakoa bullseye gakoarekin ordezkatuz. Ezarpen-fitxategia /etc/apt/sources.list izan beharko litzateke, edo /etc/apt/sources.list.d/ direktorioko fitxategiren bat.", + "group_update_aliases": "'{group}' taldearen aliasak eguneratzen", + "group_no_change": "Ez da ezer aldatu behar '{group}' talderako" +} diff --git a/locales/fa.json b/locales/fa.json index d9e3f39b3..afa86b13b 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -11,9 +11,6 @@ "app_action_broke_system": "این اقدام به نظر می رسد سرویس های مهمی را خراب کرده است: {services}", "app_action_cannot_be_ran_because_required_services_down": "برای اجرای این عملیات سرویس هایی که مورد نیازاند و باید اجرا شوند: {services}. سعی کنید آنها را مجدداً راه اندازی کنید (و علت خرابی احتمالی آنها را بررسی کنید).", "already_up_to_date": "کاری برای انجام دادن نیست. همه چیز در حال حاضر به روز است.", - "admin_password_too_long": "لطفاً گذرواژه ای کوتاهتر از 127 کاراکتر انتخاب کنید", - "admin_password_changed": "رمز مدیریت تغییر کرد", - "admin_password_change_failed": "تغییر رمز امکان پذیر نیست", "admin_password": "رمز عبور مدیریت", "additional_urls_already_removed": "نشانی اینترنتی اضافی '{url}' قبلاً در نشانی اینترنتی اضافی برای اجازه '{permission}'حذف شده است", "additional_urls_already_added": "نشانی اینترنتی اضافی '{url}' قبلاً در نشانی اینترنتی اضافی برای اجازه '{permission}' اضافه شده است", @@ -145,8 +142,6 @@ "ask_new_domain": "دامنه جدید", "ask_new_admin_password": "رمز جدید مدیریت", "ask_main_domain": "دامنه اصلی", - "ask_lastname": "نام خانوادگی", - "ask_firstname": "نام کوچک", "ask_user_domain": "دامنه ای که برای آدرس ایمیل کاربر و حساب XMPP استفاده می شود", "apps_catalog_update_success": "کاتالوگ برنامه به روز شد!", "apps_catalog_obsolete_cache": "حافظه پنهان کاتالوگ برنامه خالی یا منسوخ شده است.", @@ -171,7 +166,6 @@ "app_restore_script_failed": "خطایی در داخل اسکریپت بازیابی برنامه رخ داده است", "app_restore_failed": "{app} بازیابی نشد: {error}", "app_remove_after_failed_install": "حذف برنامه در پی شکست نصب...", - "app_requirements_unmeet": "شرایط مورد نیاز برای {app} برآورده نمی شود ، بسته {pkgname} ({version}) باید {spec} باشد", "app_requirements_checking": "در حال بررسی بسته های مورد نیاز برای {app}...", "app_removed": "{app} حذف نصب شد", "app_not_properly_removed": "{app} به درستی حذف نشده است", @@ -301,39 +295,15 @@ "group_already_exist": "گروه {group} از قبل وجود دارد", "good_practices_about_user_password": "گذرواژه باید حداقل 8 کاراکتر باشد - اگرچه استفاده از گذرواژه طولانی تر تمرین خوبی است (به عنوان مثال عبارت عبور) و/یا استفاده از تنوع کاراکترها (بزرگ ، کوچک ، رقم و کاراکتر های خاص).", "good_practices_about_admin_password": "اکنون می خواهید گذرواژه جدیدی برای مدیریت تعریف کنید. گذرواژه باید حداقل 8 کاراکتر باشد - اگرچه استفاده از گذرواژه طولانی تر تمرین خوبی است (به عنوان مثال عبارت عبور) و/یا استفاده از تنوع کاراکترها (بزرگ ، کوچک ، رقم و کاراکتر های خاص).", - "global_settings_unknown_type": "وضعیت غیرمنتظره ، به نظر می رسد که تنظیمات {setting} دارای نوع {unknown_type} است اما از نوع پشتیبانی شده توسط سیستم نیست.", - "global_settings_setting_backup_compress_tar_archives": "هنگام ایجاد پشتیبان جدید ، بایگانی های فشرده (.tar.gz) را به جای بایگانی های فشرده نشده (.tar) انتخاب کنید. N.B. : فعال کردن این گزینه به معنای ایجاد آرشیوهای پشتیبان سبک تر است ، اما روش پشتیبان گیری اولیه به طور قابل توجهی طولانی تر و سنگین تر بر روی CPU خواهد بود.", - "global_settings_setting_security_experimental_enabled": "فعال کردن ویژگی های امنیتی آزمایشی (اگر نمی دانید در حال انجام چه کاری هستید این کار را انجام ندهید!)", - "global_settings_setting_security_webadmin_allowlist": "آدرس های IP که مجاز به دسترسی مدیر وب هستند. جدا شده با ویرگول.", - "global_settings_setting_security_webadmin_allowlist_enabled": "فقط به برخی از IP ها اجازه دسترسی به مدیریت وب را بدهید.", "global_settings_setting_smtp_relay_password": "رمز عبور میزبان رله SMTP", "global_settings_setting_smtp_relay_user": "حساب کاربری رله SMTP", "global_settings_setting_smtp_relay_port": "پورت رله SMTP", - "global_settings_setting_smtp_relay_host": "میزبان رله SMTP برای ارسال نامه به جای این نمونه yunohost استفاده می شود. اگر در یکی از این شرایط قرار دارید مفید است: پورت 25 شما توسط ارائه دهنده ISP یا VPS شما مسدود شده است، شما یک IP مسکونی دارید که در DUHL ذکر شده است، نمی توانید DNS معکوس را پیکربندی کنید یا این سرور مستقیماً در اینترنت نمایش داده نمی شود و می خواهید از یکی دیگر برای ارسال ایمیل استفاده کنید.", - "global_settings_setting_smtp_allow_ipv6": "اجازه دهید از IPv6 برای دریافت و ارسال نامه استفاده شود", "global_settings_setting_ssowat_panel_overlay_enabled": "همپوشانی پانل SSOwat را فعال کنید", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "اجازه دهید از کلید میزبان DSA (منسوخ شده) برای پیکربندی SH daemon استفاده شود", - "global_settings_unknown_setting_from_settings_file": "کلید ناشناخته در تنظیمات: '{setting_key}'، آن را کنار گذاشته و در /etc/yunohost/settings-unknown.json ذخیره کنید", - "global_settings_setting_security_ssh_port": "درگاه SSH", - "global_settings_setting_security_postfix_compatibility": "سازگاری در مقابل مبادله امنیتی برای سرور Postfix. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", - "global_settings_setting_security_ssh_compatibility": "سازگاری در مقابل مبادله امنیتی برای سرور SSH. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", - "global_settings_setting_security_password_user_strength": "قدرت رمز عبور کاربر", - "global_settings_setting_security_password_admin_strength": "قدرت رمز عبور مدیر", - "global_settings_setting_security_nginx_compatibility": "سازگاری در مقابل مبادله امنیتی برای وب سرور NGINX. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", - "global_settings_setting_pop3_enabled": "پروتکل POP3 را برای سرور ایمیل فعال کنید", - "global_settings_reset_success": "تنظیمات قبلی اکنون در {path} پشتیبان گیری شده است", - "global_settings_key_doesnt_exists": "کلید '{settings_key}' در تنظیمات جهانی وجود ندارد ، با اجرای 'لیست تنظیمات yunohost' می توانید همه کلیدهای موجود را مشاهده کنید", - "global_settings_cant_write_settings": "فایل تنظیمات ذخیره نشد، به دلیل: {reason}", - "global_settings_cant_serialize_settings": "سریال سازی داده های تنظیمات انجام نشد، به دلیل: {reason}", - "global_settings_cant_open_settings": "فایل تنظیمات باز نشد ، به دلیل: {reason}", - "global_settings_bad_type_for_setting": "نوع نادرست برای تنظیم {setting} ، دریافت شده {received_type}، مورد انتظار {expected_type}", - "global_settings_bad_choice_for_enum": "انتخاب نادرست برای تنظیم {setting} ، '{choice}' دریافت شد ، اما گزینه های موجود عبارتند از: {available_choices}", "firewall_rules_cmd_failed": "برخی از دستورات قانون فایروال شکست خورده است. اطلاعات بیشتر در گزارش.", "firewall_reloaded": "فایروال بارگیری مجدد شد", "firewall_reload_failed": "بارگیری مجدد فایروال امکان پذیر نیست", "file_does_not_exist": "فایل {path} وجود ندارد.", "field_invalid": "فیلد نامعتبر '{}'", - "experimental_feature": "هشدار: این ویژگی آزمایشی است و پایدار تلقی نمی شود ، نباید از آن استفاده کنید مگر اینکه بدانید در حال انجام چه کاری هستید.", "extracting": "استخراج...", "dyndns_unavailable": "دامنه '{domain}' در دسترس نیست.", "dyndns_domain_not_provided": "ارائه دهنده DynDNS {provider} نمی تواند دامنه {domain} را ارائه دهد.", @@ -387,7 +357,6 @@ "password_too_simple_1": "رمز عبور باید حداقل 8 کاراکتر باشد", "password_listed": "این رمز در بین پر استفاده ترین رمزهای عبور در جهان قرار دارد. لطفاً چیزی منحصر به فرد تر انتخاب کنید.", "operation_interrupted": "عملیات به صورت دستی قطع شد؟", - "invalid_password": "رمز عبور نامعتبر", "invalid_number": "باید یک عدد باشد", "not_enough_disk_space": "فضای آزاد کافی در '{path}' وجود ندارد", "migrations_to_be_ran_manually": "مهاجرت {id} باید به صورت دستی اجرا شود. لطفاً به صفحه Tools → Migrations در صفحه webadmin بروید، یا `yunohost tools migrations run` را اجرا کنید.", @@ -542,7 +511,6 @@ "server_reboot": "سرور راه اندازی مجدد می شود", "server_shutdown_confirm": "آیا مطمئن هستید که سرور بلافاصله خاموش می شود؟ [{answers}]", "server_shutdown": "سرور خاموش می شود", - "root_password_replaced_by_admin_password": "گذرواژه ریشه شما با رمز مدیریت جایگزین شده است.", "root_password_desynchronized": "گذرواژه مدیریت تغییر کرد ، اما YunoHost نتوانست این را به رمز عبور ریشه منتقل کند!", "restore_system_part_failed": "بخش سیستم '{part}' بازیابی و ترمیم نشد", "restore_running_hooks": "در حال اجرای قلاب های ترمیم و بازیابی...", @@ -589,5 +557,17 @@ "permission_deletion_failed": "اجازه '{permission}' حذف نشد: {error}", "permission_deleted": "مجوز '{permission}' حذف شد", "permission_cant_add_to_all_users": "مجوز {permission} را نمی توان به همه کاربران اضافه کرد.", - "permission_currently_allowed_for_all_users": "این مجوز در حال حاضر به همه کاربران علاوه بر آن گروه های دیگر نیز اعطا شده. احتمالاً بخواهید مجوز 'all_users' را حذف کنید یا سایر گروه هایی را که در حال حاضر مجوز به آنها اعطا شده است را هم حذف کنید." + "permission_currently_allowed_for_all_users": "این مجوز در حال حاضر به همه کاربران علاوه بر آن گروه های دیگر نیز اعطا شده. احتمالاً بخواهید مجوز 'all_users' را حذف کنید یا سایر گروه هایی را که در حال حاضر مجوز به آنها اعطا شده است را هم حذف کنید.", + "global_settings_setting_backup_compress_tar_archives_help": "هنگام ایجاد پشتیبان جدید ، بایگانی های فشرده (.tar.gz) را به جای بایگانی های فشرده نشده (.tar) انتخاب کنید. N.B. : فعال کردن این گزینه به معنای ایجاد آرشیوهای پشتیبان سبک تر است ، اما روش پشتیبان گیری اولیه به طور قابل توجهی طولانی تر و سنگین تر بر روی CPU خواهد بود.", + "global_settings_setting_security_experimental_enabled_help": "فعال کردن ویژگی های امنیتی آزمایشی (اگر نمی دانید در حال انجام چه کاری هستید این کار را انجام ندهید!)", + "global_settings_setting_nginx_compatibility_help": "سازگاری در مقابل مبادله امنیتی برای وب سرور NGINX. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", + "global_settings_setting_admin_strength": "قدرت رمز عبور مدیر", + "global_settings_setting_user_strength": "قدرت رمز عبور کاربر", + "global_settings_setting_postfix_compatibility_help": "سازگاری در مقابل مبادله امنیتی برای سرور Postfix. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", + "global_settings_setting_ssh_compatibility_help": "سازگاری در مقابل مبادله امنیتی برای سرور SSH. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", + "global_settings_setting_ssh_port": "درگاه SSH", + "global_settings_setting_webadmin_allowlist_help": "آدرس های IP که مجاز به دسترسی مدیر وب هستند. جدا شده با ویرگول.", + "global_settings_setting_webadmin_allowlist_enabled_help": "فقط به برخی از IP ها اجازه دسترسی به مدیریت وب را بدهید.", + "global_settings_setting_smtp_allow_ipv6_help": "اجازه دهید از IPv6 برای دریافت و ارسال نامه استفاده شود", + "global_settings_setting_smtp_relay_enabled_help": "میزبان رله SMTP برای ارسال نامه به جای این نمونه yunohost استفاده می شود. اگر در یکی از این شرایط قرار دارید مفید است: پورت 25 شما توسط ارائه دهنده ISP یا VPS شما مسدود شده است، شما یک IP مسکونی دارید که در DUHL ذکر شده است، نمی توانید DNS معکوس را پیکربندی کنید یا این سرور مستقیماً در اینترنت نمایش داده نمی شود و می خواهید از یکی دیگر برای ارسال ایمیل استفاده کنید." } \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index 6e96e500a..9affb5869 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,8 +1,6 @@ { "action_invalid": "Action '{action}' incorrecte", "admin_password": "Mot de passe d'administration", - "admin_password_change_failed": "Impossible de changer le mot de passe", - "admin_password_changed": "Le mot de passe d'administration a été modifié", "app_already_installed": "{app} est déjà installé", "app_argument_choice_invalid": "Choisissez une valeur valide pour l'argument '{name}' : '{value}' ne fait pas partie des choix disponibles ({choices})", "app_argument_invalid": "Valeur invalide pour le paramètre '{name}' : {error}", @@ -14,15 +12,12 @@ "app_not_installed": "Nous n'avons pas trouvé {app} dans la liste des applications installées : {all_apps}", "app_not_properly_removed": "{app} n'a pas été supprimé correctement", "app_removed": "{app} désinstallé", - "app_requirements_checking": "Vérification des paquets requis pour {app}...", - "app_requirements_unmeet": "Les pré-requis de {app} ne sont pas satisfaits, le paquet {pkgname} ({version}) doit être {spec}", + "app_requirements_checking": "Vérification des prérequis pour {app} ...", "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources, l'URL est-elle correcte ?", "app_unknown": "Application inconnue", "app_unsupported_remote_type": "Ce type de commande à distance utilisé pour cette application n'est pas supporté", "app_upgrade_failed": "Impossible de mettre à jour {app} : {error}", "app_upgraded": "{app} mis à jour", - "ask_firstname": "Prénom", - "ask_lastname": "Nom", "ask_main_domain": "Domaine principal", "ask_new_admin_password": "Nouveau mot de passe d'administration", "ask_password": "Mot de passe", @@ -85,8 +80,8 @@ "pattern_backup_archive_name": "Doit être un nom de fichier valide avec un maximum de 30 caractères, et composé de caractères alphanumériques et -_. uniquement", "pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.fr)", "pattern_email": "Il faut une adresse électronique valide, sans le symbole '+' (par exemple johndoe@exemple.com)", - "pattern_firstname": "Doit être un prénom valide", - "pattern_lastname": "Doit être un nom valide", + "pattern_firstname": "Doit être un prénom valide (au moins 3 caractères)", + "pattern_lastname": "Doit être un nom de famille valide (au moins 3 caractères)", "pattern_mailbox_quota": "Doit avoir une taille suffixée avec b/k/M/G/T ou 0 pour désactiver le quota", "pattern_password": "Doit être composé d'au moins 3 caractères", "pattern_port_or_range": "Doit être un numéro de port valide compris entre 0 et 65535, ou une gamme de ports (exemple : 100:200)", @@ -151,7 +146,7 @@ "certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain} n'est pas émis par Let's Encrypt. Impossible de le renouveler automatiquement !", "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain} n'est pas sur le point d'expirer ! (Vous pouvez utiliser --force si vous savez ce que vous faites)", "certmanager_domain_http_not_working": "Le domaine {domain} ne semble pas être accessible via HTTP. Merci de vérifier la catégorie 'Web' dans le diagnostic pour plus d'informations. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "Les enregistrements DNS du domaine '{domain}' sont différents de l'adresse IP de ce serveur. Pour plus d'informations, veuillez consulter la catégorie \"Enregistrements DNS\" dans la section diagnostic. Si vous avez récemment modifié votre enregistrement A, veuillez attendre sa propagation (des vérificateurs de propagation DNS sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver ces contrôles)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Les enregistrements DNS du domaine '{domain}' sont différents de l'adresse IP de ce serveur. Pour plus d'informations, veuillez consulter la catégorie \"Enregistrements DNS\" dans la section Diagnostic. Si vous avez récemment modifié votre enregistrement A, veuillez attendre sa propagation (des vérificateurs de propagation DNS sont disponibles en ligne). Si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver ces contrôles.", "certmanager_cannot_read_cert": "Quelque chose s'est mal passé lors de la tentative d'ouverture du certificat actuel pour le domaine {domain} (fichier : {file}), la cause est : {reason}", "certmanager_cert_install_success_selfsigned": "Le certificat auto-signé est maintenant installé pour le domaine '{domain}'", "certmanager_cert_install_success": "Le certificat Let's Encrypt est maintenant installé pour le domaine '{domain}'", @@ -165,7 +160,7 @@ "mailbox_used_space_dovecot_down": "Le service Dovecot doit être démarré si vous souhaitez voir l'espace disque occupé par la messagerie", "domains_available": "Domaines disponibles :", "backup_archive_broken_link": "Impossible d'accéder à l'archive de sauvegarde (lien invalide vers {path})", - "certmanager_acme_not_configured_for_domain": "Le challenge ACME n'a pas pu être validé pour le domaine {domain} pour le moment car le code de la configuration NGINX est manquant... Merci de vérifier que votre configuration NGINX est à jour avec la commande : `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "Pour le moment le protocole de communication ACME n'a pas pu être validé pour le domaine {domain} car le code correspondant de la configuration NGINX est manquant ... Merci de vérifier que votre configuration NGINX est à jour avec la commande : `yunohost tools regen-conf nginx --dry-run --with-diff`.", "domain_hostname_failed": "Échec de l'utilisation d'un nouveau nom d'hôte. Cela pourrait causer des soucis plus tard (cela n'en causera peut-être pas).", "app_already_installed_cant_change_url": "Cette application est déjà installée. L'URL ne peut pas être changé simplement par cette fonction. Vérifiez si cela est disponible avec `app changeurl`.", "app_change_url_identical_domains": "L'ancien et le nouveau couple domaine/chemin_de_l'URL sont identiques pour ('{domain}{path}'), rien à faire.", @@ -173,14 +168,6 @@ "app_change_url_success": "L'URL de l'application {app} a été changée en {domain}{path}", "app_location_unavailable": "Cette URL n'est pas disponible ou est en conflit avec une application existante :\n{apps}", "app_already_up_to_date": "{app} est déjà à jour", - "global_settings_bad_choice_for_enum": "Valeur du paramètre {setting} incorrecte. Reçu : {choice}, mais les valeurs possibles sont : {available_choices}", - "global_settings_bad_type_for_setting": "Le type du paramètre {setting} est incorrect. Reçu {received_type} alors que {expected_type} était attendu", - "global_settings_cant_open_settings": "Échec de l'ouverture du ficher de configurations car : {reason}", - "global_settings_cant_write_settings": "Échec d'écriture du fichier de configurations car : {reason}", - "global_settings_key_doesnt_exists": "La clef '{settings_key}' n'existe pas dans les configurations générales, vous pouvez voir toutes les clefs disponibles en saisissant 'yunohost settings list'", - "global_settings_reset_success": "Vos configurations précédentes ont été sauvegardées dans {path}", - "global_settings_unknown_type": "Situation inattendue : la configuration {setting} semble avoir le type {unknown_type} mais celui-ci n'est pas pris en charge par le système.", - "global_settings_unknown_setting_from_settings_file": "Clé inconnue dans les paramètres : '{setting_key}', rejet de cette clé et sauvegarde de celle-ci dans /etc/yunohost/unkown_settings.json", "backup_abstract_method": "Cette méthode de sauvegarde reste à implémenter", "backup_applying_method_tar": "Création de l'archive TAR de la sauvegarde ...", "backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder ...", @@ -202,7 +189,6 @@ "backup_unable_to_organize_files": "Impossible d'utiliser la méthode rapide pour organiser les fichiers dans l'archive", "backup_with_no_backup_script_for_app": "L'application {app} n'a pas de script de sauvegarde. Ignorer.", "backup_with_no_restore_script_for_app": "{app} n'a pas de script de restauration, vous ne pourrez pas restaurer automatiquement la sauvegarde de cette application.", - "global_settings_cant_serialize_settings": "Échec de la sérialisation des données de paramétrage car : {reason}", "restore_removing_tmp_dir_failed": "Impossible de sauvegarder un ancien dossier temporaire", "restore_extracting": "Extraction des fichiers nécessaires depuis l'archive...", "restore_may_be_not_enough_disk_space": "Votre système ne semble pas avoir suffisamment d'espace (libre : {free_space} B, espace nécessaire : {needed_space} B, marge de sécurité : {margin} B)", @@ -240,7 +226,6 @@ "service_description_ssh": "Vous permet de vous connecter à distance à votre serveur via un terminal (protocole SSH)", "service_description_yunohost-api": "Permet les interactions entre l'interface web de YunoHost et le système", "service_description_yunohost-firewall": "Gère l'ouverture et la fermeture des ports de connexion aux services", - "experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l'utiliser à moins que vous ne sachiez ce que vous faites.", "log_corrupted_md_file": "Le fichier YAML de métadonnées associé aux logs est corrompu : '{md_file}'\nErreur : {error}", "log_link_to_log": "Journal complet de cette opération : ' {desc} '", "log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log show {name}'", @@ -274,7 +259,7 @@ "log_tools_upgrade": "Mettre à jour les paquets du système", "log_tools_shutdown": "Éteindre votre serveur", "log_tools_reboot": "Redémarrer votre serveur", - "mail_unavailable": "Cette adresse d'email est réservée et doit être automatiquement attribuée au tout premier utilisateur", + "mail_unavailable": "Cette adresse e-mail est réservée au groupe des administrateurs", "good_practices_about_admin_password": "Vous êtes sur le point de définir un nouveau mot de passe administrateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou d'utiliser une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "good_practices_about_user_password": "Vous êtes sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "password_listed": "Ce mot de passe fait partie des mots de passe les plus utilisés dans le monde. Veuillez en choisir un autre moins commun et plus robuste.", @@ -294,18 +279,14 @@ "ask_new_path": "Nouveau chemin", "backup_actually_backuping": "Création d'une archive de sauvegarde à partir des fichiers collectés ...", "backup_mount_archive_for_restore": "Préparation de l'archive pour restauration...", - "confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais n'est pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que l'authentification unique et la sauvegarde/restauration peuvent ne pas être disponibles. L'installer quand même ? [{answers}] ", + "confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais n'est pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que l'authentification unique SSO et la sauvegarde/restauration peuvent ne pas être disponibles. L'installer quand même ? [{answers}] ", "confirm_app_install_danger": "DANGER ! Cette application est connue pour être encore expérimentale (si elle ne fonctionne pas explicitement) ! Vous ne devriez probablement PAS l'installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système... Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers}'", "confirm_app_install_thirdparty": "DANGER ! Cette application ne fait pas partie du catalogue d'applications de YunoHost. L'installation d'applications tierces peut compromettre l'intégrité et la sécurité de votre système. Vous ne devriez probablement PAS l'installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système... Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers}'", - "dpkg_is_broken": "Vous ne pouvez pas faire ça maintenant car dpkg/apt (le gestionnaire de paquets du système) semble avoir laissé des choses non configurées. Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo apt install --fix-broken` et/ou `sudo dpkg --configure -a'.", + "dpkg_is_broken": "Vous ne pouvez pas faire ça maintenant car dpkg/apt (le gestionnaire de paquets du système) semble avoir laissé des choses non configurées. Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo apt install --fix-broken` et/ou `sudo dpkg --configure -a` et/ou `sudo dpkg --audit`.", "dyndns_could_not_check_available": "Impossible de vérifier si {domain} est disponible chez {provider}.", "file_does_not_exist": "Le fichier dont le chemin est {path} n'existe pas.", - "global_settings_setting_security_password_admin_strength": "Qualité du mot de passe administrateur", - "global_settings_setting_security_password_user_strength": "Qualité du mot de passe de l'utilisateur", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Autoriser l'utilisation de la clé hôte DSA (obsolète) pour la configuration du service SSH", "hook_json_return_error": "Échec de la lecture au retour du script {path}. Erreur : {msg}. Contenu brut : {raw_content}", "pattern_password_app": "Désolé, les mots de passe ne peuvent pas contenir les caractères suivants : {forbidden_chars}", - "root_password_replaced_by_admin_password": "Votre mot de passe root a été remplacé par votre mot de passe administrateur.", "service_reload_failed": "Impossible de recharger le service '{service}'.\n\nJournaux historisés récents de ce service : {logs}", "service_reloaded": "Le service '{service}' a été rechargé", "service_restart_failed": "Impossible de redémarrer le service '{service}'\n\nJournaux historisés récents de ce service : {logs}", @@ -314,7 +295,6 @@ "service_reloaded_or_restarted": "Le service '{service}' a été rechargé ou redémarré", "this_action_broke_dpkg": "Cette action a laissé des paquets non configurés par dpkg/apt (les gestionnaires de paquets du système) ... Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo apt install --fix-broken` et/ou `sudo dpkg --configure -a`.", "app_action_cannot_be_ran_because_required_services_down": "Ces services requis doivent être en cours d'exécution pour exécuter cette action : {services}. Essayez de les redémarrer pour continuer (et éventuellement rechercher pourquoi ils sont en panne).", - "admin_password_too_long": "Veuillez choisir un mot de passe comportant moins de 127 caractères", "log_regen_conf": "Régénérer les configurations du système '{}'", "regenconf_file_backed_up": "Le fichier de configuration '{conf}' a été sauvegardé sous '{backup}'", "regenconf_file_copy_failed": "Impossible de copier le nouveau fichier de configuration '{new}' vers '{conf}'", @@ -326,9 +306,6 @@ "regenconf_now_managed_by_yunohost": "Le fichier de configuration '{conf}' est maintenant géré par YunoHost (catégorie {category}).", "regenconf_up_to_date": "La configuration est déjà à jour pour la catégorie '{category}'", "already_up_to_date": "Il n'y a rien à faire. Tout est déjà à jour.", - "global_settings_setting_security_nginx_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur web Nginx. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)", - "global_settings_setting_security_ssh_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur SSH. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)", - "global_settings_setting_security_postfix_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur Postfix. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)", "regenconf_file_kept_back": "Le fichier de configuration '{conf}' devait être supprimé par 'regen-conf' (catégorie {category}) mais a été conservé.", "regenconf_updated": "La configuration a été mise à jour pour '{category}'", "regenconf_would_be_updated": "La configuration aurait dû être mise à jour pour la catégorie '{category}'", @@ -395,7 +372,7 @@ "app_install_failed": "Impossible d'installer {app} : {error}", "app_install_script_failed": "Une erreur est survenue dans le script d'installation de l'application", "permission_require_account": "Permission {permission} n'a de sens que pour les utilisateurs ayant un compte et ne peut donc pas être activé pour les visiteurs.", - "app_remove_after_failed_install": "Supprimer l'application après l'échec de l'installation...", + "app_remove_after_failed_install": "Suppression de l'application après l'échec de l'installation ...", "diagnosis_cant_run_because_of_dep": "Impossible d'exécuter le diagnostic pour {category} alors qu'il existe des problèmes importants liés à {dep}.", "diagnosis_found_errors": "Trouvé {errors} problème(s) significatif(s) lié(s) à {category} !", "diagnosis_found_errors_and_warnings": "Trouvé {errors} problème(s) significatif(s) (et {warnings} (avertissement(s)) en relation avec {category} !", @@ -405,14 +382,14 @@ "diagnosis_dns_missing_record": "Selon la configuration DNS recommandée, vous devez ajouter un enregistrement DNS
Type : {type}
Nom : {name}
Valeur : {value}", "diagnosis_diskusage_ok": "L'espace de stockage {mountpoint} (sur le périphérique {device}) a encore {free} ({free_percent}%) d'espace restant (sur {total}) !", "diagnosis_ram_ok": "Le système dispose encore de {available} ({available_percent}%) de RAM sur {total}.", - "diagnosis_regenconf_allgood": "Tous les fichiers de configuration sont conformes à la configuration recommandée !", - "diagnosis_security_vulnerable_to_meltdown": "Vous semblez vulnérable à la vulnérabilité de sécurité critique de Meltdown", + "diagnosis_regenconf_allgood": "Tous les fichiers de configuration sont conformes aux préconisations !", + "diagnosis_security_vulnerable_to_meltdown": "Vous semblez vulnérable à la faille de sécurité majeure qu'est Meltdown", "diagnosis_basesystem_host": "Le serveur utilise Debian {debian_version}", "diagnosis_basesystem_kernel": "Le serveur utilise le noyau Linux {kernel_version}", "diagnosis_basesystem_ynh_single_version": "{package} version : {version} ({repo})", "diagnosis_basesystem_ynh_main_version": "Le serveur utilise YunoHost {main_version} ({repo})", "diagnosis_basesystem_ynh_inconsistent_versions": "Vous exécutez des versions incohérentes des packages YunoHost ... très probablement en raison d'une mise à niveau échouée ou partielle.", - "diagnosis_failed_for_category": "Échec du diagnostic pour la catégorie '{category}': {error}", + "diagnosis_failed_for_category": "Échec du diagnostic pour la catégorie '{category}' : {error}", "diagnosis_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment !)", "diagnosis_ignored_issues": "(+ {nb_ignored} problème(s) ignoré(s))", "diagnosis_found_warnings": "Trouvé {warnings} objet(s) pouvant être amélioré(s) pour {category}.", @@ -423,7 +400,7 @@ "diagnosis_ip_connected_ipv6": "Le serveur est connecté à Internet en IPv6 !", "diagnosis_ip_no_ipv6": "Le serveur ne dispose pas d'une adresse IPv6.", "diagnosis_ip_dnsresolution_working": "La résolution de nom de domaine fonctionne !", - "diagnosis_ip_broken_dnsresolution": "La résolution du nom de domaine semble interrompue pour une raison quelconque ... Un pare-feu bloque-t-il les requêtes DNS ?", + "diagnosis_ip_broken_dnsresolution": "La résolution du nom de domaine semble bloquée ou interrompue pour une raison quelconque ... Un pare-feu bloque-t-il les requêtes DNS ?", "diagnosis_ip_broken_resolvconf": "La résolution du nom de domaine semble être cassée sur votre serveur, ce qui semble lié au fait que /etc/resolv.conf ne pointe pas vers 127.0.0.1.", "diagnosis_dns_good_conf": "Les enregistrements DNS sont correctement configurés pour le domaine {domain} (catégorie {category})", "diagnosis_dns_bad_conf": "Certains enregistrements DNS sont manquants ou incorrects pour le domaine {domain} (catégorie {category})", @@ -442,7 +419,7 @@ "apps_catalog_failed_to_download": "Impossible de télécharger le catalogue des applications {apps_catalog} : {error}", "diagnosis_mail_outgoing_port_25_blocked": "Le port sortant 25 semble être bloqué. Vous devriez essayer de le débloquer dans le panneau de configuration de votre fournisseur de services Internet (ou hébergeur). En attendant, le serveur ne pourra pas envoyer des emails à d'autres serveurs.", "domain_cannot_remove_main_add_new_one": "Vous ne pouvez pas supprimer '{domain}' car il s'agit du domaine principal et de votre seul domaine. Vous devez d'abord ajouter un autre domaine à l'aide de 'yunohost domain add ', puis définir comme domaine principal à l'aide de 'yunohost domain main-domain -n ' et vous pouvez ensuite supprimer le domaine '{domain}' à l'aide de 'yunohost domain remove {domain}'.'", - "diagnosis_security_vulnerable_to_meltdown_details": "Pour résoudre ce problème, vous devez mettre à niveau votre système et redémarrer pour charger le nouveau noyau Linux (ou contacter votre fournisseur de serveur si cela ne fonctionne pas). Voir https://meltdownattack.com/ pour plus d'informations.", + "diagnosis_security_vulnerable_to_meltdown_details": "Pour résoudre ce problème, vous devez mettre à jour votre système et le redémarrer pour charger le nouveau noyau linux (ou contacter votre fournisseur de serveur si cela ne fonctionne pas). Voir https://meltdownattack.com/ pour plus d'informations.", "diagnosis_description_basesystem": "Système de base", "diagnosis_description_ip": "Connectivité Internet", "diagnosis_description_dnsrecords": "Enregistrements DNS", @@ -456,8 +433,8 @@ "apps_catalog_obsolete_cache": "Le cache du catalogue d'applications est vide ou obsolète.", "apps_catalog_update_success": "Le catalogue des applications a été mis à jour !", "diagnosis_description_mail": "Email", - "diagnosis_ports_unreachable": "Le port {port} n'est pas accessible de l'extérieur.", - "diagnosis_ports_ok": "Le port {port} est accessible de l'extérieur.", + "diagnosis_ports_unreachable": "Le port {port} n'est pas accessible depuis l'extérieur.", + "diagnosis_ports_ok": "Le port {port} est accessible depuis l'extérieur.", "diagnosis_http_could_not_diagnose": "Impossible de diagnostiquer si le domaine est accessible de l'extérieur.", "diagnosis_http_could_not_diagnose_details": "Erreur : {error}", "diagnosis_http_ok": "Le domaine {domain} est accessible en HTTP depuis l'extérieur.", @@ -470,56 +447,55 @@ "diagnosis_ports_forwarding_tip": "Pour résoudre ce problème, vous devez probablement configurer la redirection de port sur votre routeur Internet comme décrit dans https://yunohost.org/isp_box_config", "diagnosis_http_connection_error": "Erreur de connexion : impossible de se connecter au domaine demandé, il est probablement injoignable.", "diagnosis_no_cache": "Pas encore de cache de diagnostique pour la catégorie '{category}'", - "yunohost_postinstall_end_tip": "La post-installation est terminée ! Pour finaliser votre configuration, il est recommandé de :\n- ajouter un premier utilisateur depuis la section \"Utilisateurs\" de l'interface web (ou 'yunohost user create ' en ligne de commande) ;\n- diagnostiquer les potentiels problèmes dans la section \"Diagnostic\" de l'interface web (ou 'yunohost diagnosis run' en ligne de commande) ;\n- lire les parties 'Finalisation de votre configuration' et 'Découverte de YunoHost' dans le guide de l'administrateur : https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "La post-installation est terminée ! Pour finaliser votre installation, il est recommandé de :\n - diagnostiquer les potentiels problèmes dans la section 'Diagnostic' de l'interface web (ou 'yunohost diagnosis run' en ligne de commande) ;\n - lire les parties 'Lancer la configuration initiale' et 'Découvrez l'auto-hébergement, comment installer et utiliser YunoHost' dans le guide d'administration : https://yunohost.org/admindoc.", "diagnosis_services_bad_status_tip": "Vous pouvez essayer de redémarrer le service, et si cela ne fonctionne pas, consultez les journaux de service dans le webadmin (à partir de la ligne de commande, vous pouvez le faire avec yunohost service restart {service} et yunohost service log {service} ).", "diagnosis_http_bad_status_code": "Le système de diagnostique n'a pas réussi à contacter votre serveur. Il se peut qu'une autre machine réponde à la place de votre serveur. Vérifiez que le port 80 est correctement redirigé, que votre configuration Nginx est à jour et qu'un reverse-proxy n'interfère pas.", - "diagnosis_http_timeout": "Expiration du délai en essayant de contacter votre serveur de l'extérieur. Il semble être inaccessible. Vérifiez que vous transférez correctement le port 80, que Nginx est en cours d'exécution et qu'un pare-feu n'interfère pas.", - "global_settings_setting_pop3_enabled": "Activer le protocole POP3 pour le serveur de messagerie", + "diagnosis_http_timeout": "Expiration du délai en essayant de contacter votre serveur depuis l'extérieur. Il semble être inaccessible.
1. La cause la plus fréquente pour ce problème est que les ports 80 et 443 ne sont pas correctement redirigés vers votre serveur.
2. Vous devriez également vérifier que le service NGINX est en cours d'exécution
3. Pour les installations plus complexes, assurez-vous qu'aucun pare-feu ou reverse-proxy n'interfère.", + "global_settings_setting_pop3_enabled_help": "Activer le protocole POP3 pour le serveur de messagerie", "log_app_action_run": "Lancer l'action de l'application '{}'", "diagnosis_never_ran_yet": "Il apparaît que le serveur a été installé récemment et qu'il n'y a pas encore eu de diagnostic. Vous devriez en lancer un depuis la webadmin ou en utilisant 'yunohost diagnosis run' depuis la ligne de commande.", "diagnosis_description_web": "Web", "diagnosis_basesystem_hardware": "L'architecture du serveur est {virt} {arch}", "group_already_exist_on_system_but_removing_it": "Le groupe {group} est déjà présent dans les groupes du système, mais YunoHost va le supprimer...", "certmanager_warning_subdomain_dns_record": "Le sous-domaine '{subdomain}' ne résout pas vers la même adresse IP que '{domain}'. Certaines fonctionnalités seront indisponibles tant que vous n'aurez pas corrigé cela et regénéré le certificat.", - "domain_cannot_add_xmpp_upload": "Vous ne pouvez pas ajouter de domaine commençant par 'xmpp-upload.'. Ce type de nom est réservé à la fonctionnalité d'upload XMPP intégrée dans YunoHost.", + "domain_cannot_add_xmpp_upload": "Vous ne pouvez pas ajouter de domaine commençant par 'xmpp-upload.'. Ce type de nom est réservé à la fonctionnalité XMPP éponyme intégrée dans YunoHost.", "diagnosis_mail_outgoing_port_25_ok": "Le serveur de messagerie SMTP peut envoyer des emails (le port sortant 25 n'est pas bloqué).", - "diagnosis_mail_outgoing_port_25_blocked_details": "Vous devez d'abord essayer de débloquer le port sortant 25 dans votre interface de routeur Internet ou votre interface d'hébergement. (Certains hébergeurs peuvent vous demander de leur envoyer un ticket de support pour cela).", + "diagnosis_mail_outgoing_port_25_blocked_details": "Vous devriez d'abord essayer d'ouvrir le port 25 dans l'interface de votre routeur, box Internet ou interface d'hébergement. (Certains hébergeurs peuvent vous demander d'ouvrir un ticket sur leur support d'assistance pour cela).", "diagnosis_mail_ehlo_bad_answer": "Un service non SMTP a répondu sur le port 25 en IPv{ipversion}", - "diagnosis_mail_ehlo_bad_answer_details": "Cela peut être dû à une autre machine qui répond au lieu de votre serveur.", + "diagnosis_mail_ehlo_bad_answer_details": "Cela peut être dû à une autre machine qui répond à la place de votre serveur.", "diagnosis_mail_ehlo_wrong": "Un autre serveur de messagerie SMTP répond sur IPv{ipversion}. Votre serveur ne sera probablement pas en mesure de recevoir des email.", "diagnosis_mail_ehlo_could_not_diagnose": "Impossible de diagnostiquer si le serveur de messagerie postfix est accessible de l'extérieur en IPv{ipversion}.", "diagnosis_mail_ehlo_could_not_diagnose_details": "Erreur : {error}", - "diagnosis_mail_fcrdns_dns_missing": "Aucun DNS inverse n'est défini pour IPv{ipversion}. Certains emails seront peut-être refusés ou considérés comme des spam.", + "diagnosis_mail_fcrdns_dns_missing": "Aucun reverse-DNS n'est défini pour IPv{ipversion}. Il se peut que certains emails ne soient pas acheminés ou soient considérés comme du spam.", "diagnosis_mail_fcrdns_ok": "Votre DNS inverse est correctement configuré !", - "diagnosis_mail_fcrdns_nok_details": "Vous devez d'abord essayer de configurer le DNS inverse avec {ehlo_domain} dans votre interface de routeur Internet ou votre interface d'hébergement. (Certains hébergeurs peuvent vous demander de leur envoyer un ticket de support pour cela).", - "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Le DNS inverse n'est pas correctement configuré en IPv{ipversion}. Certains emails seront peut-être refusés ou considérés comme des spam.", + "diagnosis_mail_fcrdns_nok_details": "Vous devez d'abord essayer de configurer le reverse-DNS avec {ehlo_domain} dans l'interface de votre routeur, box Internet ou votre interface d'hébergement. (Certains hébergeurs peuvent vous demander d'ouvrir un ticket sur leur support d'assistance pour cela).", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Le reverse-DNS n'est pas correctement configuré en IPv{ipversion}. Il se peut que certains emails ne soient pas acheminés ou soient considérés comme du spam.", "diagnosis_mail_blacklist_ok": "Les adresses IP et les domaines utilisés par ce serveur ne semblent pas être sur liste noire", "diagnosis_mail_blacklist_reason": "La raison de la liste noire est : {reason}", - "diagnosis_mail_blacklist_website": "Après avoir identifié la raison pour laquelle vous êtes répertorié et l'avoir corrigé, n'hésitez pas à demander le retrait de votre IP ou domaine sur {blacklist_website}", + "diagnosis_mail_blacklist_website": "Après avoir identifié la raison pour laquelle vous êtes répertorié sur cette liste et l'avoir corrigée, n'hésitez pas à demander le retrait de votre IP ou de votre domaine sur {blacklist_website}", "diagnosis_mail_queue_ok": "{nb_pending} emails en attente dans les files d'attente de messagerie", "diagnosis_mail_queue_unavailable_details": "Erreur : {error}", "diagnosis_mail_queue_too_big": "Trop d'emails en attente dans la file d'attente ({nb_pending} emails)", - "global_settings_setting_smtp_allow_ipv6": "Autoriser l'utilisation d'IPv6 pour recevoir et envoyer du courrier", "diagnosis_display_tip": "Pour voir les problèmes détectés, vous pouvez accéder à la section Diagnostic du webadmin ou exécuter 'yunohost diagnosis show --issues --human-readable' à partir de la ligne de commande.", "diagnosis_ip_global": "IP globale : {global}", "diagnosis_ip_local": "IP locale : {local}", - "diagnosis_dns_point_to_doc": "Veuillez consulter la documentation sur https://yunohost.org/dns_config si vous avez besoin d'aide pour configurer les enregistrements DNS.", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Certains fournisseurs ne vous laisseront pas débloquer le port sortant 25 parce qu'ils ne se soucient pas de la neutralité du Net.
- Certains d'entre eux offrent l'alternative d'utiliser un serveur de messagerie relai bien que cela implique que le relai sera en mesure d'espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Vous pouvez également envisager de passer à un fournisseur plus respectueux de la neutralité du net", + "diagnosis_dns_point_to_doc": "Veuillez consulter la documentation disponible ici https://yunohost.org/dns_config si vous avez besoin d'aide pour configurer les enregistrements DNS.", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Certains opérateurs ne vous laisseront pas débloquer le port 25 parce qu'ils ne se soucient pas de la neutralité du Net.
- Certains d'entre eux offrent la possibilité d'utiliser un serveur de messagerie relai bien que cela implique que celui-ci sera en mesure d'espionner le trafic de votre messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Vous pouvez également envisager de passer à un fournisseur plus respectueux de la neutralité du net", "diagnosis_mail_ehlo_ok": "Le serveur de messagerie SMTP est accessible de l'extérieur et peut donc recevoir des emails !", "diagnosis_mail_ehlo_unreachable": "Le serveur de messagerie SMTP est inaccessible de l'extérieur en IPv{ipversion}. Il ne pourra pas recevoir des emails.", - "diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible.
1. La cause la plus courante de ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur.
2. Vous devez également vous assurer que le service postfix est en cours d'exécution.
3. Sur les configurations plus complexes: assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.", + "diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible.
1. La cause la plus courante de ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur.
2. Vous devez également vous assurer que le service postfix est en cours d'exécution.
3. Sur les configurations plus complexes : assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.", "diagnosis_mail_ehlo_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur.
EHLO reçu : {wrong_ehlo}
Attendu : {right_ehlo}
La cause la plus courante à ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur. Vous pouvez également vous assurer qu'aucun pare-feu ou reverse-proxy n'interfère.", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes :
- Certains FAI fournissent l'alternative de à l'aide d'un relais de serveur de messagerie bien que cela implique que le relais pourra espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Enfin, il est également possible de changer de fournisseur", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si votre DNS inversé est correctement configuré en IPv4, vous pouvez essayer de désactiver l'utilisation d'IPv6 lors de l'envoi d'emails en exécutant yunohost settings set smtp.allow_ipv6 -v off. Remarque : cette dernière solution signifie que vous ne pourrez pas envoyer ou recevoir de emails avec les quelques serveurs qui ont uniquement de l'IPv6.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains opérateurs ne vous laisseront pas configurer votre reverse-DNS (ou leur fonctionnalité pourrait être cassée ...). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes :
- Certains FAI offre cette possibilité à l'aide d'un relais de serveur de messagerie bien que cela implique que le relais pourra espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Enfin, il est également possible de changer d'opérateur", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si votre DNS inversé est correctement configuré en IPv4, vous pouvez essayer de désactiver l'utilisation d'IPv6 lors de l'envoi d'emails en exécutant yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Remarque : cette dernière solution signifie que vous ne pourrez pas envoyer ou recevoir d'emails avec les quelques serveurs qui ont uniquement de l'IPv6.", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS inverse actuel : {rdns_domain}
Valeur attendue : {ehlo_domain}", "diagnosis_mail_blacklist_listed_by": "Votre IP ou domaine {item} est sur liste noire sur {blacklist_name}", "diagnosis_mail_queue_unavailable": "Impossible de consulter le nombre d'emails en attente dans la file d'attente", - "diagnosis_ports_partially_unreachable": "Le port {port} n'est pas accessible de l'extérieur en IPv{failed}.", + "diagnosis_ports_partially_unreachable": "Le port {port} n'est pas accessible depuis l'extérieur en IPv{failed}.", "diagnosis_http_hairpinning_issue": "Votre réseau local ne semble pas supporter l'hairpinning.", "diagnosis_http_hairpinning_issue_details": "C'est probablement à cause de la box/routeur de votre fournisseur d'accès internet. Par conséquent, les personnes extérieures à votre réseau local pourront accéder à votre serveur comme prévu, mais pas les personnes internes au réseau local (comme vous, probablement ?) si elles utilisent le nom de domaine ou l'IP globale. Vous pourrez peut-être améliorer la situation en consultant https://yunohost.org/dns_local_network", "diagnosis_http_partially_unreachable": "Le domaine {domain} semble inaccessible en HTTP depuis l'extérieur du réseau local en IPv{failed}, bien qu'il fonctionne en IPv{passed}.", "diagnosis_http_nginx_conf_not_up_to_date": "La configuration Nginx de ce domaine semble avoir été modifiée manuellement et empêche YunoHost de diagnostiquer si elle est accessible en HTTP.", - "diagnosis_http_nginx_conf_not_up_to_date_details": "Pour corriger la situation, inspectez la différence avec la ligne de commande en utilisant les outils yunohost tools regen-conf nginx --dry-run --with-diff et si vous êtes d'accord, appliquez les modifications avec yunohost tools regen-conf nginx --force.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Pour corriger la situation, vérifier les différences avec la ligne de commande en utilisant les outils yunohost tools regen-conf nginx --dry-run --with-diff et si vous êtes d'accord avec le résultat, appliquez les modifications avec yunohost tools regen-conf nginx --force.", "backup_archive_cant_retrieve_info_json": "Impossible d'avoir des informations sur l'archive '{archive}'... Le fichier info.json ne peut pas être trouvé (ou n'est pas un fichier json valide).", "backup_archive_corrupted": "Il semble que l'archive de la sauvegarde '{archive}' est corrompue : {error}", "diagnosis_ip_no_ipv6_tip": "L'utilisation de IPv6 n'est pas obligatoire pour le fonctionnement de votre serveur, mais cela contribue à la santé d'Internet dans son ensemble. IPv6 généralement configuré automatiquement par votre système ou votre FAI s'il est disponible. Autrement, vous devrez prendre quelque minutes pour le configurer manuellement à l'aide de cette documentation : https://yunohost.org/#/ipv6. Si vous ne pouvez pas activer IPv6 ou si c'est trop technique pour vous, vous pouvez aussi ignorer cet avertissement sans que cela pose problème.", @@ -530,27 +506,27 @@ "diagnosis_domain_expiration_warning": "Certains domaines vont expirer prochainement !", "diagnosis_domain_expiration_error": "Certains domaines vont expirer TRÈS PROCHAINEMENT !", "diagnosis_domain_expires_in": "{domain} expire dans {days} jours.", - "certmanager_domain_not_diagnosed_yet": "Il n'y a pas encore de résultat de diagnostic pour le domaine {domain}. Merci de relancer un diagnostic pour les catégories 'Enregistrements DNS' et 'Web' dans la section Diagnostique pour vérifier si le domaine est prêt pour Let's Encrypt. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)", - "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.", + "certmanager_domain_not_diagnosed_yet": "Il n'y a pas encore de résultat de diagnostic pour le domaine {domain}. Merci de relancer un diagnostic pour les catégories 'Enregistrements DNS' et 'Web' dans la section Diagnostic pour vérifier si le domaine est prêt pour Let's Encrypt. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)", + "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.", "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}", + "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}", "ask_user_domain": "Domaine à utiliser pour l'adresse email de l'utilisateur et le compte XMPP", "app_manifest_install_ask_is_public": "Cette application devrait-elle être visible par les visiteurs anonymes ?", "app_manifest_install_ask_admin": "Choisissez un administrateur pour cette application", "app_manifest_install_ask_password": "Choisissez un mot de passe administrateur pour cette application", "app_manifest_install_ask_path": "Choisissez le chemin d'URL (après le domaine) où cette application doit être installée", "app_manifest_install_ask_domain": "Choisissez le domaine sur lequel vous souhaitez installer cette application", - "global_settings_setting_smtp_relay_user": "Compte utilisateur du relais SMTP", + "global_settings_setting_smtp_relay_host": "Adresse du relais SMTP", + "global_settings_setting_smtp_relay_user": "Utilisateur du relais SMTP", "global_settings_setting_smtp_relay_port": "Port du relais SMTP", - "global_settings_setting_smtp_relay_host": "Un relais SMTP permet d'envoyer du courrier à la place de cette instance YunoHost. Cela est utile si vous êtes dans l'une de ces situations : le port 25 est bloqué par votre FAI ou par votre fournisseur VPS, vous avez une IP résidentielle répertoriée sur DUHL, vous ne pouvez pas configurer de reverse DNS ou le serveur n'est pas directement accessible depuis Internet et que vous voulez en utiliser un autre pour envoyer des mails.", "diagnosis_package_installed_from_sury_details": "Certains paquets ont été installés par inadvertance à partir d'un dépôt tiers appelé Sury. L'équipe YunoHost a amélioré la stratégie de gestion de ces paquets, mais on s'attend à ce que certaines configurations qui ont installé des applications PHP7.3 tout en étant toujours sur Stretch présentent des incohérences. Pour résoudre cette situation, vous devez essayer d'exécuter la commande suivante : {cmd_to_fix}", - "app_argument_password_no_default": "Erreur lors de l'analyse de l'argument de mot de passe '{name}' : l'argument de mot de passe ne peut pas avoir de valeur par défaut pour des raisons de sécurité", + "app_argument_password_no_default": "Erreur lors de l'analyse syntaxique du mot de passe '{name}' : le mot de passe ne peut pas avoir de valeur par défaut pour des raisons de sécurité", "pattern_email_forward": "L'adresse électronique doit être valide, le symbole '+' étant accepté (par exemple : johndoe+yunohost@exemple.com)", - "global_settings_setting_smtp_relay_password": "Mot de passe du relais de l'hôte SMTP", + "global_settings_setting_smtp_relay_password": "Mot de passe du relais SMTP", "diagnosis_package_installed_from_sury": "Des paquets du système devraient être rétrogradé de version", "additional_urls_already_added": "URL supplémentaire '{url}' déjà ajoutée pour la permission '{permission}'", "unknown_main_domain_path": "Domaine ou chemin inconnu pour '{app}'. Vous devez spécifier un domaine et un chemin pour pouvoir spécifier une URL pour l'autorisation.", @@ -564,37 +540,32 @@ "additional_urls_already_removed": "URL supplémentaire '{url}' déjà supprimées pour la permission '{permission}'", "invalid_number": "Doit être un nombre", "diagnosis_basesystem_hardware_model": "Le modèle/architecture du serveur est {model}", - "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système.", + "diagnosis_backports_in_sources_list": "Il semble que le gestionnaire de paquet APT soit configuré pour utiliser le dépôt des rétro-portages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant du dépôt 'backports', car cela risque de créer des instabilités ou des conflits sur votre système.", "postinstall_low_rootfsspace": "Le système de fichiers a une taille totale inférieure à 10 Go, ce qui est préoccupant et devrait attirer votre attention ! Vous allez certainement arriver à court d'espace disque (très) rapidement ! Il est recommandé d'avoir au moins 16 Go à la racine pour ce système de fichiers. Si vous voulez installer YunoHost malgré cet avertissement, relancez la post-installation avec --force-diskspace", "domain_remove_confirm_apps_removal": "Le retrait de ce domaine retirera aussi ces applications :\n{apps}\n\nÊtes vous sûr de vouloir cela ? [{answers}]", "diagnosis_rootfstotalspace_critical": "Le système de fichiers racine ne fait que {space} ! Vous allez certainement le remplir très rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.", "diagnosis_rootfstotalspace_warning": "Le système de fichiers racine n'est que de {space}. Cela peut suffire, mais faites attention car vous risquez de les remplir rapidement... Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.", "app_restore_script_failed": "Une erreur s'est produite dans le script de restauration de l'application", "restore_backup_too_old": "Cette sauvegarde ne peut pas être restaurée car elle provient d'une version de YunoHost trop ancienne.", - "log_backup_create": "Création d'une archive de sauvegarde", - "global_settings_setting_ssowat_panel_overlay_enabled": "Activer la superposition de la vignette SSOwat", + "log_backup_create": "Créer une archive de sauvegarde", + "global_settings_setting_ssowat_panel_overlay_enabled": "Activer la vignette 'YunoHost' (raccourci vers le portail) sur les apps", "migration_ldap_rollback_success": "Système rétabli dans son état initial.", "permission_cant_add_to_all_users": "L'autorisation {permission} ne peut pas être ajoutée à tous les utilisateurs.", "migration_ldap_migration_failed_trying_to_rollback": "Impossible de migrer... tentative de restauration du système.", "migration_ldap_can_not_backup_before_migration": "La sauvegarde du système n'a pas pu être terminée avant l'échec de la migration. Erreur : {error }", "migration_ldap_backup_before_migration": "Création d'une sauvegarde de la base de données LDAP et des paramètres des applications avant la migration proprement dite.", - "global_settings_setting_security_ssh_port": "Port SSH", - "diagnosis_sshd_config_inconsistent_details": "Veuillez exécuter yunohost settings set security.ssh.port -v VOTRE_PORT_SSH pour définir le port SSH, et vérifiez yunohost tools regen-conf ssh --dry-run --with-diff et yunohost tools regen-conf ssh --force pour réinitialiser votre configuration aux recommandations YunoHost.", - "diagnosis_sshd_config_inconsistent": "Il semble que le port SSH a été modifié manuellement dans /etc/ssh/sshd_config. Depuis YunoHost 4.2, un nouveau paramètre global 'security.ssh.port' est disponible pour éviter de modifier manuellement la configuration.", + "diagnosis_sshd_config_inconsistent_details": "Veuillez exécuter yunohost settings set security.ssh.ssh_port -v VOTRE_PORT_SSH pour définir le port SSH, et vérifiez yunohost tools regen-conf ssh --dry-run --with-diff et yunohost tools regen-conf ssh --force pour réinitialiser votre configuration aux recommandations YunoHost.", + "diagnosis_sshd_config_inconsistent": "Il semble que le port SSH ait été modifié manuellement dans /etc/ssh/sshd_config. Depuis YunoHost 4.2, un nouveau paramètre global 'security.ssh.ssh_port' est disponible pour éviter de modifier manuellement la configuration.", "diagnosis_sshd_config_insecure": "La configuration SSH semble avoir été modifiée manuellement et n'est pas sécurisée car elle ne contient aucune directive 'AllowGroups' ou 'AllowUsers' pour limiter l'accès aux utilisateurs autorisés.", "backup_create_size_estimation": "L'archive contiendra environ {size} de données.", - "global_settings_setting_security_webadmin_allowlist": "Adresses IP autorisées à accéder à la webadmin. Elles doivent être séparées par une virgule.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Autoriser seulement certaines IP à accéder à la webadmin.", "diagnosis_dns_specialusedomain": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial comme .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.", - "invalid_password": "Mot de passe incorrect", "ldap_server_is_down_restart_it": "Le service LDAP est en panne, essayez de le redémarrer...", "ldap_server_down": "Impossible d'atteindre le serveur LDAP", - "global_settings_setting_security_experimental_enabled": "Activer les fonctionnalités de sécurité expérimentales (ne l'activez pas si vous ne savez pas ce que vous faites !)", - "diagnosis_apps_deprecated_practices": "La version installée de cette application utilise toujours certaines pratiques de packaging obsolètes. Vous devriez vraiment envisager de mettre l'application à jour.", - "diagnosis_apps_outdated_ynh_requirement": "La version installée de cette application nécessite uniquement YunoHost >= 2.x, cela indique que l'application n'est pas à jour avec les bonnes pratiques de packaging et les helpers recommandées. Vous devriez vraiment envisager de mettre l'application à jour.", + "diagnosis_apps_deprecated_practices": "La version installée de cette application utilise encore de très anciennes pratiques de packaging obsolètes et dépassées. Vous devriez vraiment envisager de mettre à jour cette application.", + "diagnosis_apps_outdated_ynh_requirement": "La version installée de cette application nécessite uniquement YunoHost >= 2.x ou 3.x, ce qui tend à indiquer qu'elle n'est pas à jour avec les pratiques recommandées de packaging et des helpers . Vous devriez vraiment envisager de la mettre à jour.", "diagnosis_apps_bad_quality": "Cette application est actuellement signalée comme cassée dans le catalogue d'applications de YunoHost. Cela peut être un problème temporaire. En attendant que les mainteneurs tentent de résoudre le problème, la mise à jour de cette application est désactivée.", "diagnosis_apps_broken": "Cette application est actuellement signalée comme cassée dans le catalogue d'applications de YunoHost. Cela peut être un problème temporaire. En attendant que les mainteneurs tentent de résoudre le problème, la mise à jour de cette application est désactivée.", - "diagnosis_apps_not_in_app_catalog": "Cette application est absente ou ne figure plus dans le catalogue d'applications de YunoHost. Vous devriez envisager de la désinstaller car elle ne recevra pas de mise à jour et pourrait compromettre l'intégrité et la sécurité de votre système.", + "diagnosis_apps_not_in_app_catalog": "Cette application ne figure pas dans le catalogue de YunoHost. Si elle l'était dans le passé et a été supprimée, vous devriez envisager de désinstaller cette application car elle ne recevra pas de mises à jour et peut compromettre l'intégrité et la sécurité de votre système.", "diagnosis_apps_issue": "Un problème a été détecté pour l'application {app}", "diagnosis_apps_allgood": "Toutes les applications installées respectent les pratiques de packaging de base", "diagnosis_description_apps": "Applications", @@ -607,7 +578,6 @@ "user_import_bad_line": "Ligne incorrecte {line} : {details}", "log_user_import": "Importer des utilisateurs", "diagnosis_high_number_auth_failures": "Il y a eu récemment un grand nombre d'échecs d'authentification. Assurez-vous que Fail2Ban est en cours d'exécution et est correctement configuré, ou utilisez un port personnalisé pour SSH comme expliqué dans https://yunohost.org/security.", - "global_settings_setting_security_nginx_redirect_to_https": "Rediriger les requêtes HTTP vers HTTPS par défaut (NE PAS DÉSACTIVER à moins de savoir vraiment ce que vous faites !)", "config_validate_color": "Doit être une couleur hexadécimale RVB valide", "app_config_unable_to_apply": "Échec de l'application des valeurs du panneau de configuration.", "app_config_unable_to_read": "Échec de la lecture des valeurs du panneau de configuration.", @@ -659,7 +629,7 @@ "diagnosis_http_special_use_tld": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et n'est donc pas censé être exposé en dehors du réseau local.", "domain_dns_conf_special_use_tld": "Ce domaine est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.", "other_available_options": "... et {n} autres options disponibles non affichées", - "domain_config_auth_consumer_key": "Consumer key", + "domain_config_auth_consumer_key": "La clé utilisateur", "domain_unknown": "Domaine '{domain}' inconnu", "migration_0021_start": "Démarrage de la migration vers Bullseye", "migration_0021_patching_sources_list": "Mise à jour du fichier sources.lists...", @@ -673,9 +643,8 @@ "migration_0021_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés à la suite de la mise à niveau : {manually_modified_files}", "migration_0021_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus nécessaires...", "migration_0021_patch_yunohost_conflicts": "Application du correctif pour contourner le problème de conflit...", - "migration_0021_not_buster": "La distribution Debian actuelle n'est pas Buster !", + "migration_0021_not_buster2": "La distribution Debian actuelle n'est pas Buster ! Si vous avez déjà effectué la migration Buster->Bullseye, alors cette erreur est symptomatique du fait que la migration n'a pas été terminée correctement à 100% (sinon YunoHost aurait marqué la migration comme terminée). Il est recommandé d'étudier ce qu'il s'est passé avec l'équipe de support, qui aura besoin du log **complet** de la migration, qui peut être retrouvé dans Outils > Journaux dans la webadmin.", "migration_description_0021_migrate_to_bullseye": "Mise à niveau du système vers Debian Bullseye et YunoHost 11.x", - "global_settings_setting_security_ssh_password_authentication": "Autoriser l'authentification par mot de passe pour SSH", "domain_config_default_app": "Application par défaut", "migration_description_0022_php73_to_php74_pools": "Migration des fichiers de configuration php7.3-fpm 'pool' vers php7.4", "migration_description_0023_postgresql_11_to_13": "Migration des bases de données de PostgreSQL 11 vers 13", @@ -684,5 +653,94 @@ "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 est installé, mais pas PostgreSQL 13 ! ? Quelque chose d'anormal s'est peut-être produit sur votre système :(...", "tools_upgrade_failed": "Impossible de mettre à jour les paquets : {packages_list}", "migration_0023_not_enough_space": "Prévoyez suffisamment d'espace disponible dans {path} pour exécuter la migration.", - "migration_0023_postgresql_11_not_installed": "PostgreSQL n'a pas été installé sur votre système. Il n'y a rien à faire." + "migration_0023_postgresql_11_not_installed": "PostgreSQL n'a pas été installé sur votre système. Il n'y a rien à faire.", + "global_settings_setting_backup_compress_tar_archives_help": "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.", + "global_settings_setting_security_experimental_enabled": "Fonctionnalités de sécurité expérimentales", + "global_settings_setting_security_experimental_enabled_help": "Activer les fonctionnalités de sécurité expérimentales (ne l'activez pas si vous ne savez pas ce que vous faites !)", + "global_settings_setting_nginx_compatibility_help": "Compromis 'compatibilité versus sécurité' pour le serveur web Nginx. Affecte les cryptogrammes utilisés (et d'autres aspects liés à la sécurité)", + "global_settings_setting_nginx_redirect_to_https_help": "Rediriger les requêtes HTTP vers HTTPS par défaut (NE PAS DÉSACTIVER à moins de savoir vraiment ce que vous faites !)", + "global_settings_setting_admin_strength": "Critères pour les mots de passe administrateur", + "global_settings_setting_user_strength": "Critères pour les mots de passe utilisateurs", + "global_settings_setting_postfix_compatibility_help": "Compromis 'compatibilité versus sécurité' pour le serveur Postfix. Affecte les cryptogrammes utilisés (et d'autres aspects liés à la sécurité)", + "global_settings_setting_ssh_compatibility_help": "Compromis 'compatibilité versus sécurité' pour le serveur SSH. Affecte les cryptogrammes utilisés (et d'autres aspects liés à la sécurité).", + "global_settings_setting_ssh_password_authentication_help": "Autoriser l'authentification par mot de passe pour SSH", + "global_settings_setting_ssh_port": "Port SSH", + "global_settings_setting_webadmin_allowlist_help": "Adresses IP autorisées à accéder à la webadmin. Elles doivent être séparées par une virgule.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Autoriser seulement certaines IP à accéder à la webadmin.", + "global_settings_setting_smtp_allow_ipv6_help": "Autoriser l'utilisation d'IPv6 pour recevoir et envoyer du courrier", + "global_settings_setting_smtp_relay_enabled_help": "Un relais SMTP permet d'envoyer du courrier à la place de cette instance YunoHost. Cela est utile si vous êtes dans l'une de ces situations : le port 25 est bloqué par votre FAI ou par votre fournisseur VPS ; vous avez une IP résidentielle répertoriée sur DUHL ; vous ne pouvez pas configurer le DNS inversé ; ou le serveur n'est pas directement accessible depuis Internet et vous voulez en utiliser un autre pour envoyer des mails.", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "La reconstruction du virtualenv sera tentée pour les applications suivantes (NB : l'opération peut prendre un certain temps !) : {rebuild_apps}", + "migration_0024_rebuild_python_venv_in_progress": "Tentative de reconstruction du virtualenv Python pour `{app}`", + "migration_0024_rebuild_python_venv_failed": "Échec de la reconstruction de l'environnement virtuel Python pour {app}. L'application peut ne pas fonctionner tant que ce problème n'est pas résolu. Vous devriez corriger la situation en forçant la mise à jour de cette application en utilisant `yunohost app upgrade --force {app}`.", + "migration_description_0024_rebuild_python_venv": "Réparer l'application Python après la migration Bullseye", + "migration_0024_rebuild_python_venv_broken_app": "Ignorer {app} car virtualenv ne peut pas être facilement reconstruit pour cette application. Au lieu de cela, vous devriez corriger la situation en forçant la mise à jour de cette application en utilisant `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_disclaimer_base": "Suite à la mise à niveau vers Debian Bullseye, certaines applications Python doivent être partiellement reconstruites pour être converties vers la nouvelle version Python livrée dans Debian (en termes techniques : ce qu'on appelle le \"virtualenv\" doit être recréé). En attendant, ces applications Python peuvent ne pas fonctionner. YunoHost peut tenter de reconstruire le virtualenv pour certains d'entre eux, comme détaillé ci-dessous. Pour les autres applications, ou si la tentative de reconstruction échoue, vous devrez forcer manuellement une mise à niveau pour ces applications.", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Les virtualenvs ne peuvent pas être reconstruits automatiquement pour ces applications. Vous devez forcer une mise à jour pour ceux-ci, ce qui peut être fait à partir de la ligne de commande : `yunohost app upgrade --force APP` : {ignored_apps}", + "admins": "Administrateurs", + "all_users": "Tous les utilisateurs de YunoHost", + "app_action_failed": "Échec de la commande {action} de l'application {app}", + "app_manifest_install_ask_init_admin_permission": "Qui doit avoir accès aux fonctions d'administration de cette application ? (Ceci peut être modifié ultérieurement)", + "app_manifest_install_ask_init_main_permission": "Qui doit avoir accès à cette application ? (Ceci peut être modifié ultérieurement)", + "ask_admin_fullname": "Nom complet de l'administrateur", + "ask_admin_username": "Nom d'utilisateur de l'administrateur", + "ask_fullname": "Nom complet (Nom et Prénom)", + "certmanager_cert_install_failed": "L'installation du certificat Let's Encrypt a échoué pour {domains}", + "certmanager_cert_install_failed_selfsigned": "L'installation du certificat auto-signé a échoué pour {domains}", + "certmanager_cert_renew_failed": "Le renouvellement du certificat Let's Encrypt a échoué pour {domains}", + "diagnosis_using_stable_codename": "apt (le gestionnaire de paquets du système) est actuellement configuré pour installer les paquets du nom de code 'stable', et cela au lieu du nom de code de la version actuelle de Debian (bullseye).", + "diagnosis_using_stable_codename_details": "C'est généralement dû à une configuration incorrecte de votre fournisseur d'hébergement. C'est dangereux, car dès que la prochaine version de Debian deviendra la nouvelle 'stable', apt voudra mettre à jour tous les paquets système sans passer par une procédure de migration appropriée propre à YunoHost. Il est recommandé de corriger cela en éditant le source apt pour le dépôt Debian de base, et de remplacer le mot clé stable par bullseye. Le fichier de configuration correspondant doit être /etc/apt/sources.list, ou un fichier dans /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing_details": "C'est probablement normal si vous savez ce que vous faites, toutefois faites attention aux notes de version avant d'installer les mises à niveau de YunoHost ! Si vous voulez désactiver les mises à jour 'testing', vous devez supprimer le mot-clé testing de /etc/apt/sources.list.d/yunohost.list.", + "global_settings_setting_nginx_redirect_to_https": "Forcer HTTPS", + "global_settings_setting_postfix_compatibility": "Compatibilité Postfix", + "global_settings_setting_root_access_explain": "Sur les systèmes Linux, 'root' est l'administrateur absolu du système : il a tous les droits. Dans le contexte de YunoHost, la connexion SSH directe de 'root' est désactivée par défaut - sauf depuis le réseau local du serveur. Les membres du groupe 'admins' peuvent utiliser la commande sudo pour agir en tant que root à partir de la ligne de commande. Cependant, il peut être utile de disposer d'un mot de passe root (robuste) pour déboguer le système si, pour une raison quelconque, les administrateurs réguliers ne peuvent plus se connecter.", + "global_settings_setting_root_password_confirm": "Nouveau mot de passe root (confirmer)", + "global_settings_setting_smtp_relay_enabled": "Activer le relais SMTP", + "global_settings_setting_ssh_compatibility": "Compatibilité SSH", + "global_settings_setting_user_strength_help": "Ces paramètres ne seront appliqués que lors de l'initialisation ou de la modification du mot de passe", + "migration_description_0025_global_settings_to_configpanel": "Migrer l'ancienne terminologie des paramètres globaux vers la nouvelle terminologie modernisée", + "migration_description_0026_new_admins_group": "Migrer vers le nouveau système de gestion 'multi-administrateurs' (plusieurs utilisateurs pourront être présents dans le groupe 'Admins' avec des tous les droits d'administration sur toute l'instance YunoHost)", + "password_confirmation_not_the_same": "Le mot de passe et la confirmation de ce dernier ne correspondent pas", + "pattern_fullname": "Doit être un nom complet valide (au moins 3 caractères)", + "config_action_disabled": "Impossible d'exécuter l'action '{action}' car elle est désactivée, assurez-vous de respecter ses paramètres et contraintes. Aide : {help}", + "config_action_failed": "Échec de l'exécution de l'action '{action}' : {error}", + "config_forbidden_readonly_type": "Le type '{type}' ne peut pas être défini comme étant en lecture seule, utilisez un autre type pour obtenir cette valeur (identifiant de l'argument : '{id}').", + "global_settings_setting_pop3_enabled": "Activer POP3", + "registrar_infos": "Infos du Registrar (fournisseur du nom de domaine)", + "root_password_changed": "Le mot de passe de root a été changé", + "visitors": "Visiteurs", + "global_settings_reset_success": "Réinitialisation des paramètres généraux", + "domain_config_acme_eligible": "Éligibilité au protocole ACME (Automatic Certificate Management Environment, littéralement : environnement de gestion automatique de certificat)", + "domain_config_acme_eligible_explain": "Ce domaine ne semble pas près pour installer un certificat Let's Encrypt. Veuillez vérifier votre configuration DNS mais aussi que votre serveur est bien joignable en HTTP. Les sections 'Enregistrements DNS' et 'Web' de la page Diagnostic peuvent vous aider à comprendre ce qui est mal configuré.", + "domain_config_cert_install": "Installer un certificat Let's Encrypt", + "domain_config_cert_issuer": "Autorité de certification", + "domain_config_cert_no_checks": "Ignorer les tests et autres vérifications du diagnostic", + "domain_config_cert_renew": "Renouvellement du certificat Let's Encrypt", + "domain_config_cert_renew_help": "Le certificat sera automatiquement renouvelé dans les 15 derniers jours précédant sa fin de validité. Vous pouvez le renouveler manuellement si vous le souhaitez (non recommandé).", + "domain_config_cert_summary": "État du certificat", + "domain_config_cert_summary_abouttoexpire": "Le certificat actuel est sur le point d'expirer. Il devrait bientôt être renouvelé automatiquement.", + "domain_config_cert_summary_expired": "ATTENTION : Le certificat actuel n'est pas valide ! HTTPS ne fonctionnera pas du tout !", + "domain_config_cert_summary_letsencrypt": "Bravo ! Vous utilisez un certificat Let's Encrypt valide !", + "domain_config_cert_summary_ok": "Bien, le certificat actuel semble bon !", + "domain_config_cert_summary_selfsigned": "AVERTISSEMENT : Le certificat actuel est auto-signé. Les navigateurs afficheront un avertissement effrayant aux nouveaux visiteurs !", + "domain_config_cert_validity": "Validité", + "global_settings_setting_admin_strength_help": "Ces paramètres ne seront appliqués que lors de l'initialisation ou de la modification du mot de passe", + "global_settings_setting_nginx_compatibility": "Compatibilité NGINX", + "global_settings_setting_root_password": "Nouveau mot de passe root", + "global_settings_setting_ssh_password_authentication": "Authentification par mot de passe", + "global_settings_setting_webadmin_allowlist": "Liste des IP autorisées pour l'administration Web", + "global_settings_setting_webadmin_allowlist_enabled": "Activer la liste des IP autorisées pour l'administration Web", + "invalid_credentials": "Mot de passe ou nom d'utilisateur incorrect", + "log_resource_snippet": "Allocation/retrait/mise à jour d'une ressource", + "log_settings_reset": "Réinitialisation des paramètres", + "log_settings_reset_all": "Réinitialisation de tous les paramètres", + "log_settings_set": "Application des paramètres", + "diagnosis_using_yunohost_testing": "apt (le gestionnaire de paquets du système) est actuellement configuré pour installer toutes les mises à niveau dites 'testing' de votre instance YunoHost.", + "global_settings_setting_smtp_allow_ipv6": "Autoriser l'IPv6", + "password_too_long": "Veuillez choisir un mot de passe de moins de 127 caractères", + "domain_cannot_add_muc_upload": "Vous ne pouvez pas ajouter de domaines commençant par 'muc.'. Ce type de nom est réservé à la fonction de chat XMPP multi-utilisateurs intégrée à YunoHost.", + "group_update_aliases": "Mise à jour des alias du groupe '{group}'.", + "group_no_change": "Rien à mettre à jour pour le groupe '{group}'", + "global_settings_setting_portal_theme": "Thème du portail", + "global_settings_setting_portal_theme_help": "Pour plus d'informations sur la création de thèmes de portail personnalisés, voir https://yunohost.org/theming", + "global_settings_setting_passwordless_sudo": "Permettre aux administrateurs d'utiliser 'sudo' sans retaper leur mot de passe" } diff --git a/locales/gl.json b/locales/gl.json index 8d53051f2..beaeec801 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -7,9 +7,6 @@ "app_action_broke_system": "Esta acción semella que estragou estos servizos importantes: {services}", "app_action_cannot_be_ran_because_required_services_down": "Estos servizos requeridos deberían estar en execución para realizar esta acción: {services}. Intenta reinicialos para continuar (e tamén intenta saber por que están apagados).", "already_up_to_date": "Nada que facer. Todo está ao día.", - "admin_password_too_long": "Elixe un contrasinal menor de 127 caracteres", - "admin_password_changed": "Realizado o cambio de contrasinal de administración", - "admin_password_change_failed": "Non se puido cambiar o contrasinal", "admin_password": "Contrasinal de administración", "additional_urls_already_removed": "URL adicional '{url}' xa foi eliminada das URL adicionais para o permiso '{permission}'", "additional_urls_already_added": "URL adicional '{url}' xa fora engadida ás URL adicionais para o permiso '{permission}'", @@ -21,7 +18,7 @@ "backup_archive_writing_error": "Non se puideron engadir os ficheiros '{source}' (chamados no arquivo '{dest}' para ser copiados dentro do arquivo comprimido '{archive}'", "backup_archive_system_part_not_available": "A parte do sistema '{part}' non está dispoñible nesta copia", "backup_archive_corrupted": "Semella que o arquivo de copia '{archive}' está estragado : {error}", - "backup_archive_cant_retrieve_info_json": "Non se puido cargar a info desde arquivo '{archive}'... O info.json non s puido obter (ou é un json non válido).", + "backup_archive_cant_retrieve_info_json": "Non se puido cargar a info do arquivo '{archive}'... Non se obtivo o ficheiro info.json (ou é un json non válido).", "backup_archive_open_failed": "Non se puido abrir o arquivo de copia de apoio", "backup_archive_name_unknown": "Arquivo local de copia de apoio descoñecido con nome '{name}'", "backup_archive_name_exists": "Xa existe un arquivo de copia con este nome.", @@ -38,8 +35,6 @@ "ask_new_domain": "Novo dominio", "ask_new_admin_password": "Novo contrasinal de administración", "ask_main_domain": "Dominio principal", - "ask_lastname": "Apelido", - "ask_firstname": "Nome", "ask_user_domain": "Dominio a utilizar como enderezo de email e conta XMPP da usuaria", "apps_catalog_update_success": "O catálogo de aplicacións foi actualizado!", "apps_catalog_obsolete_cache": "A caché do catálogo de apps está baleiro ou obsoleto.", @@ -64,8 +59,7 @@ "app_restore_script_failed": "Houbo un erro interno do script de restablecemento da app", "app_restore_failed": "Non se puido restablecer {app}: {error}", "app_remove_after_failed_install": "Eliminando a app debido ao fallo na instalación...", - "app_requirements_unmeet": "Non se cumpren os requerimentos de {app}, o paquete {pkgname} ({version}) debe ser {spec}", - "app_requirements_checking": "Comprobando os paquetes requeridos por {app}...", + "app_requirements_checking": "Comprobando os requisitos de {app}...", "app_removed": "{app} desinstalada", "app_not_properly_removed": "{app} non se eliminou de xeito correcto", "app_not_installed": "Non se puido atopar {app} na lista de apps instaladas: {all_apps}", @@ -167,7 +161,7 @@ "diagnosis_ip_weird_resolvconf_details": "O ficheiro /etc/resolv.conf debería ser unha ligazón simbólica a /etc/resolvconf/run/resolv.conf apuntando el mesmo a 127.0.0.1 (dnsmasq). Se queres configurar manualmente a resolución DNS, por favor edita /etc/resolv.dnsmasq.conf.", "diagnosis_ip_weird_resolvconf": "A resolución DNS semella funcionar, mais parecese que estás a utilizar un /etc/resolv.conf personalizado.", "diagnosis_ip_broken_resolvconf": "A resolución de nomes de dominio semella non funcionar no teu servidor, que parece ter relación con que /etc/resolv.conf non sinala a 127.0.0.1.", - "diagnosis_ip_broken_dnsresolution": "A resolución de nomes de dominio semella que por algunha razón non funciona... Pode estar o cortalumes bloqueando as peticións DNS?", + "diagnosis_ip_broken_dnsresolution": "A resolución de nomes de dominio semella que non funciona... Está o cortalumes bloqueando as peticións DNS?", "diagnosis_ip_dnsresolution_working": "A resolución de nomes de dominio está a funcionar!", "diagnosis_ip_not_connected_at_all": "O servidor semella non ter ningún tipo de conexión a internet!?", "diagnosis_ip_local": "IP local: {local}", @@ -214,7 +208,7 @@ "diagnosis_mail_ehlo_bad_answer": "Un servizo non-SMTP respondeu no porto 25 en IPv{ipversion}", "diagnosis_mail_ehlo_unreachable_details": "Non se puido abrir unha conexión no porto 25 do teu servidor en IPv{ipversion}. Non semella accesible.
1. A causa máis habitual é que o porto 25 non está correctamente redirixido no servidor.
2. Asegúrate tamén de que o servizo postfix está a funcionar.
3. En configuracións máis complexas: asegúrate de que o cortalumes ou reverse-proxy non están interferindo.", "diagnosis_mail_fcrdns_nok_details": "Deberías intentar configurar o DNS inverso con {ehlo_domain} na interface do teu rúter de internet ou na interface do teu provedor de hospedaxe. (Algúns provedores de hospedaxe poderían pedirche que lle fagas unha solicitude por escrito para isto).", - "diagnosis_mail_fcrdns_dns_missing": "Non hai DNS inverso definido en IPv{ipversion}. Algúns emails poderían non ser entregrado ou ser marcados como spam.", + "diagnosis_mail_fcrdns_dns_missing": "Non hai DNS inverso definido en IPv{ipversion}. Algúns emails poderían non ser entregados ou ser marcados como spam.", "diagnosis_mail_fcrdns_ok": "O DNS inverso está correctamente configurado!", "diagnosis_mail_ehlo_could_not_diagnose_details": "Erro: {error}", "diagnosis_mail_ehlo_could_not_diagnose": "Non se puido determinar se o servidor de email postfix é accesible desde o exterior en IPv{ipversion}.", @@ -232,7 +226,7 @@ "diagnosis_mail_blacklist_ok": "Os IPs e dominios utilizados neste servidor non parecen estar en listas de bloqueo", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS inverso actual: {rdns_domain}
Valor agardado: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "O DNS inverso non está correctamente configurado para IPv{ipversion}. É posible que non se entreguen algúns emails ou sexan marcados como spam.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Algúns provedores non che permiten configurar DNS inverso (ou podería non funcionar...). Se o teu DNS inverso está correctamente configurado para IPv4, podes intentar desactivar o uso de IPv6 ao enviar os emails executando yunohost settings set smtp.allow_ipv6 -v off. Nota: esta última solución significa que non poderás enviar ou recibir emails desde os poucos servidores que só usan IPv6 que teñen esta limitación.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Algúns provedores non che permiten configurar DNS inverso (ou podería non funcionar...). Se o teu DNS inverso está correctamente configurado para IPv4, podes intentar desactivar o uso de IPv6 ao enviar os emails executando yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Nota: esta última solución significa que non poderás enviar ou recibir emails desde os poucos servidores que só usan IPv6 que teñen esta limitación.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Algúns provedores non che permiten configurar o teu DNS inverso (ou podería non ser funcional...). Se tes problemas debido a isto, considera as seguintes solucións:
- Algúns ISP proporcionan alternativas como usar un repetidor de servidor de correo pero implica que o repetidor pode ver todo o teu tráfico de email.
-Unha alternativa respetuosa coa privacidade é utilizar un VPN *cun IP público dedicado* para evitar estas limitacións. Le https://yunohost.org/#/vpn_advantage
- Ou tamén podes cambiar a un provedor diferente", "diagnosis_http_ok": "O dominio {domain} é accesible a través de HTTP desde o exterior da rede local.", "diagnosis_http_could_not_diagnose_details": "Erro: {error}", @@ -260,8 +254,8 @@ "diagnosis_rootfstotalspace_critical": "O sistema de ficheiros root só ten un total de {space} e podería ser preocupante! Probablemente esgotes o espazo no disco moi pronto! Recomendamos ter un sistema de ficheiros root de polo menos 16 GB.", "diagnosis_rootfstotalspace_warning": "O sistema de ficheiros root só ten un total de {space}. Podería ser suficiente, mais pon tino porque poderías esgotar o espazo no disco rápidamente... Recoméndase ter polo meno 16 GB para o sistema de ficheiros root.", "domain_cannot_remove_main": "Non podes eliminar '{domain}' porque é o dominio principal, primeiro tes que establecer outro dominio como principal usando 'yunohost domain main-domain -n '; aquí tes a lista dos dominios posibles: {other_domains}", - "diagnosis_sshd_config_inconsistent_details": "Executa yunohost settings set security.ssh.port -v O_TEU_PORTO_SSH para definir o porto SSH, comproba con yunohost tools regen-conf ssh --dry-run --with-diff e restablece a configuración con yunohost tools regen-conf ssh --force a configuración recomendada de YunoHost.", - "diagnosis_sshd_config_inconsistent": "Semella que o porto SSH foi modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, un novo axuste global 'security.ssh.port' está dispoñible para evitar a edición manual da configuración.", + "diagnosis_sshd_config_inconsistent_details": "Executa yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT para definir o porto SSH, comproba con yunohost tools regen-conf ssh --dry-run --with-diff e restablece a configuración con yunohost tools regen-conf ssh --force a configuración recomendada de YunoHost.", + "diagnosis_sshd_config_inconsistent": "Semella que o porto SSH foi modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, un novo axuste global 'security.ssh.ssh_port' está dispoñible para evitar a edición manual da configuración.", "diagnosis_sshd_config_insecure": "Semella que a configuración SSH modificouse manualmente, e é insegura porque non contén unha directiva 'AllowGroups' ou 'AllowUsers' para limitar o acceso ás usuarias autorizadas.", "diagnosis_processes_killed_by_oom_reaper": "Algúns procesos foron apagados recentemente polo sistema porque quedou sen memoria dispoñible. Isto acontece normalmente porque o sistema quedou sen memoria ou un proceso consumía demasiada. Resumo cos procesos apagados:\n{kills_summary}", "diagnosis_never_ran_yet": "Semella que o servidor foi configurado recentemente e aínda non hai informes diagnósticos. Deberías iniciar un diagnóstico completo, ben desde a administración web ou usando 'yunohost diagnosis run' desde a liña de comandos.", @@ -274,7 +268,6 @@ "diagnosis_http_connection_error": "Erro de conexión: non se puido conectar co dominio solicitado, moi probablemente non sexa accesible.", "diagnosis_http_timeout": "Caducou a conexión mentras se intentaba contactar o servidor desde o exterior. Non semella accesible.
1. A razón máis habitual é que o porto 80 (e 443) non están correctamente redirixidos ao teu servidor.
2. Deberías comprobar tamén que o servizo nginx está a funcionar
3. En configuracións máis avanzadas: revisa que nin o cortalumes nin o proxy-inverso están interferindo.", "field_invalid": "Campo non válido '{}'", - "experimental_feature": "Aviso: esta característica é experimental e non se considera estable, non deberías utilizala a menos que saibas o que estás a facer.", "extracting": "Extraendo...", "dyndns_unavailable": "O dominio '{domain}' non está dispoñible.", "dyndns_domain_not_provided": "O provedor DynDNS {provider} non pode proporcionar o dominio {domain}.", @@ -288,7 +281,7 @@ "dyndns_ip_update_failed": "Non se actualizou o enderezo IP en DynDNS", "dyndns_could_not_check_available": "Non se comprobou se {domain} está dispoñible en {provider}.", "dpkg_lock_not_available": "Non se pode executar agora mesmo este comando porque semella que outro programa está a utilizar dpkg (o xestos de paquetes do sistema)", - "dpkg_is_broken": "Non podes facer isto agora mesmo porque dpkg/APT (o xestor de paquetes do sistema) semella que non está a funcionar... Podes intentar solucionalo conectándote a través de SSH e executando `sudo apt install --fix-broken`e/ou `sudo dpkg --configure -a`.", + "dpkg_is_broken": "Non podes facer isto agora mesmo porque dpkg/APT (o xestor de paquetes do sistema) semella que non está a funcionar... Podes intentar solucionalo conectándote a través de SSH e executando `sudo apt install --fix-broken`e/ou `sudo dpkg --configure -a` e/ou `sudo dpkg --audit`.", "downloading": "Descargando...", "done": "Feito", "domains_available": "Dominios dispoñibles:", @@ -308,24 +301,7 @@ "domain_cannot_add_xmpp_upload": "Non podes engadir dominios que comecen con 'xmpp-upload.'. Este tipo de nome está reservado para a función se subida de XMPP integrada en YunoHost.", "file_does_not_exist": "O ficheiro {path} non existe.", "firewall_reload_failed": "Non se puido recargar o cortalumes", - "global_settings_setting_smtp_allow_ipv6": "Permitir o uso de IPv6 para recibir e enviar emais", - "global_settings_setting_ssowat_panel_overlay_enabled": "Activar as capas no panel SSOwat", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permitir o uso de DSA hostkey (en desuso) para a configuración do demoño SSH", - "global_settings_unknown_setting_from_settings_file": "Chave descoñecida nos axustes: '{setting_key}', descártaa e gárdaa en /etc/yunohost/settings-unknown.json", - "global_settings_setting_security_ssh_port": "Porto SSH", - "global_settings_setting_security_postfix_compatibility": "Compromiso entre compatibilidade e seguridade para o servidor Postfix. Aféctalle ao cifrado (e outros aspectos da seguridade)", - "global_settings_setting_security_ssh_compatibility": "Compromiso entre compatibilidade e seguridade para o servidor SSH. Aféctalle ao cifrado (e outros aspectos da seguridade)", - "global_settings_setting_security_password_user_strength": "Fortaleza do contrasinal da usuaria", - "global_settings_setting_security_password_admin_strength": "Fortaleza do contrasinal de Admin", - "global_settings_setting_security_nginx_compatibility": "Compromiso entre compatiblidade e seguridade para o servidor NGINX. Afecta ao cifrado (e outros aspectos relacionados coa seguridade)", - "global_settings_setting_pop3_enabled": "Activar protocolo POP3 no servidor de email", - "global_settings_reset_success": "Fíxose copia de apoio dos axustes en {path}", - "global_settings_key_doesnt_exists": "O axuste '{settings_key}' non existe nos axustes globais, podes ver os valores dispoñibles executando 'yunohost settings list'", - "global_settings_cant_write_settings": "Non se gardou o ficheiro de configuración, razón: {reason}", - "global_settings_cant_serialize_settings": "Non se serializaron os datos da configuración, razón: {reason}", - "global_settings_cant_open_settings": "Non se puido abrir o ficheiro de axustes, razón: {reason}", - "global_settings_bad_type_for_setting": "Tipo incorrecto do axuste {setting}, recibido {received_type}, agardábase {expected_type}", - "global_settings_bad_choice_for_enum": "Elección incorrecta para o axuste {setting}, recibido '{choice}', mais as opcións dispoñibles son: {available_choices}", + "global_settings_setting_ssowat_panel_overlay_enabled": "Activar o pequeno atallo cadrado ao portal 'YunoHost' nas apps", "firewall_rules_cmd_failed": "Fallou algún comando das regras do cortalumes. Máis info no rexistro.", "firewall_reloaded": "Recargouse o cortalumes", "group_creation_failed": "Non se puido crear o grupo '{group}': {error}", @@ -335,12 +311,9 @@ "group_already_exist": "Xa existe o grupo {group}", "good_practices_about_user_password": "Vas definir o novo contrasinal de usuaria. O contrasinal debe ter 8 caracteres como mínimo—aínda que se recomenda utilizar un máis longo (ex. unha frase de paso) e/ou utilizar caracteres variados (maiúsculas, minúsculas, números e caracteres especiais).", "good_practices_about_admin_password": "Vas definir o novo contrasinal de administración. O contrasinal debe ter 8 caracteres como mínimo—aínda que se recomenda utilizar un máis longo (ex. unha frase de paso) e/ou utilizar caracteres variados (maiúsculas, minúsculas, números e caracteres especiais).", - "global_settings_unknown_type": "Situación non agardada, o axuste {setting} semella ter o tipo {unknown_type} pero non é un valor soportado polo sistema.", - "global_settings_setting_backup_compress_tar_archives": "Ao crear novas copias de apoio, comprime os arquivos (.tar.gz) en lugar de non facelo (.tar). Nota: activando esta opción creas arquivos máis lixeiros, mais o procedemento da primeira copia será significativamente máis longo e esixente coa CPU.", - "global_settings_setting_smtp_relay_password": "Contrasinal no repetidor SMTP", - "global_settings_setting_smtp_relay_user": "Conta de usuaria no repetidor SMTP", + "global_settings_setting_smtp_relay_password": "Contrasinal do repetidor SMTP", + "global_settings_setting_smtp_relay_user": "Usuaria no repetidor SMTP", "global_settings_setting_smtp_relay_port": "Porto do repetidor SMTP", - "global_settings_setting_smtp_relay_host": "Servidor repetidor SMTP para enviar emails no lugar da túa instancia yunohost. É útil se estás nunha destas situacións: o teu porto 25 está bloqueado polo teu provedor ISP u VPN, se tes unha IP residencial nunha lista DUHL, se non podes configurar DNS inversa ou se este servidor non ten conexión directa a internet e queres utilizar outro para enviar os emails.", "group_updated": "Grupo '{group}' actualizado", "group_unknown": "Grupo descoñecido '{group}'", "group_deletion_failed": "Non se eliminou o grupo '{group}': {error}", @@ -349,8 +322,6 @@ "group_cannot_edit_primary_group": "O grupo '{group}' non se pode editar manualmente. É o grupo primario que contén só a unha usuaria concreta.", "group_cannot_edit_visitors": "O grupo 'visitors' non se pode editar manualmente. É un grupo especial que representa a tódas visitantes anónimas", "group_cannot_edit_all_users": "O grupo 'all_users' non se pode editar manualmente. É un grupo especial que contén tódalas usuarias rexistradas en YunoHost", - "global_settings_setting_security_webadmin_allowlist": "Enderezos IP con permiso para acceder á webadmin. Separados por vírgulas.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Permitir que só algúns IPs accedan á webadmin.", "disk_space_not_sufficient_update": "Non hai espazo suficiente no disco para actualizar esta aplicación", "disk_space_not_sufficient_install": "Non queda espazo suficiente no disco para instalar esta aplicación", "log_help_to_get_log": "Para ver o rexistro completo da operación '{desc}', usa o comando 'yunohost log show {name}'", @@ -397,7 +368,7 @@ "migration_ldap_backup_before_migration": "Crear copia de apoio da base de datos LDAP e axustes de apps antes de realizar a migración.", "main_domain_changed": "Foi cambiado o dominio principal", "main_domain_change_failed": "Non se pode cambiar o dominio principal", - "mail_unavailable": "Este enderezo de email está reservado e debería adxudicarse automáticamente á primeira usuaria", + "mail_unavailable": "Este enderezo de email está reservado para o grupo de admins", "mailbox_used_space_dovecot_down": "O servizo de caixa de correo Dovecot ten que estar activo se queres obter o espazo utilizado polo correo", "mailbox_disabled": "Desactivado email para usuaria {user}", "mail_forward_remove_failed": "Non se eliminou o reenvío de email '{mail}'", @@ -437,8 +408,8 @@ "pattern_port_or_range": "Debe ser un número válido de porto (entre 0-65535) ou rango de portos (ex. 100:200)", "pattern_password": "Ten que ter polo menos 3 caracteres", "pattern_mailbox_quota": "Ten que ser un tamaño co sufixo b/k/M/G/T ou 0 para non ter unha cota", - "pattern_lastname": "Ten que ser un apelido válido", - "pattern_firstname": "Ten que ser un nome válido", + "pattern_lastname": "Ten que ser un apelido válido (min. 3 caract.)", + "pattern_firstname": "Ten que ser un nome válido (min. 3 caract.)", "pattern_email": "Ten que ser un enderezo de email válido, sen o símbolo '+' (ex. persoa@exemplo.com)", "pattern_email_forward": "Ten que ser un enderezo de email válido, está aceptado o símbolo '+' (ex. persoa+etiqueta@exemplo.com)", "pattern_domain": "Ten que ser un nome de dominio válido (ex. dominiopropio.org)", @@ -454,8 +425,8 @@ "migrations_success_forward": "Migración {id} completada", "migrations_skip_migration": "Omitindo migración {id}...", "migrations_running_forward": "Realizando migración {id}...", - "migrations_pending_cant_rerun": "Esas migracións están pendentes, polo que non ser executadas outra vez: {ids}", - "migrations_not_pending_cant_skip": "Esas migracións non están pendentes, polo que non poden ser omitidas: {ids}", + "migrations_pending_cant_rerun": "Estas migracións están pendentes, polo que non ser realizadas outra vez: {ids}", + "migrations_not_pending_cant_skip": "Estas migracións non están pendentes, polo que non poden ser omitidas: {ids}", "migrations_no_such_migration": "Non hai migración co nome '{id}'", "migrations_no_migrations_to_run": "Sen migracións a executar", "migrations_need_to_accept_disclaimer": "Para executar a migración {id}, tes que aceptar o seguinte aviso:\n---\n{disclaimer}\n---\nSe aceptas executar a migración, por favor volve a executar o comando coa opción '--accept-disclaimer'.", @@ -530,7 +501,6 @@ "server_reboot": "Vaise reiniciar o servidor", "server_shutdown_confirm": "Queres apagar o servidor inmediatamente? [{answers}]", "server_shutdown": "Vaise apagar o servidor", - "root_password_replaced_by_admin_password": "O contrasinal root foi substituído polo teu contrasinal de administración.", "root_password_desynchronized": "Mudou o contrasinal de administración, pero YunoHost non puido transferir este cambio ao contrasinal root!", "restore_system_part_failed": "Non se restableceu a parte do sistema '{part}'", "restore_running_hooks": "Executando os ganchos do restablecemento...", @@ -540,11 +510,9 @@ "restore_not_enough_disk_space": "Non hai espazo abondo (espazo: {free_space.d} B, espazo necesario: {needed_space} B, marxe de seguridade: {margin} B)", "restore_may_be_not_enough_disk_space": "O teu sistema semella que non ten espazo abondo (libre: {free_space} B, espazo necesario: {needed_space} B, marxe de seguridade {margin} B)", "restore_hook_unavailable": "O script de restablecemento para '{part}' non está dispoñible no teu sistema nin no arquivo", - "invalid_password": "Contrasinal non válido", "ldap_server_is_down_restart_it": "O servidor LDAP está caído, intenta reinicialo...", "ldap_server_down": "Non se chegou ao servidor LDAP", - "global_settings_setting_security_experimental_enabled": "Activar características de seguridade experimentais (non actives isto se non sabes o que estás a facer!)", - "yunohost_postinstall_end_tip": "Post-install completada! Para rematar a configuración considera:\n- engadir unha primeira usuaria na sección 'Usuarias' na webadmin (ou 'yunohost user create ' na liña de comandos);\n- diagnosticar potenciais problemas na sección 'Diagnóstico' na webadmin (ou 'yunohost diagnosis run' na liña de comandos);\n- ler 'Rematando a configuración' e 'Coñece YunoHost' na documentación da administración: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "Post-install completada! Para rematar a configuración considera:\n- diagnosticar potenciais problemas na sección 'Diagnóstico' na webadmin (ou 'yunohost diagnosis run' na liña de comandos);\n- ler 'Rematando a configuración' e 'Coñece YunoHost' na documentación da administración: https://yunohost.org/admindoc.", "yunohost_not_installed": "YunoHost non está instalado correctamente. Executa 'yunohost tools postinstall'", "yunohost_installing": "Instalando YunoHost...", "yunohost_configured": "YunoHost está configurado", @@ -592,14 +560,13 @@ "service_enabled": "O servizo '{service}' vai ser iniciado automáticamente no inicio do sistema.", "diagnosis_apps_allgood": "Tódalas apps instaladas respectan as prácticas básicas de empaquetado", "diagnosis_apps_bad_quality": "Esta aplicación está actualmente marcada como estragada no catálogo de aplicacións de YunoHost. Podería ser un problema temporal mentras as mantedoras intentan arranxar o problema. Ata ese momento a actualización desta app está desactivada.", - "global_settings_setting_security_nginx_redirect_to_https": "Redirixir peticións HTTP a HTTPs por defecto (NON DESACTIVAR ISTO a non ser que realmente saibas o que fas!)", "log_user_import": "Importar usuarias", "user_import_failed": "A operación de importación de usuarias fracasou", "user_import_missing_columns": "Faltan as seguintes columnas: {columns}", "user_import_nothing_to_do": "Ningunha usuaria precisa ser importada", "user_import_partial_failed": "A operación de importación de usuarias fallou parcialmente", "diagnosis_apps_deprecated_practices": "A versión instalada desta app aínda utiliza algunha das antigas prácticas de empaquetado xa abandonadas. Deberías considerar actualizala.", - "diagnosis_apps_outdated_ynh_requirement": "A versión instalada desta app só require yunohost >= 2.x, que normalmente indica que non está ao día coas prácticas recomendadas de empaquetado e asistentes. Deberías considerar actualizala.", + "diagnosis_apps_outdated_ynh_requirement": "A versión instalada desta app só require yunohost >= 2.x ou 3.x, esto normalmente indica que non está ao día coas prácticas recomendadas de empaquetado e asistentes. Deberías considerar actualizala.", "user_import_success": "Usuarias importadas correctamente", "diagnosis_high_number_auth_failures": "Hai un alto número sospeitoso de intentos fallidos de autenticación. Deberías comprobar que fail2ban está a executarse e que está correctamente configurado, ou utiliza un porto personalizado para SSH tal como se explica en https://yunohost.org/security.", "user_import_bad_file": "O ficheiro CSV non ten o formato correcto e será ignorado para evitar unha potencial perda de datos", @@ -666,7 +633,6 @@ "migration_0021_main_upgrade": "Iniciando a actualización principal...", "migration_0021_still_on_buster_after_main_upgrade": "Algo fallou durante a actualización principal, o sistema semlla que aínda está en Debian Buster", "migration_0021_yunohost_upgrade": "Iniciando actualización compoñente core de YunoHost...", - "migration_0021_not_buster": "A distribución Debian actual non é Buster!", "migration_0021_not_enough_free_space": "Queda pouco espazo en /var/! Deberías ter polo menos 1GB libre para facer a migración.", "migration_0021_problematic_apps_warning": "Detectamos que están instaladas estas app que poderían ser problemáticas. Semella que non foron instaladas desde o catálogo YunoHost, ou non están marcadas como que 'funcionan'. Así, non podemos garantir que seguiran funcionando ben tras a migración: {problematic_apps}", "migration_0021_modified_files": "Ten en conta que os seguintes ficheiros semella que foron editados manualmente e poderían ser sobrescritos durante a migración: {manually_modified_files}", @@ -675,7 +641,6 @@ "migration_description_0021_migrate_to_bullseye": "Actualizar o sistema a Debian Bullseye e YunoHost 11.x", "migration_0021_system_not_fully_up_to_date": "O teu sistema non está completamente actualizado. Fai unha actualización normal antes de executar a migración a Bullseye.", "migration_0021_general_warning": "Ten en conta que a migración é unha operación delicada. O equipo de YunoHost fixo todo o que puido para revisalo e probalo, pero aínda así poderían acontecer fallos no sistema ou apps.\n\nAsí as cousas, é recomendable:\n - Facer unha copia de apoio dos datos e apps importantes. Máis info en https://yunohost.org/backup;\n - Ter paciencia unha vez inicias a migración: dependendo da túa conexión a internet e hardware, podería levarlle varias horas completar o proceso.", - "global_settings_setting_security_ssh_password_authentication": "Permitir autenticación con contrasinal para SSH", "tools_upgrade_failed": "Non se actualizaron os paquetes: {packages_list}", "migration_0023_not_enough_space": "Crear espazo suficiente en {path} para realizar a migración.", "migration_0023_postgresql_11_not_installed": "PostgreSQL non estaba instalado no sistema. Nada que facer.", @@ -684,5 +649,97 @@ "migration_description_0023_postgresql_11_to_13": "Migrar bases de datos de PostgreSQL 11 a 13", "service_description_postgresql": "Almacena datos da app (Base datos SQL)", "tools_upgrade": "Actualizando paquetes do sistema", - "domain_config_default_app": "App por defecto" -} \ No newline at end of file + "domain_config_default_app": "App por defecto", + "global_settings_setting_backup_compress_tar_archives_help": "Ao crear novas copias de apoio, comprime os arquivos (.tar.gz) en lugar de non facelo (.tar). Nota: activando esta opción creas arquivos máis lixeiros, mais o procedemento da primeira copia será significativamente máis longo e esixente coa CPU.", + "global_settings_setting_security_experimental_enabled_help": "Activar características de seguridade experimentais (non actives isto se non sabes o que estás a facer!)", + "global_settings_setting_nginx_compatibility_help": "Compromiso entre compatiblidade e seguridade para o servidor NGINX. Afecta ao cifrado (e outros aspectos relacionados coa seguridade)", + "global_settings_setting_nginx_redirect_to_https_help": "Redirixir peticións HTTP a HTTPs por defecto (NON DESACTIVAR ISTO a non ser que realmente saibas o que fas!)", + "global_settings_setting_admin_strength": "Fortaleza do contrasinal de Admin", + "global_settings_setting_user_strength": "Fortaleza do contrasinal da usuaria", + "global_settings_setting_postfix_compatibility_help": "Compromiso entre compatibilidade e seguridade para o servidor Postfix. Aféctalle ao cifrado (e outros aspectos da seguridade)", + "global_settings_setting_ssh_compatibility_help": "Compromiso entre compatibilidade e seguridade para o servidor SSH. Aféctalle ao cifrado (e outros aspectos da seguridade)", + "global_settings_setting_ssh_password_authentication_help": "Permitir autenticación con contrasinal para SSH", + "global_settings_setting_ssh_port": "Porto SSH", + "global_settings_setting_webadmin_allowlist_help": "Enderezos IP con permiso para acceder á webadmin. Separados por vírgulas.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permitir que só algúns IPs accedan á webadmin.", + "global_settings_setting_smtp_allow_ipv6_help": "Permitir o uso de IPv6 para recibir e enviar emais", + "global_settings_setting_smtp_relay_enabled_help": "Servidor repetidor SMTP para enviar emails no lugar da túa instancia yunohost. É útil se estás nunha destas situacións: o teu porto 25 está bloqueado polo teu provedor ISP u VPN, se tes unha IP residencial nunha lista DUHL, se non podes configurar DNS inversa ou se este servidor non ten conexión directa a internet e queres utilizar outro para enviar os emails.", + "migration_0024_rebuild_python_venv_broken_app": "Omitimos a app {app} porque virtualenv non se pode reconstruir para esta app. Deberías intentar resolver o problema forzando a actualización da app usando `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_disclaimer_base": "Após a actualización a Debian Bullseye, algunhas aplicacións de Python precisan ser reconstruídas para usar a nova versión de Python que inclúe Debian (técnicamente: recrear o `virtualenv`). Mentras tanto, algunhas aplicacións de Python poderían non funcionar. YunoHost pode intentar reconstruir o virtualenv para algunhas, como se indica abaixo. Para outras, ou se falla a reconstrución, pode que teñas que forzar a actualización desas apps.", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Vaise intentar a reconstrución de virtualenv para as seguintes apps (Nota: a operación podería tomar algún tempo!): {rebuild_apps}", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Non se puido reconstruir virtualenv para estas apps. Precisas forzar a súa actualización, pódelo facer desde a liña de comandos con: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_0024_rebuild_python_venv_in_progress": "Intentando reconstruir o Python virtualenv para `{app}`", + "migration_description_0024_rebuild_python_venv": "Reparar app Python após a migración a bullseye", + "migration_0024_rebuild_python_venv_failed": "Fallou a reconstrución de Python virtualenv para {app}. A app podería non funcionar mentras non se resolve. Deberías intentar arranxar a situación forzando a actualización desta app usando `yunohost app upgrade --force {app}`.", + "migration_0021_not_buster2": "A distribución actual Debian non é Buster! Se xa realizaches a migración Buster->Bullseye entón este erro indica que o proceso de migración non se realizou de xeito correcto ao 100% (se non YunoHost debería telo marcado como completado). É recomendable comprobar xunto co equipo de axuda o que aconteceu, necesitarán o rexistro **completo** da `migración`, que podes atopar na webadmin en Ferramentas > Rexistros.", + "global_settings_setting_admin_strength_help": "Estos requerimentos só se esixen ao inicializar ou cambiar o contrasinal", + "global_settings_setting_root_access_explain": "En sistemas Linux, 'root' é a administradora absoluta. No contexto YunoHost, o acceso SSH de 'root' está desactivado por defecto - excepto na rede local do servidor. Os compoñentes do grupo 'admins' poden utilizar o comando sudo para actuar como root desde a liña de comandos. É conveniente ter un contrasinal (forte) para root para xestionar o sistema por se as persoas administradoras perden o acceso por algún motivo.", + "migration_description_0025_global_settings_to_configpanel": "Migrar o nome antigo dos axustes globais aos novos nomes modernos", + "global_settings_reset_success": "Restablecer axustes globais", + "domain_config_acme_eligible": "Elixibilidade ACME", + "domain_config_acme_eligible_explain": "Este dominio non semella estar preparado para un certificado Let's Encrypt. Comproba a configuración DNS e que é accesible por HTTP. A sección 'Rexistros DNS' e 'Web' na páxina de diagnóstico pode axudarche a entender o que está a fallar.", + "domain_config_cert_install": "Instalar certificado Let's Encrypt", + "domain_config_cert_issuer": "Autoridade certificadora", + "domain_config_cert_no_checks": "Ignorar comprobacións de diagnóstico", + "domain_config_cert_renew": "Anovar certificado Let's Encrypt", + "domain_config_cert_renew_help": "O certificado anovarase automáticamente nos últimos 15 días de validez. Podes anovalo automáticamente se queres. (Non é recomendable).", + "domain_config_cert_summary": "Estado do certificado", + "domain_config_cert_summary_abouttoexpire": "O certificado actual vai caducar. Debería anovarse automáticamente..", + "domain_config_cert_summary_expired": "CRÍTICO: O certificado actual non é válido! HTTPS non funcionará!!", + "domain_config_cert_summary_letsencrypt": "Ben! Estás a usar un certificado Let's Encrypt válido!", + "domain_config_cert_summary_ok": "Correcto, o certificado ten boa pinta!", + "domain_config_cert_summary_selfsigned": "AVISO: O certificado actual está auto-asinado. Os navegadores van mostrar un aviso que mete medo a quen te visite!", + "domain_config_cert_validity": "Validez", + "global_settings_setting_ssh_password_authentication": "Autenticación con contrasinal", + "global_settings_setting_user_strength_help": "Estos requerimentos só se esixen ao inicializar ou cambiar o contrasinal", + "global_settings_setting_webadmin_allowlist": "Lista IP autorizados para Webadmin", + "global_settings_setting_webadmin_allowlist_enabled": "Activar a lista de IP autorizados", + "invalid_credentials": "Credenciais non válidas", + "log_settings_reset": "Restablecer axuste", + "log_settings_reset_all": "Restablecer tódolos axustes", + "log_settings_set": "Aplicar axustes", + "admins": "Admins", + "all_users": "Tódalas usuarias de YunoHost", + "app_action_failed": "Fallou a execución da acción {action} da app {app}", + "app_manifest_install_ask_init_admin_permission": "Quen debería ter acceso de administración a esta app? (Pode cambiarse despois)", + "app_manifest_install_ask_init_main_permission": "Quen debería ter acceso a esta app? (Pode cambiarse despois)", + "ask_admin_fullname": "Nome completo de Admin", + "ask_admin_username": "Identificador da Admin", + "ask_fullname": "Nome completo", + "certmanager_cert_install_failed": "Fallou a instalación do certificado Let's Encrypt para {domains}", + "certmanager_cert_install_failed_selfsigned": "Fallou a instalación do certificado auto-asinado para {domains}", + "certmanager_cert_renew_failed": "Fallou a renovación do certificado Let's Encrypt para {domains}", + "password_confirmation_not_the_same": "Non concordan os contrasinais escritos", + "password_too_long": "Elixe un contrasinal menor de 127 caracteres", + "pattern_fullname": "Ten que ser un nome completo válido (min. 3 caract.)", + "registrar_infos": "Info da rexistradora", + "root_password_changed": "cambiouse o contrasinal de root", + "visitors": "Visitantes", + "global_settings_setting_security_experimental_enabled": "Ferramentas experimentais de seguridade", + "diagnosis_using_stable_codename": "apt (o xestor de paquetes do sistema) está configurado para instalar paquetes co nome de código 'stable', no lugar do nome de código da versión actual de Debian (bullseye).", + "diagnosis_using_stable_codename_details": "Normalmente esto é debido a unha configuración incorrecta do teu provedor de hospedaxe. Esto é perigoso, porque tan pronto como a nova versión de Debian se convirta en 'stable', apt vai querer actualizar tódolos paquetes do sistema se realizar o procedemento de migración requerido. É recomendable arranxar isto editando a fonte de apt ao repositorio base de Debian, e substituir a palabra stable por bullseye. O ficheiro de configuración correspondente debería ser /etc/sources.list, ou ficheiro dentro de /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing": "apt (o xestor de paquetes do sistema) está configurado actualmente para instalar calquera actualización 'testing' para o núcleo YunoHost.", + "diagnosis_using_yunohost_testing_details": "Isto probablemente sexa correcto se sabes o que estás a facer, pero pon coidado e le as notas de publicación antes de realizar actualizacións de YunoHost! Se queres desactivar as actualizacións 'testing', deberías eliminar a palabra testing de /etc/apt/sources.list.d/yunohost.list.", + "global_settings_setting_backup_compress_tar_archives": "Comprimir copias de apoio", + "global_settings_setting_pop3_enabled": "Activar POP3", + "global_settings_setting_smtp_allow_ipv6": "Permitir IPv6", + "global_settings_setting_smtp_relay_host": "Sevidor repetidor SMTP", + "config_action_disabled": "Non se executou a accción '{action}' porque está desactivada, comproba os seus requerimentos. Axuda: {help}", + "config_action_failed": "Fallou a execución da acción '{action}': {error}", + "config_forbidden_readonly_type": "O tipo '{type}' non pode establecerse como só lectura, usa outro tipo para mostrar este valor (id relevante: '{id}').", + "global_settings_setting_nginx_compatibility": "Compatibilidade NGINX", + "global_settings_setting_nginx_redirect_to_https": "Forzar HTTPS", + "global_settings_setting_pop3_enabled_help": "Activar o protocolo POP3 no servidor de email", + "global_settings_setting_postfix_compatibility": "Compatibilidade Postfix", + "global_settings_setting_root_password": "Novo contrasinal root", + "global_settings_setting_root_password_confirm": "Novo contrasinal root (confirmar)", + "global_settings_setting_smtp_relay_enabled": "Activar repetidor SMTP", + "global_settings_setting_ssh_compatibility": "Compatibilidade SSH", + "migration_description_0026_new_admins_group": "Migrar ao novo sistema de 'admins múltiples'", + "group_update_aliases": "Actualizando os alias do grupo '{group}'", + "group_no_change": "Nada que cambiar para o grupo '{group}'", + "domain_cannot_add_muc_upload": "Non podes engadir dominios que comecen por 'muc.'. Este tipo de dominio está reservado para as salas de conversa de XMPP integradas en YunoHost.", + "global_settings_setting_passwordless_sudo": "Permitir a Admins usar 'sudo' sen ter que volver a escribir o contrasinal", + "global_settings_setting_portal_theme": "Decorado do Portal", + "global_settings_setting_portal_theme_help": "Tes máis info acerca da creación de decorados para o portal de acceso en https://yunohost.org/theming" +} diff --git a/locales/he.json b/locales/he.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/he.json @@ -0,0 +1 @@ +{} diff --git a/locales/hi.json b/locales/hi.json index 1eed9faa4..6f40ad1ae 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -1,8 +1,6 @@ { "action_invalid": "अवैध कार्रवाई '{action}'", "admin_password": "व्यवस्थापक पासवर्ड", - "admin_password_change_failed": "पासवर्ड बदलने में असमर्थ", - "admin_password_changed": "व्यवस्थापक पासवर्ड बदल दिया गया है", "app_already_installed": "'{app}' पहले से ही इंस्टाल्ड है", "app_argument_choice_invalid": "गलत तर्क का चयन किया गया '{name}' , तर्क इन विकल्पों में से होने चाहिए {choices}", "app_argument_invalid": "तर्क के लिए अमान्य मान '{name}': {error}", @@ -15,14 +13,11 @@ "app_not_properly_removed": "{app} ठीक ढंग से नहीं अनइन्सटॉल की गई", "app_removed": "{app} को अनइन्सटॉल कर दिया गया", "app_requirements_checking": "जरूरी पैकेजेज़ की जाँच हो रही है ....", - "app_requirements_unmeet": "आवश्यकताए पूरी नहीं हो सकी, पैकेज {pkgname}({version})यह होना चाहिए {spec}", "app_sources_fetch_failed": "सोर्स फाइल्स प्राप्त करने में असमर्थ", "app_unknown": "अनजान एप्लीकेशन", "app_unsupported_remote_type": "एप्लीकेशन के लिए उन्सुपपोर्टेड रिमोट टाइप इस्तेमाल किया गया", "app_upgrade_failed": "{app} अपडेट करने में असमर्थ", "app_upgraded": "{app} अपडेट हो गयी हैं", - "ask_firstname": "नाम", - "ask_lastname": "अंतिम नाम", "ask_main_domain": "मुख्य डोमेन", "ask_new_admin_password": "नया व्यवस्थापक पासवर्ड", "ask_password": "पासवर्ड", diff --git a/locales/hu.json b/locales/hu.json index 9c482a370..d45a95dc3 100644 --- a/locales/hu.json +++ b/locales/hu.json @@ -2,8 +2,6 @@ "aborting": "Megszakítás.", "action_invalid": "Érvénytelen művelet '{action}'", "admin_password": "Adminisztrátori jelszó", - "admin_password_change_failed": "Nem lehet a jelszót megváltoztatni", - "admin_password_changed": "Az adminisztrátori jelszó megváltozott", "app_already_installed": "{app} már telepítve van", "app_already_installed_cant_change_url": "Ez az app már telepítve van. Ezzel a funkcióval az url nem változtatható. Javaslat 'app url változtatás' ha lehetséges.", "app_already_up_to_date": "{app} napra kész", diff --git a/locales/id.json b/locales/id.json index 47aff8b2e..722d88dd2 100644 --- a/locales/id.json +++ b/locales/id.json @@ -1,8 +1,5 @@ { "admin_password": "Kata sandi administrasi", - "admin_password_change_failed": "Tidak dapat mengubah kata sandi", - "admin_password_changed": "Kata sandi administrasi diubah", - "admin_password_too_long": "Harap pilih kata sandi yang lebih pendek dari 127 karakter", "already_up_to_date": "Tak ada yang harus dilakukan. Semuanya sudah mutakhir.", "app_action_broke_system": "Tindakan ini sepertinya telah merusak layanan-layanan penting ini: {services}", "app_already_installed": "{app} sudah terpasang", @@ -11,7 +8,7 @@ "app_change_url_identical_domains": "Domain)url_path yang lama dan baru identik ('{domain}{path}'), tak ada yang perlu dilakukan.", "app_change_url_no_script": "Aplikasi '{app_name}' belum mendukung pengubahan URL. Mungkin Anda harus memperbaruinya.", "app_change_url_success": "URL {app} sekarang adalah {domain}{path}", - "app_id_invalid": "ID aplikasi tidak valid", + "app_id_invalid": "ID aplikasi tidak sah", "app_install_failed": "Tidak dapat memasang {app}: {error}", "app_install_files_invalid": "Berkas-berkas ini tidak dapat dipasang", "app_install_script_failed": "Sebuah kesalahan terjadi pada script pemasangan aplikasi", @@ -27,8 +24,6 @@ "apps_already_up_to_date": "Semua aplikasi sudah pada versi mutakhir", "apps_catalog_update_success": "Katalog aplikasi telah diperbarui!", "apps_catalog_updating": "Memperbarui katalog aplikasi...", - "ask_firstname": "Nama depan", - "ask_lastname": "Nama belakang", "ask_main_domain": "Domain utama", "ask_new_domain": "Domain baru", "ask_user_domain": "Domain yang digunakan untuk alamat surel dan akun XMPP pengguna", @@ -54,5 +49,16 @@ "backup_method_tar_finished": "Arsip TAR cadanagan dibuat", "backup_nothings_done": "Tak ada yang harus disimpan", "certmanager_cert_install_success": "Sertifikat Let's Encrypt sekarang sudah terpasang pada domain '{domain}'", - "backup_mount_archive_for_restore": "Menyiapkan arsip untuk pemulihan..." + "backup_mount_archive_for_restore": "Menyiapkan arsip untuk pemulihan...", + "aborting": "Membatalkan.", + "action_invalid": "Tindakan tidak sah '{action}'", + "app_action_cannot_be_ran_because_required_services_down": "Layanan yang dibutuhkan ini harus aktif untuk menjalankan tindakan ini: {services}. Coba memulai ulang layanan tersebut untuk melanjutkan (dan mungkin melakukan penyelidikan mengapa layanan tersebut nonaktif).", + "app_argument_choice_invalid": "Pilih nilai yang sah untuk argumen '{name}': '{value}' tidak termasuk pada pilihan yang tersedia ({choices})", + "app_argument_invalid": "Pilih nilai yang sah untuk argumen '{name}': {error}", + "app_extraction_failed": "Tidak dapat mengekstrak berkas pemasangan", + "app_full_domain_unavailable": "Maaf, aplikasi ini harus dipasang pada domain sendiri, namun aplikasi lain sudah terpasang pada domain '{domain}'. Anda dapat menggunakan subdomain hanya untuk aplikasi ini.", + "app_location_unavailable": "URL ini mungkin tidak tersedia, atau terjadi konflik dengan aplikasi yang telah terpasang:\n{apps}", + "app_not_upgraded": "Aplikasi '{failed_app}' gagal diperbarui, oleh karena itu aplikasi-aplikasi berikut juga dibatalkan: {apps}", + "app_config_unable_to_apply": "Gagal menerapkan nilai-nilai panel konfigurasi.", + "app_config_unable_to_read": "Gagal membaca nilai-nilai panel konfigurasi." } \ No newline at end of file diff --git a/locales/it.json b/locales/it.json index 27ab21473..888b6cc62 100644 --- a/locales/it.json +++ b/locales/it.json @@ -23,8 +23,6 @@ "upgrading_packages": "Aggiornamento dei pacchetti...", "user_deleted": "Utente cancellato", "admin_password": "Password dell'amministrazione", - "admin_password_change_failed": "Impossibile cambiare la password", - "admin_password_changed": "La password d'amministrazione è stata cambiata", "app_install_files_invalid": "Questi file non possono essere installati", "app_not_correctly_installed": "{app} sembra di non essere installata correttamente", "app_not_properly_removed": "{app} non è stata correttamente rimossa", @@ -34,9 +32,6 @@ "app_upgrade_failed": "Impossibile aggiornare {app}: {error}", "app_upgraded": "{app} aggiornata", "app_requirements_checking": "Controllo i pacchetti richiesti per {app}...", - "app_requirements_unmeet": "Requisiti non soddisfatti per {app}, il pacchetto {pkgname} ({version}) deve essere {spec}", - "ask_firstname": "Nome", - "ask_lastname": "Cognome", "ask_main_domain": "Dominio principale", "ask_new_admin_password": "Nuova password dell'amministrazione", "backup_app_failed": "Non è possibile fare il backup {app}", @@ -187,7 +182,6 @@ "certmanager_cannot_read_cert": "Qualcosa è andato storto nel tentativo di aprire il certificato attuale per il dominio {domain} (file: {file}), motivo: {reason}", "certmanager_cert_install_success": "Certificato Let's Encrypt per il dominio {domain} installato", "aborting": "Annullamento.", - "admin_password_too_long": "Per favore scegli una password più corta di 127 caratteri", "app_not_upgraded": "Impossibile aggiornare le applicazioni '{failed_app}' e di conseguenza l'aggiornamento delle seguenti applicazione è stato cancellato: {apps}", "app_start_install": "Installando '{app}'...", "app_start_remove": "Rimozione di {app}...", @@ -222,28 +216,12 @@ "domain_dns_conf_is_just_a_recommendation": "Questo comando ti mostra la configurazione *raccomandata*. Non ti imposta la configurazione DNS al tuo posto. È tua responsabilità configurare la tua zona DNS nel tuo registrar in accordo con queste raccomandazioni.", "dyndns_could_not_check_available": "Impossibile controllare se {domain} è disponibile su {provider}.", "dyndns_domain_not_provided": "Il fornitore DynDNS {provider} non può fornire il dominio {domain}.", - "experimental_feature": "Attenzione: Questa funzionalità è sperimentale e non è considerata stabile, non dovresti utilizzarla a meno che tu non sappia cosa stai facendo.", "file_does_not_exist": "Il file {path} non esiste.", - "global_settings_bad_choice_for_enum": "Scelta sbagliata per l'impostazione {setting}, ricevuta '{choice}', ma le scelte disponibili sono: {available_choices}", - "global_settings_bad_type_for_setting": "Tipo errato per l'impostazione {setting}, ricevuto {received_type}, atteso {expected_type}", - "global_settings_cant_open_settings": "Apertura del file delle impostazioni non riuscita, motivo: {reason}", - "global_settings_cant_serialize_settings": "Serializzazione dei dati delle impostazioni non riuscita, motivo: {reason}", - "global_settings_cant_write_settings": "Scrittura del file delle impostazioni non riuscita, motivo: {reason}", - "global_settings_key_doesnt_exists": "La chiave '{settings_key}' non esiste nelle impostazioni globali, puoi vedere tutte le chiavi disponibili eseguendo 'yunohost settings list'", - "global_settings_reset_success": "Le impostazioni precedenti sono state salvate in {path}", "already_up_to_date": "Niente da fare. Tutto è già aggiornato.", - "global_settings_setting_security_nginx_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server web NGIX. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", - "global_settings_setting_security_password_admin_strength": "Complessità della password di amministratore", - "global_settings_setting_security_password_user_strength": "Complessità della password utente", - "global_settings_setting_security_ssh_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server SSH. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", - "global_settings_unknown_setting_from_settings_file": "Chiave sconosciuta nelle impostazioni: '{setting_key}', scartata e salvata in /etc/yunohost/settings-unknown.json", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Consenti l'uso del hostkey DSA (deprecato) per la configurazione del demone SSH", - "global_settings_unknown_type": "Situazione inaspettata, l'impostazione {setting} sembra essere di tipo {unknown_type} ma non è un tipo supportato dal sistema.", "good_practices_about_admin_password": "Stai per impostare una nuova password di amministratore. La password deve essere almeno di 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una frase, una serie di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).", "log_corrupted_md_file": "Il file dei metadati YAML associato con i registri è danneggiato: '{md_file}'\nErrore: {error}", "log_link_to_log": "Registro completo di questa operazione: '{desc}'", "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log show {name}'", - "global_settings_setting_security_postfix_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", "log_link_to_failed_log": "Impossibile completare l'operazione '{desc}'! Per ricevere aiuto, per favore fornisci il registro completo dell'operazione cliccando qui", "log_help_to_get_failed_log": "L'operazione '{desc}' non può essere completata. Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log share {name}'", "log_does_exists": "Non esiste nessun registro delle operazioni chiamato '{log}', usa 'yunohost log list' per vedere tutti i registri delle operazioni disponibili", @@ -414,7 +392,6 @@ "server_reboot": "Il server si riavvierà", "server_shutdown_confirm": "Il server si spegnerà immediatamente, sei sicuro? [{answers}]", "server_shutdown": "Il server si spegnerà", - "root_password_replaced_by_admin_password": "La tua password di root è stata sostituita dalla tua password d'amministratore.", "root_password_desynchronized": "La password d'amministratore è stata cambiata, ma YunoHost non ha potuto propagarla alla password di root!", "restore_system_part_failed": "Impossibile ripristinare la sezione di sistema '{part}'", "restore_removing_tmp_dir_failed": "Impossibile rimuovere una vecchia directory temporanea", @@ -506,13 +483,9 @@ "group_already_exist_on_system_but_removing_it": "Il gruppo {group} esiste già tra i gruppi di sistema, ma YunoHost lo cancellerà...", "group_already_exist_on_system": "Il gruppo {group} esiste già tra i gruppi di sistema", "group_already_exist": "Il gruppo {group} esiste già", - "global_settings_setting_backup_compress_tar_archives": "Quando creo nuovi backup, usa un archivio (.tar.gz) al posto di un archivio non compresso (.tar). NB: abilitare quest'opzione significa create backup più leggeri, ma la procedura durerà di più e il carico CPU sarà maggiore.", "global_settings_setting_smtp_relay_password": "Password del relay SMTP", "global_settings_setting_smtp_relay_user": "User account del relay SMTP", "global_settings_setting_smtp_relay_port": "Porta del relay SMTP", - "global_settings_setting_smtp_relay_host": "Utilizza SMTP relay per inviare mail al posto di questa instanza yunohost. Utile se sei in una di queste situazioni: la tua porta 25 è bloccata dal tuo provider ISP o VPS; hai un IP residenziale listato su DUHL; non sei puoi configurare il DNS inverso; oppure questo server non è direttamente esposto a Internet e vuoi usarne un'altro per spedire email.", - "global_settings_setting_smtp_allow_ipv6": "Permetti l'utilizzo di IPv6 per ricevere e inviare mail", - "global_settings_setting_pop3_enabled": "Abilita il protocollo POP3 per il server mail", "dyndns_provider_unreachable": "Incapace di raggiungere il provider DynDNS {provider}: o il tuo YunoHost non è connesso ad internet o il server dynette è down.", "dpkg_lock_not_available": "Impossibile eseguire il comando in questo momento perché un altro programma sta bloccando dpkg (il package manager di sistema)", "domain_cannot_remove_main_add_new_one": "Non puoi rimuovere '{domain}' visto che è il dominio principale nonché il tuo unico dominio, devi prima aggiungere un altro dominio eseguendo 'yunohost domain add ', impostarlo come dominio principale con 'yunohost domain main-domain n ', e solo allora potrai rimuovere il dominio '{domain}' eseguendo 'yunohost domain remove {domain}'.'", @@ -574,20 +547,16 @@ "migration_ldap_backup_before_migration": "Sto generando il backup del database LDAP e delle impostazioni delle app prima di effettuare la migrazione.", "log_backup_create": "Crea un archivio backup", "global_settings_setting_ssowat_panel_overlay_enabled": "Abilita il pannello sovrapposto SSOwat", - "global_settings_setting_security_ssh_port": "Porta SSH", "diagnosis_sshd_config_inconsistent_details": "Esegui yunohost settings set security.ssh.port -v PORTA_SSH per definire la porta SSH, e controlla con yunohost tools regen-conf ssh --dry-run --with-diff, poi yunohost tools regen-conf ssh --force per resettare la tua configurazione con le raccomandazioni YunoHost.", "diagnosis_sshd_config_inconsistent": "Sembra che la porta SSH sia stata modificata manualmente in /etc/ssh/sshd_config: A partire da YunoHost 4.2, una nuova configurazione globale 'security.ssh.port' è disponibile per evitare di modificare manualmente la configurazione.", "diagnosis_sshd_config_insecure": "Sembra che la configurazione SSH sia stata modificata manualmente, ed non è sicuro dato che non contiene le direttive 'AllowGroups' o 'Allowusers' che limitano l'accesso agli utenti autorizzati.", "backup_create_size_estimation": "L'archivio conterrà circa {size} di dati.", "app_restore_script_failed": "C'è stato un errore all'interno dello script di recupero", - "global_settings_setting_security_webadmin_allowlist": "Indirizzi IP con il permesso di accedere al webadmin, separati da virgola.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Permetti solo ad alcuni IP di accedere al webadmin.", "disk_space_not_sufficient_update": "Non c'è abbastanza spazio libero per aggiornare questa applicazione", "disk_space_not_sufficient_install": "Non c'è abbastanza spazio libero per installare questa applicazione", "app_config_unable_to_apply": "Applicazione dei valori nel pannello di configurazione non riuscita.", "app_config_unable_to_read": "Lettura dei valori nel pannello di configurazione non riuscita.", "diagnosis_apps_issue": "È stato rilevato un errore per l’app {app}", - "global_settings_setting_security_nginx_redirect_to_https": "Reindirizza richieste HTTP a HTTPs di default (NON DISABILITARE a meno che tu non sappia veramente bene cosa stai facendo!)", "diagnosis_http_special_use_tld": "Il dominio {domain} è basato su un dominio di primo livello (TLD) dall’uso speciale, come .local o .test, perciò non è previsto che sia esposto al di fuori della rete locale.", "domain_dns_conf_special_use_tld": "Questo dominio è basato su un dominio di primo livello (TLD) dall’uso speciale, come .local o .test, perciò non è previsto abbia reali record DNS.", "domain_dns_push_not_applicable": "La configurazione automatica del DNS non è applicabile al dominio {domain}. Dovresti configurare i tuoi record DNS manualmente, seguendo la documentazione su https://yunohost.org/dns_config.", @@ -610,12 +579,10 @@ "user_import_partial_failed": "L’importazione degli utenti è parzialmente fallita", "domain_unknown": "Il dominio '{domain}' è sconosciuto", "log_user_import": "Importa utenti", - "invalid_password": "Password non valida", "diagnosis_high_number_auth_failures": "Recentemente c’è stato un numero insolitamente alto di autenticazioni fallite. Potresti assicurarti che fail2ban stia funzionando e che sia configurato correttamente, oppure usare una differente porta SSH, come spiegato in https://yunohost.org/security.", "diagnosis_apps_allgood": "Tutte le applicazioni installate rispettano le pratiche di packaging di base", "config_apply_failed": "L’applicazione della nuova configurazione è fallita: {error}", "diagnosis_apps_outdated_ynh_requirement": "La versione installata di quest’app richiede esclusivamente YunoHost >= 2.x, che tendenzialmente significa che non è aggiornata secondo le pratiche di packaging raccomandate. Dovresti proprio considerare di aggiornarla.", - "global_settings_setting_security_experimental_enabled": "Abilita funzionalità di sicurezza sperimentali (non abilitare se non sai cosa stai facendo!)", "invalid_number_min": "Deve essere più grande di {min}", "invalid_number_max": "Deve essere meno di {max}", "log_app_config_set": "Applica la configurazione all’app '{}'", @@ -659,5 +626,19 @@ "user_import_bad_line": "Linea errata {line}: {details}", "config_validate_url": "È necessario inserire un URL web valido", "ldap_server_down": "Impossibile raggiungere il server LDAP", - "ldap_server_is_down_restart_it": "Il servizio LDAP è down, prova a riavviarlo…" + "ldap_server_is_down_restart_it": "Il servizio LDAP è down, prova a riavviarlo…", + "global_settings_setting_backup_compress_tar_archives_help": "Quando creo nuovi backup, usa un archivio (.tar.gz) al posto di un archivio non compresso (.tar). NB: abilitare quest'opzione significa create backup più leggeri, ma la procedura durerà di più e il carico CPU sarà maggiore.", + "global_settings_setting_security_experimental_enabled_help": "Abilita funzionalità di sicurezza sperimentali (non abilitare se non sai cosa stai facendo!)", + "global_settings_setting_nginx_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server web NGIX. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", + "global_settings_setting_nginx_redirect_to_https_help": "Reindirizza richieste HTTP a HTTPs di default (NON DISABILITARE a meno che tu non sappia veramente bene cosa stai facendo!)", + "global_settings_setting_admin_strength": "Complessità della password di amministratore", + "global_settings_setting_user_strength": "Complessità della password utente", + "global_settings_setting_postfix_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", + "global_settings_setting_ssh_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server SSH. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", + "global_settings_setting_ssh_port": "Porta SSH", + "global_settings_setting_webadmin_allowlist_help": "Indirizzi IP con il permesso di accedere al webadmin, separati da virgola.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permetti solo ad alcuni IP di accedere al webadmin.", + "global_settings_setting_smtp_allow_ipv6_help": "Permetti l'utilizzo di IPv6 per ricevere e inviare mail", + "global_settings_setting_smtp_relay_enabled_help": "Utilizza SMTP relay per inviare mail al posto di questa instanza yunohost. Utile se sei in una di queste situazioni: la tua porta 25 è bloccata dal tuo provider ISP o VPS; hai un IP residenziale listato su DUHL; non sei puoi configurare il DNS inverso; oppure questo server non è direttamente esposto a Internet e vuoi usarne un'altro per spedire email.", + "domain_config_default_app": "Applicazione di default" } \ No newline at end of file diff --git a/locales/kab.json b/locales/kab.json index 5daa7cef0..f6e3722cf 100644 --- a/locales/kab.json +++ b/locales/kab.json @@ -1,14 +1,11 @@ { - "ask_firstname": "Isem", - "ask_lastname": "Isem n tmagit", "ask_password": "Awal n uɛeddi", "diagnosis_description_apps": "Isnasen", "diagnosis_description_mail": "Imayl", "domain_deleted": "Taɣult tettwakkes", "done": "Immed", - "invalid_password": "Yir awal uffir", "user_created": "Aseqdac yettwarna", "diagnosis_description_dnsrecords": "Ikalasen DNS", "diagnosis_description_web": "Réseau", "domain_created": "Taɣult tettwarna" -} +} \ No newline at end of file diff --git a/locales/nb_NO.json b/locales/nb_NO.json index e81d3af05..d74f47728 100644 --- a/locales/nb_NO.json +++ b/locales/nb_NO.json @@ -1,9 +1,6 @@ { "aborting": "Avbryter…", "admin_password": "Administrasjonspassord", - "admin_password_change_failed": "Kan ikke endre passord", - "admin_password_changed": "Administrasjonspassord endret", - "admin_password_too_long": "Velg et passord kortere enn 127 tegn", "app_already_installed": "{app} er allerede installert", "app_already_up_to_date": "{app} er allerede oppdatert", "app_argument_invalid": "Velg en gydlig verdi for argumentet '{name}': {error}", @@ -86,9 +83,7 @@ "dyndns_key_generating": "Oppretter DNS-nøkkel… Dette kan ta en stund.", "dyndns_no_domain_registered": "Inget domene registrert med DynDNS", "dyndns_registered": "DynDNS-domene registrert", - "global_settings_setting_security_password_admin_strength": "Admin-passordets styrke", "dyndns_registration_failed": "Kunne ikke registrere DynDNS-domene: {error}", - "global_settings_setting_security_password_user_strength": "Brukerpassordets styrke", "log_backup_restore_app": "Gjenopprett '{}' fra sikkerhetskopiarkiv", "log_remove_on_failed_install": "Fjern '{}' etter mislykket installasjon", "log_selfsigned_cert_install": "Installer selvsignert sertifikat på '{}'-domenet", @@ -100,8 +95,6 @@ "app_upgrade_failed": "Kunne ikke oppgradere {app}", "app_upgrade_some_app_failed": "Noen programmer kunne ikke oppgraderes", "app_upgraded": "{app} oppgradert", - "ask_firstname": "Fornavn", - "ask_lastname": "Etternavn", "ask_main_domain": "Hoveddomene", "ask_new_admin_password": "Nytt administrasjonspassord", "app_upgrade_several_apps": "Følgende programmer vil oppgraderes: {apps}", @@ -115,5 +108,7 @@ "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log show {name}'", "log_user_create": "Legg til '{}' bruker", "app_change_url_success": "{app} nettadressen er nå {domain}{path}", - "app_install_failed": "Kunne ikke installere {app}: {error}" + "app_install_failed": "Kunne ikke installere {app}: {error}", + "global_settings_setting_admin_strength": "Admin-passordets styrke", + "global_settings_setting_user_strength": "Brukerpassordets styrke" } \ No newline at end of file diff --git a/locales/nl.json b/locales/nl.json index f8b6df327..24ade2f5c 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -1,7 +1,6 @@ { "action_invalid": "Ongeldige actie '{action}'", "admin_password": "Administrator wachtwoord", - "admin_password_changed": "Het administratie wachtwoord is gewijzigd", "app_already_installed": "{app} is al geïnstalleerd", "app_argument_invalid": "Kies een geldige waarde voor '{name}': {error}", "app_argument_required": "Het '{name}' moet ingevuld worden", @@ -14,11 +13,9 @@ "app_unknown": "Onbekende app", "app_upgrade_failed": "Het is niet gelukt app {app} bij te werken: {error}", "app_upgraded": "{app} is bijgewerkt", - "ask_firstname": "Voornaam", - "ask_lastname": "Achternaam", "ask_new_admin_password": "Nieuw administratorwachtwoord", "ask_password": "Wachtwoord", - "backup_archive_name_exists": "Een backuparchief met dezelfde naam bestaat al", + "backup_archive_name_exists": "Er bestaat al een backuparchief met dezelfde naam.", "backup_cleaning_failed": "Kan tijdelijke backup map niet leeg maken", "backup_output_directory_not_empty": "Doelmap is niet leeg", "custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app} bij te werken", @@ -30,13 +27,13 @@ "domain_dyndns_already_subscribed": "U heeft reeds een domein bij DynDNS geregistreerd", "domain_dyndns_root_unknown": "Onbekend DynDNS root domein", "domain_exists": "Domein bestaat al", - "domain_uninstall_app_first": "Een of meerdere apps zijn geïnstalleerd op dit domein, verwijder deze voordat u het domein verwijdert", + "domain_uninstall_app_first": "Deze applicaties zijn nog steeds op je domein geïnstalleerd:\n{apps}\n\nVerwijder ze met 'yunohost app remove the_app_id' of verplaats ze naar een ander domein met 'yunohost app change-url the_app_id' voordat je doorgaat met het verwijderen van het domein", "done": "Voltooid", "downloading": "Downloaden...", "dyndns_ip_update_failed": "Kan het IP adres niet updaten bij DynDNS", "dyndns_ip_updated": "IP adres is aangepast bij DynDNS", "dyndns_key_generating": "DNS sleutel word aangemaakt, wacht een moment...", - "dyndns_unavailable": "DynDNS subdomein is niet beschikbaar", + "dyndns_unavailable": "Domein '{domain}' is niet beschikbaar.", "extracting": "Uitpakken...", "installation_complete": "Installatie voltooid", "mail_alias_remove_failed": "Kan mail-alias '{mail}' niet verwijderen", @@ -50,10 +47,10 @@ "service_add_failed": "Kan service '{service}' niet toevoegen", "service_already_started": "Service '{service}' draait al", "service_cmd_exec_failed": "Kan '{command}' niet uitvoeren", - "service_disabled": "Service '{service}' is uitgeschakeld", + "service_disabled": "Service '{service}' wordt niet meer gestart als het systeem opstart.", "service_remove_failed": "Kan service '{service}' niet verwijderen", "service_removed": "Service werd verwijderd", - "service_stop_failed": "Kan service '{service}' niet stoppen", + "service_stop_failed": "Kan service '{service}' niet stoppen\n\nRecente servicelogs: {logs}", "service_unknown": "De service '{service}' bestaat niet", "unexpected_error": "Er is een onbekende fout opgetreden", "unrestore_app": "App '{app}' wordt niet teruggezet", @@ -69,12 +66,10 @@ "user_unknown": "Gebruikersnaam {user} is onbekend", "user_update_failed": "Kan gebruiker niet bijwerken", "yunohost_configured": "YunoHost configuratie is OK", - "admin_password_change_failed": "Wachtwoord wijzigen is niet gelukt", "app_argument_choice_invalid": "Kiel een geldige waarde voor argument '{name}'; {value}' komt niet voor in de keuzelijst {choices}", "app_not_correctly_installed": "{app} schijnt niet juist geïnstalleerd te zijn", "app_not_properly_removed": "{app} werd niet volledig verwijderd", "app_requirements_checking": "Noodzakelijke pakketten voor {app} aan het controleren...", - "app_requirements_unmeet": "Er wordt niet aan de aanvorderingen voldaan, het pakket {pkgname} ({version}) moet {spec} zijn", "app_unsupported_remote_type": "Niet ondersteund besturings type voor de app", "ask_main_domain": "Hoofd-domein", "backup_app_failed": "Kon geen backup voor app '{app}' aanmaken", @@ -90,7 +85,6 @@ "backup_nothings_done": "Niets om op te slaan", "password_too_simple_1": "Het wachtwoord moet minimaal 8 tekens lang zijn", "already_up_to_date": "Er is niets te doen, alles is al up-to-date.", - "admin_password_too_long": "Gelieve een wachtwoord te kiezen met minder dan 127 karakters", "app_action_cannot_be_ran_because_required_services_down": "De volgende diensten moeten actief zijn om deze actie uit te voeren: {services}. Probeer om deze te herstarten om verder te gaan (en om eventueel te onderzoeken waarom ze niet werken).", "aborting": "Annulatie.", "app_upgrade_app_name": "Bezig {app} te upgraden...", @@ -120,7 +114,7 @@ "app_action_broke_system": "Deze actie lijkt de volgende belangrijke services te hebben kapotgemaakt: {services}", "app_config_unable_to_apply": "De waarden in het configuratiescherm konden niet toegepast worden.", "app_config_unable_to_read": "Het is niet gelukt de waarden van het configuratiescherm te lezen.", - "app_argument_password_no_default": "Foutmelding tijdens het lezen van wachtwoordargument '{name}': het wachtwoordargument mag om veiligheidsredenen geen standaardwaarde hebben.", + "app_argument_password_no_default": "Foutmelding tijdens het lezen van wachtwoordargument '{name}': het wachtwoordargument mag om veiligheidsredenen geen standaardwaarde hebben", "app_already_installed_cant_change_url": "Deze app is al geïnstalleerd. De URL kan niet veranderd worden met deze functie. Probeer of dat lukt via `app changeurl`.", "apps_catalog_init_success": "De app-catalogus is succesvol geinitieerd!", "apps_catalog_failed_to_download": "Het is niet gelukt de {apps_catalog} app-catalogus te downloaden: {error}", @@ -128,7 +122,7 @@ "additional_urls_already_added": "Extra URL '{url:s}' is al toegevoegd in de extra URL voor privilege '{permission:s}'", "additional_urls_already_removed": "Extra URL '{url}' is al verwijderd in de extra URL voor privilege '{permission}'", "app_label_deprecated": "Dit commando is vervallen. Gebruik alsjeblieft het nieuwe commando 'yunohost user permission update' om het label van de app te beheren.", - "app_change_url_no_script": "De app '{app_name}' ondersteunt nog geen URL-aanpassingen. Misschien wel na een upgrade.", + "app_change_url_no_script": "App '{app_name}' ondersteunt nog geen URL-aanpassingen. Misschien wel na een upgrade.", "app_upgrade_some_app_failed": "Sommige apps konden niet worden bijgewerkt", "other_available_options": "... en {n} andere beschikbare opties die niet getoond worden", "password_listed": "Dit wachtwoord is een van de meest gebruikte wachtwoorden ter wereld. Kies alstublieft iets wat minder voor de hand ligt.", @@ -140,5 +134,9 @@ "pattern_domain": "Moet een geldige domeinnaam zijn (mijneigendomein.nl, bijvoorbeeld)", "pattern_firstname": "Het moet een geldige voornaam zijn", "pattern_lastname": "Het moet een geldige achternaam zijn", - "password_too_simple_3": "Het wachtwoord moet minimaal 8 tekens lang zijn en moet cijfers, hoofdletters, kleine letters en speciale tekens bevatten" -} \ No newline at end of file + "password_too_simple_3": "Het wachtwoord moet minimaal 8 tekens lang zijn en moet cijfers, hoofdletters, kleine letters en speciale tekens bevatten", + "group_already_exist": "Groep {group} bestaat al", + "group_already_exist_on_system": "Groep {group} bestaat al in de systeemgroepen", + "good_practices_about_admin_password": "Je gaat nu een nieuw beheerderswachtwoordopgeven. Het wachtwoord moet minimaal 8 tekens lang zijn, hoewel het een goede gewoonte is om een langer wachtwoord te gebruiken (d.w.z. een wachtwoordzin) en/of een variatie van tekens te gebruiken (hoofdletters, kleine letters, cijfers en speciale tekens).", + "good_practices_about_user_password": "Je gaat nu een nieuw gebruikerswachtwoord pgeven. Het wachtwoord moet minimaal 8 tekens lang zijn, hoewel het een goede gewoonte is om een langer wachtwoord te gebruiken (d.w.z. een wachtwoordzin) en/of een variatie van tekens te gebruiken (hoofdletters, kleine letters, cijfers en speciale tekens)." +} diff --git a/locales/oc.json b/locales/oc.json index a6afa32e6..6282a6cec 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -1,7 +1,5 @@ { "admin_password": "Senhal d’administracion", - "admin_password_change_failed": "Impossible de cambiar lo senhal", - "admin_password_changed": "Lo senhal d’administracion es ben estat cambiat", "app_already_installed": "{app} es ja installat", "app_already_up_to_date": "{app} es ja a jorn", "installation_complete": "Installacion acabada", @@ -16,8 +14,6 @@ "app_upgrade_failed": "Impossible d’actualizar {app} : {error}", "app_upgrade_some_app_failed": "D’aplicacions se pòdon pas actualizar", "app_upgraded": "{app} es estada actualizada", - "ask_firstname": "Prenom", - "ask_lastname": "Nom", "ask_main_domain": "Domeni màger", "ask_new_admin_password": "Nòu senhal administrator", "ask_password": "Senhal", @@ -57,7 +53,6 @@ "backup_output_directory_required": "Vos cal especificar un dorsièr de sortida per la salvagarda", "backup_running_hooks": "Execucion dels scripts de salvagarda...", "backup_system_part_failed": "Impossible de salvagardar la part « {part} » del sistèma", - "app_requirements_unmeet": "Las condicions requesidas per {app} son pas complidas, lo paquet {pkgname} ({version}) deu èsser {spec}", "backup_abstract_method": "Aqueste metòde de salvagarda es pas encara implementat", "backup_applying_method_custom": "Crida del metòde de salvagarda personalizat « {method} »...", "backup_couldnt_bind": "Impossible de ligar {src} amb {dest}.", @@ -120,10 +115,6 @@ "dyndns_unavailable": "Lo domeni {domain} es pas disponible.", "extracting": "Extraccion…", "field_invalid": "Camp incorrècte : « {} »", - "global_settings_cant_open_settings": "Fracàs de la dobertura del fichièr de configuracion, rason : {reason}", - "global_settings_key_doesnt_exists": "La clau « {settings_key} » existís pas dins las configuracions globalas, podètz veire totas las claus disponiblas en executant « yunohost settings list »", - "global_settings_reset_success": "Configuracion precedenta ara salvagarda dins {path}", - "global_settings_unknown_setting_from_settings_file": "Clau desconeguda dins los paramètres : {setting_key}, apartada e salvagardada dins /etc/yunohost/settings-unknown.json", "main_domain_change_failed": "Modificacion impossibla del domeni màger", "main_domain_changed": "Lo domeni màger es estat modificat", "migrations_list_conflict_pending_done": "Podètz pas utilizar --previous e --done a l’encòp.", @@ -152,10 +143,6 @@ "firewall_reload_failed": "Impossible de recargar lo parafuòc", "firewall_reloaded": "Parafuòc recargat", "firewall_rules_cmd_failed": "Unas règlas del parafuòc an fracassat. Per mai informacions, consultatz lo jornal.", - "global_settings_bad_choice_for_enum": "La valor del paramètre {setting} es incorrècta. Recebut : {choice}, mas las opcions esperadas son : {available_choices}", - "global_settings_bad_type_for_setting": "Lo tipe del paramètre {setting} es incorrècte, recebut : {received_type}, esperat {expected_type}", - "global_settings_cant_write_settings": "Fracàs de l’escritura del fichièr de configuracion, rason : {reason}", - "global_settings_unknown_type": "Situacion inesperada, la configuracion {setting} sembla d’aver lo tipe {unknown_type} mas es pas un tipe pres en carga pel sistèma.", "hook_exec_failed": "Fracàs de l’execucion del script : « {path} »", "hook_exec_not_terminated": "Lo escript « {path} » a pas acabat corrèctament", "hook_list_by_invalid": "La proprietat de tria de las accions es invalida", @@ -216,7 +203,6 @@ "service_description_ssh": "vos permet de vos connectar a distància a vòstre servidor via un teminal (protocòl SSH)", "service_description_yunohost-api": "permet las interaccions entre l’interfàcia web de YunoHost e le sistèma", "service_description_yunohost-firewall": "gerís los pòrts de connexion dobèrts e tampats als servicis", - "global_settings_cant_serialize_settings": "Fracàs de la serializacion de las donadas de parametratge, rason : {reason}", "ip6tables_unavailable": "Podètz pas jogar amb ip6tables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", "iptables_unavailable": "Podètz pas jogar amb iptables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", "mail_alias_remove_failed": "Supression impossibla de l’alias de corrièl « {mail} »", @@ -237,7 +223,6 @@ "backup_cant_mount_uncompress_archive": "Impossible de montar en lectura sola lo repertòri de l’archiu descomprimit", "backup_no_uncompress_archive_dir": "Lo repertòri de l’archiu descomprimit existís pas", "pattern_username": "Deu èsser compausat solament de caractèrs alfanumerics en letras minusculas e de tirets basses", - "experimental_feature": "Atencion : aquesta foncionalitat es experimentala e deu pas èsser considerada coma establa, deuriatz pas l’utilizar levat que sapiatz çò que fasètz.", "log_corrupted_md_file": "Lo fichièr YAML de metadonadas ligat als jornals d’audit es damatjat : « {md_file} »\nError : {error}", "log_link_to_log": "Jornal complèt d’aquesta operacion : {desc}", "log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log show {name} »", @@ -293,11 +278,7 @@ "backup_mount_archive_for_restore": "Preparacion de l’archiu per restauracion...", "dyndns_could_not_check_available": "Verificacion impossibla de la disponibilitat de {domain} sus {provider}.", "file_does_not_exist": "Lo camin {path} existís pas.", - "global_settings_setting_security_password_admin_strength": "Fòrça del senhal administrator", - "global_settings_setting_security_password_user_strength": "Fòrça del senhal utilizaire", - "root_password_replaced_by_admin_password": "Lo senhal root es estat remplaçat pel senhal administrator.", "service_restarted": "Lo servici '{service}' es estat reaviat", - "admin_password_too_long": "Causissètz un senhal d’almens 127 caractèrs", "service_reloaded": "Lo servici « {service} » es estat tornat cargar", "already_up_to_date": "I a pas res a far ! Tot es ja a jorn !", "app_action_cannot_be_ran_because_required_services_down": "Aquestas aplicacions necessitan d’èsser lançadas per poder executar aquesta accion : {services}. Abans de contunhar deuriatz ensajar de reaviar los servicis seguents (e tanben cercar perque son tombats en pana) : {services}", @@ -308,7 +289,6 @@ "log_regen_conf": "Regenerar las configuracions del sistèma « {} »", "service_reloaded_or_restarted": "Lo servici « {service} » es estat recargat o reaviat", "dpkg_is_broken": "Podètz pas far aquò pel moment perque dpkg/APT (los gestionaris de paquets del sistèma) sembla èsser mal configurat… Podètz ensajar de solucionar aquò en vos connectar via SSH e en executar « sudo dpkg --configure -a ».", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Autorizar l’utilizacion de la clau òst DSA (obsolèta) per la configuracion del servici SSH", "hook_json_return_error": "Fracàs de la lectura del retorn de l’script {path}. Error : {msg}. Contengut brut : {raw_content}", "pattern_password_app": "O planhèm, los senhals devon pas conténer los caractèrs seguents : {forbidden_chars}", "regenconf_file_backed_up": "Lo fichièr de configuracion « {conf} » es estat salvagardat dins « {backup} »", @@ -325,9 +305,6 @@ "regenconf_dry_pending_applying": "Verificacion de la configuracion que seriá estada aplicada a la categoria « {category} »…", "regenconf_failed": "Regeneracion impossibla de la configuracion per la(s) categoria(s) : {categories}", "regenconf_pending_applying": "Aplicacion de la configuracion en espèra per la categoria « {category} »…", - "global_settings_setting_security_nginx_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor web NGINX Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", - "global_settings_setting_security_ssh_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor SSH. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", - "global_settings_setting_security_postfix_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor Postfix. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", "service_reload_failed": "Impossible de recargar lo servici « {service} »\n\nJornal d’audit recent : {logs}", "service_restart_failed": "Impossible de reaviar lo servici « {service} »\n\nJornal d’audit recent : {logs}", "service_reload_or_restart_failed": "Impossible de recargar o reaviar lo servici « {service} »\n\nJornal d’audit recent : {logs}", @@ -453,7 +430,6 @@ "diagnosis_ip_broken_resolvconf": "La resolucion del nom de domeni sembla copada sul servidor, poiriá èsser ligada al fait que /etc/resolv.conf manda pas a 127.0.0.1.", "diagnosis_ip_weird_resolvconf": "La resolucion del nom de domeni sembla foncionar, mas sembla qu’utiilizatz un fichièr /etc/resolv.conf personalizat.", "diagnosis_diskusage_verylow": "Lo lòc d’emmagazinatge {mountpoint} (sul periferic {device}) a solament {free} ({free_percent}%). Deuriatz considerar de liberar un pauc d’espaci.", - "global_settings_setting_pop3_enabled": "Activar lo protocòl POP3 pel servidor de corrièr", "diagnosis_diskusage_ok": "Lo lòc d’emmagazinatge {mountpoint} (sul periferic {device}) a encara {free} ({free_percent}%) de liure !", "diagnosis_swap_none": "Lo sistèma a pas cap de memòria d’escambi. Auriatz de considerar d’ajustar almens {recommended} d’escambi per evitar las situacions ont lo sistèma manca de memòria.", "diagnosis_swap_notsomuch": "Lo sistèma a solament {total} de memòria d’escambi. Auriatz de considerar d’ajustar almens {recommended} d’escambi per evitar las situacions ont lo sistèma manca de memòria.", @@ -488,5 +464,10 @@ "diagnosis_domain_not_found_details": "Lo domeni {domain} existís pas a la basa de donadas WHOIS o a expirat !", "diagnosis_domain_expiration_not_found": "Impossible de verificar la data d’expiracion d’unes domenis", "backup_create_size_estimation": "L’archiu contendrà apr’aquí {size} de donadas.", - "app_restore_script_failed": "Una error s’es producha a l’interior del script de restauracion de l’aplicacion" + "app_restore_script_failed": "Una error s’es producha a l’interior del script de restauracion de l’aplicacion", + "global_settings_setting_nginx_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor web NGINX Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", + "global_settings_setting_admin_strength": "Fòrça del senhal administrator", + "global_settings_setting_user_strength": "Fòrça del senhal utilizaire", + "global_settings_setting_postfix_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor Postfix. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", + "global_settings_setting_ssh_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor SSH. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)" } \ No newline at end of file diff --git a/locales/pl.json b/locales/pl.json index 01cd71471..6734e6558 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -3,9 +3,6 @@ "app_already_up_to_date": "{app} jest obecnie aktualna", "app_already_installed": "{app} jest już zainstalowane", "already_up_to_date": "Nic do zrobienia. Wszystko jest obecnie aktualne.", - "admin_password_too_long": "Proszę wybrać hasło krótsze niż 127 znaków", - "admin_password_changed": "Hasło administratora zostało zmienione", - "admin_password_change_failed": "Nie można zmienić hasła", "admin_password": "Hasło administratora", "action_invalid": "Nieprawidłowe działanie '{action:s}'", "aborting": "Przerywanie." diff --git a/locales/pt.json b/locales/pt.json index 6b462bb6f..a7b574949 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -1,8 +1,6 @@ { - "action_invalid": "Acção Inválida '{action}'", + "action_invalid": "Ação inválida '{action}'", "admin_password": "Senha de administração", - "admin_password_change_failed": "Não foi possível alterar a senha", - "admin_password_changed": "A senha da administração foi alterada", "app_already_installed": "{app} já está instalada", "app_extraction_failed": "Não foi possível extrair os arquivos para instalação", "app_id_invalid": "App ID invaĺido", @@ -13,8 +11,6 @@ "app_unknown": "Aplicação desconhecida", "app_upgrade_failed": "Não foi possível atualizar {app}: {error}", "app_upgraded": "{app} atualizado", - "ask_firstname": "Primeiro nome", - "ask_lastname": "Último nome", "ask_main_domain": "Domínio principal", "ask_new_admin_password": "Nova senha de administração", "ask_password": "Senha", @@ -123,14 +119,13 @@ "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo", "app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain}{path}'), nada para fazer.", "password_too_simple_1": "A senha precisa ter pelo menos 8 caracteres", - "admin_password_too_long": "Escolha uma senha que contenha menos de 127 caracteres", "aborting": "Abortando.", "app_change_url_no_script": "A aplicação '{app_name}' ainda não permite modificar a URL. Talvez devesse atualizá-la.", "app_argument_password_no_default": "Erro ao interpretar argumento da senha '{name}': O argumento da senha não pode ter um valor padrão por segurança", "app_action_cannot_be_ran_because_required_services_down": "Estes serviços devem estar funcionado para executar esta ação: {services}. Tente reiniciá-los para continuar (e possivelmente investigar o porquê de não estarem funcionado).", "app_action_broke_system": "Esta ação parece ter quebrado estes serviços importantes: {services}", "already_up_to_date": "Nada a ser feito. Tudo já está atualizado.", - "additional_urls_already_removed": "A URL adicional '{url}'já está removida para a permissão '{permission}'", + "additional_urls_already_removed": "A URL adicional '{url}' já foi removida da permissão '{permission}'", "additional_urls_already_added": "A URL adicional '{url}' já está adicionada para a permissão '{permission}'", "app_install_script_failed": "Ocorreu um erro dentro do script de instalação do aplicativo", "app_install_failed": "Não foi possível instalar {app}: {error}", @@ -151,7 +146,6 @@ "app_restore_script_failed": "Ocorreu um erro dentro do script de restauração da aplicação", "app_restore_failed": "Não foi possível restaurar {app}: {error}", "app_remove_after_failed_install": "Removendo a aplicação após a falha da instalação...", - "app_requirements_unmeet": "Os requisitos para a aplicação {app} não foram satisfeitos, o pacote {pkgname} ({version}) devem ser {spec}", "app_not_upgraded": "Não foi possível atualizar a aplicação '{failed_app}' e, como consequência, a atualização das seguintes aplicações foi cancelada: {apps}", "app_manifest_install_ask_is_public": "Essa aplicação deve ser visível para visitantes anônimos?", "app_manifest_install_ask_admin": "Escolha um usuário de administrador para essa aplicação", @@ -251,5 +245,6 @@ "diagnosis_basesystem_hardware_model": "O modelo do servidor é {model}", "diagnosis_backports_in_sources_list": "Parece que o apt (o gerenciador de pacotes) está configurado para usar o repositório backport. A não ser que você saiba o que você esteá fazendo, desencorajamos fortemente a instalação de pacotes de backports porque é provável que crie instabilidades ou conflitos no seu sistema.", "certmanager_cert_renew_success": "Certificado Let's Encrypt renovado para o domínio '{domain}'", - "certmanager_warning_subdomain_dns_record": "O subdomínio '{subdomain}' não resolve para o mesmo IP que '{domain}'. Algumas funcionalidades não estarão disponíveis até que você conserte isto e regenere o certificado." -} \ No newline at end of file + "certmanager_warning_subdomain_dns_record": "O subdomínio '{subdomain}' não resolve para o mesmo IP que '{domain}'. Algumas funcionalidades não estarão disponíveis até que você conserte isto e regenere o certificado.", + "admins": "Admins" +} diff --git a/locales/pt_BR.json b/locales/pt_BR.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/pt_BR.json @@ -0,0 +1 @@ +{} diff --git a/locales/ru.json b/locales/ru.json index 1546c4d6e..40e7629e3 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,8 +1,6 @@ { "action_invalid": "Неверное действие '{action}'", "admin_password": "Пароль администратора", - "admin_password_change_failed": "Невозможно изменить пароль", - "admin_password_changed": "Пароль администратора был изменен", "app_already_installed": "{app} уже установлено", "app_already_installed_cant_change_url": "Это приложение уже установлено. URL не может быть изменен только с помощью этой функции. Изучите `app changeurl`, если это доступно.", "app_argument_choice_invalid": "Выберите корректное значение аргумента '{name}'; '{value}' не входит в число возможных вариантов: '{choices}'", @@ -29,7 +27,6 @@ "app_upgraded": "{app} обновлено", "installation_complete": "Установка завершена", "password_too_simple_1": "Пароль должен быть не менее 8 символов", - "admin_password_too_long": "Пожалуйста, выберите пароль короче 127 символов", "password_listed": "Этот пароль является одним из наиболее часто используемых паролей в мире. Пожалуйста, выберите что-то более уникальное.", "backup_applying_method_copy": "Копирование всех файлов в резервную копию...", "domain_dns_conf_is_just_a_recommendation": "Эта страница показывает вам *рекомендуемую* конфигурацию. Она не создаёт для вас конфигурацию DNS. Вы должны сами конфигурировать DNS у вашего регистратора в соответствии с этой рекомендацией.", @@ -37,13 +34,11 @@ "password_too_simple_3": "Пароль должен содержать не менее 8 символов и содержать цифры, заглавные и строчные буквы, а также специальные символы", "upnp_enabled": "UPnP включен", "user_deleted": "Пользователь удалён", - "ask_lastname": "Фамилия", "app_action_broke_system": "Это действие, по-видимому, нарушило эти важные службы: {services}", "already_up_to_date": "Ничего делать не требуется. Всё уже обновлено.", "operation_interrupted": "Действие было прервано вручную?", "user_created": "Пользователь создан", "aborting": "Прерывание.", - "ask_firstname": "Имя", "ask_main_domain": "Основной домен", "ask_new_admin_password": "Новый пароль администратора", "ask_new_domain": "Новый домен", @@ -74,7 +69,6 @@ "yunohost_already_installed": "YunoHost уже установлен", "yunohost_configured": "Теперь YunoHost настроен", "upgrading_packages": "Обновление пакетов...", - "app_requirements_unmeet": "Необходимые требования для {app} не выполнены, пакет {pkgname} ({version}) должен быть {spec}", "app_make_default_location_already_used": "Невозможно сделать '{app}' приложением по умолчанию на домене, '{domain}' уже используется '{other_app}'", "app_config_unable_to_apply": "Не удалось применить значения панели конфигурации.", "app_config_unable_to_read": "Не удалось прочитать значения панели конфигурации.", @@ -238,7 +232,6 @@ "regenconf_file_removed": "Файл конфигурации '{conf}' удален", "permission_not_found": "Разрешение '{permission}' не найдено", "group_cannot_edit_all_users": "Группа 'all_users' не может быть отредактирована вручную. Это специальная группа, предназначенная для всех пользователей, зарегистрированных в YunoHost", - "global_settings_setting_smtp_allow_ipv6": "Разрешить использование IPv6 для получения и отправки почты", "log_dyndns_subscribe": "Подписаться на субдомен YunoHost '{}'", "pattern_firstname": "Должно быть настоящее имя", "migrations_pending_cant_rerun": "Эти миграции еще не завершены, поэтому не могут быть запущены снова: {ids}", @@ -264,13 +257,10 @@ "log_backup_create": "Создание резервной копии", "group_update_failed": "Не удалось обновить группу '{group}': {error}", "permission_already_allowed": "В группе '{group}' уже включено разрешение '{permission}'", - "invalid_password": "Неверный пароль", "group_already_exist": "Группа {group} уже существует", "group_cannot_be_deleted": "Группа {group} не может быть удалена вручную.", "log_app_config_set": "Примените конфигурацию приложения '{}'", "log_backup_restore_app": "Восстановление '{}' из резервной копии", - "global_settings_setting_security_webadmin_allowlist": "IP-адреса, разрешенные для доступа к веб-интерфейсу администратора. Разделенные запятыми.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Разрешите доступ к веб-интерфейсу администратора только некоторым IP-адресам.", "log_domain_remove": "Удалить домен '{}' из конфигурации системы", "user_import_success": "Пользователи успешно импортированы", "group_user_already_in_group": "Пользователь {user} уже входит в группу {group}", @@ -284,7 +274,6 @@ "diagnosis_sshd_config_inconsistent_details": "Пожалуйста, выполните yunohost settings set security.ssh.port -v YOUR_SSH_PORT, чтобы определить порт SSH, и проверьте yunohost tools regen-conf ssh --dry-run --with-diff и yunohost tools regen-conf ssh --force, чтобы сбросить ваш conf в соответствии с рекомендациями YunoHost.", "log_domain_main_domain": "Сделать '{}' основным доменом", "diagnosis_sshd_config_insecure": "Похоже, что конфигурация SSH была изменена вручную, и она небезопасна, поскольку не содержит директив 'AllowGroups' или 'AllowUsers' для ограничения доступа авторизованных пользователей.", - "global_settings_setting_security_ssh_port": "SSH порт", "group_already_exist_on_system": "Группа {group} уже существует в системных группах", "group_already_exist_on_system_but_removing_it": "Группа {group} уже существует в системных группах, но YunoHost удалит ее...", "group_unknown": "Группа '{group}' неизвестна", @@ -303,7 +292,6 @@ "regenconf_failed": "Не удалось восстановить конфигурацию для категории(й): {categories}", "diagnosis_services_conf_broken": "Конфигурация нарушена для службы {service}!", "diagnosis_sshd_config_inconsistent": "Похоже, что порт SSH был вручную изменен в /etc/ssh/sshd_config. Начиная с версии YunoHost 4.2, доступен новый глобальный параметр 'security.ssh.port', позволяющий избежать ручного редактирования конфигурации.", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Разрешить использование (устаревшего) ключа хоста DSA для конфигурации демона SSH", "hook_exec_not_terminated": "Скрипт не завершился должным образом: {path}", "ip6tables_unavailable": "Вы не можете играть с ip6tables здесь. Либо Вы находитесь в контейнере, либо ваше ядро это не поддерживает", "iptables_unavailable": "Вы не можете играть с ip6tables здесь. Либо Вы находитесь в контейнере, либо ваше ядро это не поддерживает", @@ -332,7 +320,11 @@ "permission_already_up_to_date": "Разрешение не было обновлено, потому что запросы на добавление/удаление уже соответствуют текущему состоянию.", "group_cannot_edit_primary_group": "Группа '{group}' не может быть отредактирована вручную. Это основная группа, предназначенная для содержания только одного конкретного пользователя.", "log_app_remove": "Удалите приложение '{}'", - "not_enough_disk_space": "Недостаточно свободного места в '{путь}'", + "not_enough_disk_space": "Недостаточно свободного места в '{path}'", "pattern_email_forward": "Должен быть корректный адрес электронной почты, символ '+' допустим (например, someone+tag@example.com)", - "permission_deletion_failed": "Не удалось удалить разрешение '{permission}': {error}" -} + "permission_deletion_failed": "Не удалось удалить разрешение '{permission}': {error}", + "global_settings_setting_ssh_port": "SSH порт", + "global_settings_setting_webadmin_allowlist_help": "IP-адреса, разрешенные для доступа к веб-интерфейсу администратора. Разделенные запятыми.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Разрешите доступ к веб-интерфейсу администратора только некоторым IP-адресам.", + "global_settings_setting_smtp_allow_ipv6_help": "Разрешить использование IPv6 для получения и отправки почты" +} \ No newline at end of file diff --git a/locales/sk.json b/locales/sk.json index 04daf20a9..25bd82988 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -1,8 +1,6 @@ { "additional_urls_already_removed": "Dodatočná URL adresa '{url}' už bola odstránená pre oprávnenie '{permission}'", "admin_password": "Heslo pre správu", - "admin_password_change_failed": "Nebolo možné zmeniť heslo", - "admin_password_changed": "Heslo pre správu bolo zmenené", "app_action_broke_system": "Vyzerá, že táto akcia spôsobila nefunkčnosť nasledovných dôležitých služieb: {services}", "app_already_installed": "{app} je už nainštalovaný/á", "app_already_installed_cant_change_url": "Táto aplikácia je už nainštalovaná. Adresa URL nemôže byť touto akciou zmenená. Skontrolujte `app changeurl`, ak je dostupné.", @@ -15,7 +13,6 @@ "aborting": "Zrušené.", "action_invalid": "Nesprávna akcia '{action}'", "additional_urls_already_added": "Dodatočná URL adresa '{url}' už bola pridaná pre oprávnenie '{permission}'", - "admin_password_too_long": "Prosím, vyberte heslo kratšie ako 127 znakov", "already_up_to_date": "Nič netreba robiť. Všetko je už aktuálne.", "app_action_cannot_be_ran_because_required_services_down": "Pre vykonanie tejto akcie by mali byť spustené nasledovné služby: {services}. Skúste ich reštartovať, prípadne zistite, prečo nebežia.", "app_argument_password_no_default": "Chyba pri spracovaní obsahu hesla '{name}': z bezpečnostných dôvodov nemôže obsahovať predvolenú hodnotu", @@ -39,7 +36,7 @@ "app_packaging_format_not_supported": "Túto aplikáciu nie je možné nainštalovať, pretože formát balíčkov, ktorý používa, nie je podporovaný Vašou verziou YunoHost. Mali by ste zvážiť aktualizovanie Vášho systému.", "app_remove_after_failed_install": "Aplikácia sa po chybe počas inštalácie odstraňuje…", "app_removed": "{app} bola odinštalovaná", - "app_requirements_checking": "Kontrolujem programy vyžadované aplikáciou {app}…", + "app_requirements_checking": "Kontrolujem požiadavky aplikácie {app}…", "app_restore_failed": "Nepodarilo sa obnoviť {app}: {error}", "app_restore_script_failed": "Chyba nastala vo vnútri skriptu na obnovu aplikácie", "app_sources_fetch_failed": "Nepodarilo sa získať zdrojové súbory, je adresa URL správna?", @@ -63,12 +60,9 @@ "app_label_deprecated": "Tento príkaz je zastaraný! Prosím, použite nový príkaz 'yunohost user permission update' pre správu názvu aplikácie.", "app_not_installed": "{app} sa nepodarilo nájsť v zozname nainštalovaných aplikácií: {all_apps}", "app_not_upgraded": "Aplikáciu '{failed_app}' sa nepodarilo aktualizovať v dôsledku čoho boli aktualizácie nasledovných aplikácií zrušené: {apps}", - "app_requirements_unmeet": "Požiadavky aplikácie {app} neboli splnené, balíček {pkgname} ({version}) musí byť {spec}", "app_unsupported_remote_type": "Nepodporovaný vzdialený typ použitý pre aplikáciu", "app_upgrade_several_apps": "Nasledovné aplikácie budú aktualizované: {apps}", "apps_catalog_updating": "Aktualizujem repozitár aplikácií…", - "ask_firstname": "Krstné meno", - "ask_lastname": "Priezvisko", "ask_main_domain": "Hlavná doména", "ask_new_admin_password": "Nové heslo pre správu", "ask_new_domain": "Nová doména", @@ -152,5 +146,108 @@ "config_validate_url": "Toto by mala byť platná URL adresa webu", "config_version_not_supported": "Verzie konfiguračného panela '{version}' nie sú podporované.", "danger": "Nebezpečenstvo:", - "confirm_app_install_danger": "NEBEZPEČENSTVO! Táto aplikácia je experimentálna (ak vôbec funguje)! Pravdepodobne by ste ju NEMALI inštalovať, pokiaľ si nie ste istý, čo robíte. NEPOSKYTNEME VÁM ŽIADNU POMOC, ak aplikácia nebude fungovať alebo rozbije Váš systém… Ak sa rozhodnete i napriek tomu podstúpiť toto riziko, zadajte '{answers}'" + "confirm_app_install_danger": "NEBEZPEČENSTVO! Táto aplikácia je experimentálna (ak vôbec funguje)! Pravdepodobne by ste ju NEMALI inštalovať, pokiaľ si nie ste istý, čo robíte. NEPOSKYTNEME VÁM ŽIADNU POMOC, ak táto aplikácia nebude fungovať alebo rozbije Váš systém… Ak sa rozhodnete i napriek tomu podstúpiť toto riziko, zadajte '{answers}'", + "confirm_app_install_thirdparty": "NEBEZPEČENSTVO! Táto aplikácia nie je súčasťou katalógu aplikácií YunoHost. Inštalovaním aplikácií tretích strán môžete ohroziť integritu a bezpečnosť Vášho systému. Pravdepodobne by ste NEMALI pokračovať v inštalácií, pokiaľ neviete, čo robíte. NEPOSKYTNEME VÁM ŽIADNU POMOC, ak táto aplikácia nebude fungovať alebo rozbije Váš systém… Ak sa rozhodnete i napriek tomu podstúpiť toto riziko, zadajte '{answers}'", + "custom_app_url_required": "Pre aktualizáciu Vašej vlastnej aplikácie {app} musíte zadať adresu URL", + "diagnosis_apps_allgood": "Všetky nainštalované aplikácie sa riadia základnými zásadami balíčkovania", + "diagnosis_apps_bad_quality": "Táto aplikácia je v katalógu aplikácií YunoHost momentálne označená ako rozbitá. Toto môže byť dočasný problém do momentu, kedy jej správcovia danú chybu neopravia. Kým sa tak stane sú aktualizácie tejto aplikácie vypnuté.", + "diagnosis_apps_broken": "Táto aplikácia je v katalógu aplikácií YunoHost momentálne označená ako rozbitá. Toto môže byť dočasný problém do momentu, kedy jej správcovia danú chybu neopravia. Kým sa tak stane sú aktualizácie tejto aplikácie vypnuté.", + "diagnosis_apps_deprecated_practices": "Táto verzia nainštalovanej aplikácie používa niektoré prehistorické a zastaralé zásady balíčkovania. Naozaj by ste mali zvážiť jej aktualizovanie.", + "diagnosis_apps_issue": "V aplikácií {app} sa našla chyba", + "diagnosis_apps_outdated_ynh_requirement": "Táto verzia nainštalovanej aplikácie vyžaduje yunohost iba vo verzii 2.x alebo 3.x, čo naznačuje, že neobsahuje aktuálne odporúčané zásady balíčkovania a pomocné skripty. Naozaj by ste mali zvážiť jej aktualizáciu.", + "diagnosis_basesystem_hardware": "Hardvérová architektúra servera je {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Model servera je {model}", + "diagnosis_basesystem_host": "Server beží na Debiane {debian_version}", + "diagnosis_basesystem_kernel": "Server beží na Linuxovom jadre {kernel_version}", + "diagnosis_basesystem_ynh_single_version": "verzia {package}: {version} ({repo})", + "diagnosis_cache_still_valid": "(Diagnostické údaje pre {category} sú stále platné. Nespúšťajte diagnostiku znovu!)", + "diagnosis_description_apps": "Aplikácie", + "diagnosis_description_basesystem": "Základný systém", + "diagnosis_description_dnsrecords": "DNS záznamy", + "diagnosis_description_ip": "Internetové pripojenie", + "diagnosis_description_mail": "E-mail", + "diagnosis_description_ports": "Otvorenie portov", + "diagnosis_description_regenconf": "Nastavenia systému", + "diagnosis_description_services": "Kontrola stavu služieb", + "diagnosis_description_systemresources": "Systémové prostriedky", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_ok": "Na úložisku {mountpoint} (na zariadení {device}) ostáva {free} ({free_percent} %) voľného miesta (z celkovej veľkosti {total})!", + "diagnosis_display_tip": "Pre zobrazenie nájdených problémov prejdite do časti Diagnostiky vo webovej administrácií alebo spustite 'yunohost diagnosis show --issues --human-readable' z rozhrania príkazového riadka.", + "diagnosis_dns_bad_conf": "Niektoré DNS záznamy chýbajú alebo nie sú platné pre doménu {domain} (kategória {category})", + "confirm_app_install_warning": "Upozornenie: Táto aplikácia môže fungovať, ale nie je dobre integrovaná s YunoHost. Niektoré funkcie ako spoločné prihlásenie (SSO) alebo zálohovanie/obnova nemusia byť dostupné. Nainštalovať aj napriek tomu? [{answers}] ", + "diagnosis_cant_run_because_of_dep": "Nie je možné spustiť diagnostiku pre {category}, kým existujú významné chyby súvisiace s {dep}.", + "diagnosis_diskusage_low": "Na úložisku {mountpoint} (na zariadení {device}) ostáva iba {free} ({free_percent} %) voľného miesta (z celkovej veľkosti {total}). Dávajte pozor.", + "diagnosis_diskusage_verylow": "Na úložisku {mountpoint} (na zariadení {device}) ostáva iba {free} ({free_percent} %) voľného miesta (z celkovej veľkosti {total}). Dobre zvážte vyčistenie úložiska!", + "diagnosis_apps_not_in_app_catalog": "Táto aplikácia sa nenachádza v katalógu aplikácií YunoHost. Ak sa tam v minulosti nachádzala a bola odstránená, mali by ste zvážiť jej odinštalovanie, pretože nebude dostávať žiadne aktualizácie a môže ohroziť integritu a bezpečnosť Vášho systému.", + "diagnosis_backports_in_sources_list": "Vyzerá, že apt (správca balíkov) je nastavený na používanie repozitára backports. Inštalovaním balíkov z backports môžete spôsobiť nestabilitu systému a vznik konfliktov, preto - ak naozaj neviete, čo robíte - Vás chceme pred ich používaním dôrazne vystríhať.", + "diagnosis_basesystem_ynh_inconsistent_versions": "Používate nekonzistentné verzie balíkov YunoHost… s najväčšou pravdepodobnosťou kvôli nedokončenej/chybnej aktualizácii.", + "diagnosis_basesystem_ynh_main_version": "Na serveri beží YunoHost {main_version} ({repo})", + "diagnosis_dns_discrepancy": "Nasledujúci DNS záznam nezodpovedá odporúčanej konfigurácii:
Typ:{type}
Názov:{name}
Aktuálna hodnota: {current}
Očakávaná hodnota: {value}", + "diagnosis_dns_good_conf": "DNS záznamy sú správne nastavené pre doménu {domain} (kategória {category})", + "diagnosis_dns_missing_record": "Podľa odporúčaného nastavenia DNS by ste mali pridať DNS záznam s nasledujúcimi informáciami.
Typ: {type}
Názov: {name}
Hodnota: {value}", + "diagnosis_dns_point_to_doc": "Prosím, pozrite si dokumentáciu na https://yunohost.org/dns_config, ak potrebujete pomôcť s nastavením DNS záznamov.", + "diagnosis_dns_specialusedomain": "Doména {domain} je založená na top-level doméne (TLD) pre zvláštne použitie ako napríklad .local alebo .test a preto sa neočakáva, že bude obsahovať vlastné DNS záznamy.", + "diagnosis_domain_expiration_error": "Platnosť niektorých domén expiruje VEĽMI SKORO!", + "diagnosis_domain_expiration_not_found": "Pri niektorých doménach nebolo možné skontrolovať dátum ich vypršania", + "diagnosis_domain_expiration_not_found_details": "WHOIS informácie pre doménu {domain} neobsahujú informáciu o dátume jej vypršania?", + "diagnosis_domain_expiration_success": "Vaše domény sú zaregistrované a tak skoro nevyprší ich platnosť.", + "diagnosis_domain_expiration_warning": "Niektoré z domén čoskoro vypršia!", + "diagnosis_domain_expires_in": "{domain} vyprší o {days} dní.", + "diagnosis_dns_try_dyndns_update_force": "Nastavenie DNS tejto domény by mala byť automaticky spravované YunoHost-om. Ak tomu tak nie je, môžete skúsiť vynútiť jej aktualizáciu pomocou príkazu yunohost dyndns update --force.", + "diagnosis_domain_not_found_details": "Doména {domain} neexistuje v databáze WHOIS alebo vypršala jej platnosť!", + "diagnosis_everything_ok": "V kategórii {category} vyzerá byť všetko v poriadku!", + "diagnosis_failed": "Nepodarilo sa získať výsledok diagnostiky pre kategóriu '{category}': {error}", + "diagnosis_failed_for_category": "Diagnostika pre kategóriu '{category}' skončila s chybou: {error}", + "diagnosis_found_errors": "Bolo nájdených {errors} závažných chýb týkajúcich sa {category}!", + "diagnosis_found_errors_and_warnings": "Bolo nájdených {errors} závažných chýb (a {warnings} varovaní) týkajúcich sa {category}!", + "diagnosis_found_warnings": "V kategórii {category} bolo nájdených {warnings} položiek, ktoré je možné opraviť.", + "diagnosis_http_connection_error": "Chyba pripojenia: nepodarilo sa pripojiť k požadovanej doméne, podľa všetkého je nedostupná.", + "diagnosis_http_could_not_diagnose": "Nepodarilo sa zistiť, či sú domény dostupné zvonka pomocou IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Chyba: {error}", + "diagnosis_http_hairpinning_issue": "Zdá sa, že Vaša miestna sieť nemá zapnutý NAT hairpinning.", + "diagnosis_high_number_auth_failures": "V poslednom čase bol zistený neobvykle vysoký počet neúspešných prihlásení. Uistite sa, či je služba fail2ban spustená a správne nastavená alebo použite vlastný port pre SSH ako je popísané na https://yunohost.org/security.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Pre opravu tohto problému preskúmajte rozdiely medzi konfiguráciami v termináli príkazom yunohost tools regen-conf nginx --dry-run --with-diff a ak so zmenami súhlasíte, aplikujte ich príkazom yunohost tools regen-conf nginx --force.", + "diagnosis_http_timeout": "Pri pokuse o kontaktovanie servera zvonku vypršal časový limit. Vyzerá byť nedostupný.
1. Najčastejšou príčinou tohto problému zvykne byť nesprávne nastavenie presmerovania portu 80 (a 443) na váš server.
2. Mali by ste skontrolovať, či je služba nginx spustená.
3. Pri komplexnejších inštaláciach: ubezpečte sa, že problém nie je spôsobený bránou firewall alebo reverznou proxy.", + "diagnosis_http_bad_status_code": "Zdá sa, že miesto vášho servera na vašu požiadavku zareagoval iný počítač (možno váš router).
1. Najčastejšou príčinou tohto problému zvykne byť nesprávne nastavenie presmerovania portu 80 (a 443) na váš server.
2. Pri komplexnejších inštaláciach: ubezpečte sa, že problém nie je spôsobený bránou firewall alebo reverznou proxy.", + "diagnosis_http_hairpinning_issue_details": "Toto pravdepodobne spôsobuje zariadenia od vášho poskytovateľa internetu / router. Vo výsledku pre používateľov mimo vašej miestnej siete (zvonku) funguje pripojenie na server normálne, to však neplatí pre používateľov v rámci miestnej siete (ako ste možno aj vy?) pri použití doménového mena alebo globálnej IP adresy. Túto situáciu sa vám možno podarí vyriešiť po prečítaní ", + "diagnosis_http_nginx_conf_not_up_to_date": "Nginx konfigurácia tejto domény sa zdá byť upravená ručne a znemožňuje YunoHost-u zistiť, či je dostupná na HTTP.", + "diagnosis_http_ok": "Doména {domain} je dostupná prostredníctvom HTTP mimo miestnej siete.", + "diagnosis_http_partially_unreachable": "Doména {domain} sa zdá byť nedostupná prostredníctvom HTTP mimo miestnej siete pri použití IPv{failed}, hoci funguje pri IPv{passed}.", + "diagnosis_http_special_use_tld": "Doména {domain} je založená na top-level doméne (TLD) pre zvláštne určenie ako je .local alebo .test a preto sa neočakáva, aby bola dostupná mimo miestnej siete.", + "diagnosis_http_unreachable": "Doména {domain} sa zdá byť nedostupná prostredníctvom HTTP mimo miestnej siete.", + "diagnosis_ignored_issues": "(+ {nb_ignored} ignorovaný(ch) problém(ov))", + "diagnosis_ip_no_ipv6_tip": "Váš server bude fungovať aj bez IPv6, no pre celkové zdravie internetu je lepšie ho nastaviť. V prípade, že je IPv6 dostupné, systém alebo váš poskytovateľ by ho mal automaticky nakonfigurovať. V opačnom prípade budete možno musieť nastaviť zopár vecí ručne tak, ako je vysvetlené v dokumentácii na https://yunohost.org/#/ipv6. Ak nemôžete povoliť IPv6 alebo je to na vás príliš technicky náročné, môžete pokojne toto upozornenie ignorovať.", + "diagnosis_ip_broken_dnsresolution": "Zdá sa, že z nejakého dôvodu nefunguje prekladanie názvov domén… Blokuje vaša brána firewall DNS požiadavky?", + "diagnosis_ip_broken_resolvconf": "Zdá sa, že na vašom serveri nefunguje prekladanie názvov domén, čo môže súvisieť s tým, že /etc/resolv.conf neukazuje na 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "Server nie je pripojený k internetu prostredníctvom IPv4!", + "diagnosis_ip_connected_ipv6": "Server nie je pripojený k internetu prostredníctvom IPv6!", + "diagnosis_ip_dnsresolution_working": "Preklad názvov domén nefunguje!", + "diagnosis_ip_global": "Globálna IP adresa: {global}", + "diagnosis_ip_local": "Miestna IP adresa: {local}", + "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.", + "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é", + "diagnosis_ram_verylow": "Systém má iba {available} ({available_percent} %) dostupnej pamäte RAM (z celkovej pamäte {total})!", + "diagnosis_mail_queue_unavailable_details": "Chyba: {error}", + "diagnosis_ram_ok": "Systém má ešte {available} ({available_percent} %) dostupnej pamäte RAM (z celkovej pamäte {total}).", + "diagnosis_ram_low": "Systém má {available} ({available_percent} %) dostupnej pamäte RAM (z celkovej pamäte {total}). Buďte opatrný.", + "diagnosis_sshd_config_inconsistent": "Zdá sa, že port SSH bol manuálne upravený v /etc/ssh/sshd_config. Od YunoHost 4.2 je dostupné nové globálne nastavenie 'security.ssh.port', aby ste nemuseli konfiguráciu editovať ručne.", + "domains_available": "Dostupné domény:", + "dyndns_could_not_check_available": "Nepodarilo sa zistiť, či je {domain} dostupná na {provider}.", + "dyndns_unavailable": "Doména '{domain}' nie je dostupná.", + "log_available_on_yunopaste": "Tento záznam je teraz dostupný na {url}", + "updating_apt_cache": "Získavam dostupné aktualizácie systémových balíčkov…", + "admins": "Správcovia", + "app_action_failed": "Nepodarilo sa spustiť akciu {action} v aplikácii {app}", + "app_manifest_install_ask_init_admin_permission": "Kto má mať prístup k nastaveniam určených správcovi tejto aplikácie? (Nastavenie môžete neskôr zmeniť)", + "ask_admin_fullname": "Celé meno správcu", + "ask_admin_username": "Používateľské meno správcu", + "ask_fullname": "Celé meno", + "all_users": "Všetci používatelia YunoHost", + "app_manifest_install_ask_init_main_permission": "Kto má mať prístup k tejto aplikácii? (Nastavenie môžete neskôr zmeniť)", + "certmanager_cert_install_failed": "Inštalácia Let's Encrypt certifikátu pre {domains} skončila s chybou" } diff --git a/locales/sv.json b/locales/sv.json index 39707d07c..6382612cd 100644 --- a/locales/sv.json +++ b/locales/sv.json @@ -3,9 +3,6 @@ "app_action_broke_system": "Åtgärden verkar ha fått följande viktiga tjänster att haverera: {services}", "already_up_to_date": "Ingenting att göra. Allt är redan uppdaterat.", "admin_password": "Administratörslösenord", - "admin_password_too_long": "Välj gärna ett lösenord som inte innehåller fler än 127 tecken", - "admin_password_change_failed": "Kan inte byta lösenord", "action_invalid": "Ej tillåten åtgärd '{action}'", - "admin_password_changed": "Administratörskontots lösenord ändrades", "aborting": "Avbryter." } \ No newline at end of file diff --git a/locales/te.json b/locales/te.json index fa6ac91c8..7a06f88ef 100644 --- a/locales/te.json +++ b/locales/te.json @@ -3,16 +3,42 @@ "action_invalid": "చెల్లని చర్య '{action}'", "additional_urls_already_removed": "'{permission}' అనుమతి కొరకు అదనపు URLలో అదనంగా URL '{url}' ఇప్పటికే జోడించబడింది", "admin_password": "అడ్మినిస్ట్రేషన్ పాస్వర్డ్", - "admin_password_changed": "అడ్మినిస్ట్రేషన్ పాస్వర్డ్ మార్చబడింది", "already_up_to_date": "చేయడానికి ఏమీ లేదు. ప్రతిదీ ఎప్పటికప్పుడు తాజాగా ఉంది.", "app_already_installed": "{app} ఇప్పటికే ఇన్స్టాల్ చేయబడింది", "app_already_up_to_date": "{app} ఇప్పటికే అప్-టూ-డేట్గా ఉంది", "app_argument_invalid": "ఆర్గ్యుమెంట్ '{name}' కొరకు చెల్లుబాటు అయ్యే వైల్యూ ఎంచుకోండి: {error}", "additional_urls_already_added": "'{permission}' అనుమతి కొరకు అదనపు URLలో అదనంగా URL '{url}' ఇప్పటికే జోడించబడింది", - "admin_password_change_failed": "అనుమతిపదాన్ని మార్చడం సాధ్యం కాదు", - "admin_password_too_long": "దయచేసి 127 క్యారెక్టర్ల కంటే చిన్న పాస్వర్డ్ ఎంచుకోండి", "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}'ని పార్సింగ్ చేసేటప్పుడు దోషం: భద్రతా కారణం కొరకు పాస్వర్డ్ ఆర్గ్యుమెంట్ డిఫాల్ట్ విలువను కలిగి ఉండరాదు" -} + "app_argument_choice_invalid": "ఆర్గ్యుమెంట్ '{name}' కొరకు చెల్లుబాటు అయ్యే వైల్యూ ఎంచుకోండి: '{value}' అనేది లభ్యం అవుతున్న ఎంపికల్లో ({choices}) లేదు", + "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": "కాన్ఫిగరేషన్ ప్యానెల్ విలువలను చదవడంలో విఫలమైంది." +} \ No newline at end of file diff --git a/locales/tr.json b/locales/tr.json index 6c881eec7..3ba829b95 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -1,3 +1,9 @@ { - "password_too_simple_1": "Şifre en az 8 karakter uzunluğunda olmalı" + "password_too_simple_1": "Şifre en az 8 karakter uzunluğunda olmalı", + "action_invalid": "Geçersiz işlem '{action}'", + "admin_password": "Yönetici şifresi", + "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." } \ No newline at end of file diff --git a/locales/uk.json b/locales/uk.json index ba6737d7a..2b168b65e 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -22,10 +22,7 @@ "app_action_broke_system": "Ця дія, схоже, порушила роботу наступних важливих служб: {services}", "app_action_cannot_be_ran_because_required_services_down": "Для виконання цієї дії повинні бути запущені наступні необхідні служби: {services}. Спробуйте перезапустити їх, щоб продовжити (і, можливо, з'ясувати, чому вони не працюють).", "already_up_to_date": "Нічого не потрібно робити. Все вже актуально.", - "admin_password_too_long": "Будь ласка, виберіть пароль коротше 127 символів", - "admin_password_changed": "Пароль адміністрації було змінено", - "admin_password_change_failed": "Неможливо змінити пароль", - "admin_password": "Пароль адміністрації", + "admin_password": "Пароль адмініструванні", "additional_urls_already_removed": "Додаткова URL-адреса '{url}' вже видалена в додатковій URL-адресі для дозволу '{permission}'", "additional_urls_already_added": "Додаткова URL-адреса '{url}' вже додана в додаткову URL-адресу для дозволу '{permission}'", "action_invalid": "Неприпустима дія '{action}'", @@ -66,7 +63,6 @@ "server_reboot": "Сервер буде перезавантажено", "server_shutdown_confirm": "Сервер буде негайно вимкнено, ви впевнені? [{answers}]", "server_shutdown": "Сервер буде вимкнено", - "root_password_replaced_by_admin_password": "Ваш кореневий (root) пароль було замінено на пароль адміністратора.", "root_password_desynchronized": "Пароль адміністратора було змінено, але YunoHost не зміг поширити це на кореневий (root) пароль!", "restore_system_part_failed": "Не вдалося відновити системний розділ '{part}'", "restore_running_hooks": "Запуск хуків відновлення…", @@ -126,8 +122,8 @@ "pattern_port_or_range": "Має бути припустимий номер порту (наприклад, 0-65535) або діапазон портів (наприклад, 100:200)", "pattern_password": "Має бути довжиною не менше 3 символів", "pattern_mailbox_quota": "Має бути розмір з суфіксом b/k/M/G/T або 0, щоб не мати квоти", - "pattern_lastname": "Має бути припустиме прізвище", - "pattern_firstname": "Має бути припустиме ім'я", + "pattern_lastname": "Має бути припустиме прізвище (принаймні 3 символи)", + "pattern_firstname": "Має бути припустиме ім'я (принаймні 3 символи)", "pattern_email": "Має бути припустима адреса е-пошти, без символу '+' (наприклад, someone@example.com)", "pattern_email_forward": "Має бути припустима адреса е-пошти, символ '+' приймається (наприклад, someone+tag@example.com)", "pattern_domain": "Має бути припустиме доменне ім'я (наприклад, my-domain.org)", @@ -140,7 +136,7 @@ "operation_interrupted": "Операція була вручну перервана?", "invalid_number": "Має бути числом", "not_enough_disk_space": "Недостатньо вільного місця на '{path}'", - "migrations_to_be_ran_manually": "Міграція {id} повинна бути запущена вручну. Будь ласка, перейдіть в розділ Засоби → Міграції на сторінці вебадміністрації або виконайте команду `yunohost tools migrations run`.", + "migrations_to_be_ran_manually": "Міграція {id} повинна бути запущена вручну. Будь ласка, перейдіть в розділ Засоби → Міграції на сторінці вебадмініструванні або виконайте команду `yunohost tools migrations run`.", "migrations_success_forward": "Міграцію {id} завершено", "migrations_skip_migration": "Пропускання міграції {id}...", "migrations_running_forward": "Виконання міграції {id}...", @@ -163,7 +159,7 @@ "migration_ldap_backup_before_migration": "Створення резервної копії бази даних LDAP і налаштування застосунків перед фактичною міграцією.", "main_domain_changed": "Основний домен було змінено", "main_domain_change_failed": "Неможливо змінити основний домен", - "mail_unavailable": "Ця е-пошта зарезервована і буде автоматично виділена найпершому користувачеві", + "mail_unavailable": "Ця адреса електронної пошти зарезервована для групи адміністраторів", "mailbox_used_space_dovecot_down": "Поштова служба Dovecot повинна бути запущена, якщо ви хочете отримати використане місце в поштовій скриньці", "mailbox_disabled": "Е-пошта вимкнена для користувача {user}", "mail_forward_remove_failed": "Не вдалося видалити переадресацію електронної пошти '{mail}'", @@ -184,7 +180,7 @@ "log_user_delete": "Видалення користувача '{}'", "log_user_create": "Додавання користувача '{}'", "log_regen_conf": "Перестворення системних конфігурацій '{}'", - "log_letsencrypt_cert_renew": "Оновлення сертифікату Let's Encrypt на домені '{}'", + "log_letsencrypt_cert_renew": "Поновлення сертифікату Let's Encrypt на домені '{}'", "log_selfsigned_cert_install": "Установлення самопідписаного сертифікату на домені '{}'", "log_permission_url": "Оновлення URL, пов'язаногл з дозволом '{}'", "log_permission_delete": "Видалення дозволу '{}'", @@ -239,39 +235,16 @@ "group_already_exist_on_system": "Група {group} вже існує в групах системи", "group_already_exist": "Група {group} вже існує", "good_practices_about_user_password": "Зараз ви збираєтеся поставити новий пароль користувача. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", - "good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адміністрації. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольного гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", - "global_settings_unknown_type": "Несподівана ситуація, налаштування {setting} має тип {unknown_type}, але це не тип, підтримуваний системою.", - "global_settings_setting_backup_compress_tar_archives": "При створенні нових резервних копій стискати архіви (.tar.gz) замість нестислих архівів (.tar). NB: вмикання цієї опції означає створення легших архівів резервних копій, але початкова процедура резервного копіювання буде значно довшою і важчою для CPU.", - "global_settings_setting_security_webadmin_allowlist": "IP-адреси, яким дозволений доступ до вебадміністрації. Через кому.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Дозволити доступ до вебадміністрації тільки деяким IP-адресам.", - "global_settings_setting_smtp_relay_password": "Пароль хоста SMTP-ретрансляції", - "global_settings_setting_smtp_relay_user": "Обліковий запис користувача SMTP-ретрансляції", + "good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адмініструванні. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольного гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", + "global_settings_setting_smtp_relay_password": "Пароль SMTP-ретрансляції", + "global_settings_setting_smtp_relay_user": "Користувач SMTP-ретрансляції", "global_settings_setting_smtp_relay_port": "Порт SMTP-ретрансляції", - "global_settings_setting_smtp_relay_host": "Хост SMTP-ретрансляції, який буде використовуватися для надсилання е-пошти замість цього зразка Yunohost. Корисно, якщо ви знаходитеся в одній із цих ситуацій: ваш 25 порт заблокований вашим провайдером або VPS провайдером, у вас є житловий IP в списку DUHL, ви не можете налаштувати зворотний DNS або цей сервер не доступний безпосередньо в Інтернеті і ви хочете використовувати інший сервер для відправки електронних листів.", - "global_settings_setting_smtp_allow_ipv6": "Дозволити використання IPv6 для отримання і надсилання листів е-пошти", - "global_settings_setting_ssowat_panel_overlay_enabled": "Увімкнути накладення панелі SSOwat", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Дозволити використання (застарілого) ключа DSA для конфігурації демона SSH", - "global_settings_unknown_setting_from_settings_file": "Невідомий ключ в налаштуваннях: '{setting_key}', відхиліть його і збережіть у /etc/yunohost/settings-unknown.json", - "global_settings_setting_security_ssh_port": "SSH-порт", - "global_settings_setting_security_postfix_compatibility": "Компроміс між сумісністю і безпекою для сервера Postfix. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", - "global_settings_setting_security_ssh_compatibility": "Компроміс між сумісністю і безпекою для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", - "global_settings_setting_security_password_user_strength": "Надійність пароля користувача", - "global_settings_setting_security_password_admin_strength": "Надійність пароля адміністратора", - "global_settings_setting_security_nginx_compatibility": "Компроміс між сумісністю і безпекою для вебсервера NGINX. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", - "global_settings_setting_pop3_enabled": "Увімкніть протокол POP3 для поштового сервера", - "global_settings_reset_success": "Попередні налаштування тепер збережені в {path}", - "global_settings_key_doesnt_exists": "Ключ '{settings_key}' не існує в глобальних налаштуваннях, ви можете побачити всі доступні ключі, виконавши команду 'yunohost settings list'", - "global_settings_cant_write_settings": "Неможливо зберегти файл налаштувань, причина: {reason}", - "global_settings_cant_serialize_settings": "Не вдалося серіалізувати дані налаштувань, причина: {reason}", - "global_settings_cant_open_settings": "Не вдалося відкрити файл налаштувань, причина: {reason}", - "global_settings_bad_type_for_setting": "Поганий тип для налаштування {setting}, отримано {received_type}, а очікується {expected_type}", - "global_settings_bad_choice_for_enum": "Поганий вибір для налаштування {setting}, отримано '{choice}', але доступні наступні варіанти: {available_choices}", + "global_settings_setting_ssowat_panel_overlay_enabled": "Увімкнути невеликий ярлик порталу YunoHost у застосунках", "firewall_rules_cmd_failed": "Деякі команди правил фаєрвола не спрацювали. Подробиці в журналі.", "firewall_reloaded": "Фаєрвол перезавантажено", "firewall_reload_failed": "Не вдалося перезавантажити фаєрвол", "file_does_not_exist": "Файл {path} не існує.", "field_invalid": "Неприпустиме поле '{}'", - "experimental_feature": "Попередження: Ця функція є експериментальною і не вважається стабільною, ви не повинні використовувати її, якщо не знаєте, що робите.", "extracting": "Витягнення...", "dyndns_unavailable": "Домен '{domain}' недоступний.", "dyndns_domain_not_provided": "DynDNS провайдер {provider} не може надати домен {domain}.", @@ -285,7 +258,7 @@ "dyndns_ip_update_failed": "Не вдалося оновити IP-адресу в DynDNS", "dyndns_could_not_check_available": "Не вдалося перевірити, чи {domain} доступний у {provider}.", "dpkg_lock_not_available": "Ця команда не може бути виконана прямо зараз, тому що інша програма, схоже, використовує блокування dpkg (системного менеджера пакетів)", - "dpkg_is_broken": "Ви не можете зробити це прямо зараз, тому що dpkg/APT (системні менеджери пакетів), схоже, знаходяться в зламаному стані... Ви можете спробувати вирішити цю проблему, під'єднавшись через SSH і виконавши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`.", + "dpkg_is_broken": "Ви не можете зробити це прямо зараз, тому що dpkg/APT (системні менеджери пакетів), схоже, знаходяться в зламаному стані... Ви можете спробувати вирішити цю проблему, під'єднавшись через SSH і виконавши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`та/або `sudo dpkg --audit`.", "downloading": "Завантаження…", "done": "Готово", "domains_available": "Доступні домени:", @@ -306,11 +279,11 @@ "domain_cannot_remove_main": "Ви не можете вилучити '{domain}', бо це основний домен, спочатку вам потрібно встановити інший домен в якості основного за допомогою 'yunohost domain main-domain -n '; ось список доменів-кандидатів: {other_domains}", "disk_space_not_sufficient_update": "Недостатньо місця на диску для оновлення цього застосунку", "disk_space_not_sufficient_install": "Недостатньо місця на диску для встановлення цього застосунку", - "diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте команду yunohost settings set security.ssh.port -v YOUR_SSH_PORT, щоб визначити порт SSH, і перевіртеyunohost tools regen-conf ssh --dry-run --with-diff і yunohost tools regen-conf ssh --force, щоб скинути ваш конфіг на рекомендований YunoHost.", - "diagnosis_sshd_config_inconsistent": "Схоже, що порт SSH був уручну змінений в /etc/ssh/sshd_config. Починаючи з версії YunoHost 4.2, доступний новий глобальний параметр 'security.ssh.port', що дозволяє уникнути ручного редагування конфігурації.", + "diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте команду yunohost settings set security.ssh.ssh port -v YOUR_SSH_PORT, щоб визначити порт SSH, і перевіртеyunohost tools regen-conf ssh --dry-run --with-diff і yunohost tools regen-conf ssh --force, щоб скинути ваш конфіг на рекомендований YunoHost.", + "diagnosis_sshd_config_inconsistent": "Схоже, що порт SSH був уручну змінений в /etc/ssh/sshd_config. Починаючи з версії YunoHost 4.2, доступний новий глобальний параметр 'security.ssh.ssh port', що дозволяє уникнути ручного редагування конфігурації.", "diagnosis_sshd_config_insecure": "Схоже, що конфігурація SSH була змінена вручну і є небезпечною, оскільки не містить директив 'AllowGroups' або 'AllowUsers' для обмеження доступу авторизованих користувачів.", "diagnosis_processes_killed_by_oom_reaper": "Деякі процеси було недавно вбито системою через брак пам'яті. Зазвичай це є симптомом нестачі пам'яті в системі або процесу, який з'їв дуже багато пам'яті. Зведення убитих процесів:\n{kills_summary}", - "diagnosis_never_ran_yet": "Схоже, що цей сервер був налаштований недавно, і поки немає звіту про діагностику. Вам слід почати з повної діагностики, або з вебадміністрації, або використовуючи 'yunohost diagnosis run' з командного рядка.", + "diagnosis_never_ran_yet": "Схоже, що цей сервер був налаштований недавно, і поки немає звіту про діагностику. Вам слід почати з повної діагностики, або з вебадмініструванні, або використовуючи 'yunohost diagnosis run' з командного рядка.", "diagnosis_unknown_categories": "Наступні категорії невідомі: {categories}", "diagnosis_http_nginx_conf_not_up_to_date_details": "Щоб виправити становище, перевірте різницю за допомогою командного рядка, використовуючи yunohost tools regen-conf nginx --dry-run --with-diff, і якщо все в порядку, застосуйте зміни за допомогою команди yunohost tools regen-conf nginx --force.", "diagnosis_http_nginx_conf_not_up_to_date": "Схоже, що конфігурація nginx цього домену була змінена вручну, що не дозволяє YunoHost визначити, чи доступний він по HTTP.", @@ -356,7 +329,7 @@ "diagnosis_mail_blacklist_ok": "IP-адреси і домени, які використовуються цим сервером, не внесені в чорний список", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Поточний зворотний DNS:{rdns_domain}
Очікуване значення: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Зворотний DNS неправильно налаштований в IPv{ipversion}. Деякі електронні листи можуть бути не доставлені або можуть бути відзначені як спам.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ваш зворотний DNS правильно налаштований для IPv4, ви можете спробувати вимкнути використання IPv6 при надсиланні листів, виконавши команду yunohost settings set smtp.allow_ipv6 -v off. Примітка: останнє рішення означає, що ви не зможете надсилати або отримувати електронні листи з нечисленних серверів, що використовують тільки IPv6.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ваш зворотний DNS правильно налаштований для IPv4, ви можете спробувати вимкнути використання IPv6 при надсиланні листів, виконавши команду yunohost settings set \nemail.smtp.smtp allow_ipv6 -v off. Примітка: останнє рішення означає, що ви не зможете надсилати або отримувати електронні листи з нечисленних серверів, що використовують тільки IPv6.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ви відчуваєте проблеми через це, розгляньте наступні рішення:
- Деякі провайдери надають альтернативу використання ретранслятора поштового сервера, хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN *з виділеним загальнодоступним IP* для обходу подібних обмежень. Дивіться https://yunohost.org/#/vpn_advantage
- Або можна переключитися на іншого провайдера", "diagnosis_mail_fcrdns_nok_details": "Спочатку спробуйте налаштувати зворотний DNS з {ehlo_domain} в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм запит у підтримку для цього).", "diagnosis_mail_fcrdns_dns_missing": "У IPv{ipversion} не визначений зворотний DNS. Деякі листи можуть не доставлятися або позначатися як спам.", @@ -374,7 +347,7 @@ "diagnosis_mail_outgoing_port_25_blocked_details": "Спочатку спробуйте розблокувати вихідний порт 25 в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм заявку в службу підтримки).", "diagnosis_mail_outgoing_port_25_blocked": "Поштовий сервер SMTP не може відправляти електронні листи на інші сервери, оскільки вихідний порт 25 заблоковано в IPv{ipversion}.", "app_manifest_install_ask_path": "Оберіть шлях URL (після домену), за яким має бути встановлено цей застосунок", - "yunohost_postinstall_end_tip": "Післявстановлення завершено! Щоб завершити доналаштування, будь ласка, розгляньте наступні варіанти:\n - додавання першого користувача через розділ 'Користувачі' вебадміністрації (або 'yunohost user create ' в командному рядку);\n - діагностика можливих проблем через розділ 'Діагностика' вебадміністрації (або 'yunohost diagnosis run' в командному рядку);\n - прочитання розділів 'Завершення встановлення' і 'Знайомство з YunoHost' у документації адміністратора: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "Післявстановлення завершено! Щоб завершити доналаштування, будь ласка, розгляньте наступні варіанти:\n - додавання першого користувача через розділ 'Користувачі' вебадмініструванні (або 'yunohost user create ' в командному рядку);\n - діагностика можливих проблем через розділ 'Діагностика' вебадмініструванні (або 'yunohost diagnosis run' в командному рядку);\n - прочитання розділів 'Завершення встановлення' і 'Знайомство з YunoHost' у документації адміністратора: https://yunohost.org/admindoc.", "yunohost_not_installed": "YunoHost установлений неправильно. Будь ласка, запустіть 'yunohost tools postinstall'", "yunohost_installing": "Установлення YunoHost...", "yunohost_configured": "YunoHost вже налаштовано", @@ -424,7 +397,7 @@ "diagnosis_diskusage_ok": "У сховищі {mountpoint} (на пристрої {device}) залишилося {free} ({free_percent}%) вільного місця (з {total})!", "diagnosis_diskusage_low": "Сховище {mountpoint} (на пристрої {device}) має тільки {free} ({free_percent}%) вільного місця (з {total}). Будьте уважні.", "diagnosis_diskusage_verylow": "Сховище {mountpoint} (на пристрої {device}) має тільки {free} ({free_percent}%) вільного місця (з {total}). Вам дійсно варто подумати про очищення простору!", - "diagnosis_services_bad_status_tip": "Ви можете спробувати перезапустити службу, а якщо це не допоможе, подивіться журнали служби в вебадміністрації (з командного рядка це можна зробити за допомогою yunohost service restart {service} і yunohost service log {service}).", + "diagnosis_services_bad_status_tip": "Ви можете спробувати перезапустити службу, а якщо це не допоможе, подивіться журнали служби в вебадмініструванні (з командного рядка це можна зробити за допомогою yunohost service restart {service} і yunohost service log {service}).", "diagnosis_services_bad_status": "Служба {service} у стані {status} :(", "diagnosis_services_conf_broken": "Для служби {service} порушена конфігурація!", "diagnosis_services_running": "Службу {service} запущено!", @@ -465,7 +438,7 @@ "diagnosis_cant_run_because_of_dep": "Неможливо запустити діагностику для {category}, поки є важливі проблеми, пов'язані з {dep}.", "diagnosis_cache_still_valid": "(Кеш все ще дійсний для діагностики {category}. Повторна діагностика поки не проводиться!)", "diagnosis_failed_for_category": "Не вдалося провести діагностику для категорії '{category}': {error}", - "diagnosis_display_tip": "Щоб побачити знайдені проблеми, ви можете перейти в розділ Діагностика в вебадміністрації або виконати команду 'yunohost diagnosis show --issues --human-readable' з командного рядка.", + "diagnosis_display_tip": "Щоб побачити знайдені проблеми, ви можете перейти в розділ Діагностика в вебадмініструванні або виконати команду 'yunohost diagnosis show --issues --human-readable' з командного рядка.", "diagnosis_package_installed_from_sury_details": "Деякі пакети були ненавмисно встановлені зі стороннього репозиторію під назвою Sury. Команда YunoHost поліпшила стратегію роботи з цими пакетами, але очікується, що в деяких системах, які встановили застосунки PHP7.3 ще на Stretch, залишаться деякі невідповідності. Щоб виправити це становище, спробуйте виконати наступну команду: {cmd_to_fix}", "diagnosis_package_installed_from_sury": "Деякі системні пакети мають бути зістарені у версії", "diagnosis_backports_in_sources_list": "Схоже, що apt (менеджер пакетів) налаштований на використання репозиторія backports. Якщо ви не знаєте, що робите, ми наполегливо не радимо встановлювати пакети з backports, тому що це може привести до нестабільності або конфліктів у вашій системі.", @@ -533,7 +506,7 @@ "backup_archive_writing_error": "Не вдалося додати файли '{source}' (названі в архіві '{dest}') для резервного копіювання в стислий архів '{archive}'", "backup_archive_system_part_not_available": "Системна частина '{part}' недоступна в цій резервній копії", "backup_archive_corrupted": "Схоже, що архів резервної копії '{archive}' пошкоджений: {error}", - "backup_archive_cant_retrieve_info_json": "Не вдалося завантажити відомості для архіву '{archive}'... info.json не може бути отриманий(або не є правильним json).", + "backup_archive_cant_retrieve_info_json": "Не вдалося завантажити відомості для архіву '{archive}'... Файл info.json не може бути отриманий (або не є правильним json).", "backup_archive_open_failed": "Не вдалося відкрити архів резервної копії", "backup_archive_name_unknown": "Невідомий локальний архів резервного копіювання з назвою '{name}'", "backup_archive_name_exists": "Архів резервного копіювання з такою назвою вже існує.", @@ -548,10 +521,8 @@ "ask_password": "Пароль", "ask_new_path": "Новий шлях", "ask_new_domain": "Новий домен", - "ask_new_admin_password": "Новий пароль адміністрації", + "ask_new_admin_password": "Новий пароль адмініструванні", "ask_main_domain": "Основний домен", - "ask_lastname": "Прізвище", - "ask_firstname": "Ім'я", "ask_user_domain": "Домен для адреси е-пошти користувача і облікового запису XMPP", "apps_catalog_update_success": "Каталог застосунків був оновлений!", "apps_catalog_obsolete_cache": "Кеш каталогу застосунків порожній або застарів.", @@ -576,8 +547,7 @@ "app_restore_script_failed": "Сталася помилка всередині скрипта відновлення застосунку", "app_restore_failed": "Не вдалося відновити {app}: {error}", "app_remove_after_failed_install": "Вилучення застосунку після збою встановлення...", - "app_requirements_unmeet": "Вимоги не виконані для {app}, пакет {pkgname} ({version}) повинен бути {spec}", - "app_requirements_checking": "Перевіряння необхідних пакетів для {app}...", + "app_requirements_checking": "Перевіряння необхідних пакунків для {app}...", "app_removed": "{app} видалено", "app_not_properly_removed": "{app} не було видалено належним чином", "app_not_installed": "Не вдалося знайти {app} в списку встановлених застосунків: {all_apps}", @@ -585,7 +555,7 @@ "app_not_upgraded": "Застосунок '{failed_app}' не вдалося оновити, і, як наслідок, оновлення таких застосунків було скасовано: {apps}", "app_manifest_install_ask_is_public": "Чи має цей застосунок бути відкритим для анонімних відвідувачів?", "app_manifest_install_ask_admin": "Виберіть користувача-адміністратора для цього застосунку", - "app_manifest_install_ask_password": "Виберіть пароль адміністрації для цього застосунку", + "app_manifest_install_ask_password": "Виберіть пароль адмініструванні для цього застосунку", "diagnosis_description_apps": "Застосунки", "user_import_success": "Користувачів успішно імпортовано", "user_import_nothing_to_do": "Не потрібно імпортувати жодного користувача", @@ -594,20 +564,17 @@ "user_import_missing_columns": "Відсутні такі стовпці: {columns}", "user_import_bad_file": "Ваш файл CSV неправильно відформатовано, він буде знехтуваний, щоб уникнути потенційної втрати даних", "user_import_bad_line": "Неправильний рядок {line}: {details}", - "invalid_password": "Недійсний пароль", "log_user_import": "Імпорт користувачів", "ldap_server_is_down_restart_it": "Службу LDAP вимкнено, спробуйте перезапустити її...", "ldap_server_down": "Не вдається під'єднатися до сервера LDAP", - "global_settings_setting_security_experimental_enabled": "Увімкнути експериментальні функції безпеки (не вмикайте це, якщо ви не знаєте, що робите!)", - "diagnosis_apps_deprecated_practices": "Установлена версія цього застосунку все ще використовує деякі надзастарілі практики упакування. Вам дійсно варто подумати про його оновлення.", - "diagnosis_apps_outdated_ynh_requirement": "Установлена версія цього застосунку вимагає лише Yunohost >= 2.x, що, як правило, вказує на те, що воно не відповідає сучасним рекомендаційним практикам упакування та порадникам. Вам дійсно варто подумати про його оновлення.", + "diagnosis_apps_deprecated_practices": "Установлена версія цього застосунку все ще використовує деякі надто застарілі практики упакування. Вам дійсно варто подумати про його оновлення.", + "diagnosis_apps_outdated_ynh_requirement": "Установлена версія цього застосунку вимагає лише Yunohost >= 2.x чи 3.х, що, як правило, вказує на те, що воно не відповідає сучасним рекомендаційним практикам упакування та порадникам. Вам дійсно варто подумати про його оновлення.", "diagnosis_apps_bad_quality": "Цей застосунок наразі позначено як зламаний у каталозі застосунків YunoHost. Це може бути тимчасовою проблемою, поки організатори намагаються вирішити цю проблему. Тим часом оновлення цього застосунку вимкнено.", "diagnosis_apps_broken": "Цей застосунок наразі позначено як зламаний у каталозі застосунків YunoHost. Це може бути тимчасовою проблемою, поки організатори намагаються вирішити цю проблему. Тим часом оновлення цього застосунку вимкнено.", "diagnosis_apps_not_in_app_catalog": "Цей застосунок не міститься у каталозі застосунків YunoHost. Якщо він був у минулому і був видалений, вам слід подумати про видалення цього застосунку, оскільки він не отримає оновлення, і це може поставити під загрозу цілісність та безпеку вашої системи.", "diagnosis_apps_issue": "Виявлено проблему із застосунком {app}", "diagnosis_apps_allgood": "Усі встановлені застосунки дотримуються основних способів упакування", "diagnosis_high_number_auth_failures": "Останнім часом сталася підозріло велика кількість помилок автентифікації. Ви можете переконатися, що fail2ban працює і правильно налаштований, або скористатися власним портом для SSH, як описано в https://yunohost.org/security.", - "global_settings_setting_security_nginx_redirect_to_https": "Типово переспрямовувати HTTP-запити до HTTP (НЕ ВИМИКАЙТЕ, якщо ви дійсно не знаєте, що робите!)", "app_config_unable_to_apply": "Не вдалося застосувати значення панелі конфігурації.", "app_config_unable_to_read": "Не вдалося розпізнати значення панелі конфігурації.", "config_apply_failed": "Не вдалося застосувати нову конфігурацію: {error}", @@ -665,7 +632,6 @@ "migration_0021_patching_sources_list": "Виправлення sources.lists...", "migration_0021_main_upgrade": "Початок основного оновлення...", "migration_0021_yunohost_upgrade": "Початок оновлення ядра YunoHost...", - "migration_0021_not_buster": "Поточний дистрибутив Debian не є Buster!", "migration_0021_problematic_apps_warning": "Зверніть увагу, що були виявлені наступні, ймовірно проблемні встановлені застосунки. Схоже, що вони не були встановлені з каталогу застосунків YunoHost або не зазначені як «робочі». Отже, не можна гарантувати, що вони будуть працювати після оновлення: {problematic_apps}", "migration_0021_modified_files": "Зверніть увагу, що такі файли були змінені вручну і можуть бути перезаписані після оновлення: {manually_modified_files}", "migration_0021_cleaning_up": "Очищення кеш-пам'яті і пакетів, які більше не потрібні...", @@ -675,7 +641,6 @@ "migration_0021_system_not_fully_up_to_date": "Ваша система не повністю оновлена. Будь ласка, виконайте регулярне оновлення перед запуском міграції на Bullseye.", "migration_0021_general_warning": "Будь ласка, зверніть увагу, що ця міграція є делікатною операцією. Команда YunoHost зробила все можливе, щоб перевірити і протестувати її, але міграція все ще може порушити частину системи або її застосунків.\n\nТому рекомендовано:\n - Виконати резервне копіювання всіх важливих даних або застосунків. Подробиці на сайті https://yunohost.org/backup; \n - Наберіться терпіння після запуску міграції: В залежності від вашого з'єднання з Інтернетом і апаратного забезпечення, оновлення може зайняти до декількох годин.", "migration_description_0021_migrate_to_bullseye": "Оновлення системи до Debian Bullseye і YunoHost 11.x", - "global_settings_setting_security_ssh_password_authentication": "Дозволити автентифікацію паролем для SSH", "service_description_postgresql": "Зберігає дані застосунків (база даних SQL)", "domain_config_default_app": "Типовий застосунок", "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 встановлено, але не PostgreSQL 13!? У вашій системі могло статися щось неприємне :(...", @@ -684,5 +649,94 @@ "tools_upgrade_failed": "Не вдалося оновити наступні пакети: {packages_list}", "migration_0023_not_enough_space": "Звільніть достатньо місця в {path} для виконання міграції.", "migration_0023_postgresql_11_not_installed": "PostgreSQL не було встановлено у вашій системі. Нічого робити.", - "migration_description_0022_php73_to_php74_pools": "Перенесення конфігураційних файлів php7.3-fpm 'pool' на php7.4" -} \ No newline at end of file + "migration_description_0022_php73_to_php74_pools": "Перенесення конфігураційних файлів php7.3-fpm 'pool' на php7.4", + "global_settings_setting_backup_compress_tar_archives_help": "При створенні нових резервних копій стискати архіви (.tar.gz) замість нестислих архівів (.tar). NB: вмикання цієї опції означає створення легших архівів резервних копій, але початкова процедура резервного копіювання буде значно довшою і важчою для CPU.", + "global_settings_setting_security_experimental_enabled_help": "Увімкнути експериментальні функції безпеки (не вмикайте це, якщо ви не знаєте, що робите!)", + "global_settings_setting_nginx_compatibility_help": "Компроміс між сумісністю і безпекою для вебсервера NGINX. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", + "global_settings_setting_nginx_redirect_to_https_help": "Типово переспрямовувати HTTP-запити до HTTP (НЕ ВИМИКАЙТЕ, якщо ви дійсно не знаєте, що робите!)", + "global_settings_setting_admin_strength": "Надійність пароля адміністратора", + "global_settings_setting_user_strength": "Надійність пароля користувача", + "global_settings_setting_postfix_compatibility_help": "Компроміс між сумісністю і безпекою для сервера Postfix. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", + "global_settings_setting_ssh_compatibility_help": "Компроміс між сумісністю і безпекою для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", + "global_settings_setting_ssh_password_authentication_help": "Дозволити автентифікацію паролем для SSH", + "global_settings_setting_ssh_port": "SSH-порт", + "global_settings_setting_webadmin_allowlist_help": "IP-адреси, яким дозволений доступ до вебадмініструванні. Через кому.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Дозволити доступ до вебадмініструванні тільки деяким IP-адресам.", + "global_settings_setting_smtp_allow_ipv6_help": "Дозволити використання IPv6 для отримання і надсилання листів е-пошти", + "global_settings_setting_smtp_relay_enabled_help": "Хост SMTP-ретрансляції, який буде використовуватися для надсилання е-пошти замість цього зразка Yunohost. Корисно, якщо ви знаходитеся в одній із цих ситуацій: ваш 25 порт заблокований вашим провайдером або VPS провайдером, у вас є житловий IP в списку DUHL, ви не можете налаштувати зворотний DNS або цей сервер не доступний безпосередньо в Інтернеті і ви хочете використовувати інший сервер для відправки електронних листів.", + "migration_0024_rebuild_python_venv_disclaimer_base": "Після оновлення до Debian Bullseye деякі застосунки Python потрібно частково перебудувати, щоб їх було перетворено на нову версію Python, яка постачається в Debian (з технічної точки зору: те, що називається «virtualenv», потрібно створити заново). Тим часом ці застосунки Python можуть не працювати. YunoHost може спробувати перебудувати virtualenv для деяких із них, як описано нижче. Для інших застосунків або якщо спроба відновлення не вдається, вам потрібно буде вручну примусово оновити їх.", + "migration_0024_rebuild_python_venv_broken_app": "Пропущено {app}, бо virtualenv не можна легко перебудувати для цього застосунку. Натомість вам слід виправити ситуацію, примусово оновивши застосунок за допомогою `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Буде зроблена спроба перебудувати virtualenv для таких застосунків (Примітка: операція може зайняти деякий час!): {rebuild_apps}", + "migration_0024_rebuild_python_venv_in_progress": "Намагаємося перебудувати Python virtualenv для `{app}`", + "migration_description_0024_rebuild_python_venv": "Відновлення застосунку Python після міграції до bullseye", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs не можна автоматично перебудувати для цих застосунків. Вам потрібно примусово оновити його для них, що можна зробити з командного рядка за допомогою: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_0024_rebuild_python_venv_failed": "Не вдалося перебудувати Python virtualenv для {app}. Застосунок може не працювати, доки це не вирішено. Ви повинні виправити ситуацію, примусово оновивши його за допомогою `yunohost app upgrade --force {app}`.", + "admins": "Адміністратори", + "all_users": "Усі користувачі Yunohost", + "app_manifest_install_ask_init_admin_permission": "Хто повинен мати доступ до функцій адміністратора для цього застосунку? (Пізніше це можна змінити)", + "certmanager_cert_install_failed": "Не вдалося встановити сертифікат Let's Encrypt для {domains}", + "config_forbidden_readonly_type": "Тип '{type}' не може бути встановлений як readonly, використовуйте інший тип для показу цього значення (відповідний arg id: '{id}').", + "diagnosis_using_stable_codename": "apt (системний менеджер пакунків) наразі налаштовано на встановлення пакунків з кодовою назвою \"stable\", замість кодової назви поточної версії Debian (bullseye).", + "diagnosis_using_stable_codename_details": "Зазвичай це спричинено неправильним налаштуванням від вашого хостинг-провайдера. Це небезпечно, оскільки як тільки наступна версія Debian стане новою \"стабільною\", apt захоче оновити всі системні пакунки без проходження належної процедури міграції. Радимо виправити це, відредагувавши джерело apt для базового репозиторію Debian, і замінити ключове слово stable на bullseye. Відповідний конфігураційний файл має бути в /etc/apt/sources.list, або файл у /etc/apt/sources.list.d/.", + "app_action_failed": "Не вдалося запустити дію {action} для застосунку {app}", + "app_manifest_install_ask_init_main_permission": "Хто повинен мати доступ до цього застосунку? (Пізніше це можна змінити)", + "ask_admin_fullname": "Повне ім'я адміністратора", + "ask_admin_username": "Ім'я користувача адміністратора", + "ask_fullname": "Повне ім'я", + "certmanager_cert_install_failed_selfsigned": "Не вдалося встановити самопідписаний сертифікат для {domains}", + "certmanager_cert_renew_failed": "Помилка оновлення сертифіката Let's Encrypt для {domains}", + "config_action_disabled": "Не вдалося запустити дію '{action}', оскільки вона вимкнена, переконайтеся, що виконані її обмеження. довідка: {help}", + "config_action_failed": "Не вдалося запустити дію '{action}': {error}", + "global_settings_setting_pop3_enabled": "Увімкнути POP3", + "domain_config_acme_eligible_explain": "Здається, цей домен не готовий для сертифіката Let's Encrypt. Будь ласка, перевірте конфігурацію DNS і доступність HTTP-сервера. Розділи \"DNS-записи\" та \"Веб\" на сторінці діагностики можуть допомогти вам зрозуміти, що саме налаштовано неправильно.", + "domain_config_cert_summary_letsencrypt": "Чудово! Ви використовуєте дійсний сертифікат Let's Encrypt!", + "domain_config_cert_summary_selfsigned": "ПОПЕРЕДЖЕННЯ: поточний сертифікат є самопідписаним. Браузери відображатимуть моторошне попередження новим відвідувачам!", + "global_settings_reset_success": "Скинути глобальні налаштування", + "global_settings_setting_admin_strength_help": "Ці вимоги застосовуються лише під час ініціалізації або зміни пароля", + "log_resource_snippet": "Надання/вилучення/оновлення ресурсу", + "global_settings_setting_security_experimental_enabled": "Експериментальні безпекові можливості", + "diagnosis_using_yunohost_testing": "apt (менеджер пакунків системи) наразі налаштований на встановлення будь-якого \"тестового\" оновлення для ядра YunoHost.", + "diagnosis_using_yunohost_testing_details": "Це, ймовірно, нормально, якщо ви знаєте, що робите, але зверніть увагу на примітки до випуску, перш ніж встановлювати оновлення YunoHost! Якщо ви хочете вимкнути 'тестування' оновлень, вам слід видалити ключове слово testing з /etc/apt/sources.list.d/yunohost.list.", + "domain_config_acme_eligible": "Відповідність ACME", + "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": "Стан сертифікату", + "domain_config_cert_summary_abouttoexpire": "Строк дії поточного сертифіката закінчується. Невдовзі його мають автоматично поновити.", + "domain_config_cert_summary_expired": "КРИТИЧНО: поточний сертифікат недійсний! HTTPS цілковито не працюватиме!", + "domain_config_cert_summary_ok": "Гаразд, поточний сертифікат виглядає добре!", + "domain_config_cert_validity": "Достовірність", + "global_settings_setting_backup_compress_tar_archives": "Стиснення резервних копій", + "global_settings_setting_nginx_compatibility": "Сумісність NGINX", + "global_settings_setting_nginx_redirect_to_https": "Примусово HTTPS", + "global_settings_setting_pop3_enabled_help": "Вмикає протокол POP3 для поштового сервера", + "global_settings_setting_postfix_compatibility": "Сумісність Postfix", + "global_settings_setting_root_access_explain": "У системах Linux \"root\" є абсолютним адміністратором. У контексті YunoHost прямий вхід в SSH від імені \"root\" типово вимкнено - за винятком локальної мережі сервера. Члени групи \"адміністратори\" можуть використовувати команду sudo, щоб діяти від імені root з командного рядка. Однак, може бути корисно мати (надійний) пароль root для налагодження системи, якщо з якихось причин звичайні адміністратори більше не можуть увійти в систему.", + "global_settings_setting_root_password": "Новий пароль root", + "global_settings_setting_root_password_confirm": "Новий пароль root (підтвердження)", + "global_settings_setting_smtp_allow_ipv6": "Дозвіл IPv6", + "global_settings_setting_smtp_relay_enabled": "Увімкнути ретрансляцію SMTP", + "global_settings_setting_smtp_relay_host": "Хост ретрансляції SMTP", + "global_settings_setting_ssh_compatibility": "Сумісність SSH", + "global_settings_setting_ssh_password_authentication": "Парольна автентифікація", + "global_settings_setting_user_strength_help": "Ці вимоги застосовуються лише під час ініціалізації або зміни пароля", + "global_settings_setting_webadmin_allowlist": "Білий список IP-адрес вебадміністрування", + "global_settings_setting_webadmin_allowlist_enabled": "Увімкнути білий список IP-адрес вебадміністрування", + "invalid_credentials": "Недійсний пароль чи ім'я користувача", + "log_settings_reset": "Скидання налаштування (одного)", + "log_settings_reset_all": "Скидання усіх налаштувань", + "log_settings_set": "Застосування налаштувань", + "group_update_aliases": "Оновлення псевдонімів для групи '{group}'", + "group_no_change": "Нічого не потрібно змінювати для групи '{group}'", + "registrar_infos": "Відомості про реєстратора", + "migration_0021_not_buster2": "Поточний дистрибутив Debian не Buster! Якщо ви вже виконували міграцію Buster->Bullseye, то ця помилка свідчить про те, що процедура міграції не була на 100% успішною (інакше YunoHost позначив би її як завершену). Радимо з'ясувати, що сталося, зі службою підтримки, якій знадобиться **повний** журнал `міграції, який можна знайти в розділі Засоби > Журнали у вебадмініструванні.", + "migration_description_0025_global_settings_to_configpanel": "Перенесіть застарілу номенклатуру глобальних налаштувань на нову, сучасну", + "migration_description_0026_new_admins_group": "Перейдіть до нової системи 'декількох адміністраторів'", + "root_password_changed": "пароль root було змінено", + "visitors": "Відвідувачі", + "password_confirmation_not_the_same": "Пароль і його підтвердження не збігаються", + "password_too_long": "Будь ласка, виберіть пароль коротший за 127 символів", + "pattern_fullname": "Має бути дійсне повне ім’я (принаймні 3 символи)" +} diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index d3b12f778..b31d88217 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -2,17 +2,13 @@ "password_too_simple_1": "密码长度至少为8个字符", "backup_created": "备份已创建", "app_start_remove": "正在删除{app}……", - "admin_password_change_failed": "无法修改密码", - "admin_password_too_long": "请选择一个小于127个字符的密码", "app_upgrade_failed": "不能升级{app}:{error}", "app_id_invalid": "无效 app ID", "app_unknown": "未知应用", - "admin_password_changed": "管理密码已更改", "aborting": "正在放弃。", "admin_password": "管理员密码", "app_start_restore": "正在恢复{app}……", "action_invalid": "无效操作 '{action}'", - "ask_lastname": "姓", "diagnosis_everything_ok": "{category}一切看起来不错!", "diagnosis_found_warnings": "找到{warnings}项,可能需要{category}进行改进。", "diagnosis_found_errors_and_warnings": "发现与{category}相关的{errors}个重要问题(和{warnings}警告)!", @@ -104,7 +100,6 @@ "ask_new_domain": "新域名", "ask_new_admin_password": "新的管理密码", "ask_main_domain": "主域", - "ask_firstname": "名", "ask_user_domain": "用户的电子邮件地址和XMPP帐户要使用的域", "apps_catalog_update_success": "应用程序目录已更新!", "apps_catalog_obsolete_cache": "应用程序目录缓存为空或已过时。", @@ -125,9 +120,8 @@ "app_restore_script_failed": "应用还原脚本内部发生错误", "app_restore_failed": "无法还原 {app}: {error}", "app_remove_after_failed_install": "安装失败后删除应用程序...", - "app_requirements_unmeet": "{app}不符合要求,软件包{pkgname}({version}) 必须为{spec}", "app_requirements_checking": "正在检查{app}所需的软件包...", - "app_removed": "{app} 已删除", + "app_removed": "{app} 已卸载", "app_not_properly_removed": "{app} 未正确删除", "app_not_correctly_installed": "{app} 似乎安装不正确", "app_not_upgraded": "应用程序'{failed_app}'升级失败,因此以下应用程序的升级已被取消: {apps}", @@ -135,7 +129,7 @@ "app_manifest_install_ask_admin": "选择此应用的管理员用户", "app_manifest_install_ask_password": "选择此应用的管理密码", "additional_urls_already_removed": "权限'{permission}'的其他URL中已经删除了附加URL'{url}'", - "app_manifest_install_ask_path": "选择安装此应用的路径", + "app_manifest_install_ask_path": "选择安装此应用的路径(在域名之后)", "app_manifest_install_ask_domain": "选择应安装此应用程序的域", "app_location_unavailable": "该URL不可用,或与已安装的应用冲突:\n{apps}", "app_label_deprecated": "不推荐使用此命令!请使用新命令 'yunohost user permission update'来管理应用标签。", @@ -204,7 +198,6 @@ "server_reboot": "服务器将重新启动", "server_shutdown_confirm": "服务器会立即关闭,确定吗?[{answers}]", "server_shutdown": "服务器将关闭", - "root_password_replaced_by_admin_password": "您的root密码已替换为您的管理员密码。", "root_password_desynchronized": "管理员密码已更改,但是YunoHost无法将此密码传播到root密码!", "restore_system_part_failed": "无法还原 '{part}'系统部分", "restore_running_hooks": "运行修复挂钩…", @@ -299,35 +292,15 @@ "group_already_exist_on_system": "系统组中已经存在组{group}", "group_already_exist": "群组{group}已经存在", "good_practices_about_admin_password": "现在,您将设置一个新的管理员密码。 密码至少应包含8个字符。并且出于安全考虑建议使用较长的密码同时尽可能使用各种字符(大写,小写,数字和特殊字符)。", - "global_settings_unknown_type": "意外的情况,设置{setting}似乎具有类型 {unknown_type} ,但是系统不支持该类型。", - "global_settings_setting_backup_compress_tar_archives": "创建新备份时,请压缩档案(.tar.gz) ,而不要压缩未压缩的档案(.tar)。注意:启用此选项意味着创建较小的备份存档,但是初始备份过程将明显更长且占用大量CPU。", "global_settings_setting_smtp_relay_password": "SMTP中继主机密码", "global_settings_setting_smtp_relay_user": "SMTP中继用户帐户", "global_settings_setting_smtp_relay_port": "SMTP中继端口", - "global_settings_setting_smtp_allow_ipv6": "允许使用IPv6接收和发送邮件", "global_settings_setting_ssowat_panel_overlay_enabled": "启用SSOwat面板覆盖", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "允许使用DSA主机密钥进行SSH守护程序配置(不建议使用)", - "global_settings_unknown_setting_from_settings_file": "设置中的未知密钥:'{setting_key}',将其丢弃并保存在/etc/yunohost/settings-unknown.json中", - "global_settings_setting_security_ssh_port": "SSH端口", - "global_settings_setting_security_postfix_compatibility": "Postfix服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)", - "global_settings_setting_security_ssh_compatibility": "SSH服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)", - "global_settings_setting_security_password_user_strength": "用户密码强度", - "global_settings_setting_security_password_admin_strength": "管理员密码强度", - "global_settings_setting_security_nginx_compatibility": "Web服务器NGINX的兼容性与安全性的权衡,影响密码(以及其他与安全性有关的方面)", - "global_settings_setting_pop3_enabled": "为邮件服务器启用POP3协议", - "global_settings_reset_success": "以前的设置现在已经备份到{path}", - "global_settings_key_doesnt_exists": "全局设置中不存在键'{settings_key}',您可以通过运行 'yunohost settings list'来查看所有可用键", - "global_settings_cant_write_settings": "无法保存设置文件,原因: {reason}", - "global_settings_cant_serialize_settings": "无法序列化设置数据,原因: {reason}", - "global_settings_cant_open_settings": "无法打开设置文件,原因: {reason}", - "global_settings_bad_type_for_setting": "设置 {setting},的类型错误,已收到{received_type},预期{expected_type}", - "global_settings_bad_choice_for_enum": "设置 {setting}的错误选择,收到了 '{choice}',但可用的选择有: {available_choices}", "firewall_rules_cmd_failed": "某些防火墙规则命令失败。日志中的更多信息。", "firewall_reloaded": "重新加载防火墙", "firewall_reload_failed": "无法重新加载防火墙", "file_does_not_exist": "文件{path} 不存在。", "field_invalid": "无效的字段'{}'", - "experimental_feature": "警告:此功能是实验性的,不稳定,请不要使用它,除非您知道自己在做什么。", "extracting": "提取中...", "dyndns_unavailable": "域'{domain}' 不可用。", "dyndns_domain_not_provided": "DynDNS提供者 {provider} 无法提供域 {domain}。", @@ -455,7 +428,6 @@ "regenconf_up_to_date": "类别'{category}'的配置已经是最新的", "regenconf_file_kept_back": "配置文件'{conf}'预计将被regen-conf(类别{category})删除,但被保留了下来。", "good_practices_about_user_password": "现在,您将设置一个新的管理员密码。 密码至少应包含8个字符。并且出于安全考虑建议使用较长的密码同时尽可能使用各种字符(大写,小写,数字和特殊字符)", - "global_settings_setting_smtp_relay_host": "使用SMTP中继主机来代替这个YunoHost实例发送邮件。如果你有以下情况,就很有用:你的25端口被你的ISP或VPS提供商封锁,你有一个住宅IP列在DUHL上,你不能配置反向DNS,或者这个服务器没有直接暴露在互联网上,你想使用其他服务器来发送邮件。", "domain_cannot_remove_main_add_new_one": "你不能删除'{domain}',因为它是主域和你唯一的域,你需要先用'yunohost domain add '添加另一个域,然后用'yunohost domain main-domain -n '设置为主域,然后你可以用'yunohost domain remove {domain}'删除域", "domain_cannot_add_xmpp_upload": "你不能添加以'xmpp-upload.'开头的域名。这种名称是为YunoHost中集成的XMPP上传功能保留的。", "domain_cannot_remove_main": "你不能删除'{domain}',因为它是主域,你首先需要用'yunohost domain main-domain -n '设置另一个域作为主域;这里是候选域的列表: {other_domains}", @@ -604,5 +576,21 @@ "diagnosis_apps_allgood": "所有已安装的应用程序都遵守基本的打包原则", "diagnosis_apps_deprecated_practices": "此应用程序的安装 版本仍然使用一些超旧的弃用打包原则。推荐您升级它。", "diagnosis_apps_issue": "发现应用{ app } 存在问题", - "diagnosis_description_apps": "应用" -} \ No newline at end of file + "diagnosis_description_apps": "应用", + "global_settings_setting_backup_compress_tar_archives_help": "创建新备份时,请压缩档案(.tar.gz) ,而不要压缩未压缩的档案(.tar)。注意:启用此选项意味着创建较小的备份存档,但是初始备份过程将明显更长且占用大量CPU。", + "global_settings_setting_nginx_compatibility_help": "Web服务器NGINX的兼容性与安全性的权衡,影响密码(以及其他与安全性有关的方面)", + "global_settings_setting_admin_strength": "管理员密码强度", + "global_settings_setting_user_strength": "用户密码强度", + "global_settings_setting_postfix_compatibility_help": "Postfix服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)", + "global_settings_setting_ssh_compatibility_help": "SSH服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)", + "global_settings_setting_ssh_port": "SSH端口", + "global_settings_setting_smtp_allow_ipv6_help": "允许使用IPv6接收和发送邮件", + "global_settings_setting_smtp_relay_enabled_help": "使用SMTP中继主机来代替这个YunoHost实例发送邮件。如果你有以下情况,就很有用:你的25端口被你的ISP或VPS提供商封锁,你有一个住宅IP列在DUHL上,你不能配置反向DNS,或者这个服务器没有直接暴露在互联网上,你想使用其他服务器来发送邮件。", + "all_users": "所有的YunoHost用户", + "app_manifest_install_ask_init_admin_permission": "谁应该有权访问此应用程序的管理功能?(此配置可以稍后更改)", + "app_action_failed": "对应用{app}执行动作{action}失败", + "app_manifest_install_ask_init_main_permission": "谁应该有权访问此应用程序?(此配置稍后可以更改)", + "ask_admin_fullname": "管理员全名", + "ask_admin_username": "管理员用户名", + "ask_fullname": "全名" +} diff --git a/maintenance/agplv3.tpl b/maintenance/agplv3.tpl new file mode 100644 index 000000000..82f3b4cc6 --- /dev/null +++ b/maintenance/agplv3.tpl @@ -0,0 +1,16 @@ +Copyright (c) ${years} ${owner} + +This file is part of ${projectname} (see ${projecturl}) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/maintenance/make_changelog.sh b/maintenance/make_changelog.sh index 44171c5b6..f5d1572a6 100644 --- a/maintenance/make_changelog.sh +++ b/maintenance/make_changelog.sh @@ -1,18 +1,18 @@ VERSION="?" RELEASE="testing" REPO=$(basename $(git rev-parse --show-toplevel)) -REPO_URL=$(git remote get-url origin) +REPO_URL="https://github.com/yunohost/yunohost" ME=$(git config --global --get user.name) EMAIL=$(git config --global --get user.email) -LAST_RELEASE=$(git tag --list 'debian/11.*' | tail -n 1) +LAST_RELEASE=$(git tag --list 'debian/11.*' --sort="v:refname" | tail -n 1) echo "$REPO ($VERSION) $RELEASE; urgency=low" echo "" git log $LAST_RELEASE.. -n 10000 --first-parent --pretty=tformat:' - %b%s (%h)' \ -| sed -E "s@Merge .*#([0-9]+).*\$@ \([#\1]\($REPO_URL/pull/\1\)\)@g" \ -| grep -v "Update from Weblate" \ +| sed -E "s&Merge .*#([0-9]+).*\$& \([#\1]\($REPO_URL/pull/\1\)\)&g" \ +| grep -v "Translations update from Weblate" \ | tac TRANSLATIONS=$(git log $LAST_RELEASE... -n 10000 --pretty=format:"%s" \ @@ -23,7 +23,7 @@ TRANSLATIONS=$(git log $LAST_RELEASE... -n 10000 --pretty=format:"%s" \ echo "" CONTRIBUTORS=$(git logc $LAST_RELEASE... -n 10000 --pretty=format:"%an" \ - | sort | uniq | grep -v "$ME" \ + | sort | uniq | grep -v "$ME" | grep -v 'yunohost-bot' | grep -vi 'weblate' \ | tr '\n' ', ' | sed -e 's/,$//g' -e 's/,/, /g') [[ -z "$CONTRIBUTORS" ]] || echo " Thanks to all contributors <3 ! ($CONTRIBUTORS)" echo "" diff --git a/maintenance/missing_i18n_keys.py b/maintenance/missing_i18n_keys.py index 3dbca8027..f49fc923e 100644 --- a/maintenance/missing_i18n_keys.py +++ b/maintenance/missing_i18n_keys.py @@ -99,15 +99,6 @@ def find_expected_string_keys(): for m in ("log_" + match for match in p4.findall(content)): yield m - # Global settings descriptions - # Will be on a line like : ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", ... - p5 = re.compile(r" \(\n*\s*[\"\'](\w[\w\.]+)[\"\'],") - content = open(ROOT + "src/settings.py").read() - for m in ( - "global_settings_setting_" + s.replace(".", "_") for s in p5.findall(content) - ): - yield m - # Keys for the actionmap ... for category in yaml.safe_load(open(ROOT + "share/actionsmap.yml")).values(): if "actions" not in category.keys(): @@ -143,6 +134,7 @@ def find_expected_string_keys(): for key in registrars[registrar].keys(): yield f"domain_config_{key}" + # Domain config panel domain_config = toml.load(open(ROOT + "share/config_domain.toml")) for panel in domain_config.values(): if not isinstance(panel, dict): @@ -155,6 +147,35 @@ def find_expected_string_keys(): continue yield f"domain_config_{key}" + # Global settings + global_config = toml.load(open(ROOT + "share/config_global.toml")) + # Boring hard-coding because there's no simple other way idk + settings_without_help_key = [ + "passwordless_sudo", + "smtp_relay_host", + "smtp_relay_password", + "smtp_relay_port", + "smtp_relay_user", + "ssh_port", + "ssowat_panel_overlay_enabled", + "root_password", + "root_access_explain", + "root_password_confirm", + ] + + for panel in global_config.values(): + if not isinstance(panel, dict): + continue + for section in panel.values(): + if not isinstance(section, dict): + continue + for key, values in section.items(): + if not isinstance(values, dict): + continue + yield f"global_settings_setting_{key}" + if key not in settings_without_help_key: + yield f"global_settings_setting_{key}_help" + ############################################################################### # Compare keys used and keys defined # diff --git a/maintenance/update_copyright_headers.sh b/maintenance/update_copyright_headers.sh new file mode 100644 index 000000000..bc4fe24db --- /dev/null +++ b/maintenance/update_copyright_headers.sh @@ -0,0 +1,12 @@ +# To run this you'll need to: +# +# pip3 install licenseheaders + +licenseheaders \ + -o "YunoHost Contributors" \ + -n "YunoHost" \ + -u "https://yunohost.org" \ + -t ./agplv3.tpl \ + --current-year \ + -f ../src/*.py ../src/{utils,diagnosers,authenticators}/*.py + diff --git a/share/100000-most-used-passwords-length8plus.txt.gz b/share/100000-most-used-passwords-length8plus.txt.gz new file mode 100644 index 000000000..6059a5af8 Binary files /dev/null and b/share/100000-most-used-passwords-length8plus.txt.gz differ diff --git a/share/100000-most-used-passwords.txt.gz b/share/100000-most-used-passwords.txt.gz deleted file mode 100644 index 43887119b..000000000 Binary files a/share/100000-most-used-passwords.txt.gz and /dev/null differ diff --git a/share/actionsmap.yml b/share/actionsmap.yml index e437a812b..6e60655d0 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -37,14 +37,6 @@ _global: authentication: api: ldap_admin cli: null - arguments: - -v: - full: --version - help: Display YunoHost packages versions - action: callback - callback: - method: yunohost.utils.packages.ynh_packages_version - return: true ############################# # User # @@ -73,19 +65,28 @@ user: pattern: &pattern_username - !!str ^[a-z0-9_]+$ - "pattern_username" + -F: + full: --fullname + help: The full name of the user. For example 'Camille Dupont' + extra: + ask: ask_fullname + required: False + pattern: &pattern_fullname + - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ + - "pattern_fullname" -f: full: --firstname + help: Deprecated. Use --fullname instead. extra: - ask: ask_firstname - required: True + required: False pattern: &pattern_firstname - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - "pattern_firstname" -l: full: --lastname + help: Deprecated. Use --fullname instead. extra: - ask: ask_lastname - required: True + required: False pattern: &pattern_lastname - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - "pattern_lastname" @@ -136,12 +137,19 @@ user: arguments: username: help: Username to update + -F: + full: --fullname + help: The full name of the user. For example 'Camille Dupont' + extra: + pattern: *pattern_fullname -f: full: --firstname + help: Deprecated. Use --fullname instead. extra: pattern: *pattern_firstname -l: full: --lastname + help: Deprecated. Use --fullname instead. extra: pattern: *pattern_lastname -m: @@ -305,6 +313,35 @@ user: extra: pattern: *pattern_username + add-mailalias: + action_help: Add mail aliases to group + api: PUT /users/groups//aliases/ + arguments: + groupname: + help: Name of the group to add user(s) to + extra: + pattern: *pattern_groupname + aliases: + help: Mail aliases to add + nargs: "+" + metavar: MAIL + extra: + pattern: *pattern_email + remove-mailalias: + action_help: Remove mail aliases to group + api: DELETE /users/groups//aliases/ + arguments: + groupname: + help: Name of the group to add user(s) to + extra: + pattern: *pattern_groupname + aliases: + help: Mail aliases to remove + nargs: "+" + metavar: MAIL + + + permission: subcategory_help: Manage permissions actions: @@ -443,12 +480,22 @@ 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 + --tree: + help: Display domains as a tree action: store_true - --full: - action: store_true - help: Display more information + --features: + help: List only domains with features enabled (xmpp, mail_in, mail_out, auto_push) + nargs: "*" + + ### domain_info() + info: + action_help: Get domain aggredated data + api: GET /domains/ + arguments: + domain: + help: Domain to check + extra: + pattern: *pattern_domain ### domain_add() add: @@ -517,9 +564,7 @@ domain: action_help: Check the current main domain, or change it deprecated_alias: - maindomain - api: - - GET /domains/main - - PUT /domains//main + api: PUT /domains//main arguments: -n: full: --new-main-domain @@ -577,6 +622,7 @@ domain: ### domain_url_available() url-available: + hide_in_help: True action_help: Check availability of a web path api: GET /domain//urlavailable arguments: @@ -588,6 +634,20 @@ domain: 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: dyndns: subcategory_help: Subscribe and Update DynDNS Hosts @@ -638,7 +698,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 @@ -657,7 +719,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 @@ -781,6 +843,10 @@ app: full: --with-categories help: Also return a list of app categories action: store_true + -a: + full: --with-antifeatures + help: Also return a list of antifeatures categories + action: store_true ### app_search() search: @@ -796,6 +862,10 @@ app: arguments: app: help: Name, local path or git URL of the app to fetch the manifest of + -s: + full: --with-screenshot + help: Also return a base64 screenshot if any (API only) + action: store_true ### app_list() list: @@ -939,6 +1009,7 @@ app: ### app_register_url() register-url: + hide_in_help: True action_help: Book/register a web path for a given app arguments: app: @@ -951,6 +1022,7 @@ app: ### app_makedefault() makedefault: + hide_in_help: True action_help: Redirect domain root to an app api: PUT /apps//default arguments: @@ -964,6 +1036,17 @@ app: help: Undo redirection action: store_true + ### app_dismiss_notification + dismiss-notification: + hide_in_help: True + action_help: Dismiss post_install or post_upgrade notification + api: PUT /apps//dismiss_notification/ + arguments: + app: + help: App ID to dismiss notification for + name: + help: Notification name, either post_install or post_upgrade + ### app_ssowatconf() ssowatconf: action_help: Regenerate SSOwat configuration file @@ -1012,7 +1095,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 @@ -1031,7 +1116,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 @@ -1136,6 +1221,7 @@ backup: ### backup_download() download: + hide_in_help: True action_help: (API only) Request to download the file api: GET /backups//download arguments: @@ -1164,6 +1250,11 @@ settings: list: action_help: list all entries of the settings api: GET /settings + arguments: + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true ### settings_get() get: @@ -1172,22 +1263,29 @@ settings: arguments: key: help: Settings key - --full: - help: Show more details + -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 ### settings_set() set: action_help: set an entry value in the settings - api: POST /settings/ + api: PUT /settings/ arguments: key: - help: Settings key + help: The question or form key + nargs: '?' -v: full: --value help: new value - extra: - required: True + -a: + full: --args + help: Serialized arguments for new configuration (i.e. "mail_in=0&mail_out=0") ### settings_reset_all() reset-all: @@ -1530,10 +1628,10 @@ tools: category_help: Specific tools actions: - ### tools_adminpw() - adminpw: - action_help: Change password of admin and root users - api: PUT /adminpw + ### tools_rootpw() + rootpw: + action_help: Change root password + api: PUT /rootpw arguments: -n: full: --new-password @@ -1568,6 +1666,20 @@ tools: ask: ask_main_domain pattern: *pattern_domain required: True + -u: + full: --username + help: Username for the first (admin) user. For example 'camille' + extra: + ask: ask_admin_username + pattern: *pattern_username + required: True + -F: + full: --fullname + help: The full name for the first (admin) user. For example 'Camille Dupont' + extra: + ask: ask_admin_fullname + required: True + pattern: *pattern_fullname -p: full: --password help: YunoHost admin password @@ -1588,14 +1700,10 @@ tools: 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 --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 - ### tools_update() update: action_help: YunoHost update @@ -1757,6 +1865,7 @@ hook: ### hook_info() info: + hide_in_help: True action_help: Get information about a given hook arguments: action: @@ -1786,6 +1895,7 @@ hook: ### hook_callback() callback: + hide_in_help: True action_help: Execute all scripts binded to an action arguments: action: @@ -1808,6 +1918,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 ba0706749..0aae4df26 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -1,51 +1,37 @@ version = "1.0" i18n = "domain_config" -# -# Other things we may want to implement in the future: -# -# - maindomain handling -# - default app -# - autoredirect www in nginx conf -# - ? -# - [feature] +name = "Features" + [feature.app] [feature.app.default_app] type = "app" filter = "is_webapp" default = "_none" - - [feature.mail] - #services = ['postfix', 'dovecot'] + # FIXME: i18n + help = "People will automatically be redirected to this app when opening this domain. If no app is specified, people are redirected to the user portal login form." - [feature.mail.features_disclaimer] - type = "alert" - style = "warning" - icon = "warning" + [feature.mail] [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] type = "boolean" default = 0 + # FIXME: i18n + help = "NB: some XMPP features will require that you update your DNS records and regenerate your Lets Encrypt certificate to be enabled" [dns] +name = "DNS" [dns.zone] @@ -55,13 +41,54 @@ i18n = "domain_config" help = "" [dns.registrar] - optional = true + # This part is automatically generated in DomainConfigPanel - # This part is automatically generated in DomainConfigPanel +[cert] +name = "Certificate" -# [dns.advanced] -# -# [dns.advanced.ttl] -# type = "number" -# min = 0 -# default = 3600 + [cert.cert] + + [cert.cert.cert_summary] + type = "alert" + # Automatically filled by DomainConfigPanel + + [cert.cert.cert_validity] + type = "number" + readonly = true + visible = "false" + # Automatically filled by DomainConfigPanel + + [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 || acme_elligible == null" + + [cert.cert.cert_no_checks] + ask = "Ignore diagnosis checks" + type = "boolean" + default = false + visible = "acme_eligible == false || acme_elligible == null" + + [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/share/config_global.toml b/share/config_global.toml new file mode 100644 index 000000000..1f3cc1b39 --- /dev/null +++ b/share/config_global.toml @@ -0,0 +1,162 @@ +version = "1.0" +i18n = "global_settings_setting" + +[security] +name = "Security" + [security.password] + name = "Passwords" + + [security.password.admin_strength] + type = "select" + choices.1 = "Require at least 8 chars" + choices.2 = "ditto, but also require at least one digit, one lower and one upper char" + choices.3 = "ditto, but also require at least one special char" + choices.4 = "ditto, but also require at least 12 chars" + default = "1" + + [security.password.user_strength] + type = "select" + choices.1 = "Require at least 8 chars" + choices.2 = "ditto, but also require at least one digit, one lower and one upper char" + choices.3 = "ditto, but also require at least one special char" + choices.4 = "ditto, but also require at least 12 chars" + default = "1" + + [security.password.passwordless_sudo] + type = "boolean" + # The actual value is dynamically computed by checking the sudoOption of cn=admins,ou=sudo + default = false + + [security.ssh] + name = "SSH" + [security.ssh.ssh_compatibility] + type = "select" + choices.intermediate = "Intermediate (compatible with older softwares)" + choices.modern = "Modern (recommended)" + default = "modern" + + [security.ssh.ssh_port] + type = "number" + default = 22 + + [security.ssh.ssh_password_authentication] + type = "boolean" + default = true + + [security.nginx] + name = "NGINX (web server)" + [security.nginx.nginx_redirect_to_https] + type = "boolean" + default = true + + [security.nginx.nginx_compatibility] + type = "select" + choices.intermediate = "Intermediate (compatible with Firefox 27, Android 4.4.2, Chrome 31, Edge, IE 11, Opera 20, and Safari 9)" + choices.modern = "Modern (compatible with Firefox 63, Android 10.0, Chrome 70, Edge 75, Opera 57, and Safari 12.1)" + default = "intermediate" + + [security.postfix] + name = "Postfix (SMTP email server)" + [security.postfix.postfix_compatibility] + type = "select" + choices.intermediate = "Intermediate (allows TLS 1.2)" + choices.modern = "Modern (TLS 1.3 only)" + default = "intermediate" + + [security.webadmin] + name = "Webadmin" + [security.webadmin.webadmin_allowlist_enabled] + type = "boolean" + default = false + + [security.webadmin.webadmin_allowlist] + type = "tags" + visible = "webadmin_allowlist_enabled" + optional = true + default = "" + + [security.root_access] + name = "Change root password" + + [security.root_access.root_access_explain] + type = "alert" + style = "info" + icon = "info" + + [security.root_access.root_password] + type = "password" + optional = true + default = "" + + [security.root_access.root_password_confirm] + type = "password" + optional = true + default = "" + + [security.experimental] + name = "Experimental" + [security.experimental.security_experimental_enabled] + type = "boolean" + default = false + + +[email] +name = "Email" + [email.pop3] + name = "POP3" + [email.pop3.pop3_enabled] + type = "boolean" + default = false + + [email.smtp] + name = "SMTP" + [email.smtp.smtp_allow_ipv6] + type = "boolean" + default = true + + [email.smtp.smtp_relay_enabled] + type = "boolean" + default = false + + [email.smtp.smtp_relay_host] + type = "string" + default = "" + optional = true + visible="smtp_relay_enabled" + + [email.smtp.smtp_relay_port] + type = "number" + default = 587 + visible="smtp_relay_enabled" + + [email.smtp.smtp_relay_user] + type = "string" + default = "" + optional = true + visible="smtp_relay_enabled" + + [email.smtp.smtp_relay_password] + type = "password" + default = "" + optional = true + visible="smtp_relay_enabled" + help = "" # This is empty string on purpose, otherwise the core automatically set the 'good_practice_admin_password' string here which is not relevant, because the admin is not actually "choosing" the password ... + +[misc] +name = "Other" + [misc.portal] + name = "User portal" + [misc.portal.ssowat_panel_overlay_enabled] + type = "boolean" + default = true + + [misc.portal.portal_theme] + type = "select" + # Choices are loaded dynamically in the python code + default = "default" + + [misc.backup] + name = "Backup" + [misc.backup.backup_compress_tar_archives] + type = "boolean" + default = false diff --git a/share/html/502.html b/share/html/502.html new file mode 100644 index 000000000..bef0275df --- /dev/null +++ b/share/html/502.html @@ -0,0 +1,20 @@ + + + +502 Bad Gateway + + + +

502 Bad Gateway

+

If you see this page, your connection with the server is working but the internal service providing this path is not responding.

+

Administrator, make sure that the service is running, and check its logs if it is not. +The Services page is in your webadmin, under Tools > Services.

+

Thank you for using YunoHost.

+ + diff --git a/share/registrar_list.toml b/share/registrar_list.toml index afb213aa1..01906becd 100644 --- a/share/registrar_list.toml +++ b/share/registrar_list.toml @@ -622,7 +622,14 @@ [vultr.auth_token] type = "string" redact = true - + +[webgo] + [webgo.auth_username] + type = "string" + + [webgo.auth_password] + type = "password" + [yandex] [yandex.auth_token] type = "string" diff --git a/src/__init__.py b/src/__init__.py index 608917185..af18e1fe4 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,5 +1,22 @@ #! /usr/bin/python -# -*- coding: utf-8 -*- +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import sys diff --git a/src/app.py b/src/app.py index 3b60deb6c..ed1432685 100644 --- a/src/app.py +++ b/src/app.py @@ -1,28 +1,23 @@ -# -*- coding: utf-8 -*- +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_app.py - - Manage apps -""" +import glob import os import toml import json @@ -32,8 +27,10 @@ import time import re import subprocess import tempfile +import copy from collections import OrderedDict -from typing import List, Tuple, Dict, Any +from typing import List, Tuple, Dict, Any, Iterator +from packaging import version from moulinette import Moulinette, m18n from moulinette.utils.log import getActionLogger @@ -51,7 +48,6 @@ from moulinette.utils.filesystem import ( chmod, ) -from yunohost.utils import packages from yunohost.utils.config import ( ConfigPanel, ask_questions_and_parse_answers, @@ -61,12 +57,21 @@ from yunohost.utils.config import ( ) from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.utils.filesystem import free_space_in_directory +from yunohost.utils.system import ( + free_space_in_directory, + dpkg_is_broken, + get_ynh_package_version, + system_arch, + human_to_binary, + binary_to_human, + ram_available, +) from yunohost.log import is_unit_operation, OperationLogger from yunohost.app_catalog import ( # noqa app_catalog, app_search, _load_apps_catalog, + APPS_CATALOG_LOGOS, ) logger = getActionLogger("yunohost.app") @@ -147,6 +152,13 @@ def app_info(app, full=False, upgradable=False): absolute_app_name, _ = _parse_app_instance_name(app) from_catalog = _load_apps_catalog()["apps"].get(absolute_app_name, {}) + # Check if $app.png exists in the app logo folder, this is a trick to be able to easily customize the logo + # of an app just by creating $app.png (instead of the hash.png) in the corresponding folder + ret["logo"] = ( + app + if os.path.exists(f"{APPS_CATALOG_LOGOS}/{app}.png") + else from_catalog.get("logo_hash") + ) ret["upgradable"] = _app_upgradable({**ret, "from_catalog": from_catalog}) if ret["upgradable"] == "yes": @@ -160,18 +172,42 @@ def app_info(app, full=False, upgradable=False): ret["current_version"] = f" ({current_revision})" ret["new_version"] = f" ({new_revision})" + ret["settings"] = settings + if not full: return ret ret["setting_path"] = setting_path ret["manifest"] = local_manifest - ret["manifest"]["arguments"] = _set_default_ask_questions( - ret["manifest"].get("arguments", {}) + + # FIXME: maybe this is not needed ? default ask questions are + # already set during the _get_manifest_of_app earlier ? + ret["manifest"]["install"] = _set_default_ask_questions( + ret["manifest"].get("install", {}) ) - ret["settings"] = settings ret["from_catalog"] = from_catalog + # Hydrate app notifications and doc + for pagename, content_per_lang in ret["manifest"]["doc"].items(): + for lang, content in content_per_lang.items(): + ret["manifest"]["doc"][pagename][lang] = _hydrate_app_template( + content, settings + ) + + # Filter dismissed notification + ret["manifest"]["notifications"] = { + k: v + for k, v in ret["manifest"]["notifications"].items() + if not _notification_is_dismissed(k, settings) + } + + # Hydrate notifications (also filter uneeded post_upgrade notification based on version) + for step, notifications in ret["manifest"]["notifications"].items(): + for name, content_per_lang in notifications.items(): + for lang, content in content_per_lang.items(): + notifications[name][lang] = _hydrate_app_template(content, settings) + ret["is_webapp"] = "domain" in settings and "path" in settings if ret["is_webapp"]: @@ -185,8 +221,8 @@ def app_info(app, full=False, upgradable=False): ret["supports_backup_restore"] = os.path.exists( os.path.join(setting_path, "scripts", "backup") ) and os.path.exists(os.path.join(setting_path, "scripts", "restore")) - ret["supports_multi_instance"] = is_true( - local_manifest.get("multi_instance", False) + ret["supports_multi_instance"] = local_manifest.get("integration", {}).get( + "multi_instance", False ) ret["supports_config_panel"] = os.path.exists( os.path.join(setting_path, "config_panel.toml") @@ -202,7 +238,6 @@ def app_info(app, full=False, upgradable=False): def _app_upgradable(app_infos): - from packaging import version # Determine upgradability @@ -416,7 +451,9 @@ def app_change_url(operation_logger, app, domain, path): tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app, workdir=tmp_workdir_for_app) + env_dict = _make_environment_for_app_script( + app, workdir=tmp_workdir_for_app, action="change_url" + ) env_dict["YNH_APP_OLD_DOMAIN"] = old_domain env_dict["YNH_APP_OLD_PATH"] = old_path env_dict["YNH_APP_NEW_DOMAIN"] = domain @@ -467,7 +504,6 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False no_safety_backup -- Disable the safety backup during upgrade """ - from packaging import version from yunohost.hook import ( hook_add, hook_remove, @@ -477,6 +513,12 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False from yunohost.permission import permission_sync_to_user from yunohost.regenconf import manually_modified_files from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers + from yunohost.backup import ( + backup_list, + backup_create, + backup_delete, + backup_restore, + ) apps = app # Check if disk space available @@ -502,6 +544,8 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False if len(apps) > 1: logger.info(m18n.n("app_upgrade_several_apps", apps=", ".join(apps))) + notifications = {} + for number, app_instance_name in enumerate(apps): logger.info(m18n.n("app_upgrade_app_name", app=app_instance_name)) @@ -563,34 +607,99 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False upgrade_type = "UPGRADE_FULL" # Check requirements - _check_manifest_requirements(manifest) + for name, passed, values, err in _check_manifest_requirements( + manifest, action="upgrade" + ): + if not passed: + if name == "ram": + # i18n: confirm_app_insufficient_ram + _ask_confirmation( + "confirm_app_insufficient_ram", params=values, force=force + ) + else: + raise YunohostValidationError(err, **values) + + # Display pre-upgrade notifications and ask for simple confirm + if ( + manifest["notifications"]["PRE_UPGRADE"] + and Moulinette.interface.type == "cli" + ): + settings = _get_app_settings(app_instance_name) + notifications = _filter_and_hydrate_notifications( + manifest["notifications"]["PRE_UPGRADE"], + current_version=app_current_version, + data=settings, + ) + _display_notifications(notifications, force=force) + + if manifest["packaging_format"] >= 2: + if no_safety_backup: + # FIXME: i18n + logger.warning( + "Skipping the creation of a backup prior to the upgrade." + ) + else: + # FIXME: i18n + logger.info("Creating a safety backup prior to the upgrade") + + # Switch between pre-upgrade1 or pre-upgrade2 + safety_backup_name = f"{app_instance_name}-pre-upgrade1" + other_safety_backup_name = f"{app_instance_name}-pre-upgrade2" + if safety_backup_name in backup_list()["archives"]: + safety_backup_name = f"{app_instance_name}-pre-upgrade2" + other_safety_backup_name = f"{app_instance_name}-pre-upgrade1" + + backup_create(name=safety_backup_name, apps=[app_instance_name]) + + if safety_backup_name in backup_list()["archives"]: + # if the backup suceeded, delete old safety backup to save space + if other_safety_backup_name in backup_list()["archives"]: + backup_delete(other_safety_backup_name) + else: + # Is this needed ? Shouldn't backup_create report an expcetion if backup failed ? + raise YunohostError( + "Uhoh the safety backup failed ?! Aborting the upgrade process.", + raw_msg=True, + ) + _assert_system_is_sane_for_app(manifest, "pre") - app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) - - # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script( - app_instance_name, workdir=extracted_app_folder - ) - env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type - env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version) - env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version) - env_dict["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0" - # We'll check that the app didn't brutally edit some system configuration manually_modified_files_before_install = manually_modified_files() + app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) + # Attempt to patch legacy helpers ... _patch_legacy_helpers(extracted_app_folder) # Apply dirty patch to make php5 apps compatible with php7 _patch_legacy_php_versions(extracted_app_folder) + # Prepare env. var. to pass to script + env_dict = _make_environment_for_app_script( + app_instance_name, workdir=extracted_app_folder, action="upgrade" + ) + env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type + env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version) + env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version) + if manifest["packaging_format"] < 2: + env_dict["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0" + # Start register change on system related_to = [("app", app_instance_name)] operation_logger = OperationLogger("app_upgrade", related_to, env=env_dict) operation_logger.start() + if manifest["packaging_format"] >= 2: + from yunohost.utils.resources import AppResourceManager + + AppResourceManager( + app_instance_name, wanted=manifest, current=app_dict["manifest"] + ).apply( + rollback_and_raise_exception_if_failure=True, + operation_logger=operation_logger, + ) + # Execute the app upgrade script upgrade_failed = True try: @@ -607,6 +716,26 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False ), ) finally: + + # If upgrade failed, try to restore the safety backup + if ( + upgrade_failed + and manifest["packaging_format"] >= 2 + and not no_safety_backup + ): + logger.warning( + "Upgrade failed ... attempting to restore the satefy backup (Yunohost first need to remove the app for this) ..." + ) + + app_remove(app_instance_name) + backup_restore( + name=safety_backup_name, apps=[app_instance_name], force=True + ) + if not _is_installed(app_instance_name): + logger.error( + "Uhoh ... Yunohost failed to restore the app to the way it was before the failed upgrade :|" + ) + # Whatever happened (install success or failure) we check if it broke the system # and warn the user about it try: @@ -684,6 +813,24 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False # So much win logger.success(m18n.n("app_upgraded", app=app_instance_name)) + # Format post-upgrade notifications + if manifest["notifications"]["POST_UPGRADE"]: + # Get updated settings to hydrate notifications + settings = _get_app_settings(app_instance_name) + notifications = _filter_and_hydrate_notifications( + manifest["notifications"]["POST_UPGRADE"], + current_version=app_current_version, + data=settings, + ) + if Moulinette.interface.type == "cli": + # ask for simple confirm + _display_notifications(notifications, force=force) + + # Reset the dismiss flag for post upgrade notification + app_setting( + app_instance_name, "_dismiss_notification_post_upgrade", delete=True + ) + hook_callback("post_app_upgrade", env=env_dict) operation_logger.success() @@ -691,19 +838,74 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False logger.success(m18n.n("upgrade_complete")) + if Moulinette.interface.type == "api": + return {"notifications": {"POST_UPGRADE": notifications}} -def app_manifest(app): + +def app_manifest(app, with_screenshot=False): manifest, extracted_app_folder = _extract_app(app) + raw_questions = manifest.get("install", {}).values() + manifest["install"] = hydrate_questions_with_choices(raw_questions) + + # Add a base64 image to be displayed in web-admin + if with_screenshot and Moulinette.interface.type == "api": + import base64 + + manifest["screenshot"] = None + screenshots_folder = os.path.join(extracted_app_folder, "doc", "screenshots") + + if os.path.exists(screenshots_folder): + with os.scandir(screenshots_folder) as it: + for entry in it: + ext = os.path.splitext(entry.name)[1].replace(".", "").lower() + if entry.is_file() and ext in ("png", "jpg", "jpeg", "webp", "gif"): + with open(entry.path, "rb") as img_file: + data = base64.b64encode(img_file.read()).decode("utf-8") + manifest[ + "screenshot" + ] = f"data:image/{ext};charset=utf-8;base64,{data}" + break + shutil.rmtree(extracted_app_folder) - raw_questions = manifest.get("arguments", {}).get("install", []) - manifest["arguments"]["install"] = hydrate_questions_with_choices(raw_questions) + manifest["requirements"] = {} + for name, passed, values, err in _check_manifest_requirements( + manifest, action="install" + ): + if Moulinette.interface.type == "api": + manifest["requirements"][name] = { + "pass": passed, + "values": values, + } + else: + manifest["requirements"][name] = "ok" if passed else m18n.n(err, **values) return manifest +def _confirm_app_install(app, force=False): + + # Ignore if there's nothing for confirm (good quality app), if --force is used + # or if request on the API (confirm already implemented on the API side) + if force or Moulinette.interface.type == "api": + return + + quality = _app_quality(app) + if quality == "success": + return + + # i18n: confirm_app_install_warning + # i18n: confirm_app_install_danger + # i18n: confirm_app_install_thirdparty + + if quality in ["danger", "thirdparty"]: + _ask_confirmation("confirm_app_install_" + quality, kind="hard") + else: + _ask_confirmation("confirm_app_install_" + quality, kind="soft") + + @is_unit_operation() def app_install( operation_logger, @@ -745,63 +947,50 @@ def app_install( if free_space_in_directory("/") <= 512 * 1000 * 1000: raise YunohostValidationError("disk_space_not_sufficient_install") - def confirm_install(app): - - # Ignore if there's nothing for confirm (good quality app), if --force is used - # or if request on the API (confirm already implemented on the API side) - if force or Moulinette.interface.type == "api": - return - - quality = _app_quality(app) - if quality == "success": - return - - # i18n: confirm_app_install_warning - # i18n: confirm_app_install_danger - # i18n: confirm_app_install_thirdparty - - if quality in ["danger", "thirdparty"]: - answer = Moulinette.prompt( - m18n.n("confirm_app_install_" + quality, answers="Yes, I understand"), - color="red", - ) - if answer != "Yes, I understand": - raise YunohostError("aborting") - - else: - answer = Moulinette.prompt( - m18n.n("confirm_app_install_" + quality, answers="Y/N"), color="yellow" - ) - if answer.upper() != "Y": - raise YunohostError("aborting") - - confirm_install(app) + _confirm_app_install(app, force) manifest, extracted_app_folder = _extract_app(app) + # Display pre_install notices in cli mode + if manifest["notifications"]["PRE_INSTALL"] and Moulinette.interface.type == "cli": + notifications = _filter_and_hydrate_notifications( + manifest["notifications"]["PRE_INSTALL"] + ) + _display_notifications(notifications, force=force) + + packaging_format = manifest["packaging_format"] + # Check ID if "id" not in manifest or "__" in manifest["id"] or "." in manifest["id"]: raise YunohostValidationError("app_id_invalid") app_id = manifest["id"] - label = label if label else manifest["name"] # Check requirements - _check_manifest_requirements(manifest) + for name, passed, values, err in _check_manifest_requirements( + manifest, action="install" + ): + if not passed: + if name == "ram": + _ask_confirmation( + "confirm_app_insufficient_ram", params=values, force=force + ) + else: + raise YunohostValidationError(err, **values) + _assert_system_is_sane_for_app(manifest, "pre") # Check if app can be forked instance_number = _next_instance_number_for_app(app_id) if instance_number > 1: - if "multi_instance" not in manifest or not is_true(manifest["multi_instance"]): - raise YunohostValidationError("app_already_installed", app=app_id) - # Change app_id to the forked app id app_instance_name = app_id + "__" + str(instance_number) else: app_instance_name = app_id + app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) + # Retrieve arguments list for install script - raw_questions = manifest.get("arguments", {}).get("install", {}) + raw_questions = manifest["install"] questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) args = { question.name: question.value @@ -810,11 +999,13 @@ def app_install( } # Validate domain / path availability for webapps + # (ideally this should be handled by the resource system for manifest v >= 2 path_requirement = _guess_webapp_path_requirement(extracted_app_folder) _validate_webpath_requirement(args, path_requirement) - # Attempt to patch legacy helpers ... - _patch_legacy_helpers(extracted_app_folder) + if packaging_format < 2: + # Attempt to patch legacy helpers ... + _patch_legacy_helpers(extracted_app_folder) # Apply dirty patch to make php5 apps compatible with php7 _patch_legacy_php_versions(extracted_app_folder) @@ -831,7 +1022,6 @@ def app_install( logger.info(m18n.n("app_start_install", app=app_id)) # Create app directory - app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) os.makedirs(app_setting_path) @@ -842,6 +1032,17 @@ def app_install( "install_time": int(time.time()), "current_revision": manifest.get("remote", {}).get("revision", "?"), } + + # If packaging_format v2+, save all install questions as settings + if packaging_format >= 2: + for question in questions: + + # Except user-provider passwords + if question.type == "password": + continue + + app_settings[question.name] = question.value + _set_app_settings(app_instance_name, app_settings) # Move scripts and manifest to the right place @@ -853,21 +1054,35 @@ def app_install( recursive=True, ) - # Initialize the main permission for the app - # The permission is initialized with no url associated, and with tile disabled - # For web app, the root path of the app will be added as url and the tile - # will be enabled during the app install. C.f. 'app_register_url()' below. - permission_create( - app_instance_name + ".main", - allowed=["all_users"], - label=label, - show_tile=False, - protected=False, - ) + # Override manifest name by given label + # This info is also later picked-up by the 'permission' resource initialization + if label: + manifest["name"] = label + + if packaging_format >= 2: + from yunohost.utils.resources import AppResourceManager + + AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( + rollback_and_raise_exception_if_failure=True, + operation_logger=operation_logger, + ) + else: + # Initialize the main permission for the app + # The permission is initialized with no url associated, and with tile disabled + # For web app, the root path of the app will be added as url and the tile + # will be enabled during the app install. C.f. 'app_register_url()' below + # or the webpath resource + permission_create( + app_instance_name + ".main", + allowed=["all_users"], + label=manifest["name"], + show_tile=False, + protected=False, + ) # Prepare env. var. to pass to script env_dict = _make_environment_for_app_script( - app_instance_name, args=args, workdir=extracted_app_folder + app_instance_name, args=args, workdir=extracted_app_folder, action="install" ) env_dict_for_logging = env_dict.copy() @@ -914,6 +1129,9 @@ def app_install( "Packagers /!\\ This app manually modified some system configuration files! This should not happen! If you need to do so, you should implement a proper conf_regen hook. Those configuration were affected:\n - " + "\n -".join(manually_modified_files_by_app) ) + # Actually forbid this for app packaging >= 2 + if packaging_format >= 2: + broke_the_system = True # If the install failed or broke the system, we remove it if install_failed or broke_the_system: @@ -929,7 +1147,7 @@ def app_install( # Setup environment for remove script env_dict_remove = _make_environment_for_app_script( - app_instance_name, workdir=extracted_app_folder + app_instance_name, workdir=extracted_app_folder, action="remove" ) # Execute remove script @@ -960,10 +1178,17 @@ def app_install( m18n.n("unexpected_error", error="\n" + traceback.format_exc()) ) - # Remove all permission in LDAP - for permission_name in user_permission_list()["permissions"].keys(): - if permission_name.startswith(app_instance_name + "."): - permission_delete(permission_name, force=True, sync_perm=False) + if packaging_format >= 2: + from yunohost.utils.resources import AppResourceManager + + AppResourceManager( + app_instance_name, wanted={}, current=manifest + ).apply(rollback_and_raise_exception_if_failure=False) + else: + # Remove all permission in LDAP + for permission_name in user_permission_list()["permissions"].keys(): + if permission_name.startswith(app_instance_name + "."): + permission_delete(permission_name, force=True, sync_perm=False) if remove_retcode != 0: msg = m18n.n("app_not_properly_removed", app=app_instance_name) @@ -999,8 +1224,23 @@ def app_install( logger.success(m18n.n("installation_complete")) + # Get the generated settings to hydrate notifications + settings = _get_app_settings(app_instance_name) + notifications = _filter_and_hydrate_notifications( + manifest["notifications"]["POST_INSTALL"], data=settings + ) + + # Display post_install notices in cli mode + if notifications and Moulinette.interface.type == "cli": + _display_notifications(notifications, force=force) + + # Call postinstall hook hook_callback("post_app_install", env=env_dict) + # Return hydrated post install notif for API + if Moulinette.interface.type == "api": + return {"notifications": notifications} + @is_unit_operation() def app_remove(operation_logger, app, purge=False): @@ -1044,7 +1284,9 @@ def app_remove(operation_logger, app, purge=False): remove_script = f"{tmp_workdir_for_app}/scripts/remove" env_dict = {} - env_dict = _make_environment_for_app_script(app, workdir=tmp_workdir_for_app) + env_dict = _make_environment_for_app_script( + app, workdir=tmp_workdir_for_app, action="remove" + ) env_dict["YNH_APP_PURGE"] = str(1 if purge else 0) operation_logger.extra.update({"env": env_dict}) @@ -1064,15 +1306,17 @@ def app_remove(operation_logger, app, purge=False): finally: shutil.rmtree(tmp_workdir_for_app) - if ret == 0: - logger.success(m18n.n("app_removed", app=app)) - hook_callback("post_app_remove", env=env_dict) - else: - logger.warning(m18n.n("app_not_properly_removed", app=app)) + packaging_format = manifest["packaging_format"] + if packaging_format >= 2: + from yunohost.utils.resources import AppResourceManager - # Remove all permission in LDAP - for permission_name in user_permission_list(apps=[app])["permissions"].keys(): - permission_delete(permission_name, force=True, sync_perm=False) + AppResourceManager(app, wanted={}, current=manifest).apply( + rollback_and_raise_exception_if_failure=False, purge_data_dir=purge + ) + else: + # Remove all permission in LDAP + for permission_name in user_permission_list(apps=[app])["permissions"].keys(): + permission_delete(permission_name, force=True, sync_perm=False) if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) @@ -1083,6 +1327,12 @@ def app_remove(operation_logger, app, purge=False): if domain_config_get(domain, "feature.app.default_app") == app: domain_config_set(domain, "feature.app.default_app", "_none") + if ret == 0: + logger.success(m18n.n("app_removed", app=app)) + hook_callback("post_app_remove", env=env_dict) + else: + logger.warning(m18n.n("app_not_properly_removed", app=app)) + permission_sync_to_user() _assert_system_is_sane_for_app(manifest, "post") @@ -1291,7 +1541,7 @@ def app_register_url(app, domain, path): raise YunohostValidationError("app_already_installed_cant_change_url") # Check the url is available - _assert_no_conflicting_apps(domain, path) + _assert_no_conflicting_apps(domain, path, ignore_app=app) app_setting(app, "domain", value=domain) app_setting(app, "path", value=path) @@ -1315,6 +1565,7 @@ def app_ssowatconf(): """ from yunohost.domain import domain_list, _get_maindomain, domain_config_get from yunohost.permission import user_permission_list + from yunohost.settings import settings_get main_domain = _get_maindomain() domains = domain_list()["domains"] @@ -1332,6 +1583,7 @@ def app_ssowatconf(): "uris": [domain + "/yunohost/admin" for domain in domains] + [domain + "/yunohost/api" for domain in domains] + [ + "re:^[^/]/502%.html$", "re:^[^/]*/%.well%-known/ynh%-diagnosis/.*$", "re:^[^/]*/%.well%-known/acme%-challenge/.*$", "re:^[^/]*/%.well%-known/autoconfig/mail/config%-v1%.1%.xml.*$", @@ -1345,7 +1597,7 @@ def app_ssowatconf(): for app in _installed_apps(): - app_settings = read_yaml(APPS_SETTING_PATH + app + "/settings.yml") + app_settings = read_yaml(APPS_SETTING_PATH + app + "/settings.yml") or {} # Redirected redirected_urls.update(app_settings.get("redirected_urls", {})) @@ -1393,6 +1645,7 @@ def app_ssowatconf(): } conf_dict = { + "theme": settings_get("misc.portal.portal_theme"), "portal_domain": main_domain, "portal_path": "/yunohost/sso/", "additional_headers": { @@ -1429,90 +1682,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 + return AppConfigPanel(app).run_action( + action, args=args, args_file=args_file, operation_logger=operation_logger ) - env_dict["YNH_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!") def app_config_get(app, key="", full=False, export=False): @@ -1531,8 +1710,15 @@ def app_config_get(app, key="", full=False, export=False): else: mode = "classic" - config_ = AppConfigPanel(app) - return config_.get(key, mode) + try: + config_ = AppConfigPanel(app) + return config_.get(key, mode) + except YunohostValidationError as e: + if Moulinette.interface.type == "api" and e.key == "config_no_panel": + # Be more permissive when no config panel found + return {} + else: + raise @is_unit_operation() @@ -1556,6 +1742,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) @@ -1601,6 +1791,7 @@ ynh_app_config_run $1 "app": app, "app_instance_nb": str(app_instance_nb), "final_path": settings.get("final_path", ""), + "install_dir": settings.get("install_dir", ""), "YNH_APP_BASEDIR": os.path.join(APPS_SETTING_PATH, app), } ) @@ -1609,8 +1800,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, app=app) return values @@ -1619,58 +1812,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) @@ -1680,15 +1821,7 @@ def _get_app_actions(app_id): for key, value in toml_actions.items(): action = dict(**value) action["id"] = key - - arguments = [] - for argument_name, argument in value.get("arguments", {}).items(): - argument = dict(**argument) - argument["name"] = argument_name - - arguments.append(argument) - - action["arguments"] = arguments + action["arguments"] = value.get("arguments", {}) actions.append(action) return actions @@ -1713,11 +1846,20 @@ def _get_app_settings(app): ) try: with open(os.path.join(APPS_SETTING_PATH, app, "settings.yml")) as f: - settings = yaml.safe_load(f) + settings = yaml.safe_load(f) or {} # If label contains unicode char, this may later trigger issues when building strings... # FIXME: this should be propagated to read_yaml so that this fix applies everywhere I think... settings = {k: v for k, v in settings.items()} + # App settings should never be empty, there should always be at least some standard, internal keys like id, install_time etc. + # Otherwise, this probably means that the app settings disappeared somehow... + if not settings: + logger.error( + f"It looks like settings.yml for {app} is empty ... This should not happen ..." + ) + logger.error(m18n.n("app_not_correctly_installed", app=app)) + return {} + # Stupid fix for legacy bullshit # In the past, some setups did not have proper normalization for app domain/path # Meaning some setups (as of January 2021) still have path=/foobar/ (with a trailing slash) @@ -1859,21 +2001,7 @@ def _get_manifest_of_app(path): # ¦ ¦ }, if os.path.exists(os.path.join(path, "manifest.toml")): - manifest_toml = read_toml(os.path.join(path, "manifest.toml")) - - manifest = manifest_toml.copy() - - install_arguments = [] - for name, values in ( - manifest_toml.get("arguments", {}).get("install", {}).items() - ): - args = values.copy() - args["name"] = name - - install_arguments.append(args) - - manifest["arguments"]["install"] = install_arguments - + manifest = read_toml(os.path.join(path, "manifest.toml")) elif os.path.exists(os.path.join(path, "manifest.json")): manifest = read_json(os.path.join(path, "manifest.json")) else: @@ -1882,25 +2010,182 @@ def _get_manifest_of_app(path): raw_msg=True, ) - manifest["arguments"] = _set_default_ask_questions(manifest.get("arguments", {})) + manifest["packaging_format"] = float( + str(manifest.get("packaging_format", "")).strip() or "0" + ) + + if manifest["packaging_format"] < 2: + manifest = _convert_v1_manifest_to_v2(manifest) + + manifest["install"] = _set_default_ask_questions(manifest.get("install", {})) + manifest["doc"], manifest["notifications"] = _parse_app_doc_and_notifications(path) + return manifest -def _set_default_ask_questions(arguments): +def _parse_app_doc_and_notifications(path): + + doc = {} + notification_names = ["PRE_INSTALL", "POST_INSTALL", "PRE_UPGRADE", "POST_UPGRADE"] + + for filepath in glob.glob(os.path.join(path, "doc") + "/*.md"): + + # to be improved : [a-z]{2,3} is a clumsy way of parsing the + # lang code ... some lang code are more complex that this é_è + m = re.match("([A-Z]*)(_[a-z]{2,3})?.md", filepath.split("/")[-1]) + + if not m: + # FIXME: shall we display a warning ? idk + continue + + pagename, lang = m.groups() + + if pagename in notification_names: + continue + + lang = lang.strip("_") if lang else "en" + + if pagename not in doc: + doc[pagename] = {} + doc[pagename][lang] = read_file(filepath).strip() + + notifications = {} + + for step in notification_names: + notifications[step] = {} + for filepath in glob.glob(os.path.join(path, "doc", f"{step}*.md")): + m = re.match(step + "(_[a-z]{2,3})?.md", filepath.split("/")[-1]) + if not m: + continue + pagename = "main" + lang = m.groups()[0].strip("_") if m.groups()[0] else "en" + if pagename not in notifications[step]: + notifications[step][pagename] = {} + notifications[step][pagename][lang] = read_file(filepath).strip() + + for filepath in glob.glob(os.path.join(path, "doc", f"{step}.d") + "/*.md"): + m = re.match( + r"([A-Za-z0-9\.\~]*)(_[a-z]{2,3})?.md", filepath.split("/")[-1] + ) + if not m: + continue + pagename, lang = m.groups() + lang = lang.strip("_") if lang else "en" + if pagename not in notifications[step]: + notifications[step][pagename] = {} + notifications[step][pagename][lang] = read_file(filepath).strip() + + return doc, notifications + + +def _hydrate_app_template(template, data): + + stuff_to_replace = set(re.findall(r"__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__", template)) + + for stuff in stuff_to_replace: + + varname = stuff.strip("_").lower() + + if varname in data: + template = template.replace(stuff, data[varname]) + + return template + + +def _convert_v1_manifest_to_v2(manifest): + + manifest = copy.deepcopy(manifest) + + if "upstream" not in manifest: + manifest["upstream"] = {} + + if "license" in manifest and "license" not in manifest["upstream"]: + manifest["upstream"]["license"] = manifest["license"] + + if "url" in manifest and "website" not in manifest["upstream"]: + manifest["upstream"]["website"] = manifest["url"] + + manifest["integration"] = { + "yunohost": manifest.get("requirements", {}) + .get("yunohost", "") + .replace(">", "") + .replace("=", "") + .replace(" ", ""), + "architectures": "?", + "multi_instance": manifest.get("multi_instance", False), + "ldap": "?", + "sso": "?", + "disk": "?", + "ram": {"build": "?", "runtime": "?"}, + } + + maintainers = manifest.get("maintainer", {}) + if isinstance(maintainers, list): + maintainers = [m["name"] for m in maintainers] + else: + maintainers = [maintainers["name"]] if maintainers.get("name") else [] + + manifest["maintainers"] = maintainers + + install_questions = manifest["arguments"]["install"] + + manifest["install"] = {} + for question in install_questions: + name = question.pop("name") + if "ask" in question and name in [ + "domain", + "path", + "admin", + "is_public", + "password", + ]: + question.pop("ask") + if question.get("example") and question.get("type") in [ + "domain", + "path", + "user", + "boolean", + "password", + ]: + question.pop("example") + + manifest["install"][name] = question + + manifest["resources"] = {"system_user": {}, "install_dir": {"alias": "final_path"}} + + keys_to_keep = [ + "packaging_format", + "id", + "name", + "description", + "version", + "maintainers", + "upstream", + "integration", + "install", + "resources", + ] + + keys_to_del = [key for key in manifest.keys() if key not in keys_to_keep] + for key in keys_to_del: + del manifest[key] + + return manifest + + +def _set_default_ask_questions(questions, script_name="install"): # arguments is something like - # { "install": [ - # { "name": "domain", + # { "domain": + # { # "type": "domain", # .... # }, - # { "name": "path", - # "type": "path" + # "path": { + # "type": "path", # ... # }, # ... - # ], - # "upgrade": [ ... ] # } # We set a default for any question with these matching (type, name) @@ -1912,39 +2197,38 @@ def _set_default_ask_questions(arguments): ("path", "path"), # i18n: app_manifest_install_ask_path ("password", "password"), # i18n: app_manifest_install_ask_password ("user", "admin"), # i18n: app_manifest_install_ask_admin - ("boolean", "is_public"), - ] # i18n: app_manifest_install_ask_is_public + ("boolean", "is_public"), # i18n: app_manifest_install_ask_is_public + ( + "group", + "init_main_permission", + ), # i18n: app_manifest_install_ask_init_main_permission + ( + "group", + "init_admin_permission", + ), # i18n: app_manifest_install_ask_init_admin_permission + ] - for script_name, arg_list in arguments.items(): + for question_name, question in questions.items(): + question["name"] = question_name - # We only support questions for install so far, and for other - if script_name != "install": - continue - - for arg in arg_list: - - # Do not override 'ask' field if provided by app ?... Or shall we ? - # if "ask" in arg: - # continue - - # If this arg corresponds to a question with default ask message... - if any( - (arg.get("type"), arg["name"]) == question - for question in questions_with_default - ): - # The key is for example "app_manifest_install_ask_domain" - arg_name = arg["name"] - key = f"app_manifest_{script_name}_ask_{arg_name}" - arg["ask"] = m18n.n(key) + # If this question corresponds to a question with default ask message... + if any( + (question.get("type"), question["name"]) == 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']}" + ) # Also it in fact doesn't make sense for any of those questions to have an example value nor a default value... - if arg.get("type") in ["domain", "user", "password"]: - if "example" in arg: - del arg["example"] - if "default" in arg: - del arg["default"] + if question.get("type") in ["domain", "user", "password"]: + if "example" in question: + del question["example"] + if "default" in question: + del question["default"] - return arguments + return questions def _is_app_repo_url(string: str) -> bool: @@ -2077,6 +2361,10 @@ def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: logger.debug(m18n.n("done")) manifest["remote"] = {"type": "file", "path": path} + manifest["quality"] = {"level": -1, "state": "thirdparty"} + manifest["antifeatures"] = [] + manifest["potential_alternative_to"] = [] + return manifest, extracted_app_folder @@ -2123,9 +2411,36 @@ def _extract_app_from_gitrepo( manifest["remote"]["revision"] = revision manifest["lastUpdate"] = app_info.get("lastUpdate") + manifest["quality"] = { + "level": app_info.get("level", -1), + "state": app_info.get("state", "thirdparty"), + } + manifest["antifeatures"] = app_info.get("antifeatures", []) + manifest["potential_alternative_to"] = app_info.get("potential_alternative_to", []) + return manifest, extracted_app_folder +def _list_upgradable_apps(): + upgradable_apps = list(app_list(upgradable=True)["apps"]) + + # Retrieve next manifest pre_upgrade notifications + for app in upgradable_apps: + absolute_app_name, _ = _parse_app_instance_name(app["id"]) + manifest, extracted_app_folder = _extract_app(absolute_app_name) + app["notifications"] = {} + if manifest["notifications"]["PRE_UPGRADE"]: + app["notifications"]["PRE_UPGRADE"] = _filter_and_hydrate_notifications( + manifest["notifications"]["PRE_UPGRADE"], + app["current_version"], + app["settings"], + ) + del app["settings"] + shutil.rmtree(extracted_app_folder) + + return upgradable_apps + + # # ############################### # # Small utilities # @@ -2174,33 +2489,99 @@ def _get_all_installed_apps_id(): return all_apps_ids_formatted -def _check_manifest_requirements(manifest: Dict): +def _check_manifest_requirements( + manifest: Dict, action: str = "" +) -> Iterator[Tuple[str, bool, object, str]]: """Check if required packages are met from the manifest""" - packaging_format = int(manifest.get("packaging_format", 0)) - if packaging_format not in [0, 1]: + app_id = manifest["id"] + logger.debug(m18n.n("app_requirements_checking", app=app_id)) + + # Packaging format + if manifest["packaging_format"] not in [1, 2]: raise YunohostValidationError("app_packaging_format_not_supported") - requirements = manifest.get("requirements", dict()) + # Yunohost version + required_yunohost_version = ( + manifest["integration"].get("yunohost", "4.3").strip(">= ") + ) + current_yunohost_version = get_ynh_package_version("yunohost")["version"] - if not requirements: - return + yield ( + "required_yunohost_version", + version.parse(required_yunohost_version) + <= version.parse(current_yunohost_version), + {"current": current_yunohost_version, "required": required_yunohost_version}, + "app_yunohost_version_not_supported", # i18n: app_yunohost_version_not_supported + ) - app = manifest.get("id", "?") + # Architectures + arch_requirement = manifest["integration"]["architectures"] + arch = system_arch() - logger.debug(m18n.n("app_requirements_checking", app=app)) + yield ( + "arch", + arch_requirement in ["all", "?"] or arch in arch_requirement, + {"current": arch, "required": arch_requirement}, + "app_arch_not_supported", # i18n: app_arch_not_supported + ) - # Iterate over requirements - for pkgname, spec in requirements.items(): - if not packages.meets_version_specifier(pkgname, spec): - version = packages.ynh_packages_version()[pkgname]["version"] - raise YunohostValidationError( - "app_requirements_unmeet", - pkgname=pkgname, - version=version, - spec=spec, - app=app, + # Multi-instance + if action == "install": + multi_instance = manifest["integration"]["multi_instance"] is True + if not multi_instance: + apps = _installed_apps() + sibling_apps = [ + a for a in apps if a == app_id or a.startswith(f"{app_id}__") + ] + multi_instance = len(sibling_apps) == 0 + + yield ( + "install", + multi_instance, + {"app": app_id}, + "app_already_installed", # i18n: app_already_installed + ) + + # Disk + if action == "install": + root_free_space = free_space_in_directory("/") + var_free_space = free_space_in_directory("/var") + if manifest["integration"]["disk"] == "?": + has_enough_disk = True + else: + disk_req_bin = human_to_binary(manifest["integration"]["disk"]) + has_enough_disk = ( + root_free_space > disk_req_bin and var_free_space > disk_req_bin ) + free_space = binary_to_human(min(root_free_space, var_free_space)) + + yield ( + "disk", + has_enough_disk, + {"current": free_space, "required": manifest["integration"]["disk"]}, + "app_not_enough_disk", # i18n: app_not_enough_disk + ) + + # Ram + ram_requirement = manifest["integration"]["ram"] + ram, swap = ram_available() + # Is "include_swap" really useful ? We should probably decide wether to always include it or not instead + if ram_requirement.get("include_swap", False): + ram += swap + can_build = ram_requirement["build"] == "?" or ram > human_to_binary( + ram_requirement["build"] + ) + can_run = ram_requirement["runtime"] == "?" or ram > human_to_binary( + ram_requirement["runtime"] + ) + + yield ( + "ram", + can_build and can_run, + {"current": binary_to_human(ram), "required": ram_requirement["build"]}, + "app_not_enough_ram", # i18n: app_not_enough_ram + ) def _guess_webapp_path_requirement(app_folder: str) -> str: @@ -2209,13 +2590,17 @@ def _guess_webapp_path_requirement(app_folder: str) -> str: # is an available url and normalize the path. manifest = _get_manifest_of_app(app_folder) - raw_questions = manifest.get("arguments", {}).get("install", {}) + raw_questions = manifest["install"] domain_questions = [ - question for question in raw_questions if question.get("type") == "domain" + question + for question in raw_questions.values() + if question.get("type") == "domain" ] path_questions = [ - question for question in raw_questions if question.get("type") == "path" + question + for question in raw_questions.values() + if question.get("type") == "path" ] if len(domain_questions) == 0 and len(path_questions) == 0: @@ -2287,11 +2672,7 @@ def _get_conflicting_apps(domain, path, ignore_app=None): for p, a in apps_map[domain].items(): if a["id"] == ignore_app: continue - if path == p: - conflicts.append((p, a["id"], a["label"])) - # We also don't want conflicts with other apps starting with - # same name - elif path.startswith(p) or p.startswith(path): + if path == p or path == "/" or p == "/": conflicts.append((p, a["id"], a["label"])) return conflicts @@ -2315,7 +2696,7 @@ def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False def _make_environment_for_app_script( - app, args={}, args_prefix="APP_ARG_", workdir=None + app, args={}, args_prefix="APP_ARG_", workdir=None, action=None ): app_setting_path = os.path.join(APPS_SETTING_PATH, app) @@ -2328,16 +2709,37 @@ def _make_environment_for_app_script( "YNH_APP_INSTANCE_NAME": app, "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), "YNH_APP_MANIFEST_VERSION": manifest.get("version", "?"), - "YNH_ARCH": check_output("dpkg --print-architecture"), + "YNH_APP_PACKAGING_FORMAT": str(manifest["packaging_format"]), + "YNH_ARCH": system_arch(), } if workdir: env_dict["YNH_APP_BASEDIR"] = workdir + if action: + env_dict["YNH_APP_ACTION"] = action + for arg_name, arg_value in args.items(): arg_name_upper = arg_name.upper() env_dict[f"YNH_{args_prefix}{arg_name_upper}"] = str(arg_value) + # If packaging format v2, load all settings + if manifest["packaging_format"] >= 2: + env_dict["app"] = app + for setting_name, setting_value in _get_app_settings(app).items(): + + # Ignore special internal settings like checksum__ + # (not a huge deal to load them but idk...) + if setting_name.startswith("checksum__"): + continue + + env_dict[setting_name] = str(setting_value) + + # Special weird case for backward compatibility... + # 'path' was loaded into 'path_url' ..... + if "path" in env_dict: + env_dict["path_url"] = env_dict["path"] + return env_dict @@ -2417,30 +2819,10 @@ def _make_tmp_workdir_for_app(app=None): return tmpdir -def is_true(arg): - """ - Convert a string into a boolean - - Keyword arguments: - arg -- The string to convert - - Returns: - Boolean - - """ - if isinstance(arg, bool): - return arg - elif isinstance(arg, str): - return arg.lower() in ["yes", "true", "on"] - else: - logger.debug(f"arg should be a boolean or a string, got {arg}") - return True if arg else False - - def unstable_apps(): output = [] - deprecated_apps = ["mailman"] + deprecated_apps = ["mailman", "ffsync"] for infos in app_list(full=True)["apps"]: @@ -2514,8 +2896,116 @@ def _assert_system_is_sane_for_app(manifest, when): "app_action_broke_system", services=", ".join(faulty_services) ) - if packages.dpkg_is_broken(): + if dpkg_is_broken(): if when == "pre": raise YunohostValidationError("dpkg_is_broken") elif when == "post": raise YunohostError("this_action_broke_dpkg") + + +def app_dismiss_notification(app, name): + + assert isinstance(name, str) + name = name.lower() + assert name in ["post_install", "post_upgrade"] + _assert_is_installed(app) + + app_setting(app, f"_dismiss_notification_{name}", value="1") + + +def _notification_is_dismissed(name, settings): + # Check for _dismiss_notiication_$name setting and also auto-dismiss + # notifications after one week (otherwise people using mostly CLI would + # never really dismiss the notification and it would be displayed forever) + + if name == "POST_INSTALL": + return ( + settings.get("_dismiss_notification_post_install") + or (int(time.time()) - settings.get("install_time", 0)) / (24 * 3600) > 7 + ) + elif name == "POST_UPGRADE": + # Check on update_time also implicitly prevent the post_upgrade notification + # from being displayed after install, because update_time is only set during upgrade + return ( + settings.get("_dismiss_notification_post_upgrade") + or (int(time.time()) - settings.get("update_time", 0)) / (24 * 3600) > 7 + ) + else: + return False + + +def _filter_and_hydrate_notifications(notifications, current_version=None, data={}): + def is_version_more_recent_than_current_version(name): + # 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 + name: _hydrate_app_template(_value_for_locale(content_per_lang), data) + for name, content_per_lang in notifications.items() + if current_version is None + or name == "main" + or is_version_more_recent_than_current_version(name) + } + + +def _display_notifications(notifications, force=False): + if not notifications: + return + + for name, content in notifications.items(): + print("==========") + print(content) + print("==========") + + # i18n: confirm_notifications_read + _ask_confirmation("confirm_notifications_read", kind="simple", force=force) + + +# FIXME: move this to Moulinette +def _ask_confirmation( + question: str, + params: dict = {}, + kind: str = "hard", + force: bool = False, +): + """ + Ask confirmation + + Keyword argument: + question -- m18n key or string + params -- dict of values passed to the string formating + kind -- "hard": ask with "Yes, I understand", "soft": "Y/N", "simple": "press enter" + force -- Will not ask for confirmation + + """ + if force or Moulinette.interface.type == "api": + return + + # If ran from the CLI in a non-interactive context, + # skip confirmation (except in hard mode) + if not os.isatty(1) and kind in ["simple", "soft"]: + return + if kind == "simple": + answer = Moulinette.prompt( + m18n.n(question, answers="Press enter to continue", **params), + color="yellow", + ) + answer = True + elif kind == "soft": + answer = Moulinette.prompt( + m18n.n(question, answers="Y/N", **params), color="yellow" + ) + answer = answer.upper() == "Y" + else: + answer = Moulinette.prompt( + m18n.n(question, answers="Yes, I understand", **params), color="red" + ) + answer = answer == "Yes, I understand" + + if not answer: + raise YunohostError("aborting") diff --git a/src/app_catalog.py b/src/app_catalog.py index 5ae8ef30b..5d4378544 100644 --- a/src/app_catalog.py +++ b/src/app_catalog.py @@ -1,5 +1,24 @@ +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re +import hashlib from moulinette import m18n from moulinette.utils.log import getActionLogger @@ -18,17 +37,18 @@ from yunohost.utils.error import YunohostError logger = getActionLogger("yunohost.app_catalog") APPS_CATALOG_CACHE = "/var/cache/yunohost/repo" +APPS_CATALOG_LOGOS = "/usr/share/yunohost/applogos" APPS_CATALOG_CONF = "/etc/yunohost/apps_catalog.yml" -APPS_CATALOG_API_VERSION = 2 +APPS_CATALOG_API_VERSION = 3 APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default" -def app_catalog(full=False, with_categories=False): +def app_catalog(full=False, with_categories=False, with_antifeatures=False): """ Return a dict of apps available to installation from Yunohost's app catalog """ - from yunohost.app import _installed_apps, _set_default_ask_questions + from yunohost.app import _installed_apps # Get app list from catalog cache catalog = _load_apps_catalog() @@ -47,28 +67,38 @@ def app_catalog(full=False, with_categories=False): "description": infos["manifest"]["description"], "level": infos["level"], } - else: - infos["manifest"]["arguments"] = _set_default_ask_questions( - infos["manifest"].get("arguments", {}) - ) - # Trim info for categories if not using --full - for category in catalog["categories"]: - category["title"] = _value_for_locale(category["title"]) - category["description"] = _value_for_locale(category["description"]) - for subtags in category.get("subtags", []): - subtags["title"] = _value_for_locale(subtags["title"]) + _catalog = {"apps": catalog["apps"]} - if not full: - catalog["categories"] = [ - {"id": c["id"], "description": c["description"]} - for c in catalog["categories"] - ] + if with_categories: + for category in catalog["categories"]: + category["title"] = _value_for_locale(category["title"]) + category["description"] = _value_for_locale(category["description"]) + for subtags in category.get("subtags", []): + subtags["title"] = _value_for_locale(subtags["title"]) - if not with_categories: - return {"apps": catalog["apps"]} - else: - return {"apps": catalog["apps"], "categories": catalog["categories"]} + if not full: + catalog["categories"] = [ + {"id": c["id"], "description": c["description"]} + for c in catalog["categories"] + ] + + _catalog["categories"] = catalog["categories"] + + if with_antifeatures: + for antifeature in catalog["antifeatures"]: + antifeature["title"] = _value_for_locale(antifeature["title"]) + antifeature["description"] = _value_for_locale(antifeature["description"]) + + if not full: + catalog["antifeatures"] = [ + {"id": a["id"], "description": a["description"]} + for a in catalog["antifeatures"] + ] + + _catalog["antifeatures"] = catalog["antifeatures"] + + return _catalog def app_search(string): @@ -154,7 +184,13 @@ def _update_apps_catalog(): logger.debug("Initialize folder for apps catalog cache") mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid="root") + if not os.path.exists(APPS_CATALOG_LOGOS): + mkdir(APPS_CATALOG_LOGOS, mode=0o755, parents=True, uid="root") + for apps_catalog in apps_catalog_list: + if apps_catalog["url"] is None: + continue + apps_catalog_id = apps_catalog["id"] actual_api_url = _actual_apps_catalog_api_url(apps_catalog["url"]) @@ -181,6 +217,46 @@ def _update_apps_catalog(): raw_msg=True, ) + # Download missing app logos + logos_to_download = [] + for app, infos in apps_catalog_content["apps"].items(): + logo_hash = infos.get("logo_hash") + if not logo_hash or os.path.exists(f"{APPS_CATALOG_LOGOS}/{logo_hash}.png"): + continue + logos_to_download.append(logo_hash) + + if len(logos_to_download) > 20: + logger.info( + f"(Will fetch {len(logos_to_download)} logos, this may take a couple minutes)" + ) + + import requests + from multiprocessing.pool import ThreadPool + + def fetch_logo(logo_hash): + try: + r = requests.get( + f"{apps_catalog['url']}/v{APPS_CATALOG_API_VERSION}/logos/{logo_hash}.png", + timeout=10, + ) + assert ( + r.status_code == 200 + ), f"Got status code {r.status_code}, expected 200" + if hashlib.sha256(r.content).hexdigest() != logo_hash: + raise Exception( + f"Found inconsistent hash while downloading logo {logo_hash}" + ) + open(f"{APPS_CATALOG_LOGOS}/{logo_hash}.png", "wb").write(r.content) + return True + except Exception as e: + logger.debug(f"Failed to download logo {logo_hash} : {e}") + return False + + results = ThreadPool(8).imap_unordered(fetch_logo, logos_to_download) + for result in results: + # Is this even needed to iterate on the results ? + pass + logger.success(m18n.n("apps_catalog_update_success")) @@ -190,7 +266,7 @@ def _load_apps_catalog(): corresponding to all known apps and categories """ - merged_catalog = {"apps": {}, "categories": []} + merged_catalog = {"apps": {}, "categories": [], "antifeatures": []} for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]: @@ -232,10 +308,17 @@ def _load_apps_catalog(): ) continue + if info.get("level") == "?": + info["level"] = -1 + + # FIXME: we may want to autoconvert all v0/v1 manifest to v2 here + # so that everything is consistent in terms of APIs, datastructure format etc info["repository"] = apps_catalog_id merged_catalog["apps"][app] = info - # Annnnd categories - merged_catalog["categories"] += apps_catalog_content["categories"] + # Annnnd categories + antifeatures + # (we use .get here, only because the dev catalog doesnt include the categories/antifeatures keys) + merged_catalog["categories"] += apps_catalog_content.get("categories", []) + merged_catalog["antifeatures"] += apps_catalog_content.get("antifeatures", []) return merged_catalog diff --git a/src/authenticators/ldap_admin.py b/src/authenticators/ldap_admin.py index 872dd3c8d..22b796e23 100644 --- a/src/authenticators/ldap_admin.py +++ b/src/authenticators/ldap_admin.py @@ -1,5 +1,21 @@ -# -*- coding: utf-8 -*- - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import logging import ldap @@ -11,10 +27,14 @@ from moulinette.authentication import BaseAuthenticator from moulinette.utils.text import random_ascii from yunohost.utils.error import YunohostError, YunohostAuthenticationError - -logger = logging.getLogger("yunohost.authenticators.ldap_admin") +from yunohost.utils.ldap import _get_ldap_interface session_secret = random_ascii() +logger = logging.getLogger("yunohost.authenticators.ldap_admin") + +LDAP_URI = "ldap://localhost:389" +ADMIN_GROUP = "cn=admins,ou=groups" +AUTH_DN = "uid={uid},ou=users,dc=yunohost,dc=org" class Authenticator(BaseAuthenticator): @@ -22,26 +42,59 @@ class Authenticator(BaseAuthenticator): name = "ldap_admin" def __init__(self, *args, **kwargs): - self.uri = "ldap://localhost:389" - self.basedn = "dc=yunohost,dc=org" - self.admindn = "cn=admin,dc=yunohost,dc=org" + pass def _authenticate_credentials(self, credentials=None): - # TODO : change authentication format - # to support another dn to support multi-admins + try: + admins = ( + _get_ldap_interface() + .search(ADMIN_GROUP, attrs=["memberUid"])[0] + .get("memberUid", []) + ) + except ldap.SERVER_DOWN: + # ldap is down, attempt to restart it before really failing + logger.warning(m18n.n("ldap_server_is_down_restart_it")) + os.system("systemctl restart slapd") + time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted + + # Force-reset existing LDAP interface + from yunohost.utils import ldap as ldaputils + + ldaputils._ldap_interface = None + + try: + admins = ( + _get_ldap_interface() + .search(ADMIN_GROUP, attrs=["memberUid"])[0] + .get("memberUid", []) + ) + except ldap.SERVER_DOWN: + raise YunohostError("ldap_server_down") + + try: + uid, password = credentials.split(":", 1) + except ValueError: + raise YunohostError("invalid_credentials") + + # Here we're explicitly using set() which are handled as hash tables + # and should prevent timing attacks to find out the admin usernames? + if uid not in set(admins): + raise YunohostError("invalid_credentials") + + dn = AUTH_DN.format(uid=uid) def _reconnect(): con = ldap.ldapobject.ReconnectLDAPObject( - self.uri, retry_max=10, retry_delay=0.5 + LDAP_URI, retry_max=10, retry_delay=0.5 ) - con.simple_bind_s(self.admindn, credentials) + con.simple_bind_s(dn, password) return con try: con = _reconnect() except ldap.INVALID_CREDENTIALS: - raise YunohostError("invalid_password") + raise YunohostError("invalid_credentials") except ldap.SERVER_DOWN: # ldap is down, attempt to restart it before really failing logger.warning(m18n.n("ldap_server_is_down_restart_it")) @@ -61,9 +114,9 @@ class Authenticator(BaseAuthenticator): logger.warning("Error during ldap authentication process: %s", e) raise else: - if who != self.admindn: + if who != dn: raise YunohostError( - f"Not logged with the appropriate identity ? Found {who}, expected {self.admindn} !?", + f"Not logged with the appropriate identity ? Found {who}, expected {dn} !?", raw_msg=True, ) finally: diff --git a/src/backup.py b/src/backup.py index bba60b895..c3e47bddc 100644 --- a/src/backup.py +++ b/src/backup.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_backup.py - - Manage backups -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import json @@ -39,9 +32,16 @@ from functools import reduce from packaging import version from moulinette import Moulinette, m18n -from moulinette.utils import filesystem from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_file, mkdir, write_to_yaml, read_yaml +from moulinette.utils.filesystem import ( + read_file, + mkdir, + write_to_yaml, + read_yaml, + rm, + chown, + chmod, +) from moulinette.utils.process import check_output import yunohost.domain @@ -50,6 +50,7 @@ from yunohost.app import ( _is_installed, _make_environment_for_app_script, _make_tmp_workdir_for_app, + _get_manifest_of_app, ) from yunohost.hook import ( hook_list, @@ -67,8 +68,12 @@ from yunohost.tools import ( from yunohost.regenconf import regen_conf from yunohost.log import OperationLogger, is_unit_operation from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.utils.packages import ynh_packages_version -from yunohost.utils.filesystem import free_space_in_directory +from yunohost.utils.system import ( + free_space_in_directory, + get_ynh_package_version, + binary_to_human, + space_used_by_directory, +) from yunohost.settings import settings_get BACKUP_PATH = "/home/yunohost.backup" @@ -312,7 +317,7 @@ class BackupManager: "size_details": self.size_details, "apps": self.apps_return, "system": self.system_return, - "from_yunohost_version": ynh_packages_version()["yunohost"]["version"], + "from_yunohost_version": get_ynh_package_version("yunohost")["version"], } @property @@ -342,7 +347,7 @@ class BackupManager: # FIXME replace isdir by exists ? manage better the case where the path # exists if not os.path.isdir(self.work_dir): - filesystem.mkdir(self.work_dir, 0o750, parents=True, uid="admin") + mkdir(self.work_dir, 0o750, parents=True) elif self.is_tmp_work_dir: logger.debug( @@ -357,8 +362,8 @@ class BackupManager: # If umount succeeded, remove the directory (we checked that # we're in /home/yunohost.backup/tmp so that should be okay... # c.f. method clean() which also does this) - filesystem.rm(self.work_dir, recursive=True, force=True) - filesystem.mkdir(self.work_dir, 0o750, parents=True, uid="admin") + rm(self.work_dir, recursive=True, force=True) + mkdir(self.work_dir, 0o750, parents=True) # # Backup target management # @@ -535,7 +540,7 @@ class BackupManager: successfull_system = self.targets.list("system", include=["Success", "Warning"]) if not successfull_apps and not successfull_system: - filesystem.rm(self.work_dir, True, True) + rm(self.work_dir, True, True) raise YunohostError("backup_nothings_done") # Add unlisted files from backup tmp dir @@ -577,7 +582,7 @@ class BackupManager: env_var["YNH_BACKUP_CSV"] = tmp_csv if app is not None: - env_var.update(_make_environment_for_app_script(app)) + env_var.update(_make_environment_for_app_script(app, action="backup")) env_var["YNH_APP_BACKUP_DIR"] = os.path.join( self.work_dir, "apps", app, "backup" ) @@ -647,7 +652,7 @@ class BackupManager: restore_hooks_dir = os.path.join(self.work_dir, "hooks", "restore") if not os.path.exists(restore_hooks_dir): - filesystem.mkdir(restore_hooks_dir, mode=0o700, parents=True, uid="root") + mkdir(restore_hooks_dir, mode=0o700, parents=True, uid="root") restore_hooks = hook_list("restore")["hooks"] @@ -714,7 +719,7 @@ class BackupManager: tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) try: # Prepare backup directory for the app - filesystem.mkdir(tmp_app_bkp_dir, 0o700, True, uid="root") + mkdir(tmp_app_bkp_dir, 0o700, True, uid="root") # Copy the app settings to be able to call _common.sh shutil.copytree(app_setting_path, settings_dir) @@ -753,7 +758,7 @@ class BackupManager: # Remove tmp files in all situations finally: shutil.rmtree(tmp_workdir_for_app) - filesystem.rm(env_dict["YNH_BACKUP_CSV"], force=True) + rm(env_dict["YNH_BACKUP_CSV"], force=True) # # Actual backup archive creation / method management # @@ -796,7 +801,7 @@ class BackupManager: if row["dest"] == "info.json": continue - size = disk_usage(row["source"]) + size = space_used_by_directory(row["source"], follow_symlinks=False) # Add size to apps details splitted_dest = row["dest"].split("/") @@ -949,7 +954,7 @@ class RestoreManager: ret = subprocess.call(["umount", self.work_dir]) if ret != 0: logger.warning(m18n.n("restore_cleaning_failed")) - filesystem.rm(self.work_dir, recursive=True, force=True) + rm(self.work_dir, recursive=True, force=True) # # Restore target manangement # @@ -979,7 +984,7 @@ class RestoreManager: available_restore_system_hooks = hook_list("restore")["hooks"] custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, "restore") - filesystem.mkdir(custom_restore_hook_folder, 755, parents=True, force=True) + mkdir(custom_restore_hook_folder, 755, parents=True, force=True) for system_part in target_list: # By default, we'll use the restore hooks on the current install @@ -1084,7 +1089,7 @@ class RestoreManager: else: raise YunohostError("restore_removing_tmp_dir_failed") - filesystem.mkdir(self.work_dir, parents=True) + mkdir(self.work_dir, parents=True) self.method.mount() @@ -1402,7 +1407,7 @@ class RestoreManager: # Delete _common.sh file in backup common_file = os.path.join(app_backup_in_archive, "_common.sh") - filesystem.rm(common_file, force=True) + rm(common_file, force=True) # Check if the app has a restore script app_restore_script_in_archive = os.path.join(app_scripts_in_archive, "restore") @@ -1418,14 +1423,14 @@ class RestoreManager: ) app_scripts_new_path = os.path.join(app_settings_new_path, "scripts") shutil.copytree(app_settings_in_archive, app_settings_new_path) - filesystem.chmod(app_settings_new_path, 0o400, 0o400, True) - filesystem.chown(app_scripts_new_path, "root", None, True) + chmod(app_settings_new_path, 0o400, 0o400, True) + chown(app_scripts_new_path, "root", None, True) # Copy the app scripts to a writable temporary folder tmp_workdir_for_app = _make_tmp_workdir_for_app() copytree(app_scripts_in_archive, tmp_workdir_for_app) - filesystem.chmod(tmp_workdir_for_app, 0o700, 0o700, True) - filesystem.chown(tmp_workdir_for_app, "root", None, True) + chmod(tmp_workdir_for_app, 0o700, 0o700, True) + chown(tmp_workdir_for_app, "root", None, True) restore_script = os.path.join(tmp_workdir_for_app, "restore") # Restore permissions @@ -1494,7 +1499,7 @@ class RestoreManager: # FIXME : workdir should be a tmp workdir app_workdir = os.path.join(self.work_dir, "apps", app_instance_name, "settings") env_dict = _make_environment_for_app_script( - app_instance_name, workdir=app_workdir + app_instance_name, workdir=app_workdir, action="restore" ) env_dict.update( { @@ -1509,6 +1514,15 @@ class RestoreManager: operation_logger.extra["env"] = env_dict operation_logger.flush() + manifest = _get_manifest_of_app(app_settings_in_archive) + if manifest["packaging_format"] >= 2: + from yunohost.utils.resources import AppResourceManager + + AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( + rollback_and_raise_exception_if_failure=True, + operation_logger=operation_logger, + ) + # Execute the app install script restore_failed = True try: @@ -1727,7 +1741,7 @@ class BackupMethod: raise YunohostError("backup_cleaning_failed") if self.manager.is_tmp_work_dir: - filesystem.rm(self.work_dir, True, True) + rm(self.work_dir, True, True) def _check_is_enough_free_space(self): """ @@ -1775,11 +1789,11 @@ class BackupMethod: # Be sure the parent dir of destination exists if not os.path.isdir(dest_dir): - filesystem.mkdir(dest_dir, parents=True) + mkdir(dest_dir, parents=True) # For directory, attempt to mount bind if os.path.isdir(src): - filesystem.mkdir(dest, parents=True, force=True) + mkdir(dest, parents=True, force=True) try: subprocess.check_call(["mount", "--rbind", src, dest]) @@ -1832,7 +1846,10 @@ class BackupMethod: # to mounting error # Compute size to copy - size = sum(disk_usage(path["source"]) for path in paths_needed_to_be_copied) + size = sum( + space_used_by_directory(path["source"], follow_symlinks=False) + for path in paths_needed_to_be_copied + ) size /= 1024 * 1024 # Convert bytes to megabytes # Ask confirmation for copying @@ -1884,7 +1901,7 @@ class CopyBackupMethod(BackupMethod): dest_parent = os.path.dirname(dest) if not os.path.exists(dest_parent): - filesystem.mkdir(dest_parent, 0o700, True, uid="admin") + mkdir(dest_parent, 0o700, True) if os.path.isdir(source): shutil.copytree(source, dest) @@ -1902,7 +1919,7 @@ class CopyBackupMethod(BackupMethod): if not os.path.isdir(self.repo): raise YunohostError("backup_no_uncompress_archive_dir") - filesystem.mkdir(self.work_dir, parent=True) + mkdir(self.work_dir, parent=True) ret = subprocess.call(["mount", "-r", "--rbind", self.repo, self.work_dir]) if ret == 0: return @@ -1928,7 +1945,7 @@ class TarBackupMethod(BackupMethod): def _archive_file(self): if isinstance(self.manager, BackupManager) and settings_get( - "backup.compress_tar_archives" + "misc.backup.backup_compress_tar_archives" ): return os.path.join(self.repo, self.name + ".tar.gz") @@ -1946,7 +1963,7 @@ class TarBackupMethod(BackupMethod): """ if not os.path.exists(self.repo): - filesystem.mkdir(self.repo, 0o750, parents=True, uid="admin") + mkdir(self.repo, 0o750, parents=True) # Check free space in output self._check_is_enough_free_space() @@ -2628,9 +2645,9 @@ def _create_archive_dir(): if os.path.lexists(ARCHIVES_PATH): raise YunohostError("backup_output_symlink_dir_broken", path=ARCHIVES_PATH) - # Create the archive folder, with 'admin' as owner, such that + # Create the archive folder, with 'admins' as groupowner, such that # people can scp archives out of the server - mkdir(ARCHIVES_PATH, mode=0o750, parents=True, uid="admin", gid="root") + mkdir(ARCHIVES_PATH, mode=0o770, parents=True, gid="admins") def _call_for_each_path(self, callback, csv_path=None): @@ -2667,31 +2684,3 @@ def _recursive_umount(directory): continue return everything_went_fine - - -def disk_usage(path): - # We don't do this in python with os.stat because we don't want - # to follow symlinks - - du_output = check_output(["du", "-sb", path], shell=False) - return int(du_output.split()[0]) - - -def binary_to_human(n, customary=False): - """ - Convert bytes or bits into human readable format with binary prefix - Keyword argument: - n -- Number to convert - customary -- Use customary symbol instead of IEC standard - """ - symbols = ("Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi") - if customary: - symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return "{:.1f}{}".format(value, s) - return "%s" % n diff --git a/src/certificate.py b/src/certificate.py index 2a9fb4ce9..928bea499 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -1,32 +1,24 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2016 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - - yunohost_certificate.py - - Manage certificates, in particular Let's encrypt -""" - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import sys import shutil -import pwd -import grp import subprocess import glob @@ -34,7 +26,8 @@ from datetime import datetime from moulinette import m18n from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_file +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 @@ -95,15 +88,16 @@ 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: _check_domain_is_ready_for_ACME(domain) status["ACME_eligible"] = True - except Exception: - status["ACME_eligible"] = False + except Exception as e: + if e.key == "certmanager_domain_not_diagnosed_yet": + status["ACME_eligible"] = None # = unknown status + else: + status["ACME_eligible"] = False del status["domain"] certificates[domain] = status @@ -131,6 +125,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( @@ -154,7 +149,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,20 +209,24 @@ def _certificate_install_selfsigned(domain_list, force=False): # Check new status indicate a recently created self-signed certificate status = _get_status(domain) - if ( - status - and status["CA_type"]["code"] == "self-signed" - and status["validity"] > 3648 - ): + if status and status["CA_type"] == "selfsigned" and status["validity"] > 3648: logger.success( m18n.n("certmanager_cert_install_success_selfsigned", domain=domain) ) 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 @@ -241,7 +240,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) @@ -253,12 +252,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: @@ -287,11 +287,17 @@ 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): """ @@ -314,7 +320,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? @@ -349,7 +355,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 ) @@ -361,6 +367,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: @@ -402,6 +409,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()) @@ -409,6 +418,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 # @@ -441,11 +455,16 @@ investigate : -- Certificate Manager """ - import smtplib + try: + import smtplib - smtp = smtplib.SMTP("localhost") - smtp.sendmail(from_, [to_], message.encode("utf-8")) - smtp.quit() + smtp = smtplib.SMTP("localhost") + smtp.sendmail(from_, [to_], message.encode("utf-8")) + smtp.quit() + except Exception as e: + # Dont miserably crash the whole auto renew cert when one renewal fails ... + # cf boring cases like https://github.com/YunoHost/issues/issues/2102 + logger.exception(f"Failed to send mail about cert renewal failure ... : {e}") def _check_acme_challenge_configuration(domain): @@ -537,9 +556,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 ) @@ -554,10 +573,11 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Set the domain csr.get_subject().CN = domain - from yunohost.domain import domain_list + from yunohost.domain import domain_config_get - # For "parent" domains, include xmpp-upload subdomain in subject alternate names - if domain in domain_list(exclude_subdomains=True)["domains"]: + # If XMPP is enabled for this domain, add xmpp-upload and muc subdomains + # in subject alternate names + if domain_config_get(domain, key="feature.xmpp.xmpp") == 1: subdomain = "xmpp-upload." + domain xmpp_records = ( Diagnoser.get_cached_report( @@ -565,24 +585,30 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): ).get("data") or {} ) - if xmpp_records.get("CNAME:xmpp-upload") == "OK": + sanlist = [] + for sub in ("xmpp-upload", "muc"): + subdomain = sub + "." + domain + if xmpp_records.get("CNAME:" + sub) == "OK": + sanlist.append(("DNS:" + subdomain)) + else: + logger.warning( + m18n.n( + "certmanager_warning_subdomain_dns_record", + subdomain=subdomain, + domain=domain, + ) + ) + + if sanlist: csr.add_extensions( [ crypto.X509Extension( b"subjectAltName", False, - ("DNS:" + subdomain).encode("utf8"), + (", ".join(sanlist)).encode("utf-8"), ) ] ) - else: - logger.warning( - m18n.n( - "certmanager_warning_subdomain_dns_record", - subdomain=subdomain, - domain=domain, - ) - ) # Set the key with open(key_file, "rt") as f: @@ -603,8 +629,6 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): - import yunohost.domain - cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): @@ -633,59 +657,34 @@ 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", - } - + # Identify that a domain's cert is self-signed if the cert dir + # is actually a symlink to a dir ending with -selfsigned + if os.path.realpath(os.path.join(CERT_FOLDER, domain)).endswith("-selfsigned"): + 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, @@ -693,7 +692,8 @@ def _get_status(domain): "CA_name": cert_issuer, "CA_type": CA_type, "validity": days_remaining, - "summary": status_summary, + "style": style, + "summary": summary, } @@ -719,11 +719,8 @@ def _generate_key(destination_path): def _set_permissions(path, user, group, permissions): - uid = pwd.getpwnam(user).pw_uid - gid = grp.getgrnam(group).gr_gid - - os.chown(path, uid, gid) - os.chmod(path, permissions) + chown(path, user, group) + chmod(path, permissions) def _enable_certificate(domain, new_cert_folder): @@ -746,7 +743,7 @@ def _enable_certificate(domain, new_cert_folder): logger.debug("Restarting services...") - for service in ("postfix", "dovecot", "metronome"): + for service in ("dovecot", "metronome"): # Ugly trick to not restart metronome if it's not installed if ( service == "metronome" @@ -758,7 +755,8 @@ def _enable_certificate(domain, new_cert_folder): if os.path.isfile("/etc/yunohost/installed"): # regen nginx conf to be sure it integrates OCSP Stapling # (We don't do this yet if postinstall is not finished yet) - regen_conf(names=["nginx"]) + # We also regenconf for postfix to propagate the SNI hash map thingy + regen_conf(names=["nginx", "postfix"]) _run_service_command("reload", "nginx") @@ -791,7 +789,7 @@ def _check_domain_is_ready_for_ACME(domain): or {} ) - parent_domain = _get_parent_domain_of(domain) + parent_domain = _get_parent_domain_of(domain, return_self=True) dnsrecords = ( Diagnoser.get_cached_report( @@ -908,6 +906,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/diagnosers/00-basesystem.py b/src/diagnosers/00-basesystem.py index a36394ce8..5793a00aa 100644 --- a/src/diagnosers/00-basesystem.py +++ b/src/diagnosers/00-basesystem.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import json import subprocess @@ -9,7 +25,11 @@ from moulinette.utils import log from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_file, read_json, write_to_json from yunohost.diagnosis import Diagnoser -from yunohost.utils.packages import ynh_packages_version +from yunohost.utils.system import ( + ynh_packages_version, + system_virt, + system_arch, +) logger = log.getActionLogger("yunohost.diagnosis") @@ -22,15 +42,12 @@ class MyDiagnoser(Diagnoser): def run(self): - # Detect virt technology (if not bare metal) and arch - # Gotta have this "|| true" because it systemd-detect-virt return 'none' - # with an error code on bare metal ~.~ - virt = check_output("systemd-detect-virt || true", shell=True) + virt = system_virt() if virt.lower() == "none": virt = "bare-metal" # Detect arch - arch = check_output("dpkg --print-architecture") + arch = system_arch() hardware = dict( meta={"test": "hardware"}, status="INFO", @@ -137,6 +154,37 @@ class MyDiagnoser(Diagnoser): summary="diagnosis_backports_in_sources_list", ) + # Using yunohost testing channel + if ( + os.system( + "grep -q '^\\s*deb\\s*.*yunohost.org.*\\stesting' /etc/apt/sources.list /etc/apt/sources.list.d/*" + ) + == 0 + ): + yield dict( + meta={"test": "apt_yunohost_channel"}, + status="WARNING", + summary="diagnosis_using_yunohost_testing", + details=["diagnosis_using_yunohost_testing_details"], + ) + + # Apt being mapped to 'stable' (instead of 'buster/bullseye/bookworm/trixie/...') + # will cause the machine to spontaenously upgrade everything as soon as next debian is released ... + # Note that we grep this from the policy for libc6, because it's hard to know exactly which apt repo + # is configured (it may not be simply debian.org) + if ( + os.system( + "apt policy libc6 2>/dev/null | grep '^\\s*500' | awk '{print $3}' | tr '/' ' ' | awk '{print $1}' | grep -q 'stable'" + ) + == 0 + ): + yield dict( + meta={"test": "apt_debian_codename"}, + status="WARNING", + summary="diagnosis_using_stable_codename", + details=["diagnosis_using_stable_codename_details"], + ) + if self.number_of_recent_auth_failure() > 750: yield dict( meta={"test": "high_number_auth_failure"}, diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index 247c486fc..b2bedc802 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os import random @@ -221,5 +237,5 @@ class MyDiagnoser(Diagnoser): except Exception as e: protocol = str(protocol) e = str(e) - self.logger_debug(f"Could not get public IPv{protocol} : {e}") + logger.debug(f"Could not get public IPv{protocol} : {e}") return None diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py index 4d30bb1a7..92d795ea9 100644 --- a/src/diagnosers/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re from typing import List @@ -105,7 +121,7 @@ class MyDiagnoser(Diagnoser): if r["value"] == "@": r["value"] = domain + "." elif r["type"] == "CNAME": - r["value"] = r["value"] + f".{base_dns_zone}." + r["value"] = r["value"] # + f".{base_dns_zone}." if self.current_record_match_expected(r): results[id_] = "OK" @@ -207,6 +223,11 @@ class MyDiagnoser(Diagnoser): expected = r["value"].split()[-1] current = r["current"].split()[-1] return expected == current + elif r["type"] == "CAA": + # For CAA, check only the last item, ignore the 0 / 128 nightmare + expected = r["value"].split()[-1] + current = r["current"].split()[-1] + return expected == current else: return r["current"] == r["value"] diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index be172e524..5671211b5 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os from typing import List diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index 584505ad1..4a69895b2 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -1,11 +1,27 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import random import requests from typing import List -from moulinette.utils.filesystem import read_file +from moulinette.utils.filesystem import read_file, mkdir, rm from yunohost.diagnosis import Diagnoser from yunohost.domain import domain_list @@ -46,8 +62,8 @@ class MyDiagnoser(Diagnoser): domains_to_check.append(domain) self.nonce = "".join(random.choice("0123456789abcedf") for i in range(16)) - os.system("rm -rf /tmp/.well-known/ynh-diagnosis/") - os.system("mkdir -p /tmp/.well-known/ynh-diagnosis/") + rm("/tmp/.well-known/ynh-diagnosis/", recursive=True, force=True) + mkdir("/tmp/.well-known/ynh-diagnosis/", parents=True) os.system("touch /tmp/.well-known/ynh-diagnosis/%s" % self.nonce) if not domains_to_check: diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 7fe7a08db..88d6a8259 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import dns.resolver import re @@ -291,7 +307,7 @@ class MyDiagnoser(Diagnoser): if global_ipv4: outgoing_ips.append(global_ipv4) - if settings_get("smtp.allow_ipv6"): + if settings_get("email.smtp.smtp_allow_ipv6"): ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": outgoing_ipversions.append(6) diff --git a/src/diagnosers/30-services.py b/src/diagnosers/30-services.py index f09688911..7adfd7c01 100644 --- a/src/diagnosers/30-services.py +++ b/src/diagnosers/30-services.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os from typing import List diff --git a/src/diagnosers/50-systemresources.py b/src/diagnosers/50-systemresources.py index 6ac7f0ec4..50933b9f9 100644 --- a/src/diagnosers/50-systemresources.py +++ b/src/diagnosers/50-systemresources.py @@ -1,4 +1,21 @@ -#!/usr/bin/env python +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import psutil import datetime diff --git a/src/diagnosers/70-regenconf.py b/src/diagnosers/70-regenconf.py index 591f883a4..8c0bf74cc 100644 --- a/src/diagnosers/70-regenconf.py +++ b/src/diagnosers/70-regenconf.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re from typing import List @@ -53,7 +69,7 @@ class MyDiagnoser(Diagnoser): ) # Check consistency between actual ssh port in sshd_config vs. setting - ssh_port_setting = settings_get("security.ssh.port") + ssh_port_setting = settings_get("security.ssh.ssh_port") ssh_port_line = re.findall( r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config") ) diff --git a/src/diagnosers/80-apps.py b/src/diagnosers/80-apps.py index 56e45f831..faff925e6 100644 --- a/src/diagnosers/80-apps.py +++ b/src/diagnosers/80-apps.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os from typing import List @@ -64,7 +80,9 @@ class MyDiagnoser(Diagnoser): yunohost_version_req = ( app["manifest"].get("requirements", {}).get("yunohost", "").strip(">= ") ) - if yunohost_version_req.startswith("2."): + if yunohost_version_req.startswith("2.") or yunohost_version_req.startswith( + "3." + ): yield ("error", "diagnosis_apps_outdated_ynh_requirement") deprecated_helpers = [ diff --git a/src/diagnosers/__init__.py b/src/diagnosers/__init__.py index e69de29bb..5cad500fa 100644 --- a/src/diagnosers/__init__.py +++ b/src/diagnosers/__init__.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# diff --git a/src/diagnosis.py b/src/diagnosis.py index 007719dfc..2dff6a40d 100644 --- a/src/diagnosis.py +++ b/src/diagnosis.py @@ -1,29 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" diagnosis.py - - Look for possible issues on the server -""" - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os import time diff --git a/src/dns.py b/src/dns.py index 44547d412..085f47471 100644 --- a/src/dns.py +++ b/src/dns.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_domain.py - - Manage domains -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import time @@ -35,12 +28,12 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, write_to_file, read_toml, mkdir from yunohost.domain import ( - domain_list, _assert_domain_exists, domain_config_get, _get_domain_settings, _set_domain_settings, _list_subdomains_of, + _get_parent_domain_of, ) from yunohost.utils.dns import dig, is_yunohost_dyndns_domain, is_special_use_tld from yunohost.utils.error import YunohostValidationError, YunohostError @@ -235,10 +228,10 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): "SRV", f"0 5 5269 {domain}.", ], - [f"muc{suffix}", ttl, "CNAME", basename], - [f"pubsub{suffix}", ttl, "CNAME", basename], - [f"vjud{suffix}", ttl, "CNAME", basename], - [f"xmpp-upload{suffix}", ttl, "CNAME", basename], + [f"muc{suffix}", ttl, "CNAME", f"{domain}."], + [f"pubsub{suffix}", ttl, "CNAME", f"{domain}."], + [f"vjud{suffix}", ttl, "CNAME", f"{domain}."], + [f"xmpp-upload{suffix}", ttl, "CNAME", f"{domain}."], ] ######### @@ -446,8 +439,8 @@ def _get_dns_zone_for_domain(domain): # This is another strick to try to prevent this function from being # a bottleneck on system with 1 main domain + 10ish subdomains # when building the dns conf for the main domain (which will call domain_config_get, etc...) - parent_domain = domain.split(".", 1)[1] - if parent_domain in domain_list()["domains"]: + parent_domain = _get_parent_domain_of(domain) + if parent_domain: parent_cache_file = f"{cache_folder}/{parent_domain}" if ( os.path.exists(parent_cache_file) @@ -512,17 +505,21 @@ def _get_registrar_config_section(domain): from lexicon.providers.auto import _relevant_provider_for_domain - registrar_infos = {} + registrar_infos = { + "name": m18n.n( + "registrar_infos" + ), # This is meant to name the config panel section, for proper display in the webadmin + } dns_zone = _get_dns_zone_for_domain(domain) # If parent domain exists in yunohost - parent_domain = domain.split(".", 1)[1] - if parent_domain in domain_list()["domains"]: + parent_domain = _get_parent_domain_of(domain, topest=True) + if parent_domain: # Dirty hack to have a link on the webadmin if Moulinette.interface.type == "api": - parent_domain_link = f"[{parent_domain}](#/domains/{parent_domain}/config)" + parent_domain_link = f"[{parent_domain}](#/domains/{parent_domain}/dns)" else: parent_domain_link = parent_domain @@ -532,7 +529,7 @@ def _get_registrar_config_section(domain): "style": "info", "ask": m18n.n( "domain_dns_registrar_managed_in_parent_domain", - parent_domain=domain, + parent_domain=parent_domain, parent_domain_link=parent_domain_link, ), "value": "parent_domain", @@ -664,7 +661,7 @@ def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, return {} if registrar == "parent_domain": - parent_domain = domain.split(".", 1)[1] + parent_domain = _get_parent_domain_of(domain, topest=True) registrar, registrar_credentials = _get_registar_settings(parent_domain) if any(registrar_credentials.values()): raise YunohostValidationError( diff --git a/src/domain.py b/src/domain.py index f6b99b5cb..67f39aa71 100644 --- a/src/domain.py +++ b/src/domain.py @@ -1,30 +1,25 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_domain.py - - Manage domains -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os -from typing import Dict, Any +import time +from typing import List, Optional +from collections import OrderedDict from moulinette import m18n, Moulinette from moulinette.core import MoulinetteError @@ -45,71 +40,151 @@ from yunohost.log import is_unit_operation logger = getActionLogger("yunohost.domain") -DOMAIN_CONFIG_PATH = "/usr/share/yunohost/config_domain.toml" DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" # Lazy dev caching to avoid re-query ldap every time we need the domain list -domain_list_cache: Dict[str, Any] = {} +# The cache automatically expire every 15 seconds, to prevent desync between +# yunohost CLI and API which run in different processes +domain_list_cache: List[str] = [] +domain_list_cache_timestamp = 0 +main_domain_cache: Optional[str] = None +main_domain_cache_timestamp = 0 +DOMAIN_CACHE_DURATION = 15 -def domain_list(exclude_subdomains=False, auto_push=False, full=False): +def _get_maindomain(): + global main_domain_cache + global main_domain_cache_timestamp + if ( + not main_domain_cache + or abs(main_domain_cache_timestamp - time.time()) > DOMAIN_CACHE_DURATION + ): + with open("/etc/yunohost/current_host", "r") as f: + main_domain_cache = f.readline().rstrip() + main_domain_cache_timestamp = time.time() + + return main_domain_cache + + +def _get_domains(exclude_subdomains=False): + global domain_list_cache + global domain_list_cache_timestamp + if ( + not domain_list_cache + or abs(domain_list_cache_timestamp - time.time()) > DOMAIN_CACHE_DURATION + ): + from yunohost.utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + result = [ + entry["virtualdomain"][0] + for entry in ldap.search("ou=domains", "virtualdomain=*", ["virtualdomain"]) + ] + + def cmp_domain(domain): + # Keep the main part of the domain and the extension together + # eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] + domain = domain.split(".") + domain[-1] = domain[-2] + domain.pop() + return list(reversed(domain)) + + domain_list_cache = sorted(result, key=cmp_domain) + domain_list_cache_timestamp = time.time() + + if exclude_subdomains: + return [ + domain for domain in domain_list_cache if not _get_parent_domain_of(domain) + ] + + return domain_list_cache + + +def domain_list(exclude_subdomains=False, tree=False, features=[]): """ List domains Keyword argument: exclude_subdomains -- Filter out domains that are subdomains of other declared domains + tree -- Display domains as a hierarchy tree """ - global 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 + domains = _get_domains(exclude_subdomains) + main = _get_maindomain() - ldap = _get_ldap_interface() - result = [ - entry["virtualdomain"][0] - for entry in ldap.search("ou=domains", "virtualdomain=*", ["virtualdomain"]) - ] + if 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 - result_list = [] - for domain in result: - if exclude_subdomains: - 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 + if not tree: + return {"domains": domains, "main": main} - result_list.append(domain) + if tree and exclude_subdomains: + return { + "domains": OrderedDict({domain: {} for domain in domains}), + "main": main, + } - def cmp_domain(domain): - # Keep the main part of the domain and the extension together - # eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] - domain = domain.split(".") - domain[-1] = domain[-2] + domain.pop() - domain = list(reversed(domain)) - return domain + def get_parent_dict(tree, child): + # If parent exists it should be the last added (see `_get_domains` ordering) + possible_parent = next(reversed(tree)) if tree else None + if possible_parent and child.endswith(f".{possible_parent}"): + return get_parent_dict(tree[possible_parent], child) + return tree - result_list = sorted(result_list, key=cmp_domain) + result = OrderedDict() + for domain in domains: + parent = get_parent_dict(result, domain) + parent[domain] = OrderedDict() - 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} + return {"domains": result, "main": main} - 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 +def domain_info(domain): + """ + Print aggregate data for a specific domain - return result + Keyword argument: + domain -- Domain to be checked + """ + + from yunohost.app import app_info + from yunohost.dns import _get_registar_settings + + _assert_domain_exists(domain) + + registrar, _ = _get_registar_settings(domain) + certificate = domain_cert_status([domain], full=True)["certificates"][domain] + + apps = [] + for app in _installed_apps(): + settings = _get_app_settings(app) + if settings.get("domain") == domain: + apps.append( + { + "name": app_info(app)["name"], + "id": app, + "path": settings.get("path", ""), + } + ) + + return { + "certificate": certificate, + "registrar": registrar, + "apps": apps, + "main": _get_maindomain() == domain, + "topest_parent": _get_parent_domain_of(domain, topest=True), + # TODO : add parent / child domains ? + } def _assert_domain_exists(domain): - if domain not in domain_list()["domains"]: + if domain not in _get_domains(): raise YunohostValidationError("domain_unknown", domain=domain) @@ -118,26 +193,24 @@ def _list_subdomains_of(parent_domain): _assert_domain_exists(parent_domain) out = [] - for domain in domain_list()["domains"]: + for domain in _get_domains(): if domain.endswith(f".{parent_domain}"): out.append(domain) return out -def _get_parent_domain_of(domain): +def _get_parent_domain_of(domain, return_self=False, topest=False): - _assert_domain_exists(domain) + domains = _get_domains(exclude_subdomains=topest) - if "." not in domain: - return domain + domain_ = domain + while "." in domain_: + domain_ = domain_.split(".", 1)[1] + if domain_ in domains: + return domain_ - parent_domain = domain.split(".", 1)[-1] - if parent_domain not in domain_list()["domains"]: - return domain # Domain is its own parent - - else: - return _get_parent_domain_of(parent_domain) + return domain if return_self else None @is_unit_operation(exclude=["dyndns_password_recovery"]) @@ -163,6 +236,9 @@ def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subsc if domain.startswith("xmpp-upload."): raise YunohostValidationError("domain_cannot_add_xmpp_upload") + if domain.startswith("muc."): + raise YunohostError("domain_cannot_add_muc_upload") + ldap = _get_ldap_interface() try: @@ -222,7 +298,7 @@ def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subsc raise YunohostError("domain_creation_failed", domain=domain, error=e) finally: global domain_list_cache - domain_list_cache = {} + domain_list_cache = [] # Don't regen these conf if we're still in postinstall if os.path.exists("/etc/yunohost/installed"): @@ -283,7 +359,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd # Check domain is not the main domain if domain == _get_maindomain(): - other_domains = domain_list()["domains"] + other_domains = _get_domains() other_domains.remove(domain) if other_domains: @@ -353,7 +429,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd raise YunohostError("domain_deletion_failed", domain=domain, error=e) finally: global domain_list_cache - domain_list_cache = {} + domain_list_cache = [] stuff_to_delete = [ f"/etc/yunohost/certs/{domain}", @@ -449,6 +525,8 @@ def domain_main_domain(operation_logger, new_main_domain=None): if not new_main_domain: return {"current_main_domain": _get_maindomain()} + old_main_domain = _get_maindomain() + # Check domain exists _assert_domain_exists(new_main_domain) @@ -458,8 +536,8 @@ def domain_main_domain(operation_logger, new_main_domain=None): # Apply changes to ssl certs try: write_to_file("/etc/yunohost/current_host", new_main_domain) - global domain_list_cache - domain_list_cache = {} + global main_domain_cache + main_domain_cache = new_main_domain _set_hostname(new_main_domain) except Exception as e: logger.warning(str(e), exc_info=1) @@ -472,6 +550,12 @@ def domain_main_domain(operation_logger, new_main_domain=None): if os.path.exists("/etc/yunohost/installed"): regen_conf() + from yunohost.user import _update_admins_group_aliases + + _update_admins_group_aliases( + old_main_domain=old_main_domain, new_main_domain=new_main_domain + ) + logger.success(m18n.n("main_domain_changed")) @@ -487,12 +571,6 @@ 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() - return maindomain - - def domain_config_get(domain, key="", full=False, export=False): """ Display a domain configuration @@ -538,7 +616,7 @@ class DomainConfigPanel(ConfigPanel): ): from yunohost.app import app_ssowatconf, app_map - if "/" in app_map(raw=True)[self.entity]: + if "/" in app_map(raw=True).get(self.entity, {}): raise YunohostValidationError( "app_make_default_location_already_used", app=self.future_values["default_app"], @@ -555,6 +633,30 @@ class DomainConfigPanel(ConfigPanel): ): app_ssowatconf() + stuff_to_regen_conf = [] + if ( + "xmpp" in self.future_values + and self.future_values["xmpp"] != self.values["xmpp"] + ): + stuff_to_regen_conf.append("nginx") + stuff_to_regen_conf.append("metronome") + + if ( + "mail_in" in self.future_values + and self.future_values["mail_in"] != self.values["mail_in"] + ) or ( + "mail_out" in self.future_values + and self.future_values["mail_out"] != self.values["mail_out"] + ): + if "nginx" not in stuff_to_regen_conf: + stuff_to_regen_conf.append("nginx") + stuff_to_regen_conf.append("postfix") + stuff_to_regen_conf.append("dovecot") + stuff_to_regen_conf.append("rspamd") + + if stuff_to_regen_conf: + regen_conf(names=stuff_to_regen_conf) + def _get_toml(self): toml = super()._get_toml() @@ -576,6 +678,32 @@ 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"]["cert"]["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"]["cert"]["cert_summary"]["ask"] = m18n.n( + f"domain_config_cert_summary_{status['summary']}" + ) + + # Other specific strings used in config panels + # i18n: domain_config_cert_renew_help + + # 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): @@ -588,6 +716,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/dyndns.py b/src/dyndns.py index 9fd25442c..d1049e756 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_dyndns.py - - Subscribe and Update DynDNS Hosts -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import json import glob diff --git a/src/firewall.py b/src/firewall.py index a1c0b187f..6cf68f1f7 100644 --- a/src/firewall.py +++ b/src/firewall.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_firewall.py - - Manage firewall rules -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import yaml import miniupnpc @@ -39,7 +32,13 @@ logger = getActionLogger("yunohost.firewall") def firewall_allow( - protocol, port, ipv4_only=False, ipv6_only=False, no_upnp=False, no_reload=False + protocol, + port, + ipv4_only=False, + ipv6_only=False, + no_upnp=False, + no_reload=False, + reload_only_if_change=False, ): """ Allow connections on a port @@ -77,14 +76,20 @@ def firewall_allow( "ipv6", ] + changed = False + for p in protocols: # Iterate over IP versions to add port for i in ipvs: if port not in firewall[i][p]: firewall[i][p].append(port) + changed = True else: ipv = "IPv%s" % i[3] - logger.warning(m18n.n("port_already_opened", port=port, ip_version=ipv)) + if not reload_only_if_change: + logger.warning( + m18n.n("port_already_opened", port=port, ip_version=ipv) + ) # Add port forwarding with UPnP if not no_upnp and port not in firewall["uPnP"][p]: firewall["uPnP"][p].append(port) @@ -96,12 +101,18 @@ def firewall_allow( # Update and reload firewall _update_firewall_file(firewall) - if not no_reload: + if not no_reload or (reload_only_if_change and changed): return firewall_reload() def firewall_disallow( - protocol, port, ipv4_only=False, ipv6_only=False, upnp_only=False, no_reload=False + protocol, + port, + ipv4_only=False, + ipv6_only=False, + upnp_only=False, + no_reload=False, + reload_only_if_change=False, ): """ Disallow connections on a port @@ -146,14 +157,20 @@ def firewall_disallow( elif upnp_only: ipvs = [] + changed = False + for p in protocols: # Iterate over IP versions to remove port for i in ipvs: if port in firewall[i][p]: firewall[i][p].remove(port) + changed = True else: ipv = "IPv%s" % i[3] - logger.warning(m18n.n("port_already_closed", port=port, ip_version=ipv)) + if not reload_only_if_change: + logger.warning( + m18n.n("port_already_closed", port=port, ip_version=ipv) + ) # Remove port forwarding with UPnP if upnp and port in firewall["uPnP"][p]: firewall["uPnP"][p].remove(port) @@ -163,7 +180,7 @@ def firewall_disallow( # Update and reload firewall _update_firewall_file(firewall) - if not no_reload: + if not no_reload or (reload_only_if_change and changed): return firewall_reload() diff --git a/src/hook.py b/src/hook.py index 70d3b281b..d985f5184 100644 --- a/src/hook.py +++ b/src/hook.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_hook.py - - Manage hooks -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import sys diff --git a/src/log.py b/src/log.py index 9f9e0b753..6525b904d 100644 --- a/src/log.py +++ b/src/log.py @@ -1,29 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_log.py - - Manage debug logs -""" - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import yaml @@ -38,7 +30,7 @@ from io import IOBase from moulinette import m18n, Moulinette from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.utils.packages import get_ynh_package_version +from yunohost.utils.system import get_ynh_package_version from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, read_yaml diff --git a/src/migrations/0021_migrate_to_bullseye.py b/src/migrations/0021_migrate_to_bullseye.py index 72dab1c4d..54917cf95 100644 --- a/src/migrations/0021_migrate_to_bullseye.py +++ b/src/migrations/0021_migrate_to_bullseye.py @@ -15,8 +15,8 @@ from yunohost.tools import ( ) from yunohost.app import unstable_apps from yunohost.regenconf import manually_modified_files, _force_clear_hashes -from yunohost.utils.filesystem import free_space_in_directory -from yunohost.utils.packages import ( +from yunohost.utils.system import ( + free_space_in_directory, get_ynh_package_version, _list_upgradable_apt_packages, ) @@ -27,8 +27,48 @@ logger = getActionLogger("yunohost.migration") N_CURRENT_DEBIAN = 10 N_CURRENT_YUNOHOST = 4 -N_NEXT_DEBAN = 11 -N_NEXT_YUNOHOST = 11 +VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bullseye_upgrade.txt" + + +def _get_all_venvs(dir, level=0, maxlevel=3): + """ + Returns the list of all python virtual env directories recursively + + Arguments: + dir - the directory to scan in + maxlevel - the depth of the recursion + level - do not edit this, used as an iterator + """ + if not os.path.exists(dir): + return [] + + result = [] + # Using os functions instead of glob, because glob doesn't support hidden folders, and we need recursion with a fixed depth + for file in os.listdir(dir): + path = os.path.join(dir, file) + if os.path.isdir(path): + activatepath = os.path.join(path, "bin", "activate") + if os.path.isfile(activatepath): + content = read_file(activatepath) + if ("VIRTUAL_ENV" in content) and ("PYTHONHOME" in content): + result.append(path) + continue + if level < maxlevel: + result += _get_all_venvs(path, level=level + 1) + return result + + +def _backup_pip_freeze_for_python_app_venvs(): + """ + Generate a requirements file for all python virtual env located inside /opt/ and /var/www/ + """ + + venvs = _get_all_venvs("/opt/") + _get_all_venvs("/var/www/") + for venv in venvs: + # Generate a requirements file from venv + os.system( + f"{venv}/bin/pip freeze > {venv}{VENV_REQUIREMENTS_SUFFIX} 2>/dev/null" + ) class MyMigration(Migration): @@ -56,6 +96,9 @@ class MyMigration(Migration): logger.info(m18n.n("migration_0021_patching_sources_list")) self.patch_apt_sources_list() + # Stupid OVH has some repo configured which dont work with bullseye and break apt ... + os.system("sudo rm -f /etc/apt/sources.list.d/ovh-*.list") + # Force add sury if it's not there yet # This is to solve some weird issue with php-common breaking php7.3-common, # hence breaking many php7.3-deps @@ -66,9 +109,23 @@ class MyMigration(Migration): open("/etc/apt/sources.list.d/extra_php_version.list", "w").write( "deb https://packages.sury.org/php/ bullseye main" ) - os.system( - 'wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/extra_php_version.gpg"' - ) + + # Add Sury key even if extra_php_version.list was already there, + # because some old system may be using an outdated key not valid for Bullseye + # and that'll block the migration + os.system( + 'wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/extra_php_version.gpg"' + ) + + # Remove legacy, duplicated sury entry if it exists + if os.path.exists("/etc/apt/sources.list.d/sury.list"): + os.system("rm -rf /etc/apt/sources.list.d/sury.list") + + # + # Get requirements of the different venvs from python apps + # + + _backup_pip_freeze_for_python_app_venvs() # # Run apt update @@ -141,6 +198,47 @@ class MyMigration(Migration): del services["postgresql"] _save_services(services) + # + # Critical fix for RPI otherwise network is down after rebooting + # https://forum.yunohost.org/t/20652 + # + if os.system("systemctl | grep -q dhcpcd") == 0: + logger.info("Applying fix for DHCPCD ...") + os.system("mkdir -p /etc/systemd/system/dhcpcd.service.d") + write_to_file( + "/etc/systemd/system/dhcpcd.service.d/wait.conf", + "[Service]\nExecStart=\nExecStart=/usr/sbin/dhcpcd -w", + ) + + # + # Another boring fix for the super annoying libc6-dev: Breaks libgcc-8-dev + # https://forum.yunohost.org/t/20617 + # + if ( + os.system("dpkg --list | grep '^ii' | grep -q ' libgcc-8-dev'") == 0 + and os.system( + "LC_ALL=C apt policy libgcc-8-dev | grep Candidate | grep -q rpi" + ) + == 0 + ): + logger.info( + "Attempting to fix the build-essential / libc6-dev / libgcc-8-dev hell ..." + ) + os.system("cp /var/lib/dpkg/status /root/dpkg_status.bkp") + # This removes the dependency to build-essential from $app-ynh-deps + os.system( + "perl -i~ -0777 -pe 's/(Package: .*-ynh-deps\\n(.+:.+\\n)+Depends:.*)(build-essential, ?)(.*)/$1$4/g' /var/lib/dpkg/status" + ) + self.apt_install( + "build-essential-" + ) # Note the '-' suffix to mean that we actually want to remove the packages + os.system( + "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt autoremove --assume-yes" + ) + self.apt_install( + "gcc-8- libgcc-8-dev- equivs" + ) # Note the '-' suffix to mean that we actually want to remove the packages .. we also explicitly add 'equivs' to the list because sometimes apt is dumb and will derp about it + # # Main upgrade # @@ -233,9 +331,19 @@ class MyMigration(Migration): # Clean the mess logger.info(m18n.n("migration_0021_cleaning_up")) - os.system("apt autoremove --assume-yes") + os.system( + "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt autoremove --assume-yes" + ) os.system("apt clean --assume-yes") + # + # Stupid hack for stupid dnsmasq not picking up its new init.d script then breaking everything ... + # https://forum.yunohost.org/t/20676 + # + if os.path.exists("/etc/init.d/dnsmasq.dpkg-dist"): + logger.info("Copying new version for /etc/init.d/dnsmasq ...") + os.system("cp /etc/init.d/dnsmasq.dpkg-dist /etc/init.d/dnsmasq") + # # Yunohost upgrade # @@ -290,16 +398,38 @@ class MyMigration(Migration): not self.debian_major_version() == N_CURRENT_DEBIAN and not self.yunohost_major_version() == N_CURRENT_YUNOHOST ): - raise YunohostError("migration_0021_not_buster") + try: + # Here we try to find the previous migration log, which should be somewhat recent and be at least 10k (we keep the biggest one) + maybe_previous_migration_log_id = check_output( + "cd /var/log/yunohost/categories/operation && find -name '*migrate*.log' -size +10k -mtime -100 -exec ls -s {} \\; | sort -n | tr './' ' ' | awk '{print $2}' | tail -n 1" + ) + if maybe_previous_migration_log_id: + logger.info( + f"NB: the previous migration log id seems to be {maybe_previous_migration_log_id}. You can share it with the support team with : sudo yunohost log share {maybe_previous_migration_log_id}" + ) + except Exception: + # Yeah it's not that important ... it's to simplify support ... + pass + + raise YunohostError("migration_0021_not_buster2") # Have > 1 Go free space on /var/ ? if free_space_in_directory("/var/") / (1024**3) < 1.0: raise YunohostError("migration_0021_not_enough_free_space") + # Have > 70 MB free space on /var/ ? + if free_space_in_directory("/boot/") / (1024**2) < 70.0: + raise YunohostError( + "/boot/ has less than 70MB available. This will probably trigger a crash during the upgrade because a new kernel needs to be installed. Please look for advice on the forum on how to remove old, unused kernels to free up some space in /boot/.", + raw_msg=True, + ) + # Check system is up to date # (but we don't if 'bullseye' is already in the sources.list ... # which means maybe a previous upgrade crashed and we're re-running it) - if " bullseye " not in read_file("/etc/apt/sources.list"): + if os.path.exists("/etc/apt/sources.list") and " bullseye " not in read_file( + "/etc/apt/sources.list" + ): tools_update(target="system") upgradable_system_packages = list(_list_upgradable_apt_packages()) upgradable_system_packages = [ @@ -345,15 +475,10 @@ class MyMigration(Migration): message = m18n.n("migration_0021_general_warning") - # FIXME: update this message with updated topic link once we release the migration as stable message = ( - "N.B.: **THIS MIGRATION IS STILL IN BETA-STAGE** ! If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read and share feedbacks on this forum thread: https://forum.yunohost.org/t/18531\n\n" + "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/20590\n\n" + message ) - # message = ( - # "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/12195\n\n" - # + message - # ) if problematic_apps: message += "\n\n" + m18n.n( @@ -371,7 +496,8 @@ class MyMigration(Migration): def patch_apt_sources_list(self): sources_list = glob.glob("/etc/apt/sources.list.d/*.list") - sources_list.append("/etc/apt/sources.list") + if os.path.exists("/etc/apt/sources.list"): + sources_list.append("/etc/apt/sources.list") # This : # - replace single 'buster' occurence by 'bulleye' diff --git a/src/migrations/0023_postgresql_11_to_13.py b/src/migrations/0023_postgresql_11_to_13.py index 8f03f8c5f..f0128da0b 100644 --- a/src/migrations/0023_postgresql_11_to_13.py +++ b/src/migrations/0023_postgresql_11_to_13.py @@ -1,12 +1,13 @@ import subprocess import time +import os from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils.log import getActionLogger from yunohost.tools import Migration -from yunohost.utils.filesystem import free_space_in_directory, space_used_by_directory +from yunohost.utils.system import free_space_in_directory, space_used_by_directory logger = getActionLogger("yunohost.migration") @@ -19,6 +20,15 @@ class MyMigration(Migration): def run(self): + if ( + os.system( + 'grep -A10 "ynh-deps" /var/lib/dpkg/status | grep -E "Package:|Depends:" | grep -B1 postgresql' + ) + != 0 + ): + logger.info("No YunoHost app seem to require postgresql... Skipping!") + return + if not self.package_is_installed("postgresql-11"): logger.warning(m18n.n("migration_0023_postgresql_11_not_installed")) return diff --git a/src/migrations/0024_rebuild_python_venv.py b/src/migrations/0024_rebuild_python_venv.py new file mode 100644 index 000000000..d5aa7fc10 --- /dev/null +++ b/src/migrations/0024_rebuild_python_venv.py @@ -0,0 +1,189 @@ +import os + +from moulinette import m18n +from moulinette.utils.log import getActionLogger +from moulinette.utils.process import call_async_output + +from yunohost.tools import Migration, tools_migrations_state +from moulinette.utils.filesystem import rm + + +logger = getActionLogger("yunohost.migration") + +VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bullseye_upgrade.txt" + + +def extract_app_from_venv_path(venv_path): + + venv_path = venv_path.replace("/var/www/", "") + venv_path = venv_path.replace("/opt/yunohost/", "") + venv_path = venv_path.replace("/opt/", "") + return venv_path.split("/")[0] + + +def _get_all_venvs(dir, level=0, maxlevel=3): + """ + Returns the list of all python virtual env directories recursively + + Arguments: + dir - the directory to scan in + maxlevel - the depth of the recursion + level - do not edit this, used as an iterator + """ + if not os.path.exists(dir): + return [] + + # Using os functions instead of glob, because glob doesn't support hidden + # folders, and we need recursion with a fixed depth + result = [] + for file in os.listdir(dir): + path = os.path.join(dir, file) + if os.path.isdir(path): + activatepath = os.path.join(path, "bin", "activate") + if os.path.isfile(activatepath) and os.path.isfile( + path + VENV_REQUIREMENTS_SUFFIX + ): + result.append(path) + continue + if level < maxlevel: + result += _get_all_venvs(path, level=level + 1) + return result + + +class MyMigration(Migration): + """ + After the update, recreate a python virtual env based on the previously + generated requirements file + """ + + ignored_python_apps = [ + "calibreweb", + "django-for-runners", + "ffsync", + "jupiterlab", + "librephotos", + "mautrix", + "mediadrop", + "mopidy", + "pgadmin", + "tracim", + "synapse", + "matrix-synapse", + "weblate", + ] + + dependencies = ["migrate_to_bullseye"] + state = None + + def is_pending(self): + if not self.state: + self.state = tools_migrations_state()["migrations"].get( + "0024_rebuild_python_venv", "pending" + ) + return self.state == "pending" + + @property + def mode(self): + if not self.is_pending(): + return "auto" + + if _get_all_venvs("/opt/") + _get_all_venvs("/var/www/"): + return "manual" + else: + return "auto" + + @property + def disclaimer(self): + # Avoid having a super long disclaimer to generate if migrations has + # been done + if not self.is_pending(): + return None + + # Disclaimer should be empty if in auto, otherwise it excepts the --accept-disclaimer option during debian postinst + if self.mode == "auto": + return None + + ignored_apps = [] + rebuild_apps = [] + + venvs = _get_all_venvs("/opt/") + _get_all_venvs("/var/www/") + for venv in venvs: + if not os.path.isfile(venv + VENV_REQUIREMENTS_SUFFIX): + continue + + app_corresponding_to_venv = extract_app_from_venv_path(venv) + + # Search for ignore apps + if any( + app_corresponding_to_venv.startswith(app) + for app in self.ignored_python_apps + ): + ignored_apps.append(app_corresponding_to_venv) + else: + rebuild_apps.append(app_corresponding_to_venv) + + msg = m18n.n("migration_0024_rebuild_python_venv_disclaimer_base") + if rebuild_apps: + msg += "\n\n" + m18n.n( + "migration_0024_rebuild_python_venv_disclaimer_rebuild", + rebuild_apps="\n - " + "\n - ".join(rebuild_apps), + ) + if ignored_apps: + msg += "\n\n" + m18n.n( + "migration_0024_rebuild_python_venv_disclaimer_ignored", + ignored_apps="\n - " + "\n - ".join(ignored_apps), + ) + + return msg + + def run(self): + + if self.mode == "auto": + return + + venvs = _get_all_venvs("/opt/") + _get_all_venvs("/var/www/") + for venv in venvs: + + app_corresponding_to_venv = extract_app_from_venv_path(venv) + + # Search for ignore apps + if any( + app_corresponding_to_venv.startswith(app) + for app in self.ignored_python_apps + ): + rm(venv + VENV_REQUIREMENTS_SUFFIX) + logger.info( + m18n.n( + "migration_0024_rebuild_python_venv_broken_app", + app=app_corresponding_to_venv, + ) + ) + continue + + logger.info( + m18n.n( + "migration_0024_rebuild_python_venv_in_progress", + app=app_corresponding_to_venv, + ) + ) + + # Recreate the venv + rm(venv, recursive=True) + callbacks = ( + lambda l: logger.debug("+ " + l.rstrip() + "\r"), + lambda l: logger.warning(l.rstrip()), + ) + call_async_output(["python", "-m", "venv", venv], callbacks) + status = call_async_output( + [f"{venv}/bin/pip", "install", "-r", venv + VENV_REQUIREMENTS_SUFFIX], + callbacks, + ) + if status != 0: + logger.error( + m18n.n( + "migration_0024_rebuild_python_venv_failed", + app=app_corresponding_to_venv, + ) + ) + else: + rm(venv + VENV_REQUIREMENTS_SUFFIX) diff --git a/src/migrations/0025_global_settings_to_configpanel.py b/src/migrations/0025_global_settings_to_configpanel.py new file mode 100644 index 000000000..3a43ccb13 --- /dev/null +++ b/src/migrations/0025_global_settings_to_configpanel.py @@ -0,0 +1,43 @@ +import os + +from yunohost.utils.error import YunohostError +from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_json, write_to_yaml + +from yunohost.tools import Migration +from yunohost.utils.legacy import translate_legacy_settings_to_configpanel_settings + +logger = getActionLogger("yunohost.migration") + +SETTINGS_PATH = "/etc/yunohost/settings.yml" +OLD_SETTINGS_PATH = "/etc/yunohost/settings.json" + + +class MyMigration(Migration): + + "Migrate old global settings to the new ConfigPanel global settings" + + dependencies = ["migrate_to_bullseye"] + + def run(self): + if not os.path.exists(OLD_SETTINGS_PATH): + return + + try: + old_settings = read_json(OLD_SETTINGS_PATH) + except Exception as e: + raise YunohostError(f"Can't open setting file : {e}", raw_msg=True) + + settings = { + translate_legacy_settings_to_configpanel_settings(k).split(".")[-1]: v[ + "value" + ] + for k, v in old_settings.items() + } + + if settings.get("smtp_relay_host"): + settings["smtp_relay_enabled"] = True + + # Here we don't use settings_set() from settings.py to prevent + # Questions to be asked when one run the migration from CLI. + write_to_yaml(SETTINGS_PATH, settings) diff --git a/src/migrations/0026_new_admins_group.py b/src/migrations/0026_new_admins_group.py new file mode 100644 index 000000000..5d9167ae7 --- /dev/null +++ b/src/migrations/0026_new_admins_group.py @@ -0,0 +1,139 @@ +from moulinette.utils.log import getActionLogger + +from yunohost.tools import Migration + +logger = getActionLogger("yunohost.migration") + +################################################### +# Tools used also for restoration +################################################### + + +class MyMigration(Migration): + """ + Add new permissions around SSH/SFTP features + """ + + introduced_in_version = "11.1" # FIXME? + dependencies = [] + + ldap_migration_started = False + + @Migration.ldap_migration + def run(self, *args): + + from yunohost.user import ( + user_list, + user_info, + user_group_update, + user_update, + user_group_add_mailalias, + ADMIN_ALIASES, + ) + from yunohost.utils.ldap import _get_ldap_interface + from yunohost.permission import permission_sync_to_user + from yunohost.domain import _get_maindomain + + main_domain = _get_maindomain() + ldap = _get_ldap_interface() + + all_users = user_list()["users"].keys() + new_admin_user = None + for user in all_users: + if any( + alias.startswith("root@") + for alias in user_info(user).get("mail-aliases", []) + ): + new_admin_user = user + break + + self.ldap_migration_started = True + + if new_admin_user: + aliases = user_info(new_admin_user).get("mail-aliases", []) + old_admin_aliases_to_remove = [ + alias + for alias in aliases + if any( + alias.startswith(a) + for a in [ + "root@", + "admin@", + "admins@", + "webmaster@", + "postmaster@", + "abuse@", + ] + ) + ] + + user_update(new_admin_user, remove_mailalias=old_admin_aliases_to_remove) + + admin_hashs = ldap.search("cn=admin", attrs={"userPassword"})[0]["userPassword"] + + stuff_to_delete = [ + "cn=admin,ou=sudo", + "cn=admin", + "cn=admins,ou=groups", + ] + + for stuff in stuff_to_delete: + if ldap.search(stuff): + ldap.remove(stuff) + + ldap.add( + "cn=admins,ou=sudo", + { + "cn": ["admins"], + "objectClass": ["top", "sudoRole"], + "sudoCommand": ["ALL"], + "sudoUser": ["%admins"], + "sudoHost": ["ALL"], + }, + ) + + ldap.add( + "cn=admins,ou=groups", + { + "cn": ["admins"], + "objectClass": ["top", "posixGroup", "groupOfNamesYnh"], + "gidNumber": ["4001"], + }, + ) + + user_group_add_mailalias( + "admins", [f"{alias}@{main_domain}" for alias in ADMIN_ALIASES] + ) + + permission_sync_to_user() + + if new_admin_user: + user_group_update(groupname="admins", add=new_admin_user, sync_perm=True) + + # Re-add admin as a regular user + attr_dict = { + "objectClass": [ + "mailAccount", + "inetOrgPerson", + "posixAccount", + "userPermissionYnh", + ], + "givenName": ["Admin"], + "sn": ["Admin"], + "displayName": ["Admin"], + "cn": ["Admin"], + "uid": ["admin"], + "mail": "admin_legacy", + "maildrop": ["admin"], + "mailuserquota": ["0"], + "userPassword": admin_hashs, + "gidNumber": ["1007"], + "uidNumber": ["1007"], + "homeDirectory": ["/home/admin"], + "loginShell": ["/bin/bash"], + } + ldap.add("uid=admin,ou=users", attr_dict) + user_group_update(groupname="admins", add="admin", sync_perm=True) + + def run_after_system_restore(self): + self.run() diff --git a/src/permission.py b/src/permission.py index 2a6f6d954..e451bb74c 100644 --- a/src/permission.py +++ b/src/permission.py @@ -1,29 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2014 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_permission.py - - Manage permissions -""" - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import copy import grp @@ -487,6 +479,7 @@ def permission_url( url=None, add_url=None, remove_url=None, + set_url=None, auth_header=None, clear_urls=False, sync_perm=True, @@ -499,6 +492,7 @@ def permission_url( url -- (optional) URL for which access will be allowed/forbidden. add_url -- (optional) List of additional url to add for which access will be allowed/forbidden remove_url -- (optional) List of additional url to remove for which access will be allowed/forbidden + set_url -- (optional) List of additional url to set/replace for which access will be allowed/forbidden auth_header -- (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application clear_urls -- (optional) Clean all urls (url and additional_urls) """ @@ -564,6 +558,9 @@ def permission_url( new_additional_urls = [u for u in new_additional_urls if u not in remove_url] + if set_url: + new_additional_urls = set_url + if auth_header is None: auth_header = existing_permission["auth_header"] diff --git a/src/regenconf.py b/src/regenconf.py index e513a1506..f1163e66a 100644 --- a/src/regenconf.py +++ b/src/regenconf.py @@ -1,24 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2019 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import yaml import shutil diff --git a/src/service.py b/src/service.py index 506d3223e..1f1c35c44 100644 --- a/src/service.py +++ b/src/service.py @@ -1,29 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_service.py - - Manage services -""" - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os import time @@ -710,6 +702,10 @@ def _get_services(): ) php_fpm_versions = [v for v in php_fpm_versions.split("\n") if v.strip()] for version in php_fpm_versions: + # Skip php 7.3 which is most likely dead after buster->bullseye migration + # because users get spooked + if version == "7.3": + continue services[f"php{version}-fpm"] = { "log": f"/var/log/php{version}-fpm.log", "test_conf": f"php-fpm{version} --test", # ofc the service is phpx.y-fpm but the program is php-fpmx.y because why not ... diff --git a/src/settings.py b/src/settings.py index cec416550..d9ea600a4 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,129 +1,39 @@ +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os -import json import subprocess -from datetime import datetime -from collections import OrderedDict - from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.config import ConfigPanel, Question from moulinette.utils.log import getActionLogger from yunohost.regenconf import regen_conf from yunohost.firewall import firewall_reload +from yunohost.log import is_unit_operation +from yunohost.utils.legacy import translate_legacy_settings_to_configpanel_settings logger = getActionLogger("yunohost.settings") -SETTINGS_PATH = "/etc/yunohost/settings.json" -SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json" +SETTINGS_PATH = "/etc/yunohost/settings.yml" -def is_boolean(value): - TRUE = ["true", "on", "yes", "y", "1"] - FALSE = ["false", "off", "no", "n", "0"] - - """ - Ensure a string value is intended as a boolean - - Keyword arguments: - arg -- The string to check - - Returns: - (is_boolean, boolean_value) - - """ - if isinstance(value, bool): - return True, value - if value in [0, 1]: - return True, bool(value) - elif isinstance(value, str): - if str(value).lower() in TRUE + FALSE: - return True, str(value).lower() in TRUE - else: - return False, None - else: - return False, None - - -# a settings entry is in the form of: -# namespace.subnamespace.name: {type, value, default, description, [choices]} -# choices is only for enum -# the keyname can have as many subnamespace as needed but should have at least -# one level of namespace - -# description is implied from the translated strings -# the key is "global_settings_setting_%s" % key.replace(".", "_") - -# type can be: -# * bool -# * int -# * string -# * enum (in the form of a python list) - -DEFAULTS = OrderedDict( - [ - # Password Validation - # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest - ("security.password.admin.strength", {"type": "int", "default": 1}), - ("security.password.user.strength", {"type": "int", "default": 1}), - ( - "service.ssh.allow_deprecated_dsa_hostkey", - {"type": "bool", "default": False}, - ), - ( - "security.ssh.compatibility", - { - "type": "enum", - "default": "modern", - "choices": ["intermediate", "modern"], - }, - ), - ( - "security.ssh.port", - {"type": "int", "default": 22}, - ), - ( - "security.ssh.password_authentication", - {"type": "bool", "default": True}, - ), - ( - "security.nginx.redirect_to_https", - { - "type": "bool", - "default": True, - }, - ), - ( - "security.nginx.compatibility", - { - "type": "enum", - "default": "intermediate", - "choices": ["intermediate", "modern"], - }, - ), - ( - "security.postfix.compatibility", - { - "type": "enum", - "default": "intermediate", - "choices": ["intermediate", "modern"], - }, - ), - ("pop3.enabled", {"type": "bool", "default": False}), - ("smtp.allow_ipv6", {"type": "bool", "default": True}), - ("smtp.relay.host", {"type": "string", "default": ""}), - ("smtp.relay.port", {"type": "int", "default": 587}), - ("smtp.relay.user", {"type": "string", "default": ""}), - ("smtp.relay.password", {"type": "string", "default": ""}), - ("backup.compress_tar_archives", {"type": "bool", "default": False}), - ("ssowat.panel_overlay.enabled", {"type": "bool", "default": True}), - ("security.webadmin.allowlist.enabled", {"type": "bool", "default": False}), - ("security.webadmin.allowlist", {"type": "string", "default": ""}), - ("security.experimental.enabled", {"type": "bool", "default": False}), - ] -) - - -def settings_get(key, full=False): +def settings_get(key="", full=False, export=False): """ Get an entry value in the settings @@ -131,28 +41,39 @@ def settings_get(key, full=False): key -- Settings key """ - settings = _get_settings() - - if key not in settings: + if full and export: raise YunohostValidationError( - "global_settings_key_doesnt_exists", settings_key=key + "You can't use --full and --export together.", raw_msg=True ) if full: - return settings[key] + mode = "full" + elif export: + mode = "export" + else: + mode = "classic" - return settings[key]["value"] + settings = SettingsConfigPanel() + key = translate_legacy_settings_to_configpanel_settings(key) + return settings.get(key, mode) -def settings_list(): - """ - List all entries of the settings +def settings_list(full=False): - """ - return _get_settings() + settings = settings_get(full=full) + + if full: + return settings + else: + return { + k: v + for k, v in settings.items() + if not k.startswith("security.root_access") + } -def settings_set(key, value): +@is_unit_operation() +def settings_set(operation_logger, key=None, value=None, args=None, args_file=None): """ Set an entry value in the settings @@ -161,78 +82,14 @@ def settings_set(key, value): value -- New value """ - settings = _get_settings() - - if key not in settings: - raise YunohostValidationError( - "global_settings_key_doesnt_exists", settings_key=key - ) - - key_type = settings[key]["type"] - - if key_type == "bool": - boolean_value = is_boolean(value) - if boolean_value[0]: - value = boolean_value[1] - else: - raise YunohostValidationError( - "global_settings_bad_type_for_setting", - setting=key, - received_type=type(value).__name__, - expected_type=key_type, - ) - elif key_type == "int": - if not isinstance(value, int) or isinstance(value, bool): - if isinstance(value, str): - try: - value = int(value) - except Exception: - raise YunohostValidationError( - "global_settings_bad_type_for_setting", - setting=key, - received_type=type(value).__name__, - expected_type=key_type, - ) - else: - raise YunohostValidationError( - "global_settings_bad_type_for_setting", - setting=key, - received_type=type(value).__name__, - expected_type=key_type, - ) - elif key_type == "string": - if not isinstance(value, str): - raise YunohostValidationError( - "global_settings_bad_type_for_setting", - setting=key, - received_type=type(value).__name__, - expected_type=key_type, - ) - elif key_type == "enum": - if value not in settings[key]["choices"]: - raise YunohostValidationError( - "global_settings_bad_choice_for_enum", - setting=key, - choice=str(value), - available_choices=", ".join(settings[key]["choices"]), - ) - else: - raise YunohostValidationError( - "global_settings_unknown_type", setting=key, unknown_type=key_type - ) - - old_value = settings[key].get("value") - settings[key]["value"] = value - _save_settings(settings) - - try: - trigger_post_change_hook(key, old_value, value) - except Exception as e: - logger.error(f"Post-change hook for setting {key} failed : {e}") - raise + Question.operation_logger = operation_logger + settings = SettingsConfigPanel() + key = translate_legacy_settings_to_configpanel_settings(key) + return settings.set(key, value, args, args_file, operation_logger=operation_logger) -def settings_reset(key): +@is_unit_operation() +def settings_reset(operation_logger, key): """ Set an entry value to its default one @@ -240,18 +97,14 @@ def settings_reset(key): key -- Settings key """ - settings = _get_settings() - if key not in settings: - raise YunohostValidationError( - "global_settings_key_doesnt_exists", settings_key=key - ) - - settings[key]["value"] = settings[key]["default"] - _save_settings(settings) + settings = SettingsConfigPanel() + key = translate_legacy_settings_to_configpanel_settings(key) + return settings.reset(key, operation_logger=operation_logger) -def settings_reset_all(): +@is_unit_operation() +def settings_reset_all(operation_logger): """ Reset all settings to their default value @@ -259,110 +112,155 @@ def settings_reset_all(): yes -- Yes I'm sure I want to do that """ - settings = _get_settings() - - # For now on, we backup the previous settings in case of but we don't have - # any mecanism to take advantage of those backups. It could be a nice - # addition but we'll see if this is a common need. - # Another solution would be to use etckeeper and integrate those - # modification inside of it and take advantage of its git history - old_settings_backup_path = ( - SETTINGS_PATH_OTHER_LOCATION % datetime.utcnow().strftime("%F_%X") - ) - _save_settings(settings, location=old_settings_backup_path) - - for value in settings.values(): - value["value"] = value["default"] - - _save_settings(settings) - - return { - "old_settings_backup_path": old_settings_backup_path, - "message": m18n.n( - "global_settings_reset_success", path=old_settings_backup_path - ), - } + settings = SettingsConfigPanel() + return settings.reset(operation_logger=operation_logger) -def _get_setting_description(key): - return m18n.n(f"global_settings_setting_{key}".replace(".", "_")) +class SettingsConfigPanel(ConfigPanel): + entity_type = "global" + save_path_tpl = SETTINGS_PATH + save_mode = "diff" + virtual_settings = ["root_password", "root_password_confirm", "passwordless_sudo"] + def __init__(self, config_path=None, save_path=None, creation=False): + super().__init__("settings") -def _get_settings(): + def _apply(self): - settings = {} + root_password = self.new_values.pop("root_password", None) + root_password_confirm = self.new_values.pop("root_password_confirm", None) + passwordless_sudo = self.new_values.pop("passwordless_sudo", None) - for key, value in DEFAULTS.copy().items(): - settings[key] = value - settings[key]["value"] = value["default"] - settings[key]["description"] = _get_setting_description(key) + self.values = { + k: v for k, v in self.values.items() if k not in self.virtual_settings + } + self.new_values = { + k: v for k, v in self.new_values.items() if k not in self.virtual_settings + } - if not os.path.exists(SETTINGS_PATH): - return settings + assert all(v not in self.future_values for v in self.virtual_settings) - # we have a very strict policy on only allowing settings that we know in - # the OrderedDict DEFAULTS - # For various reason, while reading the local settings we might encounter - # settings that aren't in DEFAULTS, those can come from settings key that - # we have removed, errors or the user trying to modify - # /etc/yunohost/settings.json - # To avoid to simply overwrite them, we store them in - # /etc/yunohost/settings-unknown.json in case of - unknown_settings = {} - unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown" + if root_password and root_password.strip(): - if os.path.exists(unknown_settings_path): + if root_password != root_password_confirm: + raise YunohostValidationError("password_confirmation_not_the_same") + + from yunohost.tools import tools_rootpw + + tools_rootpw(root_password, check_strength=True) + + if passwordless_sudo is not None: + from yunohost.utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + ldap.update( + "cn=admins,ou=sudo", + {"sudoOption": ["!authenticate"] if passwordless_sudo else []}, + ) + + super()._apply() + + settings = { + k: v for k, v in self.future_values.items() if self.values.get(k) != v + } + for setting_name, value in settings.items(): + try: + trigger_post_change_hook( + setting_name, self.values.get(setting_name), value + ) + except Exception as e: + logger.error(f"Post-change hook for setting failed : {e}") + raise + + def _get_toml(self): + + toml = super()._get_toml() + + # Dynamic choice list for portal themes + THEMEDIR = "/usr/share/ssowat/portal/assets/themes/" try: - unknown_settings = json.load(open(unknown_settings_path, "r")) - except Exception as e: - logger.warning(f"Error while loading unknown settings {e}") + themes = [d for d in os.listdir(THEMEDIR) if os.path.isdir(THEMEDIR + d)] + except Exception: + themes = ["unsplash", "vapor", "light", "default", "clouds"] + toml["misc"]["portal"]["portal_theme"]["choices"] = themes - try: - with open(SETTINGS_PATH) as settings_fd: - local_settings = json.load(settings_fd) + return toml - for key, value in local_settings.items(): - if key in settings: - settings[key] = value - settings[key]["description"] = _get_setting_description(key) - else: - logger.warning( - m18n.n( - "global_settings_unknown_setting_from_settings_file", - setting_key=key, - ) + def _load_current_values(self): + + super()._load_current_values() + + # Specific logic for those settings who are "virtual" settings + # and only meant to have a custom setter mapped to tools_rootpw + self.values["root_password"] = "" + self.values["root_password_confirm"] = "" + + # Specific logic for virtual setting "passwordless_sudo" + try: + from yunohost.utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + self.values["passwordless_sudo"] = "!authenticate" in ldap.search( + "ou=sudo", "cn=admins", ["sudoOption"] + )[0].get("sudoOption", []) + except: + self.values["passwordless_sudo"] = False + + def get(self, key="", mode="classic"): + + result = super().get(key=key, mode=mode) + + if mode == "full": + for panel, section, option in self._iterate(): + if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"): + option["help"] = m18n.n( + self.config["i18n"] + "_" + option["id"] + "_help" ) - unknown_settings[key] = value - except Exception as e: - raise YunohostValidationError("global_settings_cant_open_settings", reason=e) + return self.config + + # Dirty hack to let settings_get() to work from a python script + if isinstance(result, str) and result in ["True", "False"]: + result = bool(result == "True") + + return result + + def reset(self, key="", operation_logger=None): + self.filter_key = key + + # Read config panel toml + self._get_config_panel() + + if not self.config: + raise YunohostValidationError("config_no_panel") + + # Replace all values with default values + self.values = self._get_default_values() + + Question.operation_logger = operation_logger + + if operation_logger: + operation_logger.start() - if unknown_settings: try: - _save_settings(unknown_settings, location=unknown_settings_path) - _save_settings(settings) - except Exception as e: - logger.warning(f"Failed to save unknown settings (because {e}), aborting.") + self._apply() + 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_apply_failed", error=error)) + raise + # Something wrong happened in Yunohost's code (most probably hook_exec) + except Exception: + import traceback - return settings + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error(m18n.n("config_apply_failed", error=error)) + raise - -def _save_settings(settings, location=SETTINGS_PATH): - settings_without_description = {} - for key, value in settings.items(): - settings_without_description[key] = value - if "description" in value: - del settings_without_description[key]["description"] - - try: - result = json.dumps(settings_without_description, indent=4) - except Exception as e: - raise YunohostError("global_settings_cant_serialize_settings", reason=e) - - try: - with open(location, "w") as settings_fd: - settings_fd.write(result) - except Exception as e: - raise YunohostError("global_settings_cant_write_settings", reason=e) + logger.success(m18n.n("global_settings_reset_success")) + operation_logger.success() # Meant to be a dict of setting_name -> function to call @@ -370,13 +268,8 @@ post_change_hooks = {} def post_change_hook(setting_name): + # TODO: Check that setting_name exists def decorator(func): - assert ( - setting_name in DEFAULTS.keys() - ), f"The setting {setting_name} does not exists" - assert ( - setting_name not in post_change_hooks - ), f"You can only register one post change hook per setting (in particular for {setting_name})" post_change_hooks[setting_name] = func return func @@ -404,48 +297,56 @@ def trigger_post_change_hook(setting_name, old_value, new_value): # =========================================== -@post_change_hook("ssowat.panel_overlay.enabled") -@post_change_hook("security.nginx.redirect_to_https") -@post_change_hook("security.nginx.compatibility") -@post_change_hook("security.webadmin.allowlist.enabled") -@post_change_hook("security.webadmin.allowlist") +@post_change_hook("portal_theme") +def regen_ssowatconf(setting_name, old_value, new_value): + if old_value != new_value: + from yunohost.app import app_ssowatconf + + app_ssowatconf() + + +@post_change_hook("ssowat_panel_overlay_enabled") +@post_change_hook("nginx_redirect_to_https") +@post_change_hook("nginx_compatibility") +@post_change_hook("webadmin_allowlist_enabled") +@post_change_hook("webadmin_allowlist") def reconfigure_nginx(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["nginx"]) -@post_change_hook("security.experimental.enabled") +@post_change_hook("security_experimental_enabled") def reconfigure_nginx_and_yunohost(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["nginx", "yunohost"]) -@post_change_hook("security.ssh.compatibility") -@post_change_hook("security.ssh.password_authentication") +@post_change_hook("ssh_compatibility") +@post_change_hook("ssh_password_authentication") def reconfigure_ssh(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["ssh"]) -@post_change_hook("security.ssh.port") +@post_change_hook("ssh_port") def reconfigure_ssh_and_fail2ban(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["ssh", "fail2ban"]) firewall_reload() -@post_change_hook("smtp.allow_ipv6") -@post_change_hook("smtp.relay.host") -@post_change_hook("smtp.relay.port") -@post_change_hook("smtp.relay.user") -@post_change_hook("smtp.relay.password") -@post_change_hook("security.postfix.compatibility") +@post_change_hook("smtp_allow_ipv6") +@post_change_hook("smtp_relay_host") +@post_change_hook("smtp_relay_port") +@post_change_hook("smtp_relay_user") +@post_change_hook("smtp_relay_password") +@post_change_hook("postfix_compatibility") def reconfigure_postfix(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["postfix"]) -@post_change_hook("pop3.enabled") +@post_change_hook("pop3_enabled") def reconfigure_dovecot(setting_name, old_value, new_value): dovecot_package = "dovecot-pop3d" diff --git a/src/ssh.py b/src/ssh.py index b89dc6c8e..d5951cba5 100644 --- a/src/ssh.py +++ b/src/ssh.py @@ -1,4 +1,21 @@ -# encoding: utf-8 +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os @@ -158,15 +175,6 @@ def _get_user_for_ssh(username, attrs=None): "home_path": root_unix.pw_dir, } - if username == "admin": - admin_unix = pwd.getpwnam("admin") - return { - "username": "admin", - "fullname": "", - "mail": "", - "home_path": admin_unix.pw_dir, - } - # TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html from yunohost.utils.ldap import _get_ldap_interface diff --git a/src/tests/test_app_config.py b/src/tests/test_app_config.py index d6cf8045d..b524a7a51 100644 --- a/src/tests/test_app_config.py +++ b/src/tests/test_app_config.py @@ -102,14 +102,14 @@ def config_app(request): def test_app_config_get(config_app): - user_create("alice", "Alice", "White", _get_maindomain(), "test123Ynh") + user_create("alice", _get_maindomain(), "test123Ynh", fullname="Alice White") assert isinstance(app_config_get(config_app), dict) assert isinstance(app_config_get(config_app, full=True), dict) assert isinstance(app_config_get(config_app, export=True), dict) assert isinstance(app_config_get(config_app, "main"), dict) assert isinstance(app_config_get(config_app, "main.components"), dict) - assert app_config_get(config_app, "main.components.boolean") == "0" + assert app_config_get(config_app, "main.components.boolean") == 0 user_delete("alice") @@ -141,16 +141,16 @@ def test_app_config_get_nonexistentstuff(config_app): def test_app_config_regular_setting(config_app): - assert app_config_get(config_app, "main.components.boolean") == "0" + assert app_config_get(config_app, "main.components.boolean") == 0 app_config_set(config_app, "main.components.boolean", "no") - assert app_config_get(config_app, "main.components.boolean") == "0" + assert app_config_get(config_app, "main.components.boolean") == 0 assert app_setting(config_app, "boolean") == "0" app_config_set(config_app, "main.components.boolean", "yes") - assert app_config_get(config_app, "main.components.boolean") == "1" + assert app_config_get(config_app, "main.components.boolean") == 1 assert app_setting(config_app, "boolean") == "1" with pytest.raises(YunohostValidationError), patch.object( @@ -173,14 +173,14 @@ def test_app_config_bind_on_file(config_app): assert app_setting(config_app, "arg5") == "Foo Bar" -def test_app_config_custom_get(config_app): - - assert app_setting(config_app, "arg9") is None - assert ( - "Files in /var/www" - in app_config_get(config_app, "bind.function.arg9")["ask"]["en"] - ) - assert app_setting(config_app, "arg9") is None +# def test_app_config_custom_get(config_app): +# +# assert app_setting(config_app, "arg9") is None +# assert ( +# "Files in /var/www" +# in app_config_get(config_app, "bind.function.arg9")["ask"]["en"] +# ) +# assert app_setting(config_app, "arg9") is None def test_app_config_custom_validator(config_app): diff --git a/src/tests/test_app_resources.py b/src/tests/test_app_resources.py new file mode 100644 index 000000000..879f6e29a --- /dev/null +++ b/src/tests/test_app_resources.py @@ -0,0 +1,411 @@ +import os +import pytest + +from moulinette.utils.process import check_output + +from yunohost.app import app_setting +from yunohost.domain import _get_maindomain +from yunohost.utils.resources import ( + AppResource, + AppResourceManager, + AppResourceClassesByType, +) +from yunohost.permission import user_permission_list, permission_delete +from yunohost.firewall import firewall_list + +dummyfile = "/tmp/dummyappresource-testapp" + + +class DummyAppResource(AppResource): + + type = "dummy" + + default_properties = { + "file": "/tmp/dummyappresource-__APP__", + "content": "foo", + } + + def provision_or_update(self, context): + + open(self.file, "w").write(self.content) + + if self.content == "forbiddenvalue": + raise Exception("Emeged you used the forbidden value!1!£&") + + def deprovision(self, context): + + os.system(f"rm -f {self.file}") + + +AppResourceClassesByType["dummy"] = DummyAppResource + + +def setup_function(function): + + clean() + + os.system("mkdir /etc/yunohost/apps/testapp") + os.system("echo 'id: testapp' > /etc/yunohost/apps/testapp/settings.yml") + os.system("echo 'packaging_format = 2' > /etc/yunohost/apps/testapp/manifest.toml") + os.system("echo 'id = \"testapp\"' >> /etc/yunohost/apps/testapp/manifest.toml") + + +def teardown_function(function): + + clean() + + +def clean(): + + os.system(f"rm -f {dummyfile}") + os.system("rm -rf /etc/yunohost/apps/testapp") + os.system("rm -rf /var/www/testapp") + os.system("rm -rf /home/yunohost.app/testapp") + os.system("apt remove lolcat sl nyancat yarn >/dev/null 2>/dev/null") + os.system("userdel testapp 2>/dev/null") + + for p in user_permission_list()["permissions"]: + if p.startswith("testapp."): + permission_delete(p, force=True, sync_perm=False) + + +def test_provision_dummy(): + + current = {"resources": {}} + wanted = {"resources": {"dummy": {}}} + + assert not os.path.exists(dummyfile) + AppResourceManager("testapp", current=current, wanted=wanted).apply( + rollback_and_raise_exception_if_failure=False + ) + assert open(dummyfile).read().strip() == "foo" + + +def test_deprovision_dummy(): + + current = {"resources": {"dummy": {}}} + wanted = {"resources": {}} + + open(dummyfile, "w").write("foo") + + assert open(dummyfile).read().strip() == "foo" + AppResourceManager("testapp", current=current, wanted=wanted).apply( + rollback_and_raise_exception_if_failure=False + ) + assert not os.path.exists(dummyfile) + + +def test_provision_dummy_nondefaultvalue(): + + current = {"resources": {}} + wanted = {"resources": {"dummy": {"content": "bar"}}} + + assert not os.path.exists(dummyfile) + AppResourceManager("testapp", current=current, wanted=wanted).apply( + rollback_and_raise_exception_if_failure=False + ) + assert open(dummyfile).read().strip() == "bar" + + +def test_update_dummy(): + + current = {"resources": {"dummy": {}}} + wanted = {"resources": {"dummy": {"content": "bar"}}} + + open(dummyfile, "w").write("foo") + + assert open(dummyfile).read().strip() == "foo" + AppResourceManager("testapp", current=current, wanted=wanted).apply( + rollback_and_raise_exception_if_failure=False + ) + assert open(dummyfile).read().strip() == "bar" + + +def test_update_dummy_failwithrollback(): + + current = {"resources": {"dummy": {}}} + wanted = {"resources": {"dummy": {"content": "forbiddenvalue"}}} + + open(dummyfile, "w").write("foo") + + assert open(dummyfile).read().strip() == "foo" + with pytest.raises(Exception): + AppResourceManager("testapp", current=current, wanted=wanted).apply( + rollback_and_raise_exception_if_failure=True + ) + assert open(dummyfile).read().strip() == "foo" + + +def test_resource_system_user(): + + r = AppResourceClassesByType["system_user"] + + conf = {} + + assert os.system("getent passwd testapp 2>/dev/null") != 0 + + r(conf, "testapp").provision_or_update() + + assert os.system("getent passwd testapp >/dev/null") == 0 + assert os.system("groups testapp | grep -q 'sftp.app'") != 0 + + conf["allow_sftp"] = True + r(conf, "testapp").provision_or_update() + + assert os.system("getent passwd testapp >/dev/null") == 0 + assert os.system("groups testapp | grep -q 'sftp.app'") == 0 + + r(conf, "testapp").deprovision() + + assert os.system("getent passwd testapp 2>/dev/null") != 0 + + +def test_resource_install_dir(): + + r = AppResourceClassesByType["install_dir"] + conf = {"owner": "nobody:rx", "group": "nogroup:rx"} + + # FIXME: should also check settings ? + # FIXME: should also check automigrate from final_path + # FIXME: should also test changing the install folder location ? + + assert not os.path.exists("/var/www/testapp") + + r(conf, "testapp").provision_or_update() + + assert os.path.exists("/var/www/testapp") + unixperms = check_output("ls -ld /var/www/testapp").split() + assert unixperms[0] == "dr-xr-x---" + assert unixperms[2] == "nobody" + assert unixperms[3] == "nogroup" + + conf["owner"] = "nobody:rwx" + conf["group"] = "www-data:x" + + r(conf, "testapp").provision_or_update() + + assert os.path.exists("/var/www/testapp") + unixperms = check_output("ls -ld /var/www/testapp").split() + assert unixperms[0] == "drwx--x---" + assert unixperms[2] == "nobody" + assert unixperms[3] == "www-data" + + r(conf, "testapp").deprovision() + + assert not os.path.exists("/var/www/testapp") + + +def test_resource_data_dir(): + + r = AppResourceClassesByType["data_dir"] + conf = {"owner": "nobody:rx", "group": "nogroup:rx"} + + assert not os.path.exists("/home/yunohost.app/testapp") + + r(conf, "testapp").provision_or_update() + + assert os.path.exists("/home/yunohost.app/testapp") + unixperms = check_output("ls -ld /home/yunohost.app/testapp").split() + assert unixperms[0] == "dr-xr-x---" + assert unixperms[2] == "nobody" + assert unixperms[3] == "nogroup" + + conf["owner"] = "nobody:rwx" + conf["group"] = "www-data:x" + + r(conf, "testapp").provision_or_update() + + assert os.path.exists("/home/yunohost.app/testapp") + unixperms = check_output("ls -ld /home/yunohost.app/testapp").split() + assert unixperms[0] == "drwx--x---" + assert unixperms[2] == "nobody" + assert unixperms[3] == "www-data" + + r(conf, "testapp").deprovision() + + # FIXME : implement and check purge option + # assert not os.path.exists("/home/yunohost.app/testapp") + + +def test_resource_ports(): + + r = AppResourceClassesByType["ports"] + conf = {} + + assert not app_setting("testapp", "port") + + r(conf, "testapp").provision_or_update() + + assert app_setting("testapp", "port") + + r(conf, "testapp").deprovision() + + assert not app_setting("testapp", "port") + + +def test_resource_ports_several(): + + r = AppResourceClassesByType["ports"] + conf = {"main": {"default": 12345}, "foobar": {"default": 23456}} + + assert not app_setting("testapp", "port") + assert not app_setting("testapp", "port_foobar") + + r(conf, "testapp").provision_or_update() + + assert app_setting("testapp", "port") + assert app_setting("testapp", "port_foobar") + + r(conf, "testapp").deprovision() + + assert not app_setting("testapp", "port") + assert not app_setting("testapp", "port_foobar") + + +def test_resource_ports_firewall(): + + r = AppResourceClassesByType["ports"] + conf = {"main": {"default": 12345}} + + r(conf, "testapp").provision_or_update() + + assert 12345 not in firewall_list()["opened_ports"] + + conf = {"main": {"default": 12345, "exposed": "TCP"}} + + r(conf, "testapp").provision_or_update() + + assert 12345 in firewall_list()["opened_ports"] + + r(conf, "testapp").deprovision() + + assert 12345 not in firewall_list()["opened_ports"] + + +def test_resource_database(): + + r = AppResourceClassesByType["database"] + conf = {"type": "mysql"} + + assert os.system("mysqlshow 'testapp' >/dev/null 2>/dev/null") != 0 + assert not app_setting("testapp", "db_name") + assert not app_setting("testapp", "db_user") + assert not app_setting("testapp", "db_pwd") + + r(conf, "testapp").provision_or_update() + + assert os.system("mysqlshow 'testapp' >/dev/null 2>/dev/null") == 0 + assert app_setting("testapp", "db_name") + assert app_setting("testapp", "db_user") + assert app_setting("testapp", "db_pwd") + + r(conf, "testapp").deprovision() + + assert os.system("mysqlshow 'testapp' >/dev/null 2>/dev/null") != 0 + assert not app_setting("testapp", "db_name") + assert not app_setting("testapp", "db_user") + assert not app_setting("testapp", "db_pwd") + + +def test_resource_apt(): + + r = AppResourceClassesByType["apt"] + conf = { + "packages": "nyancat, sl", + "extras": { + "yarn": { + "repo": "deb https://dl.yarnpkg.com/debian/ stable main", + "key": "https://dl.yarnpkg.com/debian/pubkey.gpg", + "packages": "yarn", + } + }, + } + + assert os.system("dpkg --list | grep -q 'ii *nyancat '") != 0 + assert os.system("dpkg --list | grep -q 'ii *sl '") != 0 + assert os.system("dpkg --list | grep -q 'ii *yarn '") != 0 + assert os.system("dpkg --list | grep -q 'ii *lolcat '") != 0 + assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") != 0 + + r(conf, "testapp").provision_or_update() + + assert os.system("dpkg --list | grep -q 'ii *nyancat '") == 0 + assert os.system("dpkg --list | grep -q 'ii *sl '") == 0 + assert os.system("dpkg --list | grep -q 'ii *yarn '") == 0 + assert ( + os.system("dpkg --list | grep -q 'ii *lolcat '") != 0 + ) # Lolcat shouldnt be installed yet + assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") == 0 + + conf["packages"] += ", lolcat" + r(conf, "testapp").provision_or_update() + + assert os.system("dpkg --list | grep -q 'ii *nyancat '") == 0 + assert os.system("dpkg --list | grep -q 'ii *sl '") == 0 + assert os.system("dpkg --list | grep -q 'ii *yarn '") == 0 + assert os.system("dpkg --list | grep -q 'ii *lolcat '") == 0 + assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") == 0 + + r(conf, "testapp").deprovision() + + assert os.system("dpkg --list | grep -q 'ii *nyancat '") != 0 + assert os.system("dpkg --list | grep -q 'ii *sl '") != 0 + assert os.system("dpkg --list | grep -q 'ii *yarn '") != 0 + assert os.system("dpkg --list | grep -q 'ii *lolcat '") != 0 + assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") != 0 + + +def test_resource_permissions(): + + maindomain = _get_maindomain() + os.system(f"echo 'domain: {maindomain}' >> /etc/yunohost/apps/testapp/settings.yml") + os.system("echo 'path: /testapp' >> /etc/yunohost/apps/testapp/settings.yml") + + # A manager object is required to set the label of the app... + manager = AppResourceManager("testapp", current={}, wanted={"name": "Test App"}) + r = AppResourceClassesByType["permissions"] + conf = { + "main": { + "url": "/", + "allowed": "visitors" + # TODO: test protected? + }, + } + + res = user_permission_list(full=True)["permissions"] + assert not any(key.startswith("testapp.") for key in res) + + r(conf, "testapp", manager).provision_or_update() + + res = user_permission_list(full=True)["permissions"] + assert "testapp.main" in res + assert "visitors" in res["testapp.main"]["allowed"] + assert res["testapp.main"]["url"] == "/" + assert "testapp.admin" not in res + + conf["admin"] = {"url": "/admin", "allowed": ""} + + r(conf, "testapp", manager).provision_or_update() + + res = user_permission_list(full=True)["permissions"] + + assert "testapp.main" in list(res.keys()) + assert "visitors" in res["testapp.main"]["allowed"] + assert res["testapp.main"]["url"] == "/" + + assert "testapp.admin" in res + assert not res["testapp.admin"]["allowed"] + assert res["testapp.admin"]["url"] == "/admin" + + conf["admin"]["url"] = "/adminpanel" + + r(conf, "testapp", manager).provision_or_update() + + res = user_permission_list(full=True)["permissions"] + + assert res["testapp.admin"]["url"] == "/adminpanel" + + r(conf, "testapp").deprovision() + + res = user_permission_list(full=True)["permissions"] + assert "testapp.main" not in res diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index 2a808b5bd..6efdaa0b0 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -15,6 +15,8 @@ from yunohost.app import ( _is_installed, app_upgrade, app_map, + app_manifest, + app_info, ) from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list from yunohost.utils.error import YunohostError @@ -45,6 +47,7 @@ def clean(): "break_yo_system", "legacy_app", "legacy_app__2", + "manifestv2_app", "full_domain_app", "my_webapp", ] @@ -115,7 +118,10 @@ def app_expected_files(domain, app): if app.startswith("legacy_app"): yield "/var/www/%s/index.html" % app yield "/etc/yunohost/apps/%s/settings.yml" % app - yield "/etc/yunohost/apps/%s/manifest.json" % app + if "manifestv2" in app: + yield "/etc/yunohost/apps/%s/manifest.toml" % app + else: + yield "/etc/yunohost/apps/%s/manifest.json" % app yield "/etc/yunohost/apps/%s/scripts/install" % app yield "/etc/yunohost/apps/%s/scripts/remove" % app @@ -157,6 +163,17 @@ def install_legacy_app(domain, path, public=True): ) +def install_manifestv2_app(domain, path, public=True): + + app_install( + os.path.join(get_test_apps_dir(), "manifestv2_app_ynh"), + args="domain={}&path={}&init_main_permission={}".format( + domain, path, "visitors" if public else "all_users" + ), + force=True, + ) + + def install_full_domain_app(domain): app_install( @@ -195,6 +212,139 @@ def test_legacy_app_install_main_domain(): assert app_is_not_installed(main_domain, "legacy_app") +def test_legacy_app_manifest_preinstall(): + + m = app_manifest(os.path.join(get_test_apps_dir(), "legacy_app_ynh")) + # v1 manifesto are expected to have been autoconverted to v2 + + assert "id" in m + assert "description" in m + assert "integration" in m + assert "install" in m + assert m["doc"] == {} + assert m["notifications"] == { + "PRE_INSTALL": {}, + "PRE_UPGRADE": {}, + "POST_INSTALL": {}, + "POST_UPGRADE": {}, + } + + +def test_manifestv2_app_manifest_preinstall(): + + m = app_manifest(os.path.join(get_test_apps_dir(), "manifestv2_app_ynh")) + + assert "id" in m + assert "install" in m + assert "description" in m + assert "doc" in m + assert ( + "This is a dummy description of this app features" + in m["doc"]["DESCRIPTION"]["en"] + ) + assert ( + "Ceci est une fausse description des fonctionalités de l'app" + in m["doc"]["DESCRIPTION"]["fr"] + ) + assert "notifications" in m + assert ( + "This is a dummy disclaimer to display prior to the install" + in m["notifications"]["PRE_INSTALL"]["main"]["en"] + ) + assert ( + "Ceci est un faux disclaimer à présenter avant l'installation" + in m["notifications"]["PRE_INSTALL"]["main"]["fr"] + ) + + +def test_manifestv2_app_install_main_domain(): + + main_domain = _get_maindomain() + + install_manifestv2_app(main_domain, "/manifestv2") + + app_map_ = app_map(raw=True) + assert main_domain in app_map_ + assert "/manifestv2" in app_map_[main_domain] + assert "id" in app_map_[main_domain]["/manifestv2"] + assert app_map_[main_domain]["/manifestv2"]["id"] == "manifestv2_app" + + assert app_is_installed(main_domain, "manifestv2_app") + assert app_is_exposed_on_http(main_domain, "/manifestv2", "Hextris") + + app_remove("manifestv2_app") + + assert app_is_not_installed(main_domain, "manifestv2_app") + + +def test_manifestv2_app_info_postinstall(): + + main_domain = _get_maindomain() + install_manifestv2_app(main_domain, "/manifestv2") + m = app_info("manifestv2_app", full=True)["manifest"] + + assert "id" in m + assert "install" in m + assert "description" in m + assert "doc" in m + assert "The app install dir is /var/www/manifestv2_app" in m["doc"]["ADMIN"]["en"] + assert ( + "Le dossier d'install de l'app est /var/www/manifestv2_app" + in m["doc"]["ADMIN"]["fr"] + ) + assert "notifications" in m + assert ( + "The app install dir is /var/www/manifestv2_app" + in m["notifications"]["POST_INSTALL"]["main"]["en"] + ) + assert ( + "The app id is manifestv2_app" + in m["notifications"]["POST_INSTALL"]["main"]["en"] + ) + assert ( + f"The app url is {main_domain}/manifestv2" + in m["notifications"]["POST_INSTALL"]["main"]["en"] + ) + + +def test_manifestv2_app_info_preupgrade(monkeypatch): + + manifest = app_manifest(os.path.join(get_test_apps_dir(), "manifestv2_app_ynh")) + + from yunohost.app_catalog import _load_apps_catalog as original_load_apps_catalog + + def custom_load_apps_catalog(*args, **kwargs): + + res = original_load_apps_catalog(*args, **kwargs) + res["apps"]["manifestv2_app"] = { + "id": "manifestv2_app", + "level": 10, + "lastUpdate": 999999999, + "maintained": True, + "manifest": manifest, + "state": "working", + } + res["apps"]["manifestv2_app"]["manifest"]["version"] = "99999~ynh1" + + return res + + monkeypatch.setattr("yunohost.app._load_apps_catalog", custom_load_apps_catalog) + + main_domain = _get_maindomain() + install_manifestv2_app(main_domain, "/manifestv2") + i = app_info("manifestv2_app", full=True) + + assert i["upgradable"] == "yes" + assert i["new_version"] == "99999~ynh1" + # FIXME : as I write this test, I realize that this implies the catalog API + # does provide the notifications, which means the list builder script + # should parse the files in the original app repo, possibly with proper i18n etc + assert ( + "This is a dummy disclaimer to display prior to any upgrade" + in i["from_catalog"]["manifest"]["notifications"]["PRE_UPGRADE"]["main"]["en"] + ) + + def test_app_from_catalog(): main_domain = _get_maindomain() diff --git a/src/tests/test_backuprestore.py b/src/tests/test_backuprestore.py index 03c3aa0c7..dc37d3497 100644 --- a/src/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -77,7 +77,7 @@ def setup_function(function): if "with_permission_app_installed" in markers: assert not app_is_installed("permissions_app") - user_create("alice", "Alice", "White", maindomain, "test123Ynh") + user_create("alice", maindomain, "test123Ynh", fullname="Alice White") with patch.object(os, "isatty", return_value=False): install_app("permissions_app_ynh", "/urlpermissionapp" "&admin=alice") assert app_is_installed("permissions_app") @@ -355,13 +355,15 @@ def test_backup_script_failure_handling(monkeypatch, mocker): @pytest.mark.with_backup_recommended_app_installed def test_backup_not_enough_free_space(monkeypatch, mocker): - def custom_disk_usage(path): + def custom_space_used_by_directory(path, *args, **kwargs): return 99999999999999999 def custom_free_space_in_directory(dirpath): return 0 - monkeypatch.setattr("yunohost.backup.disk_usage", custom_disk_usage) + monkeypatch.setattr( + "yunohost.backup.space_used_by_directory", custom_space_used_by_directory + ) monkeypatch.setattr( "yunohost.backup.free_space_in_directory", custom_free_space_in_directory ) diff --git a/src/tests/test_ldapauth.py b/src/tests/test_ldapauth.py index a95dea443..e8a48aa6d 100644 --- a/src/tests/test_ldapauth.py +++ b/src/tests/test_ldapauth.py @@ -2,7 +2,8 @@ import pytest import os from yunohost.authenticators.ldap_admin import Authenticator as LDAPAuth -from yunohost.tools import tools_adminpw +from yunohost.user import user_create, user_list, user_update, user_delete +from yunohost.domain import _get_maindomain from moulinette import m18n from moulinette.core import MoulinetteError @@ -10,50 +11,75 @@ from moulinette.core import MoulinetteError def setup_function(function): - if os.system("systemctl is-active slapd") != 0: + for u in user_list()["users"]: + user_delete(u, purge=True) + + maindomain = _get_maindomain() + + if os.system("systemctl is-active slapd >/dev/null") != 0: os.system("systemctl start slapd && sleep 3") - tools_adminpw("yunohost", check_strength=False) + user_create("alice", maindomain, "Yunohost", admin=True, fullname="Alice White") + user_create("bob", maindomain, "test123Ynh", fullname="Bob Snow") + + +def teardown_function(): + + os.system("systemctl is-active slapd >/dev/null || systemctl start slapd; sleep 5") + + for u in user_list()["users"]: + user_delete(u, purge=True) def test_authenticate(): - LDAPAuth().authenticate_credentials(credentials="yunohost") + LDAPAuth().authenticate_credentials(credentials="alice:Yunohost") + + +def test_authenticate_with_no_user(): + + with pytest.raises(MoulinetteError): + LDAPAuth().authenticate_credentials(credentials="Yunohost") + + with pytest.raises(MoulinetteError): + LDAPAuth().authenticate_credentials(credentials=":Yunohost") + + +def test_authenticate_with_user_who_is_not_admin(): + + with pytest.raises(MoulinetteError) as exception: + LDAPAuth().authenticate_credentials(credentials="bob:test123Ynh") + + translation = m18n.n("invalid_credentials") + expected_msg = translation.format() + assert expected_msg in str(exception) def test_authenticate_with_wrong_password(): with pytest.raises(MoulinetteError) as exception: - LDAPAuth().authenticate_credentials(credentials="bad_password_lul") + LDAPAuth().authenticate_credentials(credentials="alice:bad_password_lul") - translation = m18n.n("invalid_password") + translation = m18n.n("invalid_credentials") expected_msg = translation.format() assert expected_msg in str(exception) def test_authenticate_server_down(mocker): - os.system("systemctl stop slapd && sleep 3") + os.system("systemctl stop slapd && sleep 5") - # Now if slapd is down, moulinette tries to restart it - mocker.patch("os.system") - mocker.patch("time.sleep") - with pytest.raises(MoulinetteError) as exception: - LDAPAuth().authenticate_credentials(credentials="yunohost") - - translation = m18n.n("ldap_server_down") - expected_msg = translation.format() - assert expected_msg in str(exception) + LDAPAuth().authenticate_credentials(credentials="alice:Yunohost") def test_authenticate_change_password(): - LDAPAuth().authenticate_credentials(credentials="yunohost") + LDAPAuth().authenticate_credentials(credentials="alice:Yunohost") - tools_adminpw("plopette", check_strength=False) + user_update("alice", change_password="plopette") with pytest.raises(MoulinetteError) as exception: - LDAPAuth().authenticate_credentials(credentials="yunohost") + LDAPAuth().authenticate_credentials(credentials="alice:Yunohost") - translation = m18n.n("invalid_password") + translation = m18n.n("invalid_credentials") expected_msg = translation.format() assert expected_msg in str(exception) - LDAPAuth().authenticate_credentials(credentials="plopette") + LDAPAuth().authenticate_credentials(credentials="alice:plopette") diff --git a/src/tests/test_permission.py b/src/tests/test_permission.py index 4e7f9f53d..acb3419c9 100644 --- a/src/tests/test_permission.py +++ b/src/tests/test_permission.py @@ -78,6 +78,7 @@ def _permission_create_with_dummy_app( "name": app, "id": app, "description": {"en": "Dummy app to test permissions"}, + "arguments": {"install": []}, }, f, ) @@ -108,7 +109,7 @@ def clean_user_groups_permission(): user_delete(u) for g in user_group_list()["groups"]: - if g not in ["all_users", "visitors"]: + if g not in ["all_users", "visitors", "admins"]: user_group_delete(g) for p in user_permission_list()["permissions"]: @@ -157,8 +158,8 @@ def setup_function(function): socket.getaddrinfo = new_getaddrinfo - user_create("alice", "Alice", "White", maindomain, dummy_password) - user_create("bob", "Bob", "Snow", maindomain, dummy_password) + user_create("alice", maindomain, dummy_password, fullname="Alice White") + user_create("bob", maindomain, dummy_password, fullname="Bob Snow") _permission_create_with_dummy_app( permission="wiki.main", url="/", @@ -257,7 +258,7 @@ def check_LDAP_db_integrity(): for user in user_search: user_dn = "uid=" + user["uid"][0] + ",ou=users,dc=yunohost,dc=org" - group_list = [_ldap_path_extract(m, "cn") for m in user["memberOf"]] + group_list = [_ldap_path_extract(m, "cn") for m in user.get("memberOf", [])] permission_list = [ _ldap_path_extract(m, "cn") for m in user.get("permission", []) ] diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 5917d32d4..e49047469 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -23,37 +23,38 @@ from yunohost.utils.error import YunohostError, YunohostValidationError """ Argument default format: { - "name": "the_name", - "type": "one_of_the_available_type", // "sting" is not specified - "ask": { - "en": "the question in english", - "fr": "the question in french" - }, - "help": { - "en": "some help text in english", - "fr": "some help text in french" - }, - "example": "an example value", // optional - "default", "some stuff", // optional, not available for all types - "optional": true // optional, will skip if not answered + "the_name": { + "type": "one_of_the_available_type", // "sting" is not specified + "ask": { + "en": "the question in english", + "fr": "the question in french" + }, + "help": { + "en": "some help text in english", + "fr": "some help text in french" + }, + "example": "an example value", // optional + "default", "some stuff", // optional, not available for all types + "optional": true // optional, will skip if not answered + } } User answers: -{"name": "value", ...} +{"the_name": "value", ...} """ def test_question_empty(): - ask_questions_and_parse_answers([], {}) == [] + ask_questions_and_parse_answers({}, {}) == [] def test_question_string(): - questions = [ - { - "name": "some_string", + + questions = { + "some_string": { "type": "string", } - ] + } answers = {"some_string": "some_value"} out = ask_questions_and_parse_answers(questions, answers)[0] @@ -65,12 +66,11 @@ def test_question_string(): def test_question_string_from_query_string(): - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "type": "string", } - ] + } answers = "foo=bar&some_string=some_value&lorem=ipsum" out = ask_questions_and_parse_answers(questions, answers)[0] @@ -81,11 +81,7 @@ def test_question_string_from_query_string(): def test_question_string_default_type(): - questions = [ - { - "name": "some_string", - } - ] + questions = {"some_string": {}} answers = {"some_string": "some_value"} out = ask_questions_and_parse_answers(questions, answers)[0] @@ -96,11 +92,7 @@ def test_question_string_default_type(): def test_question_string_no_input(): - questions = [ - { - "name": "some_string", - } - ] + questions = {"some_string": {}} answers = {} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): @@ -108,12 +100,11 @@ def test_question_string_no_input(): def test_question_string_input(): - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "ask": "some question", } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( @@ -127,11 +118,7 @@ def test_question_string_input(): def test_question_string_input_no_ask(): - questions = [ - { - "name": "some_string", - } - ] + questions = {"some_string": {}} answers = {} with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( @@ -145,12 +132,7 @@ def test_question_string_input_no_ask(): def test_question_string_no_input_optional(): - questions = [ - { - "name": "some_string", - "optional": True, - } - ] + questions = {"some_string": {"optional": True}} answers = {} with patch.object(os, "isatty", return_value=False): out = ask_questions_and_parse_answers(questions, answers)[0] @@ -161,13 +143,12 @@ def test_question_string_no_input_optional(): def test_question_string_optional_with_input(): - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "ask": "some question", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( @@ -181,13 +162,12 @@ def test_question_string_optional_with_input(): def test_question_string_optional_with_empty_input(): - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "ask": "some question", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value=""), patch.object( @@ -201,12 +181,11 @@ def test_question_string_optional_with_empty_input(): def test_question_string_optional_with_input_without_ask(): - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( @@ -220,13 +199,12 @@ def test_question_string_optional_with_input_without_ask(): def test_question_string_no_input_default(): - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "ask": "some question", "default": "some_value", } - ] + } answers = {} with patch.object(os, "isatty", return_value=False): out = ask_questions_and_parse_answers(questions, answers)[0] @@ -238,12 +216,11 @@ def test_question_string_no_input_default(): def test_question_string_input_test_ask(): ask_text = "some question" - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "ask": ask_text, } - ] + } answers = {} with patch.object( @@ -264,13 +241,12 @@ def test_question_string_input_test_ask(): def test_question_string_input_test_ask_with_default(): ask_text = "some question" default_text = "some example" - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "ask": ask_text, "default": default_text, } - ] + } answers = {} with patch.object( @@ -292,13 +268,12 @@ def test_question_string_input_test_ask_with_default(): def test_question_string_input_test_ask_with_example(): ask_text = "some question" example_text = "some example" - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "ask": ask_text, "example": example_text, } - ] + } answers = {} with patch.object( @@ -313,13 +288,12 @@ def test_question_string_input_test_ask_with_example(): def test_question_string_input_test_ask_with_help(): ask_text = "some question" help_text = "some_help" - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "ask": ask_text, "help": help_text, } - ] + } answers = {} with patch.object( @@ -331,7 +305,7 @@ def test_question_string_input_test_ask_with_help(): def test_question_string_with_choice(): - questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] + questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} answers = {"some_string": "fr"} out = ask_questions_and_parse_answers(questions, answers)[0] @@ -341,7 +315,7 @@ def test_question_string_with_choice(): def test_question_string_with_choice_prompt(): - questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] + 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 @@ -354,7 +328,7 @@ def test_question_string_with_choice_prompt(): def test_question_string_with_choice_bad(): - questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] + questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} answers = {"some_string": "bad"} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): @@ -364,13 +338,12 @@ def test_question_string_with_choice_bad(): def test_question_string_with_choice_ask(): ask_text = "some question" choices = ["fr", "en", "es", "it", "ru"] - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "ask": ask_text, "choices": choices, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="ru") as prompt, patch.object( @@ -384,14 +357,13 @@ def test_question_string_with_choice_ask(): def test_question_string_with_choice_default(): - questions = [ - { - "name": "some_string", + 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] @@ -402,12 +374,11 @@ def test_question_string_with_choice_default(): def test_question_password(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", } - ] + } answers = {"some_password": "some_value"} out = ask_questions_and_parse_answers(questions, answers)[0] @@ -417,12 +388,11 @@ def test_question_password(): def test_question_password_no_input(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", } - ] + } answers = {} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): @@ -430,13 +400,12 @@ def test_question_password_no_input(): def test_question_password_input(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", "ask": "some question", } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( @@ -450,12 +419,11 @@ def test_question_password_input(): def test_question_password_input_no_ask(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( @@ -469,13 +437,12 @@ def test_question_password_input_no_ask(): def test_question_password_no_input_optional(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", "optional": True, } - ] + } answers = {} with patch.object(os, "isatty", return_value=False): @@ -485,9 +452,7 @@ def test_question_password_no_input_optional(): assert out.type == "password" assert out.value == "" - questions = [ - {"name": "some_password", "type": "password", "optional": True, "default": ""} - ] + questions = {"some_password": {"type": "password", "optional": True, "default": ""}} with patch.object(os, "isatty", return_value=False): out = ask_questions_and_parse_answers(questions, answers)[0] @@ -498,14 +463,13 @@ def test_question_password_no_input_optional(): def test_question_password_optional_with_input(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "ask": "some question", "type": "password", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( @@ -519,14 +483,13 @@ def test_question_password_optional_with_input(): def test_question_password_optional_with_empty_input(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "ask": "some question", "type": "password", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value=""), patch.object( @@ -540,13 +503,12 @@ def test_question_password_optional_with_empty_input(): def test_question_password_optional_with_input_without_ask(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( @@ -560,14 +522,13 @@ def test_question_password_optional_with_input_without_ask(): def test_question_password_no_input_default(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", "ask": "some question", "default": "some_value", } - ] + } answers = {} # no default for password! @@ -577,14 +538,13 @@ def test_question_password_no_input_default(): @pytest.mark.skip # this should raises def test_question_password_no_input_example(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", "ask": "some question", "example": "some_value", } - ] + } answers = {"some_password": "some_value"} # no example for password! @@ -594,13 +554,12 @@ def test_question_password_no_input_example(): def test_question_password_input_test_ask(): ask_text = "some question" - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", "ask": ask_text, } - ] + } answers = {} with patch.object( @@ -622,14 +581,13 @@ def test_question_password_input_test_ask(): def test_question_password_input_test_ask_with_example(): ask_text = "some question" example_text = "some example" - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", "ask": ask_text, "example": example_text, } - ] + } answers = {} with patch.object( @@ -644,14 +602,13 @@ def test_question_password_input_test_ask_with_example(): def test_question_password_input_test_ask_with_help(): ask_text = "some question" help_text = "some_help" - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", "ask": ask_text, "help": help_text, } - ] + } answers = {} with patch.object( @@ -663,14 +620,13 @@ def test_question_password_input_test_ask_with_help(): def test_question_password_bad_chars(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", "ask": "some question", "example": "some_value", } - ] + } for i in PasswordQuestion.forbidden_chars: with pytest.raises(YunohostError), patch.object( @@ -680,14 +636,13 @@ def test_question_password_bad_chars(): def test_question_password_strong_enough(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "type": "password", "ask": "some question", "example": "some_value", } - ] + } with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): # too short @@ -698,14 +653,13 @@ def test_question_password_strong_enough(): def test_question_password_optional_strong_enough(): - questions = [ - { - "name": "some_password", + questions = { + "some_password": { "ask": "some question", "type": "password", "optional": True, } - ] + } with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): # too short @@ -716,12 +670,11 @@ def test_question_password_optional_strong_enough(): def test_question_path(): - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "type": "path", } - ] + } answers = {"some_path": "/some_value"} out = ask_questions_and_parse_answers(questions, answers)[0] @@ -731,12 +684,11 @@ def test_question_path(): def test_question_path_no_input(): - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "type": "path", } - ] + } answers = {} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): @@ -744,13 +696,12 @@ def test_question_path_no_input(): def test_question_path_input(): - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "type": "path", "ask": "some question", } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( @@ -764,12 +715,11 @@ def test_question_path_input(): def test_question_path_input_no_ask(): - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "type": "path", } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( @@ -783,13 +733,12 @@ def test_question_path_input_no_ask(): def test_question_path_no_input_optional(): - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "type": "path", "optional": True, } - ] + } answers = {} with patch.object(os, "isatty", return_value=False): out = ask_questions_and_parse_answers(questions, answers)[0] @@ -800,14 +749,13 @@ def test_question_path_no_input_optional(): def test_question_path_optional_with_input(): - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "ask": "some question", "type": "path", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( @@ -821,14 +769,13 @@ def test_question_path_optional_with_input(): def test_question_path_optional_with_empty_input(): - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "ask": "some question", "type": "path", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value=""), patch.object( @@ -842,13 +789,12 @@ def test_question_path_optional_with_empty_input(): def test_question_path_optional_with_input_without_ask(): - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "type": "path", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( @@ -862,14 +808,13 @@ def test_question_path_optional_with_input_without_ask(): def test_question_path_no_input_default(): - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "ask": "some question", "type": "path", "default": "some_value", } - ] + } answers = {} with patch.object(os, "isatty", return_value=False): out = ask_questions_and_parse_answers(questions, answers)[0] @@ -881,13 +826,12 @@ def test_question_path_no_input_default(): def test_question_path_input_test_ask(): ask_text = "some question" - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "type": "path", "ask": ask_text, } - ] + } answers = {} with patch.object( @@ -908,14 +852,13 @@ def test_question_path_input_test_ask(): def test_question_path_input_test_ask_with_default(): ask_text = "some question" default_text = "someexample" - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "type": "path", "ask": ask_text, "default": default_text, } - ] + } answers = {} with patch.object( @@ -937,14 +880,13 @@ def test_question_path_input_test_ask_with_default(): def test_question_path_input_test_ask_with_example(): ask_text = "some question" example_text = "some example" - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "type": "path", "ask": ask_text, "example": example_text, } - ] + } answers = {} with patch.object( @@ -959,14 +901,13 @@ def test_question_path_input_test_ask_with_example(): def test_question_path_input_test_ask_with_help(): ask_text = "some question" help_text = "some_help" - questions = [ - { - "name": "some_path", + questions = { + "some_path": { "type": "path", "ask": ask_text, "help": help_text, } - ] + } answers = {} with patch.object( @@ -978,12 +919,11 @@ def test_question_path_input_test_ask_with_help(): def test_question_boolean(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", } - ] + } answers = {"some_boolean": "y"} out = ask_questions_and_parse_answers(questions, answers)[0] @@ -993,12 +933,11 @@ def test_question_boolean(): def test_question_boolean_all_yes(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", } - ] + } for value in ["Y", "yes", "Yes", "YES", "1", 1, True, "True", "TRUE", "true"]: out = ask_questions_and_parse_answers(questions, {"some_boolean": value})[0] @@ -1008,12 +947,11 @@ def test_question_boolean_all_yes(): def test_question_boolean_all_no(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", } - ] + } for value in ["n", "N", "no", "No", "No", "0", 0, False, "False", "FALSE", "false"]: out = ask_questions_and_parse_answers(questions, {"some_boolean": value})[0] @@ -1024,12 +962,11 @@ def test_question_boolean_all_no(): # XXX apparently boolean are always False (0) by default, I'm not sure what to think about that def test_question_boolean_no_input(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", } - ] + } answers = {} with patch.object(os, "isatty", return_value=False): @@ -1039,12 +976,11 @@ def test_question_boolean_no_input(): def test_question_boolean_bad_input(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", } - ] + } answers = {"some_boolean": "stuff"} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): @@ -1052,13 +988,12 @@ def test_question_boolean_bad_input(): def test_question_boolean_input(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", "ask": "some question", } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="y"), patch.object( @@ -1075,12 +1010,11 @@ def test_question_boolean_input(): def test_question_boolean_input_no_ask(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="y"), patch.object( @@ -1091,13 +1025,12 @@ def test_question_boolean_input_no_ask(): def test_question_boolean_no_input_optional(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", "optional": True, } - ] + } answers = {} with patch.object(os, "isatty", return_value=False): out = ask_questions_and_parse_answers(questions, answers)[0] @@ -1105,14 +1038,13 @@ def test_question_boolean_no_input_optional(): def test_question_boolean_optional_with_input(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "ask": "some question", "type": "boolean", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="y"), patch.object( @@ -1123,14 +1055,13 @@ def test_question_boolean_optional_with_input(): def test_question_boolean_optional_with_empty_input(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "ask": "some question", "type": "boolean", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value=""), patch.object( @@ -1142,13 +1073,12 @@ def test_question_boolean_optional_with_empty_input(): def test_question_boolean_optional_with_input_without_ask(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="n"), patch.object( @@ -1160,14 +1090,13 @@ def test_question_boolean_optional_with_input_without_ask(): def test_question_boolean_no_input_default(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "ask": "some question", "type": "boolean", "default": 0, } - ] + } answers = {} with patch.object(os, "isatty", return_value=False): @@ -1177,14 +1106,13 @@ def test_question_boolean_no_input_default(): def test_question_boolean_bad_default(): - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "ask": "some question", "type": "boolean", "default": "bad default", } - ] + } answers = {} with pytest.raises(YunohostError): ask_questions_and_parse_answers(questions, answers) @@ -1192,13 +1120,12 @@ def test_question_boolean_bad_default(): def test_question_boolean_input_test_ask(): ask_text = "some question" - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", "ask": ask_text, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value=0) as prompt, patch.object( @@ -1219,14 +1146,13 @@ def test_question_boolean_input_test_ask(): def test_question_boolean_input_test_ask_with_default(): ask_text = "some question" default_text = 1 - questions = [ - { - "name": "some_boolean", + questions = { + "some_boolean": { "type": "boolean", "ask": ask_text, "default": default_text, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value=1) as prompt, patch.object( @@ -1245,12 +1171,11 @@ def test_question_boolean_input_test_ask_with_default(): def test_question_domain_empty(): - questions = [ - { - "name": "some_domain", + questions = { + "some_domain": { "type": "domain", } - ] + } main_domain = "my_main_domain.com" answers = {} @@ -1271,12 +1196,11 @@ def test_question_domain_empty(): def test_question_domain(): main_domain = "my_main_domain.com" domains = [main_domain] - questions = [ - { - "name": "some_domain", + questions = { + "some_domain": { "type": "domain", } - ] + } answers = {"some_domain": main_domain} @@ -1295,12 +1219,11 @@ def test_question_domain_two_domains(): other_domain = "some_other_domain.tld" domains = [main_domain, other_domain] - questions = [ - { - "name": "some_domain", + questions = { + "some_domain": { "type": "domain", } - ] + } answers = {"some_domain": other_domain} with patch.object( @@ -1329,12 +1252,11 @@ def test_question_domain_two_domains_wrong_answer(): other_domain = "some_other_domain.tld" domains = [main_domain, other_domain] - questions = [ - { - "name": "some_domain", + questions = { + "some_domain": { "type": "domain", } - ] + } answers = {"some_domain": "doesnt_exist.pouet"} with patch.object( @@ -1351,12 +1273,11 @@ def test_question_domain_two_domains_default_no_ask(): other_domain = "some_other_domain.tld" domains = [main_domain, other_domain] - questions = [ - { - "name": "some_domain", + questions = { + "some_domain": { "type": "domain", } - ] + } answers = {} with patch.object( @@ -1378,7 +1299,7 @@ def test_question_domain_two_domains_default(): other_domain = "some_other_domain.tld" domains = [main_domain, other_domain] - questions = [{"name": "some_domain", "type": "domain", "ask": "choose a domain"}] + questions = {"some_domain": {"type": "domain", "ask": "choose a domain"}} answers = {} with patch.object( @@ -1400,7 +1321,7 @@ def test_question_domain_two_domains_default_input(): other_domain = "some_other_domain.tld" domains = [main_domain, other_domain] - questions = [{"name": "some_domain", "type": "domain", "ask": "choose a domain"}] + questions = {"some_domain": {"type": "domain", "ask": "choose a domain"}} answers = {} with patch.object( @@ -1436,12 +1357,11 @@ def test_question_user_empty(): } } - questions = [ - { - "name": "some_user", + questions = { + "some_user": { "type": "user", } - ] + } answers = {} with patch.object(user, "user_list", return_value={"users": users}): @@ -1463,12 +1383,11 @@ def test_question_user(): } } - questions = [ - { - "name": "some_user", + questions = { + "some_user": { "type": "user", } - ] + } answers = {"some_user": username} with patch.object(user, "user_list", return_value={"users": users}), patch.object( @@ -1501,12 +1420,11 @@ def test_question_user_two_users(): }, } - questions = [ - { - "name": "some_user", + questions = { + "some_user": { "type": "user", } - ] + } answers = {"some_user": other_user} with patch.object(user, "user_list", return_value={"users": users}), patch.object( @@ -1550,12 +1468,11 @@ def test_question_user_two_users_wrong_answer(): }, } - questions = [ - { - "name": "some_user", + questions = { + "some_user": { "type": "user", } - ] + } answers = {"some_user": "doesnt_exist.pouet"} with patch.object(user, "user_list", return_value={"users": users}): @@ -1585,7 +1502,7 @@ def test_question_user_two_users_no_default(): }, } - questions = [{"name": "some_user", "type": "user", "ask": "choose a user"}] + questions = {"some_user": {"type": "user", "ask": "choose a user"}} answers = {} with patch.object(user, "user_list", return_value={"users": users}): @@ -1615,7 +1532,7 @@ def test_question_user_two_users_default_input(): }, } - questions = [{"name": "some_user", "type": "user", "ask": "choose a user"}] + questions = {"some_user": {"type": "user", "ask": "choose a user"}} answers = {} with patch.object(user, "user_list", return_value={"users": users}), patch.object( @@ -1639,12 +1556,11 @@ def test_question_user_two_users_default_input(): def test_question_number(): - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", } - ] + } answers = {"some_number": 1337} out = ask_questions_and_parse_answers(questions, answers)[0] @@ -1654,12 +1570,11 @@ def test_question_number(): def test_question_number_no_input(): - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", } - ] + } answers = {} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): @@ -1667,12 +1582,11 @@ def test_question_number_no_input(): def test_question_number_bad_input(): - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", } - ] + } answers = {"some_number": "stuff"} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): @@ -1684,13 +1598,12 @@ def test_question_number_bad_input(): def test_question_number_input(): - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", "ask": "some question", } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( @@ -1722,12 +1635,11 @@ def test_question_number_input(): def test_question_number_input_no_ask(): - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( @@ -1741,13 +1653,12 @@ def test_question_number_input_no_ask(): def test_question_number_no_input_optional(): - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", "optional": True, } - ] + } answers = {} with patch.object(os, "isatty", return_value=False): out = ask_questions_and_parse_answers(questions, answers)[0] @@ -1758,14 +1669,13 @@ def test_question_number_no_input_optional(): def test_question_number_optional_with_input(): - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "ask": "some question", "type": "number", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( @@ -1779,13 +1689,12 @@ def test_question_number_optional_with_input(): def test_question_number_optional_with_input_without_ask(): - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", "optional": True, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="0"), patch.object( @@ -1799,14 +1708,13 @@ def test_question_number_optional_with_input_without_ask(): def test_question_number_no_input_default(): - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "ask": "some question", "type": "number", "default": 1337, } - ] + } answers = {} with patch.object(os, "isatty", return_value=False): out = ask_questions_and_parse_answers(questions, answers)[0] @@ -1817,14 +1725,13 @@ def test_question_number_no_input_default(): def test_question_number_bad_default(): - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "ask": "some question", "type": "number", "default": "bad default", } - ] + } answers = {} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): ask_questions_and_parse_answers(questions, answers) @@ -1832,13 +1739,12 @@ def test_question_number_bad_default(): def test_question_number_input_test_ask(): ask_text = "some question" - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", "ask": ask_text, } - ] + } answers = {} with patch.object( @@ -1859,14 +1765,13 @@ def test_question_number_input_test_ask(): def test_question_number_input_test_ask_with_default(): ask_text = "some question" default_value = 1337 - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", "ask": ask_text, "default": default_value, } - ] + } answers = {} with patch.object( @@ -1888,14 +1793,13 @@ def test_question_number_input_test_ask_with_default(): def test_question_number_input_test_ask_with_example(): ask_text = "some question" example_value = 1337 - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", "ask": ask_text, "example": example_value, } - ] + } answers = {} with patch.object( @@ -1910,14 +1814,13 @@ def test_question_number_input_test_ask_with_example(): def test_question_number_input_test_ask_with_help(): ask_text = "some question" help_value = 1337 - questions = [ - { - "name": "some_number", + questions = { + "some_number": { "type": "number", "ask": ask_text, "help": help_value, } - ] + } answers = {} with patch.object( @@ -1929,7 +1832,7 @@ def test_question_number_input_test_ask_with_help(): def test_question_display_text(): - questions = [{"name": "some_app", "type": "display_text", "ask": "foobar"}] + questions = {"some_app": {"type": "display_text", "ask": "foobar"}} answers = {} with patch.object(sys, "stdout", new_callable=StringIO) as stdout, patch.object( @@ -1947,12 +1850,11 @@ def test_question_file_from_cli(): os.system(f"rm -f {filename}") os.system(f"echo helloworld > {filename}") - questions = [ - { - "name": "some_file", + questions = { + "some_file": { "type": "file", } - ] + } answers = {"some_file": filename} out = ask_questions_and_parse_answers(questions, answers)[0] @@ -1978,12 +1880,11 @@ def test_question_file_from_api(): from base64 import b64encode b64content = b64encode(b"helloworld") - questions = [ - { - "name": "some_file", + questions = { + "some_file": { "type": "file", } - ] + } answers = {"some_file": b64content} interface_type_bkp = Moulinette.interface.type diff --git a/src/tests/test_settings.py b/src/tests/test_settings.py index 1a9063e56..2eaebba55 100644 --- a/src/tests/test_settings.py +++ b/src/tests/test_settings.py @@ -1,179 +1,231 @@ import os -import json -import glob import pytest +import yaml +from mock import patch -from yunohost.utils.error import YunohostError - -import yunohost.settings as settings +import moulinette +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.settings import ( settings_get, settings_list, - _get_settings, settings_set, settings_reset, settings_reset_all, - SETTINGS_PATH_OTHER_LOCATION, SETTINGS_PATH, - DEFAULTS, ) -DEFAULTS["example.bool"] = {"type": "bool", "default": True} -DEFAULTS["example.int"] = {"type": "int", "default": 42} -DEFAULTS["example.string"] = {"type": "string", "default": "yolo swag"} -DEFAULTS["example.enum"] = {"type": "enum", "default": "a", "choices": ["a", "b", "c"]} +EXAMPLE_SETTINGS = """ +[example] + [example.example] + [example.example.boolean] + type = "boolean" + yes = "True" + no = "False" + default = "True" + + [example.example.number] + type = "number" + default = 42 + + [example.example.string] + type = "string" + default = "yolo swag" + + [example.example.select] + type = "select" + choices = ["a", "b", "c"] + default = "a" +""" def setup_function(function): - os.system("mv /etc/yunohost/settings.json /etc/yunohost/settings.json.saved") + # Backup settings + if os.path.exists(SETTINGS_PATH): + os.system(f"mv {SETTINGS_PATH} {SETTINGS_PATH}.saved") + # Add example settings to config panel + os.system( + "cp /usr/share/yunohost/config_global.toml /usr/share/yunohost/config_global.toml.saved" + ) + with open("/usr/share/yunohost/config_global.toml", "a") as file: + file.write(EXAMPLE_SETTINGS) def teardown_function(function): - os.system("mv /etc/yunohost/settings.json.saved /etc/yunohost/settings.json") - for filename in glob.glob("/etc/yunohost/settings-*.json"): - os.remove(filename) + if os.path.exists("/etc/yunohost/settings.yml.saved"): + os.system(f"mv {SETTINGS_PATH}.saved {SETTINGS_PATH}") + elif os.path.exists(SETTINGS_PATH): + os.remove(SETTINGS_PATH) + os.system( + "mv /usr/share/yunohost/config_global.toml.saved /usr/share/yunohost/config_global.toml" + ) -def monkey_get_setting_description(key): - return "Dummy %s setting" % key.split(".")[-1] +old_translate = moulinette.core.Translator.translate -settings._get_setting_description = monkey_get_setting_description +def _monkeypatch_translator(self, key, *args, **kwargs): + + if key.startswith("global_settings_setting_"): + return f"Dummy translation for {key}" + + return old_translate(self, key, *args, **kwargs) + + +moulinette.core.Translator.translate = _monkeypatch_translator + + +def _get_settings(): + return yaml.load(open(SETTINGS_PATH, "r")) def test_settings_get_bool(): - assert settings_get("example.bool") + assert settings_get("example.example.boolean") -def test_settings_get_full_bool(): - assert settings_get("example.bool", True) == { - "type": "bool", - "value": True, - "default": True, - "description": "Dummy bool setting", - } +# FIXME : Testing this doesn't make sense ? This should be tested in test_config.py ? +# def test_settings_get_full_bool(): +# assert settings_get("example.example.boolean", True) == {'version': '1.0', +# 'i18n': 'global_settings_setting', +# 'panels': [{'services': [], +# 'actions': {'apply': {'en': 'Apply'}}, +# 'sections': [{'name': '', +# 'services': [], +# 'optional': True, +# 'options': [{'type': 'boolean', +# 'yes': 'True', +# 'no': 'False', +# 'default': 'True', +# 'id': 'boolean', +# 'name': 'boolean', +# 'optional': True, +# 'current_value': 'True', +# 'ask': 'global_settings_setting_boolean', +# 'choices': []}], +# 'id': 'example'}], +# 'id': 'example', +# 'name': {'en': 'Example'}}]} def test_settings_get_int(): - assert settings_get("example.int") == 42 + assert settings_get("example.example.number") == 42 -def test_settings_get_full_int(): - assert settings_get("example.int", True) == { - "type": "int", - "value": 42, - "default": 42, - "description": "Dummy int setting", - } +# def test_settings_get_full_int(): +# assert settings_get("example.int", True) == { +# "type": "int", +# "value": 42, +# "default": 42, +# "description": "Dummy int setting", +# } def test_settings_get_string(): - assert settings_get("example.string") == "yolo swag" + assert settings_get("example.example.string") == "yolo swag" -def test_settings_get_full_string(): - assert settings_get("example.string", True) == { - "type": "string", - "value": "yolo swag", - "default": "yolo swag", - "description": "Dummy string setting", - } +# def test_settings_get_full_string(): +# assert settings_get("example.example.string", True) == { +# "type": "string", +# "value": "yolo swag", +# "default": "yolo swag", +# "description": "Dummy string setting", +# } -def test_settings_get_enum(): - assert settings_get("example.enum") == "a" +def test_settings_get_select(): + assert settings_get("example.example.select") == "a" -def test_settings_get_full_enum(): - assert settings_get("example.enum", True) == { - "type": "enum", - "value": "a", - "default": "a", - "description": "Dummy enum setting", - "choices": ["a", "b", "c"], - } +# def test_settings_get_full_select(): +# option = settings_get("example.example.select", full=True).get('panels')[0].get('sections')[0].get('options')[0] +# assert option.get('choices') == ["a", "b", "c"] def test_settings_get_doesnt_exists(): - with pytest.raises(YunohostError): + with pytest.raises(YunohostValidationError): settings_get("doesnt.exists") -def test_settings_list(): - assert settings_list() == _get_settings() +# def test_settings_list(): +# assert settings_list() == _get_settings() def test_settings_set(): - settings_set("example.bool", False) - assert settings_get("example.bool") is False + settings_set("example.example.boolean", False) + assert settings_get("example.example.boolean") == 0 - settings_set("example.bool", "on") - assert settings_get("example.bool") is True + settings_set("example.example.boolean", "on") + assert settings_get("example.example.boolean") == 1 def test_settings_set_int(): - settings_set("example.int", 21) - assert settings_get("example.int") == 21 + settings_set("example.example.number", 21) + assert settings_get("example.example.number") == 21 -def test_settings_set_enum(): - settings_set("example.enum", "c") - assert settings_get("example.enum") == "c" +def test_settings_set_select(): + settings_set("example.example.select", "c") + assert settings_get("example.example.select") == "c" def test_settings_set_doesexit(): - with pytest.raises(YunohostError): + with pytest.raises(YunohostValidationError): settings_set("doesnt.exist", True) def test_settings_set_bad_type_bool(): - with pytest.raises(YunohostError): - settings_set("example.bool", 42) - with pytest.raises(YunohostError): - settings_set("example.bool", "pouet") + + with patch.object(os, "isatty", return_value=False): + with pytest.raises(YunohostError): + settings_set("example.example.boolean", 42) + with pytest.raises(YunohostError): + settings_set("example.example.boolean", "pouet") def test_settings_set_bad_type_int(): - with pytest.raises(YunohostError): - settings_set("example.int", True) - with pytest.raises(YunohostError): - settings_set("example.int", "pouet") + # with pytest.raises(YunohostError): + # settings_set("example.example.number", True) + with patch.object(os, "isatty", return_value=False): + with pytest.raises(YunohostError): + settings_set("example.example.number", "pouet") -def test_settings_set_bad_type_string(): - with pytest.raises(YunohostError): - settings_set("example.string", True) - with pytest.raises(YunohostError): - settings_set("example.string", 42) +# def test_settings_set_bad_type_string(): +# with pytest.raises(YunohostError): +# settings_set(eexample.example.string", True) +# with pytest.raises(YunohostError): +# settings_set("example.example.string", 42) -def test_settings_set_bad_value_enum(): - with pytest.raises(YunohostError): - settings_set("example.enum", True) - with pytest.raises(YunohostError): - settings_set("example.enum", "e") - with pytest.raises(YunohostError): - settings_set("example.enum", 42) - with pytest.raises(YunohostError): - settings_set("example.enum", "pouet") +def test_settings_set_bad_value_select(): + with patch.object(os, "isatty", return_value=False): + with pytest.raises(YunohostError): + settings_set("example.example.select", True) + with pytest.raises(YunohostError): + settings_set("example.example.select", "e") + with pytest.raises(YunohostError): + settings_set("example.example.select", 42) + with pytest.raises(YunohostError): + settings_set("example.example.select", "pouet") def test_settings_list_modified(): - settings_set("example.int", 21) - assert settings_list()["example.int"] == { - "default": 42, - "description": "Dummy int setting", - "type": "int", - "value": 21, - } + settings_set("example.example.number", 21) + assert int(settings_list()["example.example.number"]["value"]) == 21 def test_reset(): - settings_set("example.int", 21) - assert settings_get("example.int") == 21 - settings_reset("example.int") - assert settings_get("example.int") == settings_get("example.int", True)["default"] + option = ( + settings_get("example.example.number", full=True) + .get("panels")[0] + .get("sections")[0] + .get("options")[0] + ) + settings_set("example.example.number", 21) + assert settings_get("example.example.number") == 21 + settings_reset("example.example.number") + assert settings_get("example.example.number") == option["default"] def test_settings_reset_doesexit(): @@ -183,10 +235,10 @@ def test_settings_reset_doesexit(): def test_reset_all(): settings_before = settings_list() - settings_set("example.bool", False) - settings_set("example.int", 21) - settings_set("example.string", "pif paf pouf") - settings_set("example.enum", "c") + settings_set("example.example.boolean", False) + settings_set("example.example.number", 21) + settings_set("example.example.string", "pif paf pouf") + settings_set("example.example.select", "c") assert settings_before != settings_list() settings_reset_all() if settings_before != settings_list(): @@ -194,30 +246,30 @@ def test_reset_all(): assert settings_before[i] == settings_list()[i] -def test_reset_all_backup(): - settings_before = settings_list() - settings_set("example.bool", False) - settings_set("example.int", 21) - settings_set("example.string", "pif paf pouf") - settings_set("example.enum", "c") - settings_after_modification = settings_list() - assert settings_before != settings_after_modification - old_settings_backup_path = settings_reset_all()["old_settings_backup_path"] - - for i in settings_after_modification: - del settings_after_modification[i]["description"] - - assert settings_after_modification == json.load(open(old_settings_backup_path, "r")) +# def test_reset_all_backup(): +# settings_before = settings_list() +# settings_set("example.bool", False) +# settings_set("example.int", 21) +# settings_set("example.string", "pif paf pouf") +# settings_set("example.select", "c") +# settings_after_modification = settings_list() +# assert settings_before != settings_after_modification +# old_settings_backup_path = settings_reset_all()["old_settings_backup_path"] +# +# for i in settings_after_modification: +# del settings_after_modification[i]["description"] +# +# assert settings_after_modification == json.load(open(old_settings_backup_path, "r")) -def test_unknown_keys(): - unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown" - unknown_setting = { - "unkown_key": {"value": 42, "default": 31, "type": "int"}, - } - open(SETTINGS_PATH, "w").write(json.dumps(unknown_setting)) - - # stimulate a write - settings_reset_all() - - assert unknown_setting == json.load(open(unknown_settings_path, "r")) +# def test_unknown_keys(): +# unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown" +# unknown_setting = { +# "unkown_key": {"value": 42, "default": 31, "type": "int"}, +# } +# open(SETTINGS_PATH, "w").write(json.dumps(unknown_setting)) +# +# # stimulate a write +# settings_reset_all() +# +# assert unknown_setting == json.load(open(unknown_settings_path, "r")) diff --git a/src/tests/test_user-group.py b/src/tests/test_user-group.py index e561118e0..343431b69 100644 --- a/src/tests/test_user-group.py +++ b/src/tests/test_user-group.py @@ -11,7 +11,6 @@ from yunohost.user import ( user_import, user_export, FIELDS_FOR_IMPORT, - FIRST_ALIASES, user_group_list, user_group_create, user_group_delete, @@ -29,7 +28,7 @@ def clean_user_groups(): user_delete(u, purge=True) for g in user_group_list()["groups"]: - if g not in ["all_users", "visitors"]: + if g not in ["all_users", "visitors", "admins"]: user_group_delete(g) @@ -39,9 +38,9 @@ def setup_function(function): global maindomain maindomain = _get_maindomain() - user_create("alice", "Alice", "White", maindomain, "test123Ynh") - user_create("bob", "Bob", "Snow", maindomain, "test123Ynh") - user_create("jack", "Jack", "Black", maindomain, "test123Ynh") + user_create("alice", maindomain, "test123Ynh", admin=True, fullname="Alice White") + user_create("bob", maindomain, "test123Ynh", fullname="Bob Snow") + user_create("jack", maindomain, "test123Ynh", fullname="Jack Black") user_group_create("dev") user_group_create("apps") @@ -80,6 +79,7 @@ def test_list_groups(): assert "alice" in res assert "bob" in res assert "jack" in res + assert "alice" in res["admins"]["members"] for u in ["alice", "bob", "jack"]: assert u in res assert u in res[u]["members"] @@ -94,7 +94,7 @@ def test_list_groups(): def test_create_user(mocker): with message(mocker, "user_created"): - user_create("albert", "Albert", "Good", maindomain, "test123Ynh") + user_create("albert", maindomain, "test123Ynh", fullname="Albert Good") group_res = user_group_list()["groups"] assert "albert" in user_list()["users"] @@ -175,10 +175,9 @@ def test_import_user(mocker): def test_export_user(mocker): result = user_export() - aliases = ",".join([alias + maindomain for alias in FIRST_ALIASES]) should_be = ( "username;firstname;lastname;password;mail;mail-alias;mail-forward;mailbox-quota;groups\r\n" - f"alice;Alice;White;;alice@{maindomain};{aliases};;0;dev\r\n" + f"alice;Alice;White;;alice@{maindomain};;;0;admins,dev\r\n" f"bob;Bob;Snow;;bob@{maindomain};;;0;apps\r\n" f"jack;Jack;Black;;jack@{maindomain};;;0;" ) @@ -212,17 +211,17 @@ def test_del_group(mocker): def test_create_user_with_password_too_simple(mocker): with raiseYunohostError(mocker, "password_listed"): - user_create("other", "Alice", "White", maindomain, "12") + user_create("other", maindomain, "12", fullname="Alice White") def test_create_user_already_exists(mocker): with raiseYunohostError(mocker, "user_already_exists"): - user_create("alice", "Alice", "White", maindomain, "test123Ynh") + user_create("alice", maindomain, "test123Ynh", fullname="Alice White") def test_create_user_with_domain_that_doesnt_exists(mocker): with raiseYunohostError(mocker, "domain_unknown"): - user_create("alice", "Alice", "White", "doesnt.exists", "test123Ynh") + user_create("alice", "doesnt.exists", "test123Ynh", fullname="Alice White") def test_update_user_with_mail_address_already_taken(mocker): @@ -272,8 +271,13 @@ def test_update_user(mocker): user_update("alice", firstname="NewName", lastname="NewLast") info = user_info("alice") - assert info["firstname"] == "NewName" - assert info["lastname"] == "NewLast" + assert info["fullname"] == "NewName NewLast" + + with message(mocker, "user_updated"): + user_update("alice", fullname="New2Name New2Last") + + info = user_info("alice") + assert info["fullname"] == "New2Name New2Last" def test_update_group_add_user(mocker): diff --git a/src/tools.py b/src/tools.py index 21262c64b..02b4f58d1 100644 --- a/src/tools.py +++ b/src/tools.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_tools.py - - Specific tools -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os import subprocess @@ -34,9 +27,13 @@ from typing import List from moulinette import Moulinette, m18n from moulinette.utils.log import getActionLogger from moulinette.utils.process import call_async_output -from moulinette.utils.filesystem import read_yaml, write_to_yaml, cp, mkdir, rm +from moulinette.utils.filesystem import read_yaml, write_to_yaml, cp, mkdir, rm, chown -from yunohost.app import app_upgrade, app_list +from yunohost.app import ( + app_upgrade, + app_list, + _list_upgradable_apps, +) from yunohost.app_catalog import ( _initialize_apps_catalog_system, _update_apps_catalog, @@ -45,10 +42,12 @@ from yunohost.domain import domain_add from yunohost.firewall import firewall_upnp from yunohost.service import service_start, service_enable from yunohost.regenconf import regen_conf -from yunohost.utils.packages import ( +from yunohost.utils.system import ( _dump_sources_list, _list_upgradable_apt_packages, ynh_packages_version, + dpkg_is_broken, + dpkg_lock_available, ) from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation, OperationLogger @@ -62,63 +61,42 @@ def tools_versions(): return ynh_packages_version() -def tools_adminpw(new_password, check_strength=True): - """ - Change admin password +def tools_rootpw(new_password, check_strength=True): - Keyword argument: - new_password - - """ from yunohost.user import _hash_user_password - from yunohost.utils.password import assert_password_is_strong_enough + from yunohost.utils.password import ( + assert_password_is_strong_enough, + assert_password_is_compatible, + ) import spwd + assert_password_is_compatible(new_password) if check_strength: assert_password_is_strong_enough("admin", new_password) - # UNIX seems to not like password longer than 127 chars ... - # e.g. SSH login gets broken (or even 'su admin' when entering the password) - if len(new_password) >= 127: - raise YunohostValidationError("admin_password_too_long") - new_hash = _hash_user_password(new_password) - from yunohost.utils.ldap import _get_ldap_interface - - ldap = _get_ldap_interface() - + # Write as root password try: - ldap.update( - "cn=admin", - {"userPassword": [new_hash]}, - ) - except Exception as e: - logger.error(f"unable to change admin password : {e}") - raise YunohostError("admin_password_change_failed") - else: - # Write as root password - try: - hash_root = spwd.getspnam("root").sp_pwd + hash_root = spwd.getspnam("root").sp_pwd - with open("/etc/shadow", "r") as before_file: - before = before_file.read() + with open("/etc/shadow", "r") as before_file: + before = before_file.read() - with open("/etc/shadow", "w") as after_file: - after_file.write( - before.replace( - "root:" + hash_root, "root:" + new_hash.replace("{CRYPT}", "") - ) + with open("/etc/shadow", "w") as after_file: + after_file.write( + before.replace( + "root:" + hash_root, "root:" + new_hash.replace("{CRYPT}", "") ) - # An IOError may be thrown if for some reason we can't read/write /etc/passwd - # A KeyError could also be thrown if 'root' is not in /etc/passwd in the first place (for example because no password defined ?) - # (c.f. the line about getspnam) - except (IOError, KeyError): - logger.warning(m18n.n("root_password_desynchronized")) - return - - logger.info(m18n.n("root_password_replaced_by_admin_password")) - logger.success(m18n.n("admin_password_changed")) + ) + # An IOError may be thrown if for some reason we can't read/write /etc/passwd + # A KeyError could also be thrown if 'root' is not in /etc/passwd in the first place (for example because no password defined ?) + # (c.f. the line about getspnam) + except (IOError, KeyError): + logger.warning(m18n.n("root_password_desynchronized")) + return + else: + logger.info(m18n.n("root_password_changed")) def tools_maindomain(new_main_domain=None): @@ -166,44 +144,26 @@ def _set_hostname(hostname, pretty_hostname=None): logger.debug(out) -def _detect_virt(): - """ - Returns the output of systemd-detect-virt (so e.g. 'none' or 'lxc' or ...) - You can check the man of the command to have a list of possible outputs... - """ - - p = subprocess.Popen( - "systemd-detect-virt".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT - ) - - out, _ = p.communicate() - return out.split()[0] - - @is_unit_operation(exclude=["dyndns_password_recovery", "password"]) def tools_postinstall( operation_logger, domain, + username, + fullname, password, dyndns_password_recovery=None, no_subscribe=False, - force_password=False, force_diskspace=False, ): - """ - YunoHost post-install - Keyword argument: - domain -- YunoHost main domain - ignore_dyndns -- Do not subscribe domain to a DynDNS service (only - needed for nohost.me, noho.st domains) - password -- YunoHost admin password - - """ from yunohost.dyndns import _dyndns_available from yunohost.utils.dns import is_yunohost_dyndns_domain - from yunohost.utils.password import assert_password_is_strong_enough + from yunohost.utils.password import ( + assert_password_is_strong_enough, + assert_password_is_compatible, + ) from yunohost.domain import domain_main_domain + from yunohost.user import user_create import psutil # Do some checks at first @@ -217,7 +177,9 @@ def tools_postinstall( ) # Check there's at least 10 GB on the rootfs... - disk_partitions = sorted(psutil.disk_partitions(), key=lambda k: k.mountpoint) + disk_partitions = sorted( + psutil.disk_partitions(all=True), key=lambda k: k.mountpoint + ) main_disk_partitions = [d for d in disk_partitions if d.mountpoint in ["/", "/var"]] main_space = sum( psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions @@ -227,8 +189,8 @@ def tools_postinstall( raise YunohostValidationError("postinstall_low_rootfsspace") # Check password - if not force_password: - assert_password_is_strong_enough("admin", password) + assert_password_is_compatible(password) + assert_password_is_strong_enough("admin", password) # If this is a nohost.me/noho.st, actually check for availability if is_yunohost_dyndns_domain(domain): @@ -260,8 +222,10 @@ def tools_postinstall( domain_add(domain, dyndns_password_recovery=dyndns_password_recovery, no_subscribe=no_subscribe) domain_main_domain(domain) + user_create(username, domain, password, admin=True, fullname=fullname) + # Update LDAP admin and create home dir - tools_adminpw(password, check_strength=not force_password) + tools_rootpw(password) # Enable UPnP silently and reload firewall firewall_upnp("enable", no_refresh=True) @@ -311,6 +275,18 @@ def tools_postinstall( def tools_regen_conf( names=[], with_diff=False, force=False, dry_run=False, list_pending=False ): + + # Make sure the settings are migrated before running the migration, + # which may otherwise fuck things up such as the ssh config ... + # We do this here because the regen-conf is called before the migration in debian/postinst + if os.path.exists("/etc/yunohost/settings.json") and not os.path.exists( + "/etc/yunohost/settings.yml" + ): + try: + tools_migrations_run(["0025_global_settings_to_configpanel"]) + except Exception as e: + logger.error(e) + return regen_conf(names, with_diff, force, dry_run, list_pending) @@ -333,7 +309,9 @@ def tools_update(target=None): # Update APT cache # LC_ALL=C is here to make sure the results are in english - command = "LC_ALL=C apt-get update -o Acquire::Retries=3" + command = ( + "LC_ALL=C apt-get update -o Acquire::Retries=3 --allow-releaseinfo-change" + ) # Filter boring message about "apt not having a stable CLI interface" # Also keep track of wether or not we encountered a warning... @@ -383,12 +361,34 @@ def tools_update(target=None): except YunohostError as e: logger.error(str(e)) - upgradable_apps = list(app_list(upgradable=True)["apps"]) + upgradable_apps = _list_upgradable_apps() if len(upgradable_apps) == 0 and len(upgradable_system_packages) == 0: logger.info(m18n.n("already_up_to_date")) - return {"system": upgradable_system_packages, "apps": upgradable_apps} + important_yunohost_upgrade = False + if upgradable_system_packages and any( + p["name"] == "yunohost" for p in upgradable_system_packages + ): + yunohost = [p for p in upgradable_system_packages if p["name"] == "yunohost"][0] + current_version = yunohost["current_version"].split(".")[:2] + new_version = yunohost["new_version"].split(".")[:2] + important_yunohost_upgrade = current_version != new_version + + # Wrapping this in a try/except just in case for some reason we can't load + # the migrations, which would result in the update/upgrade process being blocked... + try: + pending_migrations = tools_migrations_list(pending=True)["migrations"] + except Exception as e: + logger.error(e) + pending_migrations = [] + + return { + "system": upgradable_system_packages, + "apps": upgradable_apps, + "important_yunohost_upgrade": important_yunohost_upgrade, + "pending_migrations": pending_migrations, + } @is_unit_operation() @@ -400,13 +400,12 @@ def tools_upgrade(operation_logger, target=None): apps -- List of apps to upgrade (or [] to update all apps) system -- True to upgrade system """ - from yunohost.utils import packages - if packages.dpkg_is_broken(): + if dpkg_is_broken(): raise YunohostValidationError("dpkg_is_broken") # Check for obvious conflict with other dpkg/apt commands already running in parallel - if not packages.dpkg_lock_available(): + if not dpkg_lock_available(): raise YunohostValidationError("dpkg_lock_not_available") if target not in ["apps", "system"]: @@ -488,7 +487,10 @@ def tools_upgrade(operation_logger, target=None): returncode = call_async_output(dist_upgrade, callbacks, shell=True) # If yunohost is being upgraded from the webadmin - if "yunohost" in upgradables and Moulinette.interface.type == "api": + if ( + any(p["name"] == "yunohost" for p in upgradables) + and Moulinette.interface.type == "api" + ): # Restart the API after 10 sec (at now doesn't support sub-minute times...) # We do this so that the API / webadmin still gets the proper HTTP response @@ -530,6 +532,8 @@ def _apt_log_line_is_relevant(line): "==> Keeping old config file as default.", "is a disabled or a static unit", " update-rc.d: warning: start and stop actions are no longer supported; falling back to defaults", + "insserv: warning: current stop runlevel", + "insserv: warning: current start runlevel", ] return line.rstrip() and all(i not in line.rstrip() for i in irrelevants) @@ -972,7 +976,7 @@ class Migration: def description(self): return m18n.n(f"migration_description_{self.id}") - def ldap_migration(self, run): + def ldap_migration(run): def func(self): # Backup LDAP before the migration @@ -1000,22 +1004,28 @@ class Migration: try: run(self, backup_folder) except Exception: - logger.warning( - m18n.n("migration_ldap_migration_failed_trying_to_rollback") - ) - os.system("systemctl stop slapd") - # To be sure that we don't keep some part of the old config - rm("/etc/ldap/slapd.d", force=True, recursive=True) - cp(f"{backup_folder}/ldap_config", "/etc/ldap", recursive=True) - cp(f"{backup_folder}/ldap_db", "/var/lib/ldap", recursive=True) - cp( - f"{backup_folder}/apps_settings", - "/etc/yunohost/apps", - recursive=True, - ) - os.system("systemctl start slapd") - rm(backup_folder, force=True, recursive=True) - logger.info(m18n.n("migration_ldap_rollback_success")) + if self.ldap_migration_started: + logger.warning( + m18n.n("migration_ldap_migration_failed_trying_to_rollback") + ) + os.system("systemctl stop slapd") + # To be sure that we don't keep some part of the old config + rm("/etc/ldap", force=True, recursive=True) + cp(f"{backup_folder}/ldap_config", "/etc/ldap", recursive=True) + chown("/etc/ldap/schema/", "openldap", "openldap", recursive=True) + chown("/etc/ldap/slapd.d/", "openldap", "openldap", recursive=True) + rm("/var/lib/ldap", force=True, recursive=True) + cp(f"{backup_folder}/ldap_db", "/var/lib/ldap", recursive=True) + rm("/etc/yunohost/apps", force=True, recursive=True) + chown("/var/lib/ldap/", "openldap", recursive=True) + cp( + f"{backup_folder}/apps_settings", + "/etc/yunohost/apps", + recursive=True, + ) + os.system("systemctl start slapd") + rm(backup_folder, force=True, recursive=True) + logger.info(m18n.n("migration_ldap_rollback_success")) raise else: rm(backup_folder, force=True, recursive=True) diff --git a/src/user.py b/src/user.py index 7d023fd83..deaebba5b 100644 --- a/src/user.py +++ b/src/user.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2014 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_user.py - - Manage users -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import pwd @@ -40,6 +33,7 @@ from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.service import service_status from yunohost.log import is_unit_operation +from yunohost.utils.system import binary_to_human logger = getActionLogger("yunohost.user") @@ -55,7 +49,7 @@ FIELDS_FOR_IMPORT = { "groups": r"^|([a-z0-9_]+(,?[a-z0-9_]+)*)$", } -FIRST_ALIASES = ["root@", "admin@", "webmaster@", "postmaster@", "abuse@"] +ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] def user_list(fields=None): @@ -133,21 +127,48 @@ def user_list(fields=None): def user_create( operation_logger, username, - firstname, - lastname, domain, password, + fullname=None, + firstname=None, + lastname=None, mailbox_quota="0", + admin=False, from_import=False, ): + if firstname or lastname: + logger.warning( + "Options --firstname / --lastname of 'yunohost user create' are deprecated. We recommend using --fullname instead." + ) + + if not fullname or not fullname.strip(): + if not firstname.strip(): + raise YunohostValidationError( + "You should specify the fullname of the user using option -F" + ) + lastname = ( + lastname or " " + ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... + fullname = f"{firstname} {lastname}".strip() + else: + fullname = fullname.strip() + firstname = fullname.split()[0] + lastname = ( + " ".join(fullname.split()[1:]) or " " + ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... + from yunohost.domain import domain_list, _get_maindomain, _assert_domain_exists from yunohost.hook import hook_callback - from yunohost.utils.password import assert_password_is_strong_enough + from yunohost.utils.password import ( + assert_password_is_strong_enough, + assert_password_is_compatible, + ) from yunohost.utils.ldap import _get_ldap_interface - # Ensure sufficiently complex password - assert_password_is_strong_enough("user", password) + # Ensure compatibility and sufficiently complex password + assert_password_is_compatible(password) + assert_password_is_strong_enough("admin" if admin else "user", password) # Validate domain used for email address/xmpp account if domain is None: @@ -188,10 +209,7 @@ def user_create( if username in all_existing_usernames: raise YunohostValidationError("system_username_exists") - main_domain = _get_maindomain() - aliases = [alias + main_domain for alias in FIRST_ALIASES] - - if mail in aliases: + if mail.split("@")[0] in ADMIN_ALIASES: raise YunohostValidationError("mail_unavailable") if not from_import: @@ -201,15 +219,17 @@ def user_create( all_uid = {str(x.pw_uid) for x in pwd.getpwall()} all_gid = {str(x.gr_gid) for x in grp.getgrall()} + # Prevent users from obtaining uid 1007 which is the uid of the legacy admin, + # and there could be a edge case where a new user becomes owner of an old, removed admin user + all_uid.add("1007") + all_gid.add("1007") + uid_guid_found = False while not uid_guid_found: # LXC uid number is limited to 65536 by default uid = str(random.randint(1001, 65000)) uid_guid_found = uid not in all_uid and uid not in all_gid - # Adapt values for LDAP - fullname = f"{firstname} {lastname}" - attr_dict = { "objectClass": [ "mailAccount", @@ -232,10 +252,6 @@ def user_create( "loginShell": ["/bin/bash"], } - # If it is the first user, add some aliases - if not ldap.search(base="ou=users", filter="uid=*"): - attr_dict["mail"] = [attr_dict["mail"]] + aliases - try: ldap.add(f"uid={username},ou=users", attr_dict) except Exception as e: @@ -261,6 +277,8 @@ def user_create( # Create group for user and add to group 'all_users' user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) user_group_update(groupname="all_users", add=username, force=True, sync_perm=True) + if admin: + user_group_update(groupname="admins", add=username, sync_perm=True) # Trigger post_user_create hooks env_dict = { @@ -282,14 +300,7 @@ def user_create( @is_unit_operation([("username", "user")]) def user_delete(operation_logger, username, purge=False, from_import=False): - """ - Delete user - Keyword argument: - username -- Username to delete - purge - - """ from yunohost.hook import hook_callback from yunohost.utils.ldap import _get_ldap_interface @@ -347,25 +358,27 @@ def user_update( remove_mailalias=None, mailbox_quota=None, from_import=False, + fullname=None, ): - """ - Update user informations - Keyword argument: - lastname - mail - firstname - add_mailalias -- Mail aliases to add - remove_mailforward -- Mailforward addresses to remove - username -- Username of user to update - add_mailforward -- Mailforward addresses to add - change_password -- New password to set - remove_mailalias -- Mail aliases to remove + if firstname or lastname: + logger.warning( + "Options --firstname / --lastname of 'yunohost user create' are deprecated. We recommend using --fullname instead." + ) - """ - from yunohost.domain import domain_list, _get_maindomain + if fullname and fullname.strip(): + fullname = fullname.strip() + firstname = fullname.split()[0] + lastname = ( + " ".join(fullname.split()[1:]) or " " + ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... + + from yunohost.domain import domain_list from yunohost.app import app_ssowatconf - from yunohost.utils.password import assert_password_is_strong_enough + from yunohost.utils.password import ( + assert_password_is_strong_enough, + assert_password_is_compatible, + ) from yunohost.utils.ldap import _get_ldap_interface from yunohost.hook import hook_callback @@ -373,7 +386,7 @@ def user_update( # Populate user informations ldap = _get_ldap_interface() - attrs_to_fetch = ["givenName", "sn", "mail", "maildrop"] + attrs_to_fetch = ["givenName", "sn", "mail", "maildrop", "memberOf"] result = ldap.search( base="ou=users", filter="uid=" + username, @@ -389,20 +402,20 @@ def user_update( if firstname: new_attr_dict["givenName"] = [firstname] # TODO: Validate new_attr_dict["cn"] = new_attr_dict["displayName"] = [ - firstname + " " + user["sn"][0] + (firstname + " " + user["sn"][0]).strip() ] env_dict["YNH_USER_FIRSTNAME"] = firstname if lastname: new_attr_dict["sn"] = [lastname] # TODO: Validate new_attr_dict["cn"] = new_attr_dict["displayName"] = [ - user["givenName"][0] + " " + lastname + (user["givenName"][0] + " " + lastname).strip() ] env_dict["YNH_USER_LASTNAME"] = lastname if lastname and firstname: new_attr_dict["cn"] = new_attr_dict["displayName"] = [ - firstname + " " + lastname + (firstname + " " + lastname).strip() ] # change_password is None if user_update is not called to change the password @@ -414,16 +427,18 @@ def user_update( change_password = Moulinette.prompt( m18n.n("ask_password"), is_password=True, confirm=True ) - # Ensure sufficiently complex password - assert_password_is_strong_enough("user", change_password) + + # Ensure compatibility and sufficiently complex password + assert_password_is_compatible(change_password) + is_admin = "cn=admins,ou=groups,dc=yunohost,dc=org" in user["memberOf"] + assert_password_is_strong_enough( + "admin" if is_admin else "user", change_password + ) new_attr_dict["userPassword"] = [_hash_user_password(change_password)] env_dict["YNH_USER_PASSWORD"] = change_password if mail: - main_domain = _get_maindomain() - aliases = [alias + main_domain for alias in FIRST_ALIASES] - # If the requested mail address is already as main address or as an alias by this user if mail in user["mail"]: user["mail"].remove(mail) @@ -437,7 +452,8 @@ def user_update( raise YunohostError( "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] ) - if mail in aliases: + + if mail.split("@")[0] in ADMIN_ALIASES: raise YunohostValidationError("mail_unavailable") new_attr_dict["mail"] = [mail] + user["mail"][1:] @@ -446,6 +462,9 @@ def user_update( if not isinstance(add_mailalias, list): add_mailalias = [add_mailalias] for mail in add_mailalias: + if mail.split("@")[0] in ADMIN_ALIASES: + raise YunohostValidationError("mail_unavailable") + # (c.f. similar stuff as before) if mail in user["mail"]: user["mail"].remove(mail) @@ -529,7 +548,7 @@ def user_info(username): ldap = _get_ldap_interface() - user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"] + user_attrs = ["cn", "mail", "uid", "maildrop", "mailuserquota"] if len(username.split("@")) == 2: filter = "mail=" + username @@ -546,8 +565,6 @@ def user_info(username): result_dict = { "username": user["uid"][0], "fullname": user["cn"][0], - "firstname": user["givenName"][0], - "lastname": user["sn"][0], "mail": user["mail"][0], "mail-aliases": [], "mail-forward": [], @@ -588,7 +605,7 @@ def user_info(username): if has_value: storage_use = int(has_value.group(1)) - storage_use = _convertSize(storage_use) + storage_use = binary_to_human(storage_use) if is_limited: has_percent = re.search(r"%=(\d+)", cmd_result) @@ -841,10 +858,9 @@ def user_import(operation_logger, csvfile, update=False, delete=False): user_update( new_infos["username"], - new_infos["firstname"], - new_infos["lastname"], - new_infos["mail"], - new_infos["password"], + firstname=new_infos["firstname"], + lastname=new_infos["lastname"], + change_password=new_infos["password"], mailbox_quota=new_infos["mailbox-quota"], mail=new_infos["mail"], add_mailalias=new_infos["mail-alias"], @@ -884,12 +900,12 @@ def user_import(operation_logger, csvfile, update=False, delete=False): try: user_create( user["username"], - user["firstname"], - user["lastname"], user["domain"], user["password"], user["mailbox-quota"], from_import=True, + firstname=user["firstname"], + lastname=user["lastname"], ) update(user) result["created"] += 1 @@ -1062,7 +1078,7 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): # # We also can't delete "all_users" because that's a special group... existing_users = list(user_list()["users"].keys()) - undeletable_groups = existing_users + ["all_users", "visitors"] + undeletable_groups = existing_users + ["all_users", "visitors", "admins"] if groupname in undeletable_groups and not force: raise YunohostValidationError("group_cannot_be_deleted", group=groupname) @@ -1088,22 +1104,15 @@ def user_group_update( groupname, add=None, remove=None, + add_mailalias=None, + remove_mailalias=None, force=False, sync_perm=True, from_import=False, ): - """ - Update user informations - - Keyword argument: - groupname -- Groupname to update - add -- User(s) to add in group - remove -- User(s) to remove in group - - """ from yunohost.permission import permission_sync_to_user - from yunohost.utils.ldap import _get_ldap_interface + from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract existing_users = list(user_list()["users"].keys()) @@ -1120,54 +1129,133 @@ def user_group_update( "group_cannot_edit_primary_group", group=groupname ) + ldap = _get_ldap_interface() + + # Fetch info for this group + result = ldap.search( + "ou=groups", + "cn=" + groupname, + ["cn", "member", "permission", "mail", "objectClass"], + ) + + if not result: + raise YunohostValidationError("group_unknown", group=groupname) + + group = result[0] + # We extract the uid for each member of the group to keep a simple flat list of members - current_group = user_group_info(groupname)["members"] - new_group = copy.copy(current_group) + current_group_mail = group.get("mail", []) + new_group_mail = copy.copy(current_group_mail) + current_group_members = [ + _ldap_path_extract(p, "uid") for p in group.get("member", []) + ] + new_group_members = copy.copy(current_group_members) + new_attr_dict = {} if add: + users_to_add = [add] if not isinstance(add, list) else add for user in users_to_add: if user not in existing_users: raise YunohostValidationError("user_unknown", user=user) - if user in current_group: + if user in current_group_members: logger.warning( m18n.n("group_user_already_in_group", user=user, group=groupname) ) else: operation_logger.related_to.append(("user", user)) - new_group += users_to_add + new_group_members += users_to_add if remove: users_to_remove = [remove] if not isinstance(remove, list) else remove for user in users_to_remove: - if user not in current_group: + if user not in current_group_members: logger.warning( m18n.n("group_user_not_in_group", user=user, group=groupname) ) else: operation_logger.related_to.append(("user", user)) - # Remove users_to_remove from new_group - # Kinda like a new_group -= users_to_remove - new_group = [u for u in new_group if u not in users_to_remove] + # Remove users_to_remove from new_group_members + # Kinda like a new_group_members -= users_to_remove + new_group_members = [u for u in new_group_members if u not in users_to_remove] - new_group_dns = [ - "uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group - ] + # If something changed, we add this to the stuff to commit later in the code + if set(new_group_members) != set(current_group_members): + new_group_members_dns = [ + "uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group_members + ] + new_attr_dict["member"] = set(new_group_members_dns) + new_attr_dict["memberUid"] = set(new_group_members) - if set(new_group) != set(current_group): + # Check the whole alias situation + if add_mailalias: + + from yunohost.domain import domain_list + + domains = domain_list()["domains"] + + if not isinstance(add_mailalias, list): + add_mailalias = [add_mailalias] + for mail in add_mailalias: + if mail.split("@")[0] in ADMIN_ALIASES and groupname != "admins": + raise YunohostValidationError("mail_unavailable") + if mail in current_group_mail: + continue + try: + ldap.validate_uniqueness({"mail": mail}) + except Exception as e: + raise YunohostError("group_update_failed", group=groupname, error=e) + if mail[mail.find("@") + 1 :] not in domains: + raise YunohostError( + "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] + ) + new_group_mail.append(mail) + + if remove_mailalias: + from yunohost.domain import _get_maindomain + + if not isinstance(remove_mailalias, list): + remove_mailalias = [remove_mailalias] + for mail in remove_mailalias: + if ( + "@" in mail + and mail.split("@")[0] in ADMIN_ALIASES + and groupname == "admins" + and mail.split("@")[1] == _get_maindomain() + ): + raise YunohostValidationError( + f"The alias {mail} can not be removed from the 'admins' group", + raw_msg=True, + ) + if mail in new_group_mail: + new_group_mail.remove(mail) + else: + raise YunohostValidationError("mail_alias_remove_failed", mail=mail) + + if set(new_group_mail) != set(current_group_mail): + + logger.info(m18n.n("group_update_aliases", group=groupname)) + new_attr_dict["mail"] = set(new_group_mail) + + if new_attr_dict["mail"] and "mailGroup" not in group["objectClass"]: + new_attr_dict["objectClass"] = group["objectClass"] + ["mailGroup"] + if not new_attr_dict["mail"] and "mailGroup" in group["objectClass"]: + new_attr_dict["objectClass"] = [ + c + for c in group["objectClass"] + if c != "mailGroup" and c != "mailAccount" + ] + + if new_attr_dict: if not from_import: operation_logger.start() - ldap = _get_ldap_interface() try: - ldap.update( - f"cn={groupname},ou=groups", - {"member": set(new_group_dns), "memberUid": set(new_group)}, - ) + ldap.update(f"cn={groupname},ou=groups", new_attr_dict) except Exception as e: raise YunohostError("group_update_failed", group=groupname, error=e) @@ -1176,7 +1264,10 @@ def user_group_update( if not from_import: if groupname != "all_users": - logger.success(m18n.n("group_updated", group=groupname)) + if not new_attr_dict: + logger.info(m18n.n("group_no_change", group=groupname)) + else: + logger.success(m18n.n("group_updated", group=groupname)) else: logger.debug(m18n.n("group_updated", group=groupname)) @@ -1200,7 +1291,7 @@ def user_group_info(groupname): result = ldap.search( "ou=groups", "cn=" + groupname, - ["cn", "member", "permission"], + ["cn", "member", "permission", "mail"], ) if not result: @@ -1215,6 +1306,7 @@ def user_group_info(groupname): "permissions": [ _ldap_path_extract(p, "cn") for p in infos.get("permission", []) ], + "mail-aliases": [m for m in infos.get("mail", [])], } @@ -1244,6 +1336,14 @@ def user_group_remove(groupname, usernames, force=False, sync_perm=True): ) +def user_group_add_mailalias(groupname, aliases): + return user_group_update(groupname, add_mailalias=aliases, sync_perm=False) + + +def user_group_remove_mailalias(groupname, aliases): + return user_group_update(groupname, remove_mailalias=aliases, sync_perm=False) + + # # Permission subcategory # @@ -1316,14 +1416,6 @@ def user_ssh_remove_key(username, key): # -def _convertSize(num, suffix=""): - for unit in ["K", "M", "G", "T", "P", "E", "Z"]: - if abs(num) < 1024.0: - return "{:3.1f}{}{}".format(num, unit, suffix) - num /= 1024.0 - return "{:.1f}{}{}".format(num, "Yi", suffix) - - def _hash_user_password(password): """ This function computes and return a salted hash for the password in input. @@ -1351,3 +1443,21 @@ def _hash_user_password(password): salt = "$6$" + salt + "$" return "{CRYPT}" + crypt.crypt(str(password), salt) + + +def _update_admins_group_aliases(old_main_domain, new_main_domain): + + current_admin_aliases = user_group_info("admins")["mail-aliases"] + + aliases_to_remove = [ + a + for a in current_admin_aliases + if "@" in a + and a.split("@")[1] == old_main_domain + and a.split("@")[0] in ADMIN_ALIASES + ] + aliases_to_add = [f"{a}@{new_main_domain}" for a in ADMIN_ALIASES] + + user_group_update( + "admins", add_mailalias=aliases_to_add, remove_mailalias=aliases_to_remove + ) diff --git a/src/utils/__init__.py b/src/utils/__init__.py index e69de29bb..5cad500fa 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# diff --git a/src/utils/config.py b/src/utils/config.py index c8f997a74..27e4b9509 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1,24 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import glob import os import re @@ -267,13 +264,26 @@ class ConfigPanel: # In 'classic' mode, we display the current value if key refer to an option if self.filter_key.count(".") == 2 and mode == "classic": + option = self.filter_key.split(".")[-1] - return self.values.get(option, None) + value = self.values.get(option, None) + + option_type = None + for _, _, option_ in self._iterate(): + if option_["id"] == option: + option_type = ARGUMENTS_TYPE_PARSERS[option_["type"]] + break + + return option_type.normalize(value) if option_type else value # Format result in 'classic' or 'export' mode 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") @@ -312,6 +322,80 @@ 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 ): @@ -418,6 +502,7 @@ class ConfigPanel: "name": "", "services": [], "optional": True, + "is_action_section": False, }, }, "options": { @@ -444,6 +529,9 @@ class ConfigPanel: "accept", "redact", "filter", + "readonly", + "enabled", + # "confirm", # TODO: to ask confirmation before running an action ], "defaults": {}, }, @@ -486,6 +574,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: @@ -526,19 +617,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 - logger.debug("Hydrating config with current values") - 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" ): @@ -549,20 +660,39 @@ class ConfigPanel: raw_msg=True, ) value = self.values[option["name"]] + + # 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"'` + # For example hotspot used it... + # See https://github.com/YunoHost/yunohost/pull/1546 + if ( + isinstance(value, dict) + and "value" in value + and "current_value" not in value + ): + value["current_value"] = value["value"] + # In general, the value is just a simple value. # Sometimes it could be a dict used to overwrite the option itself value = value if isinstance(value, dict) else {"current_value": value} option.update(value) + self.values[option["id"]] = value.get("current_value") + 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""" @@ -570,20 +700,46 @@ 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.future_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() prefilled_answers.update(self.new_values) questions = ask_questions_and_parse_answers( - section["options"], + {question["name"]: question for question in section["options"]}, prefilled_answers=prefilled_answers, current_values=self.values, hooks=self.hooks, @@ -596,8 +752,6 @@ class ConfigPanel: } ) - self.errors = None - def _get_default_values(self): return { option["id"]: option["default"] @@ -704,7 +858,9 @@ class Question: self.default = question.get("default", None) self.optional = question.get("optional", False) self.visible = question.get("visible", None) - self.choices = question.get("choices", []) + 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", {"en": self.name}) self.help = question.get("help") @@ -743,7 +899,7 @@ class Question: confirm=False, prefill=prefill, is_multiline=(self.type == "text"), - autocomplete=self.choices, + autocomplete=self.choices or [], help=_value_for_locale(self.help), ) @@ -763,8 +919,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) @@ -824,7 +982,14 @@ 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...) @@ -910,7 +1075,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 } @@ -924,6 +1089,7 @@ class ColorQuestion(StringQuestion): class TagsQuestion(Question): argument_type = "tags" + default_value = "" @staticmethod def humanize(value, option={}): @@ -1095,7 +1261,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 @@ -1185,6 +1352,8 @@ class UserQuestion(Question): ) if self.default is None: + # FIXME: this code is obsolete with the new admins group + # Should be replaced by something like "any first user we find in the admin group" root_mail = "root@%s" % _get_maindomain() for user in self.choices.keys(): if root_mail in user_info(user).get("mail-aliases", []): @@ -1192,6 +1361,31 @@ class UserQuestion(Question): break +class GroupQuestion(Question): + argument_type = "group" + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + + from yunohost.user import user_group_list + + super().__init__(question, context) + + self.choices = list(user_group_list(short=True)["groups"]) + + def _human_readable_group(g): + # i18n: visitors + # i18n: all_users + # i18n: admins + return m18n.n(g) if g in ["visitors", "all_users", "admins"] else g + + self.choices = {g: _human_readable_group(g) for g in self.choices} + + if self.default is None: + self.default = "all_users" + + class NumberQuestion(Question): argument_type = "number" default_value = None @@ -1248,7 +1442,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] = {} @@ -1256,6 +1449,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 "" ) @@ -1335,6 +1529,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, @@ -1350,6 +1555,7 @@ ARGUMENTS_TYPE_PARSERS = { "boolean": BooleanQuestion, "domain": DomainQuestion, "user": UserQuestion, + "group": GroupQuestion, "number": NumberQuestion, "range": NumberQuestion, "display_text": DisplayTextQuestion, @@ -1357,6 +1563,7 @@ ARGUMENTS_TYPE_PARSERS = { "markdown": DisplayTextQuestion, "file": FileQuestion, "app": AppQuestion, + "button": ButtonQuestion, } @@ -1394,10 +1601,23 @@ def ask_questions_and_parse_answers( context = {**current_values, **answers} out = [] - for raw_question in raw_questions: + for name, raw_question in raw_questions.items(): + raw_question["name"] = name question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")] - raw_question["value"] = answers.get(raw_question["name"]) + 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) diff --git a/src/utils/dns.py b/src/utils/dns.py index ccb6c5406..091168615 100644 --- a/src/utils/dns.py +++ b/src/utils/dns.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import dns.resolver from typing import List diff --git a/src/utils/error.py b/src/utils/error.py index a92f3bd5a..e7046540d 100644 --- a/src/utils/error.py +++ b/src/utils/error.py @@ -1,24 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# from moulinette.core import MoulinetteError, MoulinetteAuthenticationError from moulinette import m18n diff --git a/src/utils/filesystem.py b/src/utils/filesystem.py deleted file mode 100644 index 04d7d3906..000000000 --- a/src/utils/filesystem.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" -import os - - -def free_space_in_directory(dirpath): - stat = os.statvfs(dirpath) - return stat.f_frsize * stat.f_bavail - - -def space_used_by_directory(dirpath): - stat = os.statvfs(dirpath) - return stat.f_frsize * stat.f_blocks diff --git a/src/utils/i18n.py b/src/utils/i18n.py index a0daf8181..ecbfe36e8 100644 --- a/src/utils/i18n.py +++ b/src/utils/i18n.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# from moulinette import m18n diff --git a/src/utils/ldap.py b/src/utils/ldap.py index 98c0fecf7..ee50d0b98 100644 --- a/src/utils/ldap.py +++ b/src/utils/ldap.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- -""" License - - Copyright (C) 2019 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import atexit import logging @@ -84,7 +82,7 @@ class LDAPInterface: def connect(self): def _reconnect(): con = ldap.ldapobject.ReconnectLDAPObject( - self.uri, retry_max=10, retry_delay=0.5 + self.uri, retry_max=10, retry_delay=2 ) con.sasl_non_interactive_bind_s("EXTERNAL") return con @@ -145,6 +143,8 @@ class LDAPInterface: try: result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs) + except ldap.SERVER_DOWN as e: + raise e except Exception as e: raise MoulinetteError( "error during LDAP search operation with: base='%s', " diff --git a/src/utils/legacy.py b/src/utils/legacy.py index 5e5d15fe8..3334632c2 100644 --- a/src/utils/legacy.py +++ b/src/utils/legacy.py @@ -1,3 +1,21 @@ +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import glob @@ -62,6 +80,32 @@ LEGACY_PERMISSION_LABEL = { ): "api", # $excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-receive%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-upload%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/info/refs } +LEGACY_SETTINGS = { + "security.password.admin.strength": "security.password.admin_strength", + "security.password.user.strength": "security.password.user_strength", + "security.ssh.compatibility": "security.ssh.ssh_compatibility", + "security.ssh.port": "security.ssh.ssh_port", + "security.ssh.password_authentication": "security.ssh.ssh_password_authentication", + "security.nginx.redirect_to_https": "security.nginx.nginx_redirect_to_https", + "security.nginx.compatibility": "security.nginx.nginx_compatibility", + "security.postfix.compatibility": "security.postfix.postfix_compatibility", + "pop3.enabled": "email.pop3.pop3_enabled", + "smtp.allow_ipv6": "email.smtp.smtp_allow_ipv6", + "smtp.relay.host": "email.smtp.smtp_relay_host", + "smtp.relay.port": "email.smtp.smtp_relay_port", + "smtp.relay.user": "email.smtp.smtp_relay_user", + "smtp.relay.password": "email.smtp.smtp_relay_password", + "backup.compress_tar_archives": "misc.backup.backup_compress_tar_archives", + "ssowat.panel_overlay.enabled": "misc.portal.ssowat_panel_overlay_enabled", + "security.webadmin.allowlist.enabled": "security.webadmin.webadmin_allowlist_enabled", + "security.webadmin.allowlist": "security.webadmin.webadmin_allowlist", + "security.experimental.enabled": "security.experimental.security_experimental_enabled", +} + + +def translate_legacy_settings_to_configpanel_settings(settings): + return LEGACY_SETTINGS.get(settings, settings) + def legacy_permission_label(app, permission_type): return LEGACY_PERMISSION_LABEL.get( @@ -209,6 +253,11 @@ def _patch_legacy_helpers(app_folder): "yunohost app checkport": {"important": True}, "yunohost tools port-available": {"important": True}, "yunohost app checkurl": {"important": True}, + "yunohost user create": { + "pattern": r"yunohost user create (\S+) (-f|--firstname) (\S+) (-l|--lastname) \S+ (.*)", + "replace": r"yunohost user create \1 --fullname \3 \5", + "important": False, + }, # Remove # Automatic diagnosis data from YunoHost # __PRE_TAG1__$(yunohost tools diagnosis | ...)__PRE_TAG2__" diff --git a/src/utils/network.py b/src/utils/network.py index 28dcb204c..06dd3493d 100644 --- a/src/utils/network.py +++ b/src/utils/network.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2017 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import logging diff --git a/src/utils/password.py b/src/utils/password.py index 5b8372962..3202e8055 100644 --- a/src/utils/password.py +++ b/src/utils/password.py @@ -1,29 +1,26 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import sys import os -import json import string import subprocess +import yaml SMALL_PWD_LIST = [ "yunohost", @@ -36,7 +33,14 @@ SMALL_PWD_LIST = [ "rpi", ] -MOST_USED_PASSWORDS = "/usr/share/yunohost/100000-most-used-passwords.txt" +# +# 100k firsts "most used password" with length 8+ +# +# List obtained with: +# curl -L https://github.com/danielmiessler/SecLists/raw/master/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt \ +# | grep -v -E "^[a-zA-Z0-9]{1,7}$" | head -n 100000 | gzip > 100000-most-used-passwords-length8plus.txt.gz +# +MOST_USED_PASSWORDS = "/usr/share/yunohost/100000-most-used-passwords-length8plus.txt" # Length, digits, lowers, uppers, others STRENGTH_LEVELS = [ @@ -47,7 +51,25 @@ STRENGTH_LEVELS = [ ] +def assert_password_is_compatible(password): + """ + UNIX seems to not like password longer than 127 chars ... + e.g. SSH login gets broken (or even 'su admin' when entering the password) + """ + + if len(password) >= 127: + + # Note that those imports are made here and can't be put + # on top (at least not the moulinette ones) + # because the moulinette needs to be correctly initialized + # as well as modules available in python's path. + from yunohost.utils.error import YunohostValidationError + + raise YunohostValidationError("password_too_long") + + def assert_password_is_strong_enough(profile, password): + PasswordValidator(profile).validate(password) @@ -58,7 +80,7 @@ class PasswordValidator: The profile shall be either "user" or "admin" and will correspond to a validation strength - defined via the setting "security.password..strength" + defined via the setting "security.password._strength" """ self.profile = profile @@ -67,9 +89,9 @@ class PasswordValidator: # from settings.py because this file is also meant to be # use as a script by ssowat. # (or at least that's my understanding -- Alex) - settings = json.load(open("/etc/yunohost/settings.json", "r")) - setting_key = "security.password." + profile + ".strength" - self.validation_strength = int(settings[setting_key]["value"]) + settings = yaml.safe_load(open("/etc/yunohost/settings.yml", "r")) + setting_key = profile + "_strength" + self.validation_strength = int(settings[setting_key]) except Exception: # Fallback to default value if we can't fetch settings for some reason self.validation_strength = 1 diff --git a/src/utils/resources.py b/src/utils/resources.py new file mode 100644 index 000000000..7b500ad3f --- /dev/null +++ b/src/utils/resources.py @@ -0,0 +1,1031 @@ +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import os +import copy +import shutil +import random +from typing import Dict, Any, List + +from moulinette import m18n +from moulinette.utils.process import check_output +from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import mkdir, chown, chmod, write_to_file +from moulinette.utils.filesystem import ( + rm, +) + +from yunohost.utils.error import YunohostError, YunohostValidationError + +logger = getActionLogger("yunohost.app_resources") + + +class AppResourceManager: + def __init__(self, app: str, current: Dict, wanted: Dict): + + self.app = app + self.current = current + self.wanted = wanted + + if "resources" not in self.current: + self.current["resources"] = {} + if "resources" not in self.wanted: + self.wanted["resources"] = {} + + def apply( + self, rollback_and_raise_exception_if_failure, operation_logger=None, **context + ): + + todos = list(self.compute_todos()) + completed = [] + rollback = False + exception = None + + for todo, name, old, new in todos: + try: + if todo == "deprovision": + # FIXME : i18n, better info strings + logger.info(f"Deprovisionning {name} ...") + old.deprovision(context=context) + elif todo == "provision": + logger.info(f"Provisionning {name} ...") + new.provision_or_update(context=context) + elif todo == "update": + logger.info(f"Updating {name} ...") + new.provision_or_update(context=context) + except (KeyboardInterrupt, Exception) as e: + exception = e + if isinstance(e, KeyboardInterrupt): + logger.error(m18n.n("operation_interrupted")) + else: + logger.warning(f"Failed to {todo} {name} : {e}") + if rollback_and_raise_exception_if_failure: + rollback = True + completed.append((todo, name, old, new)) + break + else: + pass + else: + completed.append((todo, name, old, new)) + + if rollback: + for todo, name, old, new in completed: + try: + # (NB. here we want to undo the todo) + if todo == "deprovision": + # FIXME : i18n, better info strings + logger.info(f"Reprovisionning {name} ...") + old.provision_or_update(context=context) + elif todo == "provision": + logger.info(f"Deprovisionning {name} ...") + new.deprovision(context=context) + elif todo == "update": + logger.info(f"Reverting {name} ...") + old.provision_or_update(context=context) + except (KeyboardInterrupt, Exception) as e: + if isinstance(e, KeyboardInterrupt): + logger.error(m18n.n("operation_interrupted")) + else: + logger.error(f"Failed to rollback {name} : {e}") + + if exception: + if rollback_and_raise_exception_if_failure: + logger.error( + m18n.n("app_resource_failed", app=self.app, error=exception) + ) + if operation_logger: + failure_message_with_debug_instructions = operation_logger.error( + str(exception) + ) + raise YunohostError( + failure_message_with_debug_instructions, raw_msg=True + ) + else: + raise YunohostError(str(exception), raw_msg=True) + else: + logger.error(exception) + + def compute_todos(self): + + for name, infos in reversed(self.current["resources"].items()): + if name not in self.wanted["resources"].keys(): + resource = AppResourceClassesByType[name](infos, self.app, self) + yield ("deprovision", name, resource, None) + + for name, infos in self.wanted["resources"].items(): + wanted_resource = AppResourceClassesByType[name](infos, self.app, self) + if name not in self.current["resources"].keys(): + yield ("provision", name, None, wanted_resource) + else: + infos_ = self.current["resources"][name] + current_resource = AppResourceClassesByType[name]( + infos_, self.app, self + ) + yield ("update", name, current_resource, wanted_resource) + + +class AppResource: + + type: str = "" + default_properties: Dict[str, Any] = {} + + def __init__(self, properties: Dict[str, Any], app: str, manager=None): + + self.app = app + self.manager = manager + + for key, value in self.default_properties.items(): + if isinstance(value, str): + value = value.replace("__APP__", self.app) + setattr(self, key, value) + + for key, value in properties.items(): + if isinstance(value, str): + value = value.replace("__APP__", self.app) + setattr(self, key, value) + + def get_setting(self, key): + from yunohost.app import app_setting + + return app_setting(self.app, key) + + def set_setting(self, key, value): + from yunohost.app import app_setting + + app_setting(self.app, key, value=value) + + def delete_setting(self, key): + from yunohost.app import app_setting + + app_setting(self.app, key, delete=True) + + def _run_script(self, action, script, env={}, user="root"): + + from yunohost.app import ( + _make_tmp_workdir_for_app, + _make_environment_for_app_script, + ) + from yunohost.hook import hook_exec_with_script_debug_if_failure + + tmpdir = _make_tmp_workdir_for_app(app=self.app) + + env_ = _make_environment_for_app_script( + self.app, workdir=tmpdir, action=f"{action}_{self.type}" + ) + env_.update(env) + + script_path = f"{tmpdir}/{action}_{self.type}" + script = f""" +source /usr/share/yunohost/helpers +ynh_abort_if_errors + +{script} +""" + + write_to_file(script_path, script) + + from yunohost.log import OperationLogger + + if OperationLogger._instances: + # FIXME ? : this is an ugly hack :( + operation_logger = OperationLogger._instances[-1] + else: + operation_logger = OperationLogger( + "resource_snippet", [("app", self.app)], env=env_ + ) + operation_logger.start() + + try: + ( + call_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( + script_path, + env=env_, + operation_logger=operation_logger, + error_message_if_script_failed="An error occured inside the script snippet", + error_message_if_failed=lambda e: f"{action} failed for {self.type} : {e}", + ) + finally: + if call_failed: + raise YunohostError( + failure_message_with_debug_instructions, raw_msg=True + ) + else: + # FIXME: currently in app install code, we have + # more sophisticated code checking if this broke something on the system etc ... + # dunno if we want to do this here or manage it elsewhere + pass + + # print(ret) + + +class PermissionsResource(AppResource): + """ + Configure the SSO permissions/tiles. Typically, webapps are expected to have a 'main' permission mapped to '/', meaning that a tile pointing to the `$domain/$path` will be available in the SSO for users allowed to access that app. + + Additional permissions can be created, typically to have a specific tile and/or access rules for the admin part of a webapp. + + The list of allowed user/groups may be initialized using the content of the `init_{perm}_permission` question from the manifest, hence `init_main_permission` replaces the `is_public` question and shall contain a group name (typically, `all_users` or `visitors`). + + ##### Example: + ```toml + [resources.permissions] + main.url = "/" + # (these two previous lines should be enough in the majority of cases) + + admin.url = "/admin" + admin.show_tile = false + admin.allowed = "admins" # Assuming the "admins" group exists (cf future developments ;)) + ``` + + ##### Properties (for each perm name): + - `url`: The relative URI corresponding to this permission. Typically `/` or `/something`. This property may be omitted for non-web permissions. + - `show_tile`: (default: `true` if `url` is defined) Wether or not a tile should be displayed for that permission in the user portal + - `allowed`: (default: nobody) The group initially allowed to access this perm, if `init_{perm}_permission` is not defined in the manifest questions. Note that the admin may tweak who is allowed/unallowed on that permission later on, this is only meant to **initialize** the permission. + - `auth_header`: (default: `true`) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true + - `protected`: (default: `false`) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission. Defaults to 'false'. + - `additional_urls`: (default: none) List of additional URL for which access will be allowed/forbidden + + ##### Provision/Update: + - Delete any permissions that may exist and be related to this app yet is not declared anymore + - Loop over the declared permissions and create them if needed or update them with the new values + + ##### Deprovision: + - Delete all permission related to this app + + ##### Legacy management: + - Legacy `is_public` setting will be deleted if it exists + """ + + # Notes for future ? + # deep_clean -> delete permissions for any __APP__.foobar where app not in app list... + # backup -> handled elsewhere by the core, should be integrated in there (dump .ldif/yml?) + # restore -> handled by the core, should be integrated in there (restore .ldif/yml?) + + type = "permissions" + priority = 80 + + default_properties: Dict[str, Any] = {} + + default_perm_properties: Dict[str, Any] = { + "url": None, + "additional_urls": [], + "auth_header": True, + "allowed": None, + "show_tile": None, # To be automagically set to True by default if an url is defined and show_tile not provided + "protected": False, + } + + permissions: Dict[str, Dict[str, Any]] = {} + + def __init__(self, properties: Dict[str, Any], *args, **kwargs): + + # FIXME : if url != None, we should check that there's indeed a domain/path defined ? ie that app is a webapp + + for perm, infos in properties.items(): + properties[perm] = copy.copy(self.default_perm_properties) + properties[perm].update(infos) + if properties[perm]["show_tile"] is None: + properties[perm]["show_tile"] = bool(properties[perm]["url"]) + + if ( + isinstance(properties["main"]["url"], str) + and properties["main"]["url"] != "/" + ): + raise YunohostError( + "URL for the 'main' permission should be '/' for webapps (or undefined/None for non-webapps). Note that / refers to the install url of the app" + ) + + super().__init__({"permissions": properties}, *args, **kwargs) + + def provision_or_update(self, context: Dict = {}): + + from yunohost.permission import ( + permission_create, + permission_url, + permission_delete, + user_permission_list, + user_permission_update, + permission_sync_to_user, + ) + + # Delete legacy is_public setting if not already done + self.delete_setting("is_public") + + existing_perms = user_permission_list(short=True, apps=[self.app])[ + "permissions" + ] + for perm in existing_perms: + if perm.split(".")[1] not in self.permissions.keys(): + permission_delete(perm, force=True, sync_perm=False) + + for perm, infos in self.permissions.items(): + perm_id = f"{self.app}.{perm}" + if perm_id not in existing_perms: + # Use the 'allowed' key from the manifest, + # or use the 'init_{perm}_permission' from the install questions + # which is temporarily saved as a setting as an ugly hack to pass the info to this piece of code... + init_allowed = ( + infos["allowed"] + or self.get_setting(f"init_{perm}_permission") + or [] + ) + permission_create( + perm_id, + allowed=init_allowed, + # This is why the ugly hack with self.manager exists >_> + label=self.manager.wanted["name"] if perm == "main" else perm, + url=infos["url"], + additional_urls=infos["additional_urls"], + auth_header=infos["auth_header"], + sync_perm=False, + ) + self.delete_setting(f"init_{perm}_permission") + + user_permission_update( + perm_id, + show_tile=infos["show_tile"], + protected=infos["protected"], + sync_perm=False, + ) + permission_url( + perm_id, + url=infos["url"], + set_url=infos["additional_urls"], + auth_header=infos["auth_header"], + sync_perm=False, + ) + + permission_sync_to_user() + + def deprovision(self, context: Dict = {}): + + from yunohost.permission import ( + permission_delete, + user_permission_list, + permission_sync_to_user, + ) + + existing_perms = user_permission_list(short=True, apps=[self.app])[ + "permissions" + ] + for perm in existing_perms: + permission_delete(perm, force=True, sync_perm=False) + + permission_sync_to_user() + + +class SystemuserAppResource(AppResource): + """ + Provision a system user to be used by the app. The username is exactly equal to the app id + + ##### Example: + ```toml + [resources.system_user] + # (empty - defaults are usually okay) + ``` + + ##### Properties: + - `allow_ssh`: (default: False) Adds the user to the ssh.app group, allowing SSH connection via this user + - `allow_sftp`: (defalt: False) Adds the user to the sftp.app group, allowing SFTP connection via this user + + ##### Provision/Update: + - will create the system user if it doesn't exists yet + - will add/remove the ssh/sftp.app groups + + ##### Deprovision: + - deletes the user and group + """ + + # Notes for future? + # + # deep_clean -> uuuuh ? delete any user that could correspond to an app x_x ? + # + # backup -> nothing + # restore -> provision + + type = "system_user" + priority = 20 + + default_properties: Dict[str, Any] = {"allow_ssh": False, "allow_sftp": False} + + # FIXME : wat do regarding ssl-cert, multimedia + # FIXME : wat do about home dir + + allow_ssh: bool = False + allow_sftp: bool = False + + def provision_or_update(self, context: Dict = {}): + + # FIXME : validate that no yunohost user exists with that name? + # and/or that no system user exists during install ? + + if not check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): + # FIXME: improve logging ? os.system wont log stdout / stderr + cmd = f"useradd --system --user-group {self.app}" + ret = os.system(cmd) + assert ret == 0, f"useradd command failed with exit code {ret}" + + if not check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): + raise YunohostError( + f"Failed to create system user for {self.app}", raw_msg=True + ) + + groups = set(check_output(f"groups {self.app}").strip().split()[2:]) + + if self.allow_ssh: + groups.add("ssh.app") + elif "ssh.app" in groups: + groups.remove("ssh.app") + + if self.allow_sftp: + groups.add("sftp.app") + elif "sftp.app" in groups: + groups.remove("sftp.app") + + os.system(f"usermod -G {','.join(groups)} {self.app}") + + def deprovision(self, context: Dict = {}): + + if check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): + os.system(f"deluser {self.app} >/dev/null") + if check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): + raise YunohostError(f"Failed to delete system user for {self.app}") + + if check_output(f"getent group {self.app} &>/dev/null || true").strip(): + os.system(f"delgroup {self.app} >/dev/null") + if check_output(f"getent group {self.app} &>/dev/null || true").strip(): + raise YunohostError(f"Failed to delete system user for {self.app}") + + # FIXME : better logging and error handling, add stdout/stderr from the deluser/delgroup commands... + + +class InstalldirAppResource(AppResource): + """ + Creates a directory to be used by the app as the installation directory, typically where the app sources and assets are located. The corresponding path is stored in the settings as `install_dir` + + ##### Example: + ```toml + [resources.install_dir] + # (empty - defaults are usually okay) + ``` + + ##### Properties: + - `dir`: (default: `/var/www/__APP__`) The full path of the install dir + - `owner`: (default: `__APP__:rx`) The owner (and owner permissions) for the install dir + - `group`: (default: `__APP__:rx`) The group (and group permissions) for the install dir + + ##### Provision/Update: + - during install, the folder will be deleted if it already exists (FIXME: is this what we want?) + - if the dir path changed and a folder exists at the old location, the folder will be `mv`'ed to the new location + - otherwise, creates the directory if it doesn't exists yet + - (re-)apply permissions (only on the folder itself, not recursively) + - save the value of `dir` as `install_dir` in the app's settings, which can be then used by the app scripts (`$install_dir`) and conf templates (`__INSTALL_DIR__`) + + ##### Deprovision: + - recursively deletes the directory if it exists + + ##### Legacy management: + - In the past, the setting was called `final_path`. The code will automatically rename it as `install_dir`. + - As explained in the 'Provision/Update' section, the folder will also be moved if the location changed + + """ + + # Notes for future? + # deep_clean -> uuuuh ? delete any dir in /var/www/ that would not correspond to an app x_x ? + # backup -> cp install dir + # restore -> cp install dir + + type = "install_dir" + priority = 30 + + default_properties: Dict[str, Any] = { + "dir": "/var/www/__APP__", + "owner": "__APP__:rx", + "group": "__APP__:rx", + } + + dir: str = "" + owner: str = "" + group: str = "" + + # FIXME: change default dir to /opt/stuff if app ain't a webapp ... + + def provision_or_update(self, context: Dict = {}): + + assert self.dir.strip() # Be paranoid about self.dir being empty... + assert self.owner.strip() + assert self.group.strip() + + current_install_dir = self.get_setting("install_dir") or self.get_setting( + "final_path" + ) + + # If during install, /var/www/$app already exists, assume that it's okay to remove and recreate it + # FIXME : is this the right thing to do ? + if not current_install_dir and os.path.isdir(self.dir): + rm(self.dir, recursive=True) + + # isdir will be True if the path is a symlink pointing to a dir + # This should cover cases where people moved the data dir to another place via a symlink (ie we dont enter the if) + if not os.path.isdir(self.dir): + # Handle case where install location changed, in which case we shall move the existing install dir + # FIXME: confirm that's what we wanna do + # Maybe a middle ground could be to compute the size, check that it's not too crazy (eg > 1G idk), + # and check for available space on the destination + if current_install_dir and os.path.isdir(current_install_dir): + logger.warning( + f"Moving {current_install_dir} to {self.dir} ... (this may take a while)" + ) + shutil.move(current_install_dir, self.dir) + else: + mkdir(self.dir) + + owner, owner_perm = self.owner.split(":") + group, group_perm = self.group.split(":") + owner_perm_octal = ( + (4 if "r" in owner_perm else 0) + + (2 if "w" in owner_perm else 0) + + (1 if "x" in owner_perm else 0) + ) + group_perm_octal = ( + (4 if "r" in group_perm else 0) + + (2 if "w" in group_perm else 0) + + (1 if "x" in group_perm else 0) + ) + + perm_octal = 0o100 * owner_perm_octal + 0o010 * group_perm_octal + + # NB: we use realpath here to cover cases where self.dir could actually be a symlink + # in which case we want to apply the perm to the pointed dir, not to the symlink + chmod(os.path.realpath(self.dir), perm_octal) + chown(os.path.realpath(self.dir), owner, group) + # FIXME: shall we apply permissions recursively ? + + self.set_setting("install_dir", self.dir) + self.delete_setting("final_path") # Legacy + + def deprovision(self, context: Dict = {}): + + assert self.dir.strip() # Be paranoid about self.dir being empty... + assert self.owner.strip() + assert self.group.strip() + + # FIXME : check that self.dir has a sensible value to prevent catastrophes + if os.path.isdir(self.dir): + rm(self.dir, recursive=True) + # FIXME : in fact we should delete settings to be consistent + + +class DatadirAppResource(AppResource): + """ + Creates a directory to be used by the app as the data store directory, typically where the app multimedia or large assets added by users are located. The corresponding path is stored in the settings as `data_dir`. This resource behaves very similarly to install_dir. + + ##### Example: + ```toml + [resources.data_dir] + # (empty - defaults are usually okay) + ``` + + ##### Properties: + - `dir`: (default: `/home/yunohost.app/__APP__`) The full path of the data dir + - `owner`: (default: `__APP__:rx`) The owner (and owner permissions) for the data dir + - `group`: (default: `__APP__:rx`) The group (and group permissions) for the data dir + + ##### Provision/Update: + - if the dir path changed and a folder exists at the old location, the folder will be `mv`'ed to the new location + - otherwise, creates the directory if it doesn't exists yet + - (re-)apply permissions (only on the folder itself, not recursively) + - save the value of `dir` as `data_dir` in the app's settings, which can be then used by the app scripts (`$data_dir`) and conf templates (`__DATA_DIR__`) + + ##### Deprovision: + - (only if the purge option is chosen by the user) recursively deletes the directory if it exists + - also delete the corresponding setting + + ##### Legacy management: + - In the past, the setting may have been called `datadir`. The code will automatically rename it as `data_dir`. + - As explained in the 'Provision/Update' section, the folder will also be moved if the location changed + + """ + + # notes for future ? + # deep_clean -> zblerg idk nothing + # backup -> cp data dir ? (if not backup_core_only) + # restore -> cp data dir ? (if in backup) + + type = "data_dir" + priority = 40 + + default_properties: Dict[str, Any] = { + "dir": "/home/yunohost.app/__APP__", + "owner": "__APP__:rx", + "group": "__APP__:rx", + } + + dir: str = "" + owner: str = "" + group: str = "" + + def provision_or_update(self, context: Dict = {}): + + assert self.dir.strip() # Be paranoid about self.dir being empty... + assert self.owner.strip() + assert self.group.strip() + + current_data_dir = self.get_setting("data_dir") or self.get_setting("datadir") + + # isdir will be True if the path is a symlink pointing to a dir + # This should cover cases where people moved the data dir to another place via a symlink (ie we dont enter the if) + if not os.path.isdir(self.dir): + # Handle case where install location changed, in which case we shall move the existing install dir + # FIXME: same as install_dir, is this what we want ? + if current_data_dir and os.path.isdir(current_data_dir): + logger.warning( + f"Moving {current_data_dir} to {self.dir} ... (this may take a while)" + ) + shutil.move(current_data_dir, self.dir) + else: + mkdir(self.dir) + + owner, owner_perm = self.owner.split(":") + group, group_perm = self.group.split(":") + owner_perm_octal = ( + (4 if "r" in owner_perm else 0) + + (2 if "w" in owner_perm else 0) + + (1 if "x" in owner_perm else 0) + ) + group_perm_octal = ( + (4 if "r" in group_perm else 0) + + (2 if "w" in group_perm else 0) + + (1 if "x" in group_perm else 0) + ) + perm_octal = 0o100 * owner_perm_octal + 0o010 * group_perm_octal + + # NB: we use realpath here to cover cases where self.dir could actually be a symlink + # in which case we want to apply the perm to the pointed dir, not to the symlink + chmod(os.path.realpath(self.dir), perm_octal) + chown(os.path.realpath(self.dir), owner, group) + + self.set_setting("data_dir", self.dir) + self.delete_setting("datadir") # Legacy + + def deprovision(self, context: Dict = {}): + + assert self.dir.strip() # Be paranoid about self.dir being empty... + assert self.owner.strip() + assert self.group.strip() + + if context.get("purge_data_dir", False) and os.path.isdir(self.dir): + rm(self.dir, recursive=True) + + self.delete_setting("data_dir") + + +class AptDependenciesAppResource(AppResource): + """ + Create a virtual package in apt, depending on the list of specified packages that the app needs. The virtual packages is called `$app-ynh-deps` (with `_` being replaced by `-` in the app name, see `ynh_install_app_dependencies`) + + ##### Example: + ```toml + [resources.apt] + packages = "nyancat, lolcat, sl" + + # (this part is optional and corresponds to the legacy ynh_install_extra_app_dependencies helper) + extras.yarn.repo = "deb https://dl.yarnpkg.com/debian/ stable main" + extras.yarn.key = "https://dl.yarnpkg.com/debian/pubkey.gpg" + extras.yarn.packages = "yarn" + ``` + + ##### Properties: + - `packages`: Comma-separated list of packages to be installed via `apt` + - `extras`: A dict of (repo, key, packages) corresponding to "extra" repositories to fetch dependencies from + + ##### Provision/Update: + - The code literally calls the bash helpers `ynh_install_app_dependencies` and `ynh_install_extra_app_dependencies`, similar to what happens in v1. + + ##### Deprovision: + - The code literally calls the bash helper `ynh_remove_app_dependencies` + """ + + # Notes for future? + # deep_clean -> remove any __APP__-ynh-deps for app not in app list + # backup -> nothing + # restore = provision + + type = "apt" + priority = 50 + + default_properties: Dict[str, Any] = {"packages": [], "extras": {}} + + packages: List = [] + extras: Dict[str, Dict[str, str]] = {} + + def __init__(self, properties: Dict[str, Any], *args, **kwargs): + + for key, values in properties.get("extras", {}).items(): + if not all( + isinstance(values.get(k), str) for k in ["repo", "key", "packages"] + ): + raise YunohostError( + "In apt resource in the manifest: 'extras' repo should have the keys 'repo', 'key' and 'packages' defined and be strings" + ) + + super().__init__(properties, *args, **kwargs) + + def provision_or_update(self, context: Dict = {}): + + script = [f"ynh_install_app_dependencies {self.packages}"] + for repo, values in self.extras.items(): + script += [ + f"ynh_install_extra_app_dependencies --repo='{values['repo']}' --key='{values['key']}' --package='{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", "\n".join(script)) + + def deprovision(self, context: Dict = {}): + + self._run_script("deprovision", "ynh_remove_app_dependencies") + + +class PortsResource(AppResource): + """ + Book port(s) to be used by the app, typically to be used to the internal reverse-proxy between nginx and the app process. + + Note that because multiple ports can be booked, each properties is prefixed by the name of the port. `main` is a special name and will correspond to the setting `$port`, whereas for example `xmpp_client` will correspond to the setting `$port_xmpp_client`. + + ##### Example: + ```toml + [resources.port] + # (empty should be fine for most apps ... though you can customize stuff if absolutely needed) + + main.default = 12345 # if you really want to specify a prefered value .. but shouldnt matter in the majority of cases + + xmpp_client.default = 5222 # if you need another port, pick a name for it (here, "xmpp_client") + xmpp_client.exposed = "TCP" # here, we're telling that the port needs to be publicly exposed on TCP on the firewall + ``` + + ##### Properties (for every port name): + - `default`: The prefered value for the port. If this port is already being used by another process right now, or is booked in another app's setting, the code will increment the value until it finds a free port and store that value as the setting. If no value is specified, a random value between 10000 and 60000 is used. + - `exposed`: (default: `false`) Wether this port should be opened on the firewall and be publicly reachable. This should be kept to `false` for the majority of apps than only need a port for internal reverse-proxying! Possible values: `false`, `true`(=`Both`), `Both`, `TCP`, `UDP`. This will result in the port being opened on the firewall, and the diagnosis checking that a program answers on that port. + - `fixed`: (default: `false`) Tells that the app absolutely needs the specific value provided in `default`, typically because it's needed for a specific protocol + + ##### Provision/Update (for every port name): + - If not already booked, look for a free port, starting with the `default` value (or a random value between 10000 and 60000 if no `default` set) + - If `exposed` is not `false`, open the port in the firewall accordingly - otherwise make sure it's closed. + - The value of the port is stored in the `$port` setting for the `main` port, or `$port_NAME` for other `NAME`s + + ##### Deprovision: + - Close the ports on the firewall if relevant + - Deletes all the port settings + + ##### Legacy management: + - In the past, some settings may have been named `NAME_port` instead of `port_NAME`, in which case the code will automatically rename the old setting. + """ + + # Notes for future? + # deep_clean -> ? + # backup -> nothing (backup port setting) + # restore -> nothing (restore port setting) + + type = "ports" + priority = 70 + + default_properties: Dict[str, Any] = {} + + default_port_properties = { + "default": None, + "exposed": False, # or True(="Both"), "TCP", "UDP" + "fixed": False, + } + + ports: Dict[str, Dict[str, Any]] + + def __init__(self, properties: Dict[str, Any], *args, **kwargs): + + if "main" not in properties: + properties["main"] = {} + + for port, infos in properties.items(): + properties[port] = copy.copy(self.default_port_properties) + properties[port].update(infos) + + if properties[port]["default"] is None: + properties[port]["default"] = random.randint(10000, 60000) + + super().__init__({"ports": properties}, *args, **kwargs) + + def _port_is_used(self, port): + + # FIXME : this could be less brutal than two os.system ... + cmd1 = ( + "ss --numeric --listening --tcp --udp | awk '{print$5}' | grep --quiet --extended-regexp ':%s$'" + % port + ) + # This second command is mean to cover (most) case where an app is using a port yet ain't currently using it for some reason (typically service ain't up) + cmd2 = f"grep --quiet \"port: '{port}'\" /etc/yunohost/apps/*/settings.yml" + return os.system(cmd1) == 0 and os.system(cmd2) == 0 + + def provision_or_update(self, context: Dict = {}): + + from yunohost.firewall import firewall_allow, firewall_disallow + + for name, infos in self.ports.items(): + + setting_name = f"port_{name}" if name != "main" else "port" + port_value = self.get_setting(setting_name) + if not port_value and name != "main": + # Automigrate from legacy setting foobar_port (instead of port_foobar) + legacy_setting_name = "{name}_port" + port_value = self.get_setting(legacy_setting_name) + if port_value: + self.set_setting(setting_name, port_value) + self.delete_setting(legacy_setting_name) + continue + + if not port_value: + port_value = infos["default"] + + if infos["fixed"]: + if self._port_is_used(port_value): + raise YunohostValidationError( + f"Port {port_value} is already used by another process or app." + ) + else: + while self._port_is_used(port_value): + port_value += 1 + + self.set_setting(setting_name, port_value) + + if infos["exposed"]: + firewall_allow(infos["exposed"], port_value, reload_only_if_change=True) + else: + firewall_disallow( + infos["exposed"], port_value, reload_only_if_change=True + ) + + def deprovision(self, context: Dict = {}): + + from yunohost.firewall import firewall_disallow + + for name, infos in self.ports.items(): + setting_name = f"port_{name}" if name != "main" else "port" + value = self.get_setting(setting_name) + self.delete_setting(setting_name) + if value and str(value).strip(): + firewall_disallow( + infos["exposed"], int(value), reload_only_if_change=True + ) + + +class DatabaseAppResource(AppResource): + """ + Initialize a database, either using MySQL or Postgresql. Relevant DB infos are stored in settings `$db_name`, `$db_user` and `$db_pwd`. + + NB: only one DB can be handled in such a way (is there really an app that would need two completely different DB ?...) + + NB2: no automagic migration will happen in an suddenly change `type` from `mysql` to `postgresql` or viceversa in its life + + ##### Example: + ```toml + [resources.database] + type = "mysql" # or : "postgresql". Only these two values are supported + ``` + + ##### Properties: + - `type`: The database type, either `mysql` or `postgresql` + + ##### Provision/Update: + - (Re)set the `$db_name` and `$db_user` settings with the sanitized app name (replacing `-` and `.` with `_`) + - If `$db_pwd` doesn't already exists, pick a random database password and store it in that setting + - If the database doesn't exists yet, create the SQL user and DB using `ynh_mysql_create_db` or `ynh_psql_create_db`. + + ##### Deprovision: + - Drop the DB using `ynh_mysql_remove_db` or `ynh_psql_remove_db` + - Deletes the `db_name`, `db_user` and `db_pwd` settings + + ##### Legacy management: + - In the past, the sql passwords may have been named `mysqlpwd` or `psqlpwd`, in which case it will automatically be renamed as `db_pwd` + """ + + # Notes for future? + # deep_clean -> ... idk look into any db name that would not be related to any app ... + # backup -> dump db + # restore -> setup + inject db dump + + type = "database" + priority = 90 + dbtype: str = "" + + default_properties: Dict[str, Any] = { + "dbtype": None, + } + + def __init__(self, properties: Dict[str, Any], *args, **kwargs): + + if "type" not in properties or properties["type"] not in [ + "mysql", + "postgresql", + ]: + raise YunohostError( + "Specifying the type of db ('mysql' or 'postgresql') is mandatory for db resources", + raw_msg=True, + ) + + # Hack so that people can write type = "mysql/postgresql" in toml but it's loaded as dbtype + # to avoid conflicting with the generic self.type of the resource object ... + # dunno if that's really a good idea :| + properties = {"dbtype": properties["type"]} + + super().__init__(properties, *args, **kwargs) + + def db_exists(self, db_name): + + if self.dbtype == "mysql": + return os.system(f"mysqlshow '{db_name}' >/dev/null 2>/dev/null") == 0 + elif self.dbtype == "postgresql": + return ( + os.system( + f"sudo --login --user=postgres psql -c '' '{db_name}' >/dev/null 2>/dev/null" + ) + == 0 + ) + else: + return False + + def provision_or_update(self, context: Dict = {}): + + # This is equivalent to ynh_sanitize_dbid + db_name = self.app.replace("-", "_").replace(".", "_") + db_user = db_name + self.set_setting("db_name", db_name) + self.set_setting("db_user", db_user) + + db_pwd = None + if self.get_setting("db_pwd"): + db_pwd = self.get_setting("db_pwd") + else: + # Legacy setting migration + legacypasswordsetting = ( + "psqlpwd" if self.dbtype == "postgresql" else "mysqlpwd" + ) + if self.get_setting(legacypasswordsetting): + db_pwd = self.get_setting(legacypasswordsetting) + self.delete_setting(legacypasswordsetting) + 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) + + if not self.db_exists(db_name): + + if self.dbtype == "mysql": + self._run_script( + "provision", + f"ynh_mysql_create_db '{db_name}' '{db_user}' '{db_pwd}'", + ) + elif self.dbtype == "postgresql": + self._run_script( + "provision", + f"ynh_psql_create_user '{db_user}' '{db_pwd}'; ynh_psql_create_db '{db_name}' '{db_user}'", + ) + + def deprovision(self, context: Dict = {}): + + db_name = self.app.replace("-", "_").replace(".", "_") + db_user = db_name + + if self.dbtype == "mysql": + self._run_script( + "deprovision", f"ynh_mysql_remove_db '{db_name}' '{db_user}'" + ) + elif self.dbtype == "postgresql": + self._run_script( + "deprovision", f"ynh_psql_remove_db '{db_name}' '{db_user}'" + ) + + self.delete_setting("db_name") + self.delete_setting("db_user") + self.delete_setting("db_pwd") + + +AppResourceClassesByType = {c.type: c for c in AppResource.__subclasses__()} diff --git a/src/utils/packages.py b/src/utils/system.py similarity index 52% rename from src/utils/packages.py rename to src/utils/system.py index 3105bc4c7..8b0ed7092 100644 --- a/src/utils/packages.py +++ b/src/utils/system.py @@ -1,35 +1,110 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2015 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os import logging from moulinette.utils.process import check_output -from packaging import version +from yunohost.utils.error import YunohostError logger = logging.getLogger("yunohost.utils.packages") YUNOHOST_PACKAGES = ["yunohost", "yunohost-admin", "moulinette", "ssowat"] +def system_arch(): + return check_output("dpkg --print-architecture") + + +def system_virt(): + """ + Returns the output of systemd-detect-virt (so e.g. 'none' or 'lxc' or ...) + You can check the man of the command to have a list of possible outputs... + """ + # Detect virt technology (if not bare metal) and arch + # Gotta have this "|| true" because it systemd-detect-virt return 'none' + # with an error code on bare metal ~.~ + return check_output("systemd-detect-virt || true") + + +def free_space_in_directory(dirpath): + stat = os.statvfs(dirpath) + return stat.f_frsize * stat.f_bavail + + +def space_used_by_directory(dirpath, follow_symlinks=True): + + if not follow_symlinks: + du_output = check_output(["du", "-sb", dirpath], shell=False) + return int(du_output.split()[0]) + + stat = os.statvfs(dirpath) + return ( + stat.f_frsize * stat.f_blocks + ) # FIXME : this doesnt do what the function name suggest this does ... + + +def human_to_binary(size: str) -> int: + + symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") + factor = {} + for i, s in enumerate(symbols): + factor[s] = 1 << (i + 1) * 10 + + suffix = size[-1] + size = size[:-1] + + if suffix not in symbols: + raise YunohostError( + f"Invalid size suffix '{suffix}', expected one of {symbols}" + ) + + try: + size_ = float(size) + except Exception: + raise YunohostError(f"Failed to convert size {size} to float") + + return int(size_ * factor[suffix]) + + +def binary_to_human(n: int) -> str: + """ + Convert bytes or bits into human readable format with binary prefix + """ + symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") + prefix = {} + for i, s in enumerate(symbols): + prefix[s] = 1 << (i + 1) * 10 + for s in reversed(symbols): + if n >= prefix[s]: + value = float(n) / prefix[s] + return "%.1f%s" % (value, s) + return "%s" % n + + +def ram_available(): + + import psutil + + return (psutil.virtual_memory().available, psutil.swap_memory().free) + + def get_ynh_package_version(package): # Returns the installed version and release version ('stable' or 'testing' @@ -40,7 +115,7 @@ def get_ynh_package_version(package): # may handle changelog differently ! changelog = "/usr/share/doc/%s/changelog.gz" % package - cmd = "gzip -cd %s 2>/dev/null | head -n1" % changelog + cmd = "gzip -cd %s 2>/dev/null | grep -v 'BASH_XTRACEFD' | head -n1" % changelog if not os.path.exists(changelog): return {"version": "?", "repo": "?"} out = check_output(cmd).split() @@ -48,43 +123,6 @@ def get_ynh_package_version(package): return {"version": out[1].strip("()"), "repo": out[2].strip(";")} -def meets_version_specifier(pkg_name, specifier): - """ - Check if a package installed version meets specifier - - specifier is something like ">> 1.2.3" - """ - - # In practice, this function is only used to check the yunohost version - # installed. - # We'll trim any ~foobar in the current installed version because it's not - # handled correctly by version.parse, but we don't care so much in that - # context - assert pkg_name in YUNOHOST_PACKAGES - pkg_version = get_ynh_package_version(pkg_name)["version"] - pkg_version = re.split(r"\~|\+|\-", pkg_version)[0] - pkg_version = version.parse(pkg_version) - - # Extract operator and version specifier - op, req_version = re.search(r"(<<|<=|=|>=|>>) *([\d\.]+)", specifier).groups() - req_version = version.parse(req_version) - - # Python2 had a builtin that returns (-1, 0, 1) depending on comparison - # c.f. https://stackoverflow.com/a/22490617 - def cmp(a, b): - return (a > b) - (a < b) - - deb_operators = { - "<<": lambda v1, v2: cmp(v1, v2) in [-1], - "<=": lambda v1, v2: cmp(v1, v2) in [-1, 0], - "=": lambda v1, v2: cmp(v1, v2) in [0], - ">=": lambda v1, v2: cmp(v1, v2) in [0, 1], - ">>": lambda v1, v2: cmp(v1, v2) in [1], - } - - return deb_operators[op](pkg_version, req_version) - - def ynh_packages_version(*args, **kwargs): # from cli the received arguments are: # (Namespace(_callbacks=deque([]), _tid='_global', _to_return={}), []) {} diff --git a/src/utils/yunopaste.py b/src/utils/yunopaste.py index 35e829991..0edcc721b 100644 --- a/src/utils/yunopaste.py +++ b/src/utils/yunopaste.py @@ -1,5 +1,21 @@ -# -*- coding: utf-8 -*- - +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import requests import json import logging diff --git a/tests/test_helpers.d/ynhtest_setup_source.sh b/tests/test_helpers.d/ynhtest_setup_source.sh index fe61e7401..6a74a587c 100644 --- a/tests/test_helpers.d/ynhtest_setup_source.sh +++ b/tests/test_helpers.d/ynhtest_setup_source.sh @@ -18,51 +18,51 @@ _make_dummy_src() { } ynhtest_setup_source_nominal() { - final_path="$(mktemp -d -p $VAR_WWW)" + install_dir="$(mktemp -d -p $VAR_WWW)" _make_dummy_src > ../conf/dummy.src - ynh_setup_source --dest_dir="$final_path" --source_id="dummy" + ynh_setup_source --dest_dir="$install_dir" --source_id="dummy" - test -e "$final_path" - test -e "$final_path/index.html" + test -e "$install_dir" + test -e "$install_dir/index.html" } ynhtest_setup_source_nominal_upgrade() { - final_path="$(mktemp -d -p $VAR_WWW)" + install_dir="$(mktemp -d -p $VAR_WWW)" _make_dummy_src > ../conf/dummy.src - ynh_setup_source --dest_dir="$final_path" --source_id="dummy" + ynh_setup_source --dest_dir="$install_dir" --source_id="dummy" - test "$(cat $final_path/index.html)" == "Lorem Ipsum" + test "$(cat $install_dir/index.html)" == "Lorem Ipsum" # Except index.html to get overwritten during next ynh_setup_source - echo "IEditedYou!" > $final_path/index.html - test "$(cat $final_path/index.html)" == "IEditedYou!" + echo "IEditedYou!" > $install_dir/index.html + test "$(cat $install_dir/index.html)" == "IEditedYou!" - ynh_setup_source --dest_dir="$final_path" --source_id="dummy" + ynh_setup_source --dest_dir="$install_dir" --source_id="dummy" - test "$(cat $final_path/index.html)" == "Lorem Ipsum" + test "$(cat $install_dir/index.html)" == "Lorem Ipsum" } ynhtest_setup_source_with_keep() { - final_path="$(mktemp -d -p $VAR_WWW)" + install_dir="$(mktemp -d -p $VAR_WWW)" _make_dummy_src > ../conf/dummy.src - echo "IEditedYou!" > $final_path/index.html - echo "IEditedYou!" > $final_path/test.txt + echo "IEditedYou!" > $install_dir/index.html + echo "IEditedYou!" > $install_dir/test.txt - ynh_setup_source --dest_dir="$final_path" --source_id="dummy" --keep="index.html test.txt" + ynh_setup_source --dest_dir="$install_dir" --source_id="dummy" --keep="index.html test.txt" - test -e "$final_path" - test -e "$final_path/index.html" - test -e "$final_path/test.txt" - test "$(cat $final_path/index.html)" == "IEditedYou!" - test "$(cat $final_path/test.txt)" == "IEditedYou!" + test -e "$install_dir" + test -e "$install_dir/index.html" + test -e "$install_dir/test.txt" + test "$(cat $install_dir/index.html)" == "IEditedYou!" + test "$(cat $install_dir/test.txt)" == "IEditedYou!" } ynhtest_setup_source_with_patch() { - final_path="$(mktemp -d -p $VAR_WWW)" + install_dir="$(mktemp -d -p $VAR_WWW)" _make_dummy_src > ../conf/dummy.src mkdir -p ../sources/patches @@ -74,7 +74,7 @@ ynhtest_setup_source_with_patch() { +Lorem Ipsum dolor sit amet EOF - ynh_setup_source --dest_dir="$final_path" --source_id="dummy" + ynh_setup_source --dest_dir="$install_dir" --source_id="dummy" - test "$(cat $final_path/index.html)" == "Lorem Ipsum dolor sit amet" + test "$(cat $install_dir/index.html)" == "Lorem Ipsum dolor sit amet" }