From 2845914d44562aa39956386dedde23e5e10316cf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 4 Dec 2021 03:27:23 +0100 Subject: [PATCH 0001/1116] WIP: foundation for a new portal API to partially replace SSOwat --- bin/yunohost-portal-api | 53 ++++++++++++++++++ share/actionsmap-portal.yml | 51 +++++++++++++++++ src/__init__.py | 20 ++++++- src/authenticators/ldap_ynhuser.py | 59 ++++++++++++++++++++ src/portal.py | 89 ++++++++++++++++++++++++++++++ 5 files changed, 271 insertions(+), 1 deletion(-) create mode 100755 bin/yunohost-portal-api create mode 100644 share/actionsmap-portal.yml create mode 100644 src/authenticators/ldap_ynhuser.py create mode 100644 src/portal.py diff --git a/bin/yunohost-portal-api b/bin/yunohost-portal-api new file mode 100755 index 000000000..66751e66f --- /dev/null +++ b/bin/yunohost-portal-api @@ -0,0 +1,53 @@ +#! /usr/bin/python3 +# -*- coding: utf-8 -*- + +import argparse +import yunohost + +# Default server configuration +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 6788 + + +def _parse_api_args(): + """Parse main arguments for the api""" + parser = argparse.ArgumentParser( + add_help=False, + description="Run the YunoHost API to manage your server.", + ) + srv_group = parser.add_argument_group("server configuration") + srv_group.add_argument( + "-h", + "--host", + action="store", + default=DEFAULT_HOST, + help="Host to listen on (default: %s)" % DEFAULT_HOST, + ) + srv_group.add_argument( + "-p", + "--port", + action="store", + default=DEFAULT_PORT, + type=int, + help="Port to listen on (default: %d)" % DEFAULT_PORT, + ) + glob_group = parser.add_argument_group("global arguments") + glob_group.add_argument( + "--debug", + action="store_true", + default=False, + help="Set log level to DEBUG", + ) + glob_group.add_argument( + "--help", + action="help", + help="Show this help message and exit", + ) + + return parser.parse_args() + + +if __name__ == "__main__": + opts = _parse_api_args() + # Run the server + yunohost.portalapi(debug=opts.debug, host=opts.host, port=opts.port) diff --git a/share/actionsmap-portal.yml b/share/actionsmap-portal.yml new file mode 100644 index 000000000..3d07656ae --- /dev/null +++ b/share/actionsmap-portal.yml @@ -0,0 +1,51 @@ +_global: + namespace: yunohost + cookie_name: yunohost.portal + authentication: + api: ldap_ynhuser + cli: null + +portal: + category_help: Portal routes + actions: + + ### portal_me() + me: + action_help: Allow user to fetch their own infos + api: GET /me + + ### portal_apps() + apps: + action_help: Allow users to fetch lit of apps they have access to + api: GET /me/apps + + ### portal_update() + update: + action_help: Allow user to update their infos (display name, mail aliases/forward, password, ...) + api: PUT /me + # FIXME: add args etc + + ### portal_reset_password() + reset_password: + action_help: Allow user to update their infos (display name, mail aliases/forward, ...) + api: PUT /me/reset_password + authentication: + # FIXME: to be implemented ? + api: reset_password_token + # FIXME: add args etc + + ### portal_register() + register: + action_help: Allow user to register using an invite token or ??? + api: POST /me + authentication: + # FIXME: to be implemented ? + api: register_invite_token + # FIXME: add args etc + + ### portal_public() + public: + action_help: Allow anybody to list public apps and other infos regarding the public portal + api: GET /public + authentication: + api: null diff --git a/src/__init__.py b/src/__init__.py index b9dcd93d9..aaeea7751 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -53,6 +53,20 @@ def api(debug, host, port): sys.exit(ret) +def portalapi(debug, host, port): + + # FIXME : is this the logdir we want ? (yolo to work around permission issue) + init_logging(interface="portalapi", debug=debug, logdir="/var/log") + + ret = moulinette.api( + host=host, + port=port, + actionsmap="/usr/share/yunohost/actionsmap-portal.yml", + locales_dir="/usr/share/yunohost/locales/" + ) + sys.exit(ret) + + def check_command_is_valid_before_postinstall(args): allowed_if_not_postinstalled = [ @@ -125,6 +139,10 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun "level": "DEBUG" if debug else "INFO", "class": "moulinette.interfaces.api.APIQueueHandler", }, + "portalapi": { + "level": "DEBUG" if debug else "INFO", + "class": "moulinette.interfaces.api.APIQueueHandler", + }, "file": { "class": "logging.FileHandler", "formatter": "precise", @@ -151,7 +169,7 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun } # Logging configuration for CLI (or any other interface than api...) # - if interface != "api": + if interface not in ["api", "portalapi"]: configure_logging(logging_configuration) # Logging configuration for API # diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py new file mode 100644 index 000000000..50dca3cc9 --- /dev/null +++ b/src/authenticators/ldap_ynhuser.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +import logging +import ldap +import ldap.sasl + +from moulinette import m18n +from moulinette.authentication import BaseAuthenticator +from yunohost.utils.error import YunohostError + +logger = logging.getLogger("yunohostportal.authenticators.ldap_ynhuser") + +URI = "ldap://localhost:389" +USERDN = "uid={username},ou=users,dc=yunohost,dc=org" + + +class Authenticator(BaseAuthenticator): + + name = "ldap_ynhuser" + + def _authenticate_credentials(self, credentials=None): + + # FIXME ':' should a legit char in the password ? shall we encode the password as base64 or something idk + if ":" not in credentials or len(credentials.split(":")) != 2: + raise YunohostError("invalid_credentials_format") + + username, password = credentials.split(":") + + def _reconnect(): + con = ldap.ldapobject.ReconnectLDAPObject( + URI, retry_max=2, retry_delay=0.5 + ) + con.simple_bind_s(USERDN.format(username=username), password) + return con + + try: + con = _reconnect() + except ldap.INVALID_CREDENTIALS: + raise YunohostError("invalid_password") + except ldap.SERVER_DOWN: + logger.warning(m18n.n("ldap_server_down")) + + # Check that we are indeed logged in with the expected identity + try: + # whoami_s return dn:..., then delete these 3 characters + who = con.whoami_s()[3:] + except Exception as e: + logger.warning("Error during ldap authentication process: %s", e) + raise + else: + if who != USERDN.format(username=username): + raise YunohostError( + "Not logged with the appropriate identity ?!", + raw_msg=True, + ) + finally: + # Free the connection, we don't really need it to keep it open as the point is only to check authentication... + if con: + con.unbind_s() diff --git a/src/portal.py b/src/portal.py new file mode 100644 index 000000000..4a2b449b2 --- /dev/null +++ b/src/portal.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2021 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 + +""" + +# from moulinette import Moulinette, m18n +from moulinette.utils.log import getActionLogger + +from yunohost.utils.error import YunohostValidationError + +logger = getActionLogger("yunohostportal.user") + + +def me(): + """ + Get user informations + + Keyword argument: + username -- Username to get informations + + """ + + username = None # FIXME : this info should come from the authentication layer + + from yunohost.utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + + user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"] + + filter = "uid=" + username + result = ldap.search("ou=users,dc=yunohost,dc=org", filter, user_attrs) + + if result: + user = result[0] + else: + raise YunohostValidationError("user_unknown", user=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": [], + } + + if len(user["mail"]) > 1: + result_dict["mail-aliases"] = user["mail"][1:] + + if len(user["maildrop"]) > 1: + result_dict["mail-forward"] = user["maildrop"][1:] + + if "mailuserquota" in user: + pass + # FIXME + # result_dict["mailbox-quota"] = { + # "limit": userquota if is_limited else m18n.n("unlimit"), + # "use": storage_use, + # } + + # FIXME : should also parse "permission" key in ldap maybe ? + # and list of groups / memberof ? + # (in particular to have e.g. the mail / xmpp / ssh / ... perms) + + return result_dict + + +def apps(username): + return {"foo": "bar"} + # FIXME: should list available apps and corresponding infos ? + # from /etc/ssowat/conf.json ? From 1efb50c7abc0141b0f6325ae155a653ed9027ff0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 25 Dec 2021 15:44:14 +0100 Subject: [PATCH 0002/1116] Iterate on new portal API design: nginx config, cookie format, be able to open a non-root ldap session, --- conf/nginx/plain/yunohost_sso.conf.inc | 4 +- conf/nginx/yunohost_api.conf.inc | 20 +++++++ debian/control | 3 +- share/actionsmap-portal.yml | 3 +- src/authenticators/ldap_ynhuser.py | 83 +++++++++++++++++++++++++- src/portal.py | 12 ++-- src/utils/ldap.py | 38 ++++++++---- 7 files changed, 141 insertions(+), 22 deletions(-) diff --git a/conf/nginx/plain/yunohost_sso.conf.inc b/conf/nginx/plain/yunohost_sso.conf.inc index 308e5a9a4..984440679 100644 --- a/conf/nginx/plain/yunohost_sso.conf.inc +++ b/conf/nginx/plain/yunohost_sso.conf.inc @@ -2,6 +2,6 @@ rewrite ^/yunohost/sso$ /yunohost/sso/ permanent; location /yunohost/sso/ { - # This is an empty location, only meant to avoid other locations - # from matching /yunohost/sso, such that it's correctly handled by ssowat + alias /usr/share/ssowat/portal/; + index index.html; } diff --git a/conf/nginx/yunohost_api.conf.inc b/conf/nginx/yunohost_api.conf.inc index c9ae34f82..3a463c23b 100644 --- a/conf/nginx/yunohost_api.conf.inc +++ b/conf/nginx/yunohost_api.conf.inc @@ -23,3 +23,23 @@ location = /yunohost/api/error/502 { add_header Content-Type text/plain; internal; } + +location /yunohost/portalapi/ { + proxy_read_timeout 3600s; + proxy_pass http://127.0.0.1:6788/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_host; + + # Custom 502 error page + error_page 502 /yunohost/portalapi/error/502; +} + + +# Yunohost admin output complete 502 error page, so use only plain text. +location = /yunohost/portalapi/error/502 { + return 502 '502 - Bad Gateway'; + add_header Content-Type text/plain; + internal; +} diff --git a/debian/control b/debian/control index 31204a180..8a9d841c5 100644 --- a/debian/control +++ b/debian/control @@ -14,7 +14,8 @@ Depends: ${python3:Depends}, ${misc:Depends} , python3-psutil, python3-requests, python3-dnspython, python3-openssl , python3-miniupnpc, python3-dbus, python3-jinja2 , python3-toml, python3-packaging, python3-publicsuffix2 - , python3-ldap, python3-zeroconf, python3-lexicon, + , python3-ldap, python3-zeroconf, python3-lexicon + , python3-jwt , python-is-python3 , nginx, nginx-extras (>=1.18) , apt, apt-transport-https, apt-utils, dirmngr diff --git a/share/actionsmap-portal.yml b/share/actionsmap-portal.yml index 3d07656ae..761d5a6ce 100644 --- a/share/actionsmap-portal.yml +++ b/share/actionsmap-portal.yml @@ -1,9 +1,10 @@ _global: namespace: yunohost - cookie_name: yunohost.portal authentication: api: ldap_ynhuser cli: null + lock: false + cache: false portal: category_help: Portal routes diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index 50dca3cc9..28b8c49fd 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -1,12 +1,17 @@ # -*- coding: utf-8 -*- +import jwt import logging import ldap import ldap.sasl +import datetime from moulinette import m18n from moulinette.authentication import BaseAuthenticator -from yunohost.utils.error import YunohostError +from moulinette.utils.text import random_ascii +from yunohost.utils.error import YunohostError, YunohostAuthenticationError + +session_secret = random_ascii() logger = logging.getLogger("yunohostportal.authenticators.ldap_ynhuser") @@ -57,3 +62,79 @@ class Authenticator(BaseAuthenticator): # Free the connection, we don't really need it to keep it open as the point is only to check authentication... if con: con.unbind_s() + + + + + + + + # FIXME FIXME FIXME : the password is to be encrypted to not expose it in the JWT cookie which is only signed and base64 encoded but not encrypted + + + + + + + + + + return {"user": username, "password": password} + + def set_session_cookie(self, infos): + + from bottle import response + + assert isinstance(infos, dict) + + # This allows to generate a new session id or keep the existing one + current_infos = self.get_session_cookie(raise_if_no_session_exists=False) + new_infos = { + "id": current_infos["id"], + # See https://pyjwt.readthedocs.io/en/latest/usage.html#registered-claim-names + # for explanations regarding nbf, exp + "nbf": int(datetime.datetime.now().timestamp()), + "exp": int(datetime.datetime.now().timestamp()) + (7 * 24 * 3600) # One week validity + } + new_infos.update(infos) + + response.set_cookie( + "yunohost.portal", + jwt.encode(new_infos, session_secret, algorithm="HS256").decode(), + secure=True, + httponly=True, + path="/", + # samesite="strict", # Bottle 0.12 doesn't support samesite, to be added in next versions + # FIXME : add Expire clause + ) + + def get_session_cookie(self, raise_if_no_session_exists=True): + + from bottle import request + + try: + token = request.get_cookie("yunohost.portal", default="").encode() + infos = jwt.decode(token, session_secret, algorithms="HS256", options={"require": ["id", "user", "exp", "nbf"]}) + except Exception: + if not raise_if_no_session_exists: + return {"id": random_ascii()} + raise YunohostAuthenticationError("unable_authenticate") + + if not infos and raise_if_no_session_exists: + raise YunohostAuthenticationError("unable_authenticate") + + if "id" not in infos: + infos["id"] = random_ascii() + + # FIXME: Here, maybe we want to re-authenticate the session via the authenticator + # For example to check that the username authenticated is still in the admin group... + + return infos + + @staticmethod + def delete_session_cookie(self): + + from bottle import response + + response.set_cookie("yunohost.portal", "", max_age=-1) + response.delete_cookie("yunohost.portal") diff --git a/src/portal.py b/src/portal.py index 4a2b449b2..2eaa59dd4 100644 --- a/src/portal.py +++ b/src/portal.py @@ -22,12 +22,14 @@ # from moulinette import Moulinette, m18n from moulinette.utils.log import getActionLogger +from yunohost.authenticators.ldap_ynhuser import Authenticator as Auth +from yunohost.utils.ldap import LDAPInterface from yunohost.utils.error import YunohostValidationError logger = getActionLogger("yunohostportal.user") -def me(): +def portal_me(): """ Get user informations @@ -36,11 +38,13 @@ def me(): """ - username = None # FIXME : this info should come from the authentication layer + import pdb; pdb.set_trace() - from yunohost.utils.ldap import _get_ldap_interface + auth = Auth().get_session_cookie() + username = auth["user"] + password = auth["password"] - ldap = _get_ldap_interface() + ldap = LDAPInterface(username, password) user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"] diff --git a/src/utils/ldap.py b/src/utils/ldap.py index 651d09f75..852fa89c2 100644 --- a/src/utils/ldap.py +++ b/src/utils/ldap.py @@ -42,7 +42,7 @@ def _get_ldap_interface(): global _ldap_interface if _ldap_interface is None: - _ldap_interface = LDAPInterface() + _ldap_interface = LDAPInterface(user="root") return _ldap_interface @@ -71,22 +71,34 @@ def _destroy_ldap_interface(): atexit.register(_destroy_ldap_interface) +URI = "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi" +BASEDN = "dc=yunohost,dc=org" +ROOTDN = "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" +USERDN = "uid={username},ou=users,dc=yunohost,dc=org" + class LDAPInterface: - def __init__(self): - logger.debug("initializing ldap interface") - self.uri = "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi" - self.basedn = "dc=yunohost,dc=org" - self.rootdn = "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" + def __init__(self, user="root", password=None): + + if user == "root": + logger.debug("initializing root ldap interface") + self.userdn = ROOTDN + self._connect = lambda con: con.sasl_non_interactive_bind_s("EXTERNAL") + else: + logger.debug("initializing user ldap interface") + self.userdn = USERDN.format(username=user) + self._connect = lambda con: con.simple_bind_s(self.userdn, password) + self.connect() def connect(self): + def _reconnect(): con = ldap.ldapobject.ReconnectLDAPObject( - self.uri, retry_max=10, retry_delay=0.5 + URI, retry_max=10, retry_delay=0.5 ) - con.sasl_non_interactive_bind_s("EXTERNAL") + self._connect(con) return con try: @@ -113,7 +125,7 @@ class LDAPInterface: logger.warning("Error during ldap authentication process: %s", e) raise else: - if who != self.rootdn: + if who != self.userdn: raise MoulinetteError("Not logged in with the expected userdn ?!") else: self.con = con @@ -139,7 +151,7 @@ class LDAPInterface: """ if not base: - base = self.basedn + base = BASEDN try: result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs) @@ -184,7 +196,7 @@ class LDAPInterface: Boolean | MoulinetteError """ - dn = rdn + "," + self.basedn + dn = f"{rdn},{BASEDN}" ldif = modlist.addModlist(attr_dict) for i, (k, v) in enumerate(ldif): if isinstance(v, list): @@ -215,7 +227,7 @@ class LDAPInterface: Boolean | MoulinetteError """ - dn = rdn + "," + self.basedn + dn = f"{rdn},{BASEDN}" try: self.con.delete_s(dn) except Exception as e: @@ -240,7 +252,7 @@ class LDAPInterface: Boolean | MoulinetteError """ - dn = rdn + "," + self.basedn + dn = f"{rdn},{BASEDN}" actual_entry = self.search(base=dn, attrs=None) ldif = modlist.modifyModlist(actual_entry[0], attr_dict, ignore_oldexistent=1) From 62808152eecf4fd5da659e1785b64460e9775a7f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 26 Dec 2021 16:31:05 +0100 Subject: [PATCH 0003/1116] Cookie handling for the new portal API --- src/app.py | 3 +++ src/authenticators/ldap_ynhuser.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 27cf7caec..9db7a4e4e 100644 --- a/src/app.py +++ b/src/app.py @@ -1323,6 +1323,7 @@ def app_ssowatconf(): "public": True, "uris": [domain + "/yunohost/admin" for domain in domains] + [domain + "/yunohost/api" for domain in domains] + + [domain + "/yunohost/portalapi" for domain in domains] + [ "re:^[^/]*/%.well%-known/ynh%-diagnosis/.*$", "re:^[^/]*/%.well%-known/acme%-challenge/.*$", @@ -1368,6 +1369,8 @@ def app_ssowatconf(): } conf_dict = { + "cookie_secret_file": "/etc/yunohost/.ssowat_cookie_secret", + "cookie_name": "yunohost.portal", "portal_domain": main_domain, "portal_path": "/yunohost/sso/", "additional_headers": { diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index 28b8c49fd..0e51d1925 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -11,7 +11,9 @@ from moulinette.authentication import BaseAuthenticator from moulinette.utils.text import random_ascii from yunohost.utils.error import YunohostError, YunohostAuthenticationError -session_secret = random_ascii() +# FIXME : we shall generate this somewhere if it doesnt exists yet +# FIXME : fix permissions +session_secret = open("/etc/yunohost/.ssowat_cookie_secret").read() logger = logging.getLogger("yunohostportal.authenticators.ldap_ynhuser") From 45baaead3689703b257c8ce16aee763a3a67c9ba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 26 Dec 2021 18:22:33 +0100 Subject: [PATCH 0004/1116] Fix typo + unused import --- src/authenticators/ldap_admin.py | 1 - src/authenticators/ldap_ynhuser.py | 1 - src/tools.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/authenticators/ldap_admin.py b/src/authenticators/ldap_admin.py index 7f96165cb..872dd3c8d 100644 --- a/src/authenticators/ldap_admin.py +++ b/src/authenticators/ldap_admin.py @@ -120,7 +120,6 @@ class Authenticator(BaseAuthenticator): return infos - @staticmethod def delete_session_cookie(self): from bottle import response diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index 0e51d1925..fe2a657f5 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -133,7 +133,6 @@ class Authenticator(BaseAuthenticator): return infos - @staticmethod def delete_session_cookie(self): from bottle import response diff --git a/src/tools.py b/src/tools.py index b66d20811..467e33cfa 100644 --- a/src/tools.py +++ b/src/tools.py @@ -33,7 +33,7 @@ from typing import List from moulinette import Moulinette, m18n from moulinette.utils.log import getActionLogger -from moulinette.utils.process import check_output, call_async_output +from moulinette.utils.process import call_async_output from moulinette.utils.filesystem import read_yaml, write_to_yaml, cp, mkdir, rm from yunohost.app import ( From bd564e6a536f0b0b54c09fdd687a23f76372a59a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 27 Dec 2021 12:44:20 +0100 Subject: [PATCH 0005/1116] Add systemd conf for new service yunohost-portal-api --- conf/yunohost/yunohost-portal-api.service | 14 ++++++++++++++ debian/postinst | 2 ++ hooks/conf_regen/01-yunohost | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 conf/yunohost/yunohost-portal-api.service diff --git a/conf/yunohost/yunohost-portal-api.service b/conf/yunohost/yunohost-portal-api.service new file mode 100644 index 000000000..0ba6e8b3d --- /dev/null +++ b/conf/yunohost/yunohost-portal-api.service @@ -0,0 +1,14 @@ +[Unit] +Description=YunoHost Portal API +After=network.target + +[Service] +User=ynh-portal +Type=simple +ExecStart=/usr/bin/yunohost-portal-api +Restart=always +RestartSec=5 +TimeoutStopSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/debian/postinst b/debian/postinst index e93845e88..c62926a30 100644 --- a/debian/postinst +++ b/debian/postinst @@ -29,6 +29,8 @@ do_configure() { yunohost diagnosis run --force fi + systemctl restart yunohost-portal-api + # Trick to let yunohost handle the restart of the API, # to prevent the webadmin from cutting the branch it's sitting on if systemctl is-enabled yunohost-api --quiet diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 14840e2f1..597595231 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -56,7 +56,10 @@ do_init_regen() { chown root:root /var/cache/yunohost chmod 700 /var/cache/yunohost + getent passwd ynh-portal &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group ynh-portal + cp yunohost-api.service /etc/systemd/system/yunohost-api.service + cp yunohost-portal-api.service /etc/systemd/system/yunohost-portal-api.service cp yunohost-firewall.service /etc/systemd/system/yunohost-firewall.service cp yunoprompt.service /etc/systemd/system/yunoprompt.service @@ -64,6 +67,10 @@ do_init_regen() { systemctl enable yunohost-api.service systemctl start yunohost-api.service + + systemctl enable yunohost-portal-api.service + systemctl start yunohost-portal-api.service + # Yunohost-firewall is enabled only during postinstall, not init, not 100% sure why cp dpkg-origins /etc/dpkg/origins/yunohost @@ -152,6 +159,7 @@ HandleLidSwitchExternalPower=ignore EOF cp yunohost-api.service ${pending_dir}/etc/systemd/system/yunohost-api.service + cp yunohost-portal-api.service ${pending_dir}/etc/systemd/system/yunohost-portal-api.service cp yunohost-firewall.service ${pending_dir}/etc/systemd/system/yunohost-firewall.service cp yunoprompt.service ${pending_dir}/etc/systemd/system/yunoprompt.service @@ -169,6 +177,13 @@ EOF do_post_regen() { regen_conf_files=$1 + getent passwd ynh-portal &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group ynh-portal + if [ ! -e /etc/yunohost/.ssowat_cookie_secret ]; then + dd if=/dev/urandom bs=1 count=1000 2>/dev/null | tr --complement --delete 'A-Za-z0-9' | head -c 64 > /etc/yunohost/.ssowat_cookie_secret + fi + chown ynh-portal:root /etc/yunohost/.ssowat_cookie_secret + chmod 400 /etc/yunohost/.ssowat_cookie_secret + ###################### # Enfore permissions # ###################### @@ -225,10 +240,12 @@ do_post_regen() { systemctl daemon-reload 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" =~ "yunohost-firewall.service" ]] || systemctl daemon-reload [[ ! "$regen_conf_files" =~ "yunohost-api.service" ]] || systemctl daemon-reload + [[ ! "$regen_conf_files" =~ "yunohost-portal-api.service" ]] || systemctl daemon-reload if [[ "$regen_conf_files" =~ "yunoprompt.service" ]]; then systemctl daemon-reload @@ -241,6 +258,9 @@ do_post_regen() { systemctl $action proc-hidepid --quiet --now fi + systemctl enable yunohost-portal-api.service --quiet + systemctl is-active yunohost-portal-api --quiet || systemctl start yunohost-portal-api.service + # Change dpkg vendor # see https://wiki.debian.org/Derivatives/Guidelines#Vendor if readlink -f /etc/dpkg/origins/default | grep -q debian; From 76eba6fc88814c1900cb94a3477eb3fea9a93801 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 27 Dec 2021 13:05:11 +0100 Subject: [PATCH 0006/1116] Fix log permission issue for yunohost-portal-api --- hooks/conf_regen/01-yunohost | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 597595231..4dfd59912 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -184,6 +184,10 @@ do_post_regen() { chown ynh-portal:root /etc/yunohost/.ssowat_cookie_secret chmod 400 /etc/yunohost/.ssowat_cookie_secret + touch /var/log/yunohost-portalapi.log + chown ynh-portal:root /var/log/yunohost-portalapi.log + chmod 600 /var/log/yunohost-portalapi.log + ###################### # Enfore permissions # ###################### From f9afc19ed40622956076b936d384cf3bff8d7daa Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Thu, 30 Jun 2022 11:22:46 +0200 Subject: [PATCH 0007/1116] Added an optionnal "password" argument to the "yunohost dyndns subscribe" command --- locales/en.json | 4 +++- share/actionsmap.yml | 6 ++++++ src/dyndns.py | 12 ++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 2b2f10179..d7179cd7e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -356,6 +356,8 @@ "dyndns_key_generating": "Generating DNS key... It may take a while.", "dyndns_key_not_found": "DNS key not found for the domain", "dyndns_no_domain_registered": "No domain registered with DynDNS", + "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", + "dyndns_added_password": "Remember your recovery password, you can use it to delete this domain record.", "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", "dyndns_registered": "DynDNS domain registered", "dyndns_registration_failed": "Could not register DynDNS domain: {error}", @@ -685,4 +687,4 @@ "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - adding a first user through the 'Users' section of the webadmin (or 'yunohost user create ' in command-line);\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." -} \ No newline at end of file +} diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 89c6e914d..bf2f53371 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1406,6 +1406,12 @@ dyndns: -k: full: --key help: Public DNS key + -p: + full: --password + help: Password used to later delete the domain + extra: + pattern: *pattern_password + comment: dyndns_added_password ### dyndns_update() update: diff --git a/src/dyndns.py b/src/dyndns.py index 34f3dd5dc..39e8a7213 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -29,6 +29,7 @@ import json import glob import base64 import subprocess +import hashlib from moulinette import m18n from moulinette.core import MoulinetteError @@ -75,15 +76,19 @@ def _dyndns_available(domain): @is_unit_operation() -def dyndns_subscribe(operation_logger, domain=None, key=None): +def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): """ Subscribe to a DynDNS service Keyword argument: domain -- Full domain to subscribe with key -- Public DNS key + password -- Password that will be used to delete the domain """ + if password is None: + logger.warning(m18n.n('dyndns_no_recovery_password')) + if _guess_current_dyndns_domain() != (None, None): raise YunohostValidationError("domain_dyndns_already_subscribed") @@ -138,9 +143,12 @@ def dyndns_subscribe(operation_logger, domain=None, key=None): try: # Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever... b64encoded_key = base64.b64encode(secret.encode()).decode() + data = {"subdomain": domain} + if password: + data["recovery_password"]=hashlib.sha256((domain+":"+password.strip()).encode('utf-8')).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", - data={"subdomain": domain}, + data=data, timeout=30, ) except Exception as e: From 4a9080bdfd5057085dc962f733cd6b27c98bdef0 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Thu, 30 Jun 2022 12:23:51 +0200 Subject: [PATCH 0008/1116] Added a new command to delete dyndns records --- locales/en.json | 5 +++++ share/actionsmap.yml | 17 +++++++++++++++ src/dyndns.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/locales/en.json b/locales/en.json index d7179cd7e..0bbd41387 100644 --- a/locales/en.json +++ b/locales/en.json @@ -361,6 +361,10 @@ "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", "dyndns_registered": "DynDNS domain registered", "dyndns_registration_failed": "Could not register DynDNS domain: {error}", + "dyndns_unregistration_failed": "Could not unregister DynDNS domain: {error}", + "dyndns_unregistered": "Domain successfully deleted!", + "dyndns_unsubscribe_wrong_password": "Invalid password", + "dyndns_unsubscribe_wrong_domain": "Domain is not registered", "dyndns_unavailable": "The domain '{domain}' is unavailable.", "experimental_feature": "Warning: This feature is experimental and not considered stable, you should not use it unless you know what you are doing.", "extracting": "Extracting...", @@ -451,6 +455,7 @@ "log_domain_main_domain": "Make '{}' the main domain", "log_domain_remove": "Remove '{}' domain from system configuration", "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", + "log_dyndns_unsubscribe": "Unsubscribe to a YunoHost subdomain '{}'", "log_dyndns_update": "Update the IP associated with your YunoHost subdomain '{}'", "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help", "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}'", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index bf2f53371..f8d082a70 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1412,6 +1412,23 @@ dyndns: extra: pattern: *pattern_password comment: dyndns_added_password + + ### dyndns_unsubscribe() + unsubscribe: + action_help: Unsubscribe to a DynDNS service + arguments: + -d: + full: --domain + help: Full domain to subscribe with + extra: + pattern: *pattern_domain + required: True + -p: + full: --password + help: Password used to delete the domain + extra: + required: True + pattern: *pattern_password ### dyndns_update() update: diff --git a/src/dyndns.py b/src/dyndns.py index 39e8a7213..67a8b293d 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -178,6 +178,56 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): logger.success(m18n.n("dyndns_registered")) +@is_unit_operation() +def dyndns_unsubscribe(operation_logger, domain, password): + """ + Unsubscribe from a DynDNS service + + Keyword argument: + domain -- Full domain to unsubscribe with + password -- Password that is used to delete the domain ( defined when subscribing ) + """ + + operation_logger.start() + + # '165' is the convention identifier for hmac-sha512 algorithm + # '1234' is idk? doesnt matter, but the old format contained a number here... + key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" + + import requests # lazy loading this module for performance reasons + + # Send delete request + try: + r = requests.delete( + f"https://{DYNDNS_PROVIDER}/domains/{domain}", + data={"recovery_password":hashlib.sha256((str(domain)+":"+str(password).strip()).encode('utf-8')).hexdigest()}, + timeout=30, + ) + except Exception as e: + raise YunohostError("dyndns_unregistration_failed", error=str(e)) + + if r.status_code == 200: # Deletion was successful + rm(key_file, force=True) + # Yunohost regen conf will add the dyndns cron job if a key exists + # in /etc/yunohost/dyndns + regen_conf(["yunohost"]) + + # Add some dyndns update in 2 and 4 minutes from now such that user should + # not have to wait 10ish minutes for the conf to propagate + cmd = ( + "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" + ) + # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... + subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) + subprocess.check_call(["bash", "-c", cmd.format(t="4 min")]) + + logger.success(m18n.n("dyndns_unregistered")) + elif r.status_code == 403: # Wrong password + raise YunohostError("dyndns_unsubscribe_wrong_password") + elif r.status_code == 404: # Invalid domain + raise YunohostError("dyndns_unsubscribe_wrong_domain") + + @is_unit_operation() def dyndns_update( operation_logger, From fdca22ca5bea6268d282063146b15ef781d199f2 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Thu, 30 Jun 2022 12:26:53 +0200 Subject: [PATCH 0009/1116] Fixed typo --- share/actionsmap.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index f8d082a70..cfc1d6151 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1419,7 +1419,7 @@ dyndns: arguments: -d: full: --domain - help: Full domain to subscribe with + help: Full domain to unsubscribe with extra: pattern: *pattern_domain required: True From 4f2a111470219c04596b123e4731dba874bb7b8f Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 1 Jul 2022 10:38:25 +0200 Subject: [PATCH 0010/1116] We can now specify a password using the yunohost domain add command --- locales/en.json | 1 + share/actionsmap.yml | 6 ++++++ src/domain.py | 8 +++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 0bbd41387..60243b6bd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -338,6 +338,7 @@ "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", + "domain_password_no_dyndns": "The password is only used for subscribing to (and maybe later unsubscribing from) the DynDNS service", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index cfc1d6151..619b1207d 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -457,6 +457,12 @@ domain: full: --dyndns help: Subscribe to the DynDNS service action: store_true + -p: + full: --password + help: Password used to later delete the domain ( if subscribing to the DynDNS service ) + extra: + pattern: *pattern_password + comment: dyndns_added_password ### domain_remove() remove: diff --git a/src/domain.py b/src/domain.py index e40b4f03c..2426412c4 100644 --- a/src/domain.py +++ b/src/domain.py @@ -131,14 +131,14 @@ def _get_parent_domain_of(domain): @is_unit_operation() -def domain_add(operation_logger, domain, dyndns=False): +def domain_add(operation_logger, domain, dyndns=False,password=None): """ Create a custom domain Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS - + password -- Password used to later unsubscribe from DynDNS """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf @@ -183,7 +183,9 @@ def domain_add(operation_logger, domain, dyndns=False): from yunohost.dyndns import dyndns_subscribe # Actually subscribe - dyndns_subscribe(domain=domain) + dyndns_subscribe(domain=domain,password=password) + elif password: # If a password is provided, while not subscribing to a DynDNS service + logger.warning(m18n.n("domain_password_no_dyndns")) _certificate_install_selfsigned([domain], True) From 882c024bc8cdf2d03b3ccabf08eba76fbd6103f3 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 1 Jul 2022 14:16:50 +0200 Subject: [PATCH 0011/1116] `yunohost domain remove` now accepts a -p argument --- share/actionsmap.yml | 5 +++++ src/domain.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 619b1207d..ea1242825 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -481,6 +481,11 @@ domain: full: --force help: Do not ask confirmation to remove apps action: store_true + -p: + full: --password + help: Password used to delete the domain from DynDNS + extra: + pattern: *pattern_password ### domain_dns_conf() diff --git a/src/domain.py b/src/domain.py index 2426412c4..a8a9560cb 100644 --- a/src/domain.py +++ b/src/domain.py @@ -235,7 +235,7 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): @is_unit_operation() -def domain_remove(operation_logger, domain, remove_apps=False, force=False): +def domain_remove(operation_logger, domain, remove_apps=False, force=False, password=None): """ Delete domains @@ -244,7 +244,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): remove_apps -- Remove applications installed on the domain force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified - + password -- Recovery password used at the creation of the DynDNS domain """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove @@ -356,6 +356,13 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): hook_callback("post_domain_remove", args=[domain]) + # If a password is provided, delete the DynDNS record + if password: + from yunohost.dyndns import dyndns_unsubscribe + + # Actually unsubscribe + dyndns_unsubscribe(domain=domain,password=password) + logger.success(m18n.n("domain_deleted")) From 02a4a5fecfe18f4dacd79cdd8438a1c45925d801 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 1 Jul 2022 14:40:52 +0200 Subject: [PATCH 0012/1116] The option -d is deprecated, -p is preferred --- locales/en.json | 1 - share/actionsmap.yml | 4 ++-- src/domain.py | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index 60243b6bd..0bbd41387 100644 --- a/locales/en.json +++ b/locales/en.json @@ -338,7 +338,6 @@ "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", - "domain_password_no_dyndns": "The password is only used for subscribing to (and maybe later unsubscribing from) the DynDNS service", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index ea1242825..8f2e90569 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -455,11 +455,11 @@ domain: pattern: *pattern_domain -d: full: --dyndns - help: Subscribe to the DynDNS service + help: (Deprecated, using the -p option in order to set a password is recommended) Subscribe to the DynDNS service action: store_true -p: full: --password - help: Password used to later delete the domain ( if subscribing to the DynDNS service ) + help: Subscribe to the DynDNS service with a password, used to later delete the domain extra: pattern: *pattern_password comment: dyndns_added_password diff --git a/src/domain.py b/src/domain.py index a8a9560cb..3c5823037 100644 --- a/src/domain.py +++ b/src/domain.py @@ -163,6 +163,7 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): domain = domain.encode("idna").decode("utf-8") # DynDNS domain + dyndns = dyndns or (password!=None) # If a password is specified, then it is obviously a dyndns domain, no need for the extra option if dyndns: from yunohost.utils.dns import is_yunohost_dyndns_domain @@ -184,8 +185,6 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): # Actually subscribe dyndns_subscribe(domain=domain,password=password) - elif password: # If a password is provided, while not subscribing to a DynDNS service - logger.warning(m18n.n("domain_password_no_dyndns")) _certificate_install_selfsigned([domain], True) From 150614645028f54236cd9cccedfaab53865d846c Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Mon, 4 Jul 2022 10:07:30 +0200 Subject: [PATCH 0013/1116] Passwords can be set interactively --- share/actionsmap.yml | 9 ++++++++- src/domain.py | 2 +- src/dyndns.py | 22 +++++++++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 8f2e90569..fae7ab8f8 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -459,6 +459,8 @@ domain: action: store_true -p: full: --password + nargs: "?" + const: 0 help: Subscribe to the DynDNS service with a password, used to later delete the domain extra: pattern: *pattern_password @@ -483,6 +485,8 @@ domain: action: store_true -p: full: --password + nargs: "?" + const: 0 help: Password used to delete the domain from DynDNS extra: pattern: *pattern_password @@ -1419,6 +1423,8 @@ dyndns: help: Public DNS key -p: full: --password + nargs: "?" + const: 0 help: Password used to later delete the domain extra: pattern: *pattern_password @@ -1436,9 +1442,10 @@ dyndns: required: True -p: full: --password + nargs: "?" + const: 0 help: Password used to delete the domain extra: - required: True pattern: *pattern_password ### dyndns_update() diff --git a/src/domain.py b/src/domain.py index 3c5823037..5bdecf651 100644 --- a/src/domain.py +++ b/src/domain.py @@ -356,7 +356,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, pass hook_callback("post_domain_remove", args=[domain]) # If a password is provided, delete the DynDNS record - if password: + if password!=None: from yunohost.dyndns import dyndns_unsubscribe # Actually unsubscribe diff --git a/src/dyndns.py b/src/dyndns.py index 67a8b293d..a5532f101 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -31,7 +31,7 @@ import base64 import subprocess import hashlib -from moulinette import m18n +from moulinette import Moulinette, m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file, rm, chown, chmod @@ -144,7 +144,14 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): # Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever... b64encoded_key = base64.b64encode(secret.encode()).decode() data = {"subdomain": domain} - if password: + if password!=None: + from yunohost.utils.password import assert_password_is_strong_enough + # Ensure sufficiently complex password + if Moulinette.interface.type == "cli" and password==0: + password = Moulinette.prompt( + m18n.n("ask_password"), is_password=True, confirm=True + ) + assert_password_is_strong_enough("admin", password) data["recovery_password"]=hashlib.sha256((domain+":"+password.strip()).encode('utf-8')).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", @@ -179,7 +186,7 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): @is_unit_operation() -def dyndns_unsubscribe(operation_logger, domain, password): +def dyndns_unsubscribe(operation_logger, domain, password=None): """ Unsubscribe from a DynDNS service @@ -189,6 +196,15 @@ def dyndns_unsubscribe(operation_logger, domain, password): """ operation_logger.start() + + from yunohost.utils.password import assert_password_is_strong_enough + + # Ensure sufficiently complex password + if Moulinette.interface.type == "cli" and not password: + password = Moulinette.prompt( + m18n.n("ask_password"), is_password=True, confirm=True + ) + assert_password_is_strong_enough("admin", password) # '165' is the convention identifier for hmac-sha512 algorithm # '1234' is idk? doesnt matter, but the old format contained a number here... From 40fbc8d1ea8db5a58c46301c3022fa8ba48f8da4 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Mon, 4 Jul 2022 10:09:08 +0200 Subject: [PATCH 0014/1116] Clarification --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 0bbd41387..96946acd3 100644 --- a/locales/en.json +++ b/locales/en.json @@ -362,7 +362,7 @@ "dyndns_registered": "DynDNS domain registered", "dyndns_registration_failed": "Could not register DynDNS domain: {error}", "dyndns_unregistration_failed": "Could not unregister DynDNS domain: {error}", - "dyndns_unregistered": "Domain successfully deleted!", + "dyndns_unregistered": "DynDNS domain successfully unregistered", "dyndns_unsubscribe_wrong_password": "Invalid password", "dyndns_unsubscribe_wrong_domain": "Domain is not registered", "dyndns_unavailable": "The domain '{domain}' is unavailable.", From fbfcec4873b801ef9d252383a911e625e9bda280 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 09:06:52 +0200 Subject: [PATCH 0015/1116] `yunohost domain dns push` works with Dynette domains --- hooks/conf_regen/01-yunohost | 2 +- locales/en.json | 1 + share/actionsmap.yml | 71 ++++++++++++++++++++++++++++++++++-- src/dns.py | 34 ++++++++++++++--- src/domain.py | 37 +++++++++++++++---- src/dyndns.py | 4 +- 6 files changed, 128 insertions(+), 21 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index dc0bfc689..29da2b183 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -117,7 +117,7 @@ SHELL=/bin/bash # - if ip.yunohost.org answers ping (basic check to validate that we're connected to the internet and yunohost infra aint down) # - and if lock ain't already taken by another command # - trigger yunohost dyndns update -*/10 * * * * root : YunoHost DynDNS update; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || yunohost dyndns update >> /dev/null +*/10 * * * * root : YunoHost DynDNS update ; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || yunohost domain list --exclude-subdomains --output json | jq --raw-output '.domains[]' | grep -E "\.(noho\.st|nohost\.me|ynh\.fr)$" | xargs -I {} yunohost domain dns push "{}" >> /dev/null EOF else # (Delete cron if no dyndns domain found) diff --git a/locales/en.json b/locales/en.json index 96946acd3..a1a90d686 100644 --- a/locales/en.json +++ b/locales/en.json @@ -323,6 +323,7 @@ "domain_dns_conf_special_use_tld": "This domain is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", "domain_dns_push_already_up_to_date": "Records already up to date, nothing to do.", "domain_dns_push_failed": "Updating the DNS records failed miserably.", + "domain_dns_push_failed_domain": "Updating the DNS records for {domain} failed : {error}", "domain_dns_push_failed_to_authenticate": "Failed to authenticate on registrar's API for domain '{domain}'. Most probably the credentials are incorrect? (Error: {error})", "domain_dns_push_failed_to_list": "Failed to list current records using the registrar's API: {error}", "domain_dns_push_managed_in_parent_domain": "The automatic DNS configuration feature is managed in the parent domain {parent_domain}.", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index fae7ab8f8..85b240aa3 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -578,10 +578,69 @@ domain: help: The path to check (e.g. /coffee) subcategories: + dyndns: + subcategory_help: Subscribe and Update DynDNS Hosts + actions: + ### domain_dyndns_subscribe() + subscribe: + action_help: Subscribe to a DynDNS service + arguments: + -d: + full: --domain + help: Full domain to subscribe with + extra: + pattern: *pattern_domain + -k: + full: --key + help: Public DNS key + -p: + full: --password + nargs: "?" + const: 0 + help: Password used to later delete the domain + extra: + pattern: *pattern_password + comment: dyndns_added_password + + ### domain_dyndns_unsubscribe() + unsubscribe: + action_help: Unsubscribe from a DynDNS service + arguments: + -d: + full: --domain + help: Full domain to unsubscribe with + extra: + pattern: *pattern_domain + required: True + -p: + full: --password + nargs: "?" + const: 0 + help: Password used to delete the domain + extra: + pattern: *pattern_password config: subcategory_help: Domain settings actions: + ### domain_config_get() + get: + action_help: Display a domain configuration + api: GET /domains//config + arguments: + domain: + help: Domain name + key: + help: A specific panel, section or a question identifier + nargs: '?' + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true + -e: + full: --export + help: Only export key/values, meant to be reimported using "config set --args-file" + action: store_true ### domain_config_get() get: @@ -641,6 +700,7 @@ domain: arguments: domain: help: Domain name to push DNS conf for + nargs: "*" extra: pattern: *pattern_domain -d: @@ -1406,16 +1466,17 @@ firewall: # DynDNS # ############################# dyndns: - category_help: Subscribe and Update DynDNS Hosts + category_help: Subscribe and Update DynDNS Hosts ( deprecated, use 'yunohost domain dyndns' instead ) actions: ### dyndns_subscribe() subscribe: action_help: Subscribe to a DynDNS service + deprecated: true arguments: -d: full: --domain - help: Full domain to subscribe with + help: Full domain to subscribe with ( deprecated, use 'yunohost domain dyndns subscribe' instead ) extra: pattern: *pattern_domain -k: @@ -1432,7 +1493,8 @@ dyndns: ### dyndns_unsubscribe() unsubscribe: - action_help: Unsubscribe to a DynDNS service + action_help: Unsubscribe to a DynDNS service ( deprecated, use 'yunohost domain dyndns unsubscribe' instead ) + deprecated: true arguments: -d: full: --domain @@ -1450,7 +1512,8 @@ dyndns: ### dyndns_update() update: - action_help: Update IP on DynDNS platform + action_help: Update IP on DynDNS platform ( deprecated, use 'yunohost domain dns push DOMAIN' instead ) + deprecated: true arguments: -d: full: --domain diff --git a/src/dns.py b/src/dns.py index c8bebed41..144f8a3a2 100644 --- a/src/dns.py +++ b/src/dns.py @@ -47,6 +47,7 @@ from yunohost.utils.error import YunohostValidationError, YunohostError from yunohost.utils.network import get_public_ip from yunohost.log import is_unit_operation from yunohost.hook import hook_callback +from yunohost.dyndns import dyndns_update logger = getActionLogger("yunohost.domain") @@ -622,7 +623,17 @@ def _get_registar_settings(domain): @is_unit_operation() -def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge=False): +def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False): + if type(domains).__name__!="list": # If we provide only a domain as an argument + domains = [domains] + for domain in domains: + try: + domain_dns_push_unique(domain,dry_run=dry_run,force=force,purge=purge) + except YunohostError as e: + logger.error(m18n.n("domain_dns_push_failed_domain",domain=domain,error=str(e))) + +@is_unit_operation() +def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, purge=False): """ Send DNS records to the previously-configured registrar of the domain. """ @@ -643,12 +654,14 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= # FIXME: in the future, properly unify this with yunohost dyndns update if registrar == "yunohost": - logger.info(m18n.n("domain_dns_registrar_yunohost")) + #logger.info(m18n.n("domain_dns_registrar_yunohost")) + from yunohost.dyndns import dyndns_update + dyndns_update(domain=domain) return {} if registrar == "parent_domain": parent_domain = domain.split(".", 1)[1] - registar, registrar_credentials = _get_registar_settings(parent_domain) + registrar, registrar_credentials = _get_registar_settings(parent_domain) if any(registrar_credentials.values()): raise YunohostValidationError( "domain_dns_push_managed_in_parent_domain", @@ -656,9 +669,18 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= parent_domain=parent_domain, ) else: - raise YunohostValidationError( - "domain_registrar_is_not_configured", domain=parent_domain - ) + new_parent_domain = ".".join(parent_domain.split(".")[-3:]) + registrar, registrar_credentials = _get_registar_settings(new_parent_domain) + if registrar == "yunohost": + raise YunohostValidationError( + "domain_dns_push_managed_in_parent_domain", + domain=domain, + parent_domain=new_parent_domain, + ) + else: + raise YunohostValidationError( + "domain_registrar_is_not_configured", domain=parent_domain + ) if not all(registrar_credentials.values()): raise YunohostValidationError( diff --git a/src/domain.py b/src/domain.py index 5bdecf651..770c2931b 100644 --- a/src/domain.py +++ b/src/domain.py @@ -181,10 +181,8 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): operation_logger.start() if dyndns: - from yunohost.dyndns import dyndns_subscribe - # Actually subscribe - dyndns_subscribe(domain=domain,password=password) + domain_dyndns_subscribe(domain=domain,password=password) _certificate_install_selfsigned([domain], True) @@ -357,14 +355,37 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, pass # If a password is provided, delete the DynDNS record if password!=None: - from yunohost.dyndns import dyndns_unsubscribe - # Actually unsubscribe - dyndns_unsubscribe(domain=domain,password=password) + domain_dyndns_unsubscribe(domain=domain,password=password) logger.success(m18n.n("domain_deleted")) +def domain_dyndns_subscribe(**kwargs): + """ + Subscribe to a DynDNS domain + """ + from yunohost.dyndns import dyndns_subscribe + + dyndns_subscribe(**kwargs) + +def domain_dyndns_unsubscribe(**kwargs): + """ + Unsubscribe from a DynDNS domain + """ + from yunohost.dyndns import dyndns_unsubscribe + + dyndns_unsubscribe(**kwargs) + +def domain_dyndns_update(**kwargs): + """ + Update a DynDNS domain + """ + from yunohost.dyndns import dyndns_update + + dyndns_update(**kwargs) + + @is_unit_operation() def domain_main_domain(operation_logger, new_main_domain=None): """ @@ -572,7 +593,7 @@ def domain_dns_suggest(domain): return domain_dns_suggest(domain) -def domain_dns_push(domain, dry_run, force, purge): +def domain_dns_push(domain, dry_run=None, force=None, purge=None): from yunohost.dns import domain_dns_push - return domain_dns_push(domain, dry_run, force, purge) + return domain_dns_push(domain, dry_run=dry_run, force=force, purge=purge) diff --git a/src/dyndns.py b/src/dyndns.py index a5532f101..741bb81dc 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -176,7 +176,7 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): # Add some dyndns update in 2 and 4 minutes from now such that user should # not have to wait 10ish minutes for the conf to propagate cmd = ( - "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" + f"at -M now + {{t}} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost domain dns push {domain}'\"" ) # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) @@ -231,7 +231,7 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): # Add some dyndns update in 2 and 4 minutes from now such that user should # not have to wait 10ish minutes for the conf to propagate cmd = ( - "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" + f"at -M now + {{t}} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost domain dns push {domain}'\"" ) # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) From 11684675d7a0c10e1dbb7c36f4de45dbda997329 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 09:16:17 +0200 Subject: [PATCH 0016/1116] Fix --- share/actionsmap.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 85b240aa3..23e6094bf 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -642,25 +642,6 @@ domain: help: Only export key/values, meant to be reimported using "config set --args-file" action: store_true - ### domain_config_get() - get: - action_help: Display a domain configuration - api: GET /domains//config - arguments: - domain: - help: Domain name - key: - help: A specific panel, section or a question identifier - nargs: '?' - -f: - full: --full - help: Display all details (meant to be used by the API) - action: store_true - -e: - full: --export - help: Only export key/values, meant to be reimported using "config set --args-file" - action: store_true - ### domain_config_set() set: action_help: Apply a new configuration From 273c10f17d1fd3d405328a617c46c4102deae623 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 09:45:26 +0200 Subject: [PATCH 0017/1116] Updated locales --- locales/ca.json | 2 +- locales/de.json | 4 ++-- locales/en.json | 4 ++-- locales/es.json | 4 ++-- locales/eu.json | 4 ++-- locales/fa.json | 2 +- locales/fr.json | 4 ++-- locales/gl.json | 4 ++-- locales/it.json | 4 ++-- locales/uk.json | 4 ++-- locales/zh_Hans.json | 2 +- src/dns.py | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/locales/ca.json b/locales/ca.json index b660032d2..57ee0234c 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -531,7 +531,7 @@ "diagnosis_swap_tip": "Vigileu i tingueu en compte que els servidor està allotjant memòria d'intercanvi en una targeta SD o en l'emmagatzematge SSD, això pot reduir dràsticament l'esperança de vida del dispositiu.", "restore_already_installed_apps": "No s'han pogut restaurar les següents aplicacions perquè ja estan instal·lades: {apps}", "app_packaging_format_not_supported": "No es pot instal·lar aquesta aplicació ja que el format del paquet no és compatible amb la versió de YunoHost del sistema. Hauríeu de considerar actualitzar el sistema.", - "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost domain dns push DOMAIN --force.", "regenconf_need_to_explicitly_specify_ssh": "La configuració ssh ha estat modificada manualment, però heu d'especificar explícitament la categoria «ssh» amb --force per fer realment els canvis.", "global_settings_setting_backup_compress_tar_archives": "Comprimir els arxius (.tar.gz) en lloc d'arxius no comprimits (.tar) al crear noves còpies de seguretat. N.B.: activar aquesta opció permet fer arxius de còpia de seguretat més lleugers, però el procés inicial de còpia de seguretat serà significativament més llarg i més exigent a nivell de CPU.", "global_settings_setting_smtp_relay_host": "L'amfitrió de tramesa SMTP que s'ha d'utilitzar per enviar correus electrònics en lloc d'aquesta instància de YunoHost. És útil si esteu en una de les següents situacions: el port 25 està bloquejat per el vostre proveïdor d'accés a internet o proveïdor de servidor privat virtual, si teniu una IP residencial llistada a DUHL, si no podeu configurar el DNS invers o si el servidor no està directament exposat a internet i voleu utilitzar-ne un altre per enviar correus electrònics.", diff --git a/locales/de.json b/locales/de.json index 686eb9251..824616abd 100644 --- a/locales/de.json +++ b/locales/de.json @@ -312,7 +312,7 @@ "diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.", "diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!", "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden", - "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost dyndns update --force ein Update erzwingen.", + "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost domain dns push DOMAIN --force ein Update erzwingen.", "diagnosis_dns_point_to_doc": "Bitte schaue in der Dokumentation unter https://yunohost.org/dns_config nach, wenn du Hilfe bei der Konfiguration der DNS-Einträge benötigst.", "diagnosis_dns_discrepancy": "Der folgende DNS Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}", "diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration solltest du einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}", @@ -644,7 +644,7 @@ "log_app_config_set": "Konfiguration auf die Applikation '{}' anwenden", "log_user_import": "Konten importieren", "diagnosis_high_number_auth_failures": "In letzter Zeit gab es eine verdächtig hohe Anzahl von Authentifizierungsfehlern. Stelle sicher, dass fail2ban läuft und korrekt konfiguriert ist, oder verwende einen benutzerdefinierten Port für SSH, wie unter https://yunohost.org/security beschrieben.", - "domain_dns_registrar_yunohost": "Dies ist eine nohost.me / nohost.st / ynh.fr Domäne, ihre DNS-Konfiguration wird daher automatisch von YunoHost ohne weitere Konfiguration übernommen. (siehe Befehl 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Dies ist eine nohost.me / nohost.st / ynh.fr Domäne, ihre DNS-Konfiguration wird daher automatisch von YunoHost ohne weitere Konfiguration übernommen. (siehe Befehl 'yunohost domain dns push DOMAIN')", "domain_config_auth_entrypoint": "API-Einstiegspunkt", "domain_config_auth_application_key": "Anwendungsschlüssel", "domain_config_auth_application_secret": "Geheimer Anwendungsschlüssel", diff --git a/locales/en.json b/locales/en.json index a1a90d686..9ea928b1a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -192,7 +192,7 @@ "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}", "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help about configuring DNS records.", "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", - "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost domain dns push DOMAIN --force.", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", "diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains", "diagnosis_domain_expiration_not_found_details": "The WHOIS information for domain {domain} doesn't seem to contain the information about the expiration date?", @@ -336,7 +336,7 @@ "domain_dns_registrar_managed_in_parent_domain": "This domain is a subdomain of {parent_domain_link}. DNS registrar configuration should be managed in {parent_domain}'s configuration panel.", "domain_dns_registrar_not_supported": "YunoHost could not automatically detect the registrar handling this domain. You should manually configure your DNS records following the documentation at https://yunohost.org/dns.", "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", - "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", + "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "The domain already exists", diff --git a/locales/es.json b/locales/es.json index aebb959a8..93236189e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -491,7 +491,7 @@ "diagnosis_domain_expiration_not_found_details": "¿Parece que la información de WHOIS para el dominio {domain} no contiene información sobre la fecha de expiración?", "diagnosis_domain_not_found_details": "¡El dominio {domain} no existe en la base de datos WHOIS o ha expirado!", "diagnosis_domain_expiration_not_found": "No se pudo revisar la fecha de expiración para algunos dominios", - "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por YunoHost. Si no es el caso, puedes intentar forzar una actualización mediante yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por YunoHost. Si no es el caso, puedes intentar forzar una actualización mediante yunohost domain dns push DOMAIN --force.", "diagnosis_ip_local": "IP Local: {local}", "diagnosis_ip_no_ipv6_tip": "Tener IPv6 funcionando no es obligatorio para que su servidor funcione, pero es mejor para la salud del Internet en general. IPv6 debería ser configurado automáticamente por el sistema o su proveedor si está disponible. De otra manera, es posible que tenga que configurar varias cosas manualmente, tal y como se explica en esta documentación https://yunohost.org/#/ipv6. Si no puede habilitar IPv6 o si parece demasiado técnico, puede ignorar esta advertencia con toda seguridad.", "diagnosis_display_tip": "Para ver los problemas encontrados, puede ir a la sección de diagnóstico del webadmin, o ejecutar 'yunohost diagnosis show --issues --human-readable' en la línea de comandos.", @@ -616,7 +616,7 @@ "domain_config_auth_application_key": "LLave de Aplicación", "domain_dns_registrar_supported": "YunoHost detectó automáticamente que este dominio es manejado por el registrador **{registrar}**. Si lo desea, YunoHost configurará automáticamente esta zona DNS, si le proporciona las credenciales de API adecuadas. Puede encontrar documentación sobre cómo obtener sus credenciales de API en esta página: https://yunohost.org/registar_api_{registrar}. (También puede configurar manualmente sus registros DNS siguiendo la documentación en https://yunohost.org/dns)", "domain_dns_registrar_managed_in_parent_domain": "Este dominio es un subdominio de {parent_domain_link}. La configuración del registrador de DNS debe administrarse en el panel de configuración de {parent_domain}.", - "domain_dns_registrar_yunohost": "Este dominio es un nohost.me / nohost.st / ynh.fr y, por lo tanto, YunoHost maneja automáticamente su configuración de DNS sin ninguna configuración adicional. (vea el comando 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Este dominio es un nohost.me / nohost.st / ynh.fr y, por lo tanto, YunoHost maneja automáticamente su configuración de DNS sin ninguna configuración adicional. (vea el comando 'yunohost domain dns push DOMAIN')", "domain_dns_registrar_not_supported": "YunoHost no pudo detectar automáticamente el registrador que maneja este dominio. Debe configurar manualmente sus registros DNS siguiendo la documentación en https://yunohost.org/dns.", "global_settings_setting_security_nginx_redirect_to_https": "Redirija las solicitudes HTTP a HTTPs de forma predeterminada (¡NO LO DESACTIVE a menos que realmente sepa lo que está haciendo!)", "global_settings_setting_security_webadmin_allowlist": "Direcciones IP permitidas para acceder al webadmin. Separado por comas.", diff --git a/locales/eu.json b/locales/eu.json index e0ce226d5..60676d287 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -160,7 +160,7 @@ "certmanager_domain_not_diagnosed_yet": "Oraindik ez dago {domain} domeinurako diagnostikorik. Mesedez, berrabiarazi diagnostikoak 'DNS balioak' eta 'Web' ataletarako diagnostikoen gunean Let's Encrypt ziurtagirirako prest ote dagoen egiaztatzeko. (Edo zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztatzea desgaitzeko.)", "diagnosis_domain_expiration_warning": "Domeinu batzuk iraungitzear daude!", "app_packaging_format_not_supported": "Aplikazio hau ezin da instalatu YunoHostek ez duelako paketea ezagutzen. Sistema eguneratzea hausnartu beharko zenuke ziur asko.", - "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost dyndns update --force erabiliz.", + "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost domain dns push DOMAIN --force erabiliz.", "app_manifest_install_ask_path": "Aukeratu aplikazio hau instalatzeko URLaren bidea (domeinuaren atzeko aldean)", "app_manifest_install_ask_admin": "Aukeratu administrari bat aplikazio honetarako", "app_manifest_install_ask_password": "Aukeratu administrazio-pasahitz bat aplikazio honetarako", @@ -333,7 +333,7 @@ "domain_dns_push_not_applicable": "Ezin da {domain} domeinurako DNS konfigurazio automatiko funtzioa erabili. DNS erregistroak eskuz ezarri beharko zenituzke gidaorriei erreparatuz: https://yunohost.org/dns_config.", "domain_dns_push_managed_in_parent_domain": "DNS ezarpenak automatikoki konfiguratzeko funtzioa {parent_domain} domeinu nagusian kudeatzen da.", "domain_dns_registrar_managed_in_parent_domain": "Domeinu hau {parent_domain_link} (r)en azpidomeinua da. DNS ezarpenak {parent_domain}(r)en konfigurazio atalean kudeatu behar dira.", - "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost dyndns update' komandoa)", + "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost domain dns push DOMAIN' komandoa)", "domain_dns_registrar_not_supported": "YunoHostek ezin izan du domeinu honen erregistro-enpresa automatikoki antzeman. Eskuz konfiguratu beharko dituzu DNS ezarpenak gidalerroei erreparatuz: https://yunohost.org/dns.", "domain_dns_registrar_experimental": "Oraingoz, YunoHosten kideek ez dute **{registrar}** erregistro-enpresaren APIa nahi beste probatu eta aztertu. Funtzioa **oso esperimentala** da — kontuz!", "domain_config_mail_in": "Jasotako mezuak", diff --git a/locales/fa.json b/locales/fa.json index 599ab1ea7..d9e3f39b3 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -31,7 +31,7 @@ "diagnosis_domain_not_found_details": "دامنه {domain} در پایگاه داده WHOIS وجود ندارد یا منقضی شده است!", "diagnosis_domain_expiration_not_found": "بررسی تاریخ انقضا برخی از دامنه ها امکان پذیر نیست", "diagnosis_dns_specialusedomain": "دامنه {domain} بر اساس یک دامنه سطح بالا (TLD) مخصوص استفاده است و بنابراین انتظار نمی رود که دارای سوابق DNS واقعی باشد.", - "diagnosis_dns_try_dyndns_update_force": "پیکربندی DNS این دامنه باید به طور خودکار توسط YunoHost مدیریت شود. اگر اینطور نیست ، می توانید سعی کنید به زور یک به روز رسانی را با استفاده از yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "پیکربندی DNS این دامنه باید به طور خودکار توسط YunoHost مدیریت شود. اگر اینطور نیست ، می توانید سعی کنید به زور یک به روز رسانی را با استفاده از yunohost domain dns push DOMAIN --force.", "diagnosis_dns_point_to_doc": "لطفاً اسناد را در https://yunohost.org/dns_config برسی و مطالعه کنید، اگر در مورد پیکربندی سوابق DNS به کمک نیاز دارید.", "diagnosis_dns_discrepancy": "به نظر می رسد پرونده DNS زیر از پیکربندی توصیه شده پیروی نمی کند:
نوع: {type}
نام: {name}
ارزش فعلی: {current}
مقدار مورد انتظار: {value}", "diagnosis_dns_missing_record": "با توجه به پیکربندی DNS توصیه شده ، باید یک رکورد DNS با اطلاعات زیر اضافه کنید.
نوع: {type}
نام: {name}
ارزش: {value}", diff --git a/locales/fr.json b/locales/fr.json index 2773d0bee..6e96e500a 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -534,7 +534,7 @@ "diagnosis_swap_tip": "Merci d'être prudent et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire drastiquement l'espérance de vie du périphérique.", "restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}", "regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.", - "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost domain dns push DOMAIN --force.", "app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de YunoHost. Vous devriez probablement envisager de mettre à jour votre système.", "global_settings_setting_backup_compress_tar_archives": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar). N.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.", "diagnosis_processes_killed_by_oom_reaper": "Certains processus ont été arrêtés récemment par le système car il manquait de mémoire. Cela apparaît généralement quand le système manque de mémoire ou qu'un processus consomme trop de mémoire. Liste des processus tués :\n{kills_summary}", @@ -628,7 +628,7 @@ "service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}", "domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.", "domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.", - "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost domain dns push DOMAIN')", "domain_dns_registrar_supported": "YunoHost a détecté automatiquement que ce domaine est géré par le registrar **{registrar}**. Si vous le souhaitez, YunoHost configurera automatiquement cette zone DNS, si vous lui fournissez les identifiants API appropriés. Vous pouvez trouver de la documentation sur la façon d'obtenir vos identifiants API sur cette page : https://yunohost.org/registar_api_{registrar}. (Vous pouvez également configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns )", "domain_config_features_disclaimer": "Jusqu'à présent, l'activation/désactivation des fonctionnalités de messagerie ou XMPP n'a d'impact que sur la configuration DNS recommandée et automatique, et non sur les configurations système !", "domain_dns_push_managed_in_parent_domain": "La fonctionnalité de configuration DNS automatique est gérée dans le domaine parent {parent_domain}.", diff --git a/locales/gl.json b/locales/gl.json index 4a77645d6..8d53051f2 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -191,7 +191,7 @@ "diagnosis_domain_expiration_not_found_details": "A información WHOIS para o dominio {domain} non semella conter información acerca da data de caducidade?", "diagnosis_domain_not_found_details": "O dominio {domain} non existe na base de datos de WHOIS ou está caducado!", "diagnosis_domain_expiration_not_found": "Non se puido comprobar a data de caducidade para algúns dominios", - "diagnosis_dns_try_dyndns_update_force": "A xestión DNS deste dominio debería estar xestionada directamente por YunoHost. Se non fose o caso, podes intentar forzar unha actualización executando yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "A xestión DNS deste dominio debería estar xestionada directamente por YunoHost. Se non fose o caso, podes intentar forzar unha actualización executando yunohost domain dns push DOMAIN --force.", "diagnosis_swap_ok": "O sistema ten {total} de swap!", "diagnosis_swap_notsomuch": "O sistema só ten {total} de swap. Deberías considerar ter polo menos {recommended} para evitar situacións onde o sistema esgote a memoria.", "diagnosis_swap_none": "O sistema non ten partición swap. Deberías considerar engadir polo menos {recommended} de swap para evitar situación onde o sistema esgote a memoria.", @@ -648,7 +648,7 @@ "domain_config_auth_consumer_key": "Chave consumidora", "log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'", "other_available_options": "... e outras {n} opcións dispoñibles non mostradas", - "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost domain dns push DOMAIN')", "domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registrar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registrar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )", "domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.", "domain_config_auth_token": "Token de autenticación", diff --git a/locales/it.json b/locales/it.json index 844b756ea..27ab21473 100644 --- a/locales/it.json +++ b/locales/it.json @@ -321,7 +321,7 @@ "diagnosis_domain_expiration_not_found_details": "Le informazioni WHOIS per il dominio {domain} non sembrano contenere la data di scadenza, giusto?", "diagnosis_domain_not_found_details": "Il dominio {domain} non esiste nel database WHOIS o è scaduto!", "diagnosis_domain_expiration_not_found": "Non riesco a controllare la data di scadenza di alcuni domini", - "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da YunoHost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da YunoHost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost domain dns push DOMAIN --force.", "diagnosis_dns_point_to_doc": "Controlla la documentazione a https://yunohost.org/dns_config se hai bisogno di aiuto nel configurare i record DNS.", "diagnosis_dns_discrepancy": "Il record DNS non sembra seguire la configurazione DNS raccomandata:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", "diagnosis_dns_missing_record": "Stando alla configurazione DNS raccomandata, dovresti aggiungere un record DNS con le seguenti informazioni.
Type: {type}
Name: {name}
Value: {value}", @@ -641,7 +641,7 @@ "diagnosis_description_apps": "Applicazioni", "domain_registrar_is_not_configured": "Il registrar non è ancora configurato per il dominio {domain}.", "domain_dns_registrar_managed_in_parent_domain": "Questo dominio è un sotto-dominio di {parent_domain_link}. La configurazione del registrar DNS dovrebbe essere gestita dal pannello di configurazione di {parent_domain}.", - "domain_dns_registrar_yunohost": "Questo dominio è un nohost.me / nohost.st / ynh.fr, perciò la sua configurazione DNS è gestita automaticamente da YunoHost, senza alcuna ulteriore configurazione. (vedi il comando yunohost dyndns update)", + "domain_dns_registrar_yunohost": "Questo dominio è un nohost.me / nohost.st / ynh.fr, perciò la sua configurazione DNS è gestita automaticamente da YunoHost, senza alcuna ulteriore configurazione. (vedi il comando yunohost domain dns push DOMAIN)", "domain_dns_push_success": "Record DNS aggiornati!", "domain_dns_push_failed": "L’aggiornamento dei record DNS è miseramente fallito.", "domain_dns_push_partial_failure": "Record DNS parzialmente aggiornati: alcuni segnali/errori sono stati riportati.", diff --git a/locales/uk.json b/locales/uk.json index 9a32a597b..ba6737d7a 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -436,7 +436,7 @@ "diagnosis_domain_not_found_details": "Домен {domain} не існує в базі даних WHOIS або строк його дії сплив!", "diagnosis_domain_expiration_not_found": "Неможливо перевірити строк дії деяких доменів", "diagnosis_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) такого як .local або .test і тому не очікується, що у нього будуть актуальні записи DNS.", - "diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди yunohost domain dns push DOMAIN --force.", "diagnosis_dns_point_to_doc": "Якщо вам потрібна допомога з налаштування DNS-записів, зверніться до документації на сайті https://yunohost.org/dns_config.", "diagnosis_dns_discrepancy": "Наступний запис DNS, схоже, не відповідає рекомендованій конфігурації:
Тип: {type}
Назва: {name}
Поточне значення: {current}
Очікуване значення: {value}", "diagnosis_dns_missing_record": "Згідно рекомендованої конфігурації DNS, ви повинні додати запис DNS з наступними відомостями.
Тип: {type}
Назва: {name}
Значення: {value}", @@ -632,7 +632,7 @@ "diagnosis_http_special_use_tld": "Домен {domain} базується на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він буде відкритий за межами локальної мережі.", "domain_dns_push_managed_in_parent_domain": "Функцією автоконфігурації DNS керує батьківський домен {parent_domain}.", "domain_dns_registrar_managed_in_parent_domain": "Цей домен є піддоменом {parent_domain_link}. Конфігурацією реєстратора DNS слід керувати на панелі конфігурації {parent_domain}.", - "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost domain dns push DOMAIN')", "domain_dns_conf_special_use_tld": "Цей домен засновано на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він матиме актуальні записи DNS.", "domain_dns_registrar_supported": "YunoHost автоматично визначив, що цей домен обслуговується реєстратором **{registrar}**. Якщо ви хочете, YunoHost автоматично налаштує цю DNS-зону, якщо ви надасте йому відповідні облікові дані API. Ви можете знайти документацію про те, як отримати реєстраційні дані API на цій сторінці: https://yunohost.org/registar_api_{registrar}. (Ви також можете вручну налаштувати свої DNS-записи, дотримуючись документації на https://yunohost.org/dns)", "domain_dns_registrar_experimental": "Поки що інтерфейс з API **{registrar}** не був належним чином протестований і перевірений спільнотою YunoHost. Підтримка є **дуже експериментальною** - будьте обережні!", diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index 2daf45483..d3b12f778 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -477,7 +477,7 @@ "diagnosis_diskusage_low": "存储器{mountpoint}(在设备{device}上)只有{free} ({free_percent}%) 的空间。({free_percent}%)的剩余空间(在{total}中)。要小心。", "diagnosis_diskusage_verylow": "存储器{mountpoint}(在设备{device}上)仅剩余{free} ({free_percent}%) (剩余{total})个空间。您应该真正考虑清理一些空间!", "diagnosis_services_bad_status_tip": "你可以尝试重新启动服务,如果没有效果,可以看看webadmin中的服务日志(从命令行,你可以用yunohost service restart {service}yunohost service log {service})来做。", - "diagnosis_dns_try_dyndns_update_force": "该域的DNS配置应由YunoHost自动管理,如果不是这种情况,您可以尝试使用 yunohost dyndns update --force强制进行更新。", + "diagnosis_dns_try_dyndns_update_force": "该域的DNS配置应由YunoHost自动管理,如果不是这种情况,您可以尝试使用 yunohost domain dns push DOMAIN --force强制进行更新。", "diagnosis_dns_point_to_doc": "如果您需要有关配置DNS记录的帮助,请查看 https://yunohost.org/dns_config 上的文档。", "diagnosis_dns_discrepancy": "以下DNS记录似乎未遵循建议的配置:
类型: {type}
名称: {name}
代码> 当前值: {current}期望值: {value}", "log_backup_create": "创建备份档案", diff --git a/src/dns.py b/src/dns.py index 144f8a3a2..9cb2d4044 100644 --- a/src/dns.py +++ b/src/dns.py @@ -656,7 +656,7 @@ def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, if registrar == "yunohost": #logger.info(m18n.n("domain_dns_registrar_yunohost")) from yunohost.dyndns import dyndns_update - dyndns_update(domain=domain) + dyndns_update(domain=domain,force=force) return {} if registrar == "parent_domain": From ab37617f91d482a4ac7b75827f0e254859971a18 Mon Sep 17 00:00:00 2001 From: theo-is-taken <108329355+theo-is-taken@users.noreply.github.com> Date: Tue, 5 Jul 2022 09:54:23 +0200 Subject: [PATCH 0018/1116] Update src/dns.py Co-authored-by: ljf (zamentur) --- src/dns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dns.py b/src/dns.py index 9cb2d4044..00bb72d42 100644 --- a/src/dns.py +++ b/src/dns.py @@ -624,7 +624,8 @@ def _get_registar_settings(domain): @is_unit_operation() def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False): - if type(domains).__name__!="list": # If we provide only a domain as an argument + # If we provide only a domain as an argument + if isinstance(domains, str): domains = [domains] for domain in domains: try: From e58aaa6db607d2d51be9f842b1e8831afbf91ffc Mon Sep 17 00:00:00 2001 From: theo-is-taken <108329355+theo-is-taken@users.noreply.github.com> Date: Tue, 5 Jul 2022 09:55:19 +0200 Subject: [PATCH 0019/1116] Update src/dyndns.py Co-authored-by: ljf (zamentur) --- src/dyndns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index 741bb81dc..fd86df9ff 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -216,7 +216,8 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): try: r = requests.delete( f"https://{DYNDNS_PROVIDER}/domains/{domain}", - data={"recovery_password":hashlib.sha256((str(domain)+":"+str(password).strip()).encode('utf-8')).hexdigest()}, + secret = str(domain) + ":" + str(password).strip() + data = {"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, timeout=30, ) except Exception as e: From 0903460fc4a0d520e195ec1b43d43510b1bcba2f Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 09:59:15 +0200 Subject: [PATCH 0020/1116] Fix --- src/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index fd86df9ff..e755c803d 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -214,9 +214,9 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): # Send delete request try: + secret = str(domain) + ":" + str(password).strip() r = requests.delete( f"https://{DYNDNS_PROVIDER}/domains/{domain}", - secret = str(domain) + ":" + str(password).strip() data = {"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, timeout=30, ) From dd51adcd3f467968c2663f985832e43395d0b995 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 10:02:58 +0200 Subject: [PATCH 0021/1116] Removed useless call to dns push --- src/dyndns.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/dyndns.py b/src/dyndns.py index e755c803d..3db4b7521 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -229,15 +229,6 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): # in /etc/yunohost/dyndns regen_conf(["yunohost"]) - # Add some dyndns update in 2 and 4 minutes from now such that user should - # not have to wait 10ish minutes for the conf to propagate - cmd = ( - f"at -M now + {{t}} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost domain dns push {domain}'\"" - ) - # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... - subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) - subprocess.check_call(["bash", "-c", cmd.format(t="4 min")]) - logger.success(m18n.n("dyndns_unregistered")) elif r.status_code == 403: # Wrong password raise YunohostError("dyndns_unsubscribe_wrong_password") From ac60516638dc0fdf16cfc571ecc099a89da34797 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 10:15:56 +0200 Subject: [PATCH 0022/1116] Raise an actual error (instead of log) --- locales/en.json | 1 + src/dns.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/locales/en.json b/locales/en.json index 9ea928b1a..3c56c207b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -324,6 +324,7 @@ "domain_dns_push_already_up_to_date": "Records already up to date, nothing to do.", "domain_dns_push_failed": "Updating the DNS records failed miserably.", "domain_dns_push_failed_domain": "Updating the DNS records for {domain} failed : {error}", + "domain_dns_push_failed_domains": "Updating the DNS records for {domains} failed.", "domain_dns_push_failed_to_authenticate": "Failed to authenticate on registrar's API for domain '{domain}'. Most probably the credentials are incorrect? (Error: {error})", "domain_dns_push_failed_to_list": "Failed to list current records using the registrar's API: {error}", "domain_dns_push_managed_in_parent_domain": "The automatic DNS configuration feature is managed in the parent domain {parent_domain}.", diff --git a/src/dns.py b/src/dns.py index 00bb72d42..0a7ce7ea2 100644 --- a/src/dns.py +++ b/src/dns.py @@ -627,11 +627,15 @@ def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge # If we provide only a domain as an argument if isinstance(domains, str): domains = [domains] + error_domains = [] for domain in domains: try: domain_dns_push_unique(domain,dry_run=dry_run,force=force,purge=purge) except YunohostError as e: logger.error(m18n.n("domain_dns_push_failed_domain",domain=domain,error=str(e))) + error_domains.append(domain) + if len(error_domains)>0: + raise YunohostError("domain_dns_push_failed_domains",domains=', '.join(error_domains)) @is_unit_operation() def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, purge=False): From cf6eaf364d2deb9fb0aa81b25bafed2997ecd21a Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 10:45:21 +0200 Subject: [PATCH 0023/1116] Better password assert placement --- src/dyndns.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/dyndns.py b/src/dyndns.py index 3db4b7521..02ebe2cca 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -88,6 +88,14 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): if password is None: logger.warning(m18n.n('dyndns_no_recovery_password')) + else: + from yunohost.utils.password import assert_password_is_strong_enough + # Ensure sufficiently complex password + if Moulinette.interface.type == "cli" and password==0: + password = Moulinette.prompt( + m18n.n("ask_password"), is_password=True, confirm=True + ) + assert_password_is_strong_enough("admin", password) if _guess_current_dyndns_domain() != (None, None): raise YunohostValidationError("domain_dyndns_already_subscribed") @@ -145,13 +153,6 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): b64encoded_key = base64.b64encode(secret.encode()).decode() data = {"subdomain": domain} if password!=None: - from yunohost.utils.password import assert_password_is_strong_enough - # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and password==0: - password = Moulinette.prompt( - m18n.n("ask_password"), is_password=True, confirm=True - ) - assert_password_is_strong_enough("admin", password) data["recovery_password"]=hashlib.sha256((domain+":"+password.strip()).encode('utf-8')).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", @@ -195,17 +196,17 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): password -- Password that is used to delete the domain ( defined when subscribing ) """ - operation_logger.start() - from yunohost.utils.password import assert_password_is_strong_enough # Ensure sufficiently complex password if Moulinette.interface.type == "cli" and not password: password = Moulinette.prompt( m18n.n("ask_password"), is_password=True, confirm=True - ) + ) assert_password_is_strong_enough("admin", password) + operation_logger.start() + # '165' is the convention identifier for hmac-sha512 algorithm # '1234' is idk? doesnt matter, but the old format contained a number here... key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" From e4c631c171df131b4d7bb5efd4ae239a9696a25f Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 11:30:40 +0200 Subject: [PATCH 0024/1116] Added `yunohost domain dyndns list` command --- share/actionsmap.yml | 4 ++++ src/domain.py | 8 ++++++++ src/dyndns.py | 12 ++++++++++++ 3 files changed, 24 insertions(+) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 23e6094bf..5327edcb2 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -620,6 +620,10 @@ domain: extra: pattern: *pattern_password + ### domain_dyndns_list() + list: + action_help: List all subscribed DynDNS domains + config: subcategory_help: Domain settings actions: diff --git a/src/domain.py b/src/domain.py index 770c2931b..4203e1c0f 100644 --- a/src/domain.py +++ b/src/domain.py @@ -377,6 +377,14 @@ def domain_dyndns_unsubscribe(**kwargs): dyndns_unsubscribe(**kwargs) +def domain_dyndns_list(): + """ + Returns all currently subscribed DynDNS domains + """ + from yunohost.dyndns import dyndns_list + + return dyndns_list() + def domain_dyndns_update(**kwargs): """ Update a DynDNS domain diff --git a/src/dyndns.py b/src/dyndns.py index 02ebe2cca..1673b6d16 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -236,6 +236,18 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): elif r.status_code == 404: # Invalid domain raise YunohostError("dyndns_unsubscribe_wrong_domain") +def dyndns_list(): + """ + Returns all currently subscribed DynDNS domains ( deduced from the key files ) + """ + + files = glob.glob("/etc/yunohost/dyndns/K*key") + # Get the domain names + for i in range(len(files)): + files[i] = files[i].split(".+",1)[0] + files[i] = files[i].split("/etc/yunohost/dyndns/K")[1] + + return {"domains":files} @is_unit_operation() def dyndns_update( From 986b42fc1daa00ac79f748463703514997f6a262 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 14:37:14 +0200 Subject: [PATCH 0025/1116] `yunohost domain add` auto-detect DynDNS domains and asks for a --subscribe or --no-subscribe option --- share/actionsmap.yml | 13 +++++++------ src/domain.py | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 5327edcb2..bb9fa2ae7 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -453,15 +453,16 @@ domain: help: Domain name to add extra: pattern: *pattern_domain - -d: - full: --dyndns - help: (Deprecated, using the -p option in order to set a password is recommended) Subscribe to the DynDNS service + -n: + full: --no-subscribe + help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true - -p: - full: --password + -s: + full: --subscribe + metavar: PASSWORD nargs: "?" const: 0 - help: Subscribe to the DynDNS service with a password, used to later delete the domain + help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain extra: pattern: *pattern_password comment: dyndns_added_password diff --git a/src/domain.py b/src/domain.py index 4203e1c0f..e6a61cea0 100644 --- a/src/domain.py +++ b/src/domain.py @@ -40,6 +40,7 @@ from yunohost.app import ( from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf from yunohost.utils.config import ConfigPanel, Question from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.log import is_unit_operation logger = getActionLogger("yunohost.domain") @@ -131,7 +132,7 @@ def _get_parent_domain_of(domain): @is_unit_operation() -def domain_add(operation_logger, domain, dyndns=False,password=None): +def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): """ Create a custom domain @@ -163,10 +164,12 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): domain = domain.encode("idna").decode("utf-8") # DynDNS domain - dyndns = dyndns or (password!=None) # If a password is specified, then it is obviously a dyndns domain, no need for the extra option + dyndns = is_yunohost_dyndns_domain(domain) if dyndns: + print(subscribe,no_subscribe) + if ((subscribe==None) == (no_subscribe==False)): + raise YunohostValidationError("domain_dyndns_instruction_unclear") - from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.dyndns import _guess_current_dyndns_domain # Do not allow to subscribe to multiple dyndns domains... @@ -178,11 +181,14 @@ def domain_add(operation_logger, domain, dyndns=False,password=None): if not is_yunohost_dyndns_domain(domain): raise YunohostValidationError("domain_dyndns_root_unknown") - operation_logger.start() - if dyndns: + operation_logger.start() + if not dyndns and (subscribe is not None or no_subscribe): + logger.warning("This domain is not a DynDNS one, no need for the --subscribe or --no-subscribe option") + + if dyndns and not no_subscribe: # Actually subscribe - domain_dyndns_subscribe(domain=domain,password=password) + domain_dyndns_subscribe(domain=domain,password=subscribe) _certificate_install_selfsigned([domain], True) From 840bed5222f23dd3cc07713226282d3505047ad2 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 15:10:04 +0200 Subject: [PATCH 0026/1116] `yunohost domain remove` auto-detects DynDNS domains and now uses --unsubscribe and --no-unsubscribe --- share/actionsmap.yml | 11 ++++++++--- src/domain.py | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index bb9fa2ae7..eb24de49f 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -484,11 +484,16 @@ domain: full: --force help: Do not ask confirmation to remove apps action: store_true - -p: - full: --password + -n: + full: --no-unsubscribe + help: If removing a DynDNS domain, only remove the domain, without unsubscribing from the DynDNS service + action: store_true + -u: + full: --unsubscribe + metavar: PASSWORD nargs: "?" const: 0 - help: Password used to delete the domain from DynDNS + help: If removing a DynDNS domain, unsubscribe from the DynDNS service with a password extra: pattern: *pattern_password diff --git a/src/domain.py b/src/domain.py index e6a61cea0..9bd6a05bd 100644 --- a/src/domain.py +++ b/src/domain.py @@ -139,7 +139,8 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS - password -- Password used to later unsubscribe from DynDNS + subscribe -- Password used to later unsubscribe from DynDNS + no_unsubscribe -- If we want to just add the DynDNS domain to the list, without subscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf @@ -166,7 +167,6 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): # DynDNS domain dyndns = is_yunohost_dyndns_domain(domain) if dyndns: - print(subscribe,no_subscribe) if ((subscribe==None) == (no_subscribe==False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") @@ -238,7 +238,7 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): @is_unit_operation() -def domain_remove(operation_logger, domain, remove_apps=False, force=False, password=None): +def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsubscribe=None,no_unsubscribe=False): """ Delete domains @@ -247,7 +247,8 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, pass remove_apps -- Remove applications installed on the domain force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified - password -- Recovery password used at the creation of the DynDNS domain + unsubscribe -- Recovery password used at the creation of the DynDNS domain + no_unsubscribe -- If we just remove the DynDNS domain, without unsubscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove @@ -312,9 +313,18 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, pass "domain_uninstall_app_first", apps="\n".join([x[1] for x in apps_on_that_domain]), ) + + # DynDNS domain + dyndns = is_yunohost_dyndns_domain(domain) + if dyndns: + if ((unsubscribe==None) == (no_unsubscribe==False)): + raise YunohostValidationError("domain_dyndns_instruction_unclear") operation_logger.start() + if not dyndns and (unsubscribe!=None or no_unsubscribe!=False): + logger.warning("This domain is not a DynDNS one, no need for the --unsubscribe or --no-unsubscribe option") + ldap = _get_ldap_interface() try: ldap.remove("virtualdomain=" + domain + ",ou=domains") @@ -360,9 +370,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, pass hook_callback("post_domain_remove", args=[domain]) # If a password is provided, delete the DynDNS record - if password!=None: + if dyndns and not no_unsubscribe: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain,password=password) + domain_dyndns_unsubscribe(domain=domain,password=unsubscribe) logger.success(m18n.n("domain_deleted")) From 3bd427afab266d34c3a8b0543c6625b6f5dd40e0 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 15:14:33 +0200 Subject: [PATCH 0027/1116] Password is only asked once if we unsubscribe --- src/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index 1673b6d16..f5531d518 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -201,7 +201,7 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): # Ensure sufficiently complex password if Moulinette.interface.type == "cli" and not password: password = Moulinette.prompt( - m18n.n("ask_password"), is_password=True, confirm=True + m18n.n("ask_password"), is_password=True ) assert_password_is_strong_enough("admin", password) From 7117c61bbff87c847695275e8d6ff89f9f607f39 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 5 Jul 2022 15:25:43 +0200 Subject: [PATCH 0028/1116] Removed useless error --- locales/en.json | 1 - src/domain.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/locales/en.json b/locales/en.json index 3c56c207b..f5a38709a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -339,7 +339,6 @@ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", - "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", diff --git a/src/domain.py b/src/domain.py index 9bd6a05bd..f9597b813 100644 --- a/src/domain.py +++ b/src/domain.py @@ -176,12 +176,6 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): if _guess_current_dyndns_domain() != (None, None): raise YunohostValidationError("domain_dyndns_already_subscribed") - # Check that this domain can effectively be provided by - # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st) - if not is_yunohost_dyndns_domain(domain): - raise YunohostValidationError("domain_dyndns_root_unknown") - - operation_logger.start() if not dyndns and (subscribe is not None or no_subscribe): logger.warning("This domain is not a DynDNS one, no need for the --subscribe or --no-subscribe option") From bc3521fd0452295378177033496976f6d1813cda Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 6 Jul 2022 09:30:00 +0200 Subject: [PATCH 0029/1116] `yunohost tools postinstall` auto-detect DynDNS domains and asks for a --subscribe or --no-subscribe option --- share/actionsmap.yml | 14 ++++++++++++-- src/tools.py | 18 ++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index eb24de49f..d69a35f1f 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1574,9 +1574,19 @@ tools: pattern: *pattern_password required: True comment: good_practices_about_admin_password - --ignore-dyndns: - help: Do not subscribe domain to a DynDNS service + -n: + full: --no-subscribe + help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true + -s: + full: --subscribe + metavar: PASSWORD + nargs: "?" + const: 0 + help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain + extra: + pattern: *pattern_password + comment: dyndns_added_password --force-password: help: Use this if you really want to set a weak password action: store_true diff --git a/src/tools.py b/src/tools.py index bb7ded03a..543f835e6 100644 --- a/src/tools.py +++ b/src/tools.py @@ -186,6 +186,8 @@ def tools_postinstall( domain, password, ignore_dyndns=False, + subscribe=None, + no_subscribe=False, force_password=False, force_diskspace=False, ): @@ -230,10 +232,13 @@ def tools_postinstall( assert_password_is_strong_enough("admin", password) # If this is a nohost.me/noho.st, actually check for availability - if not ignore_dyndns and is_yunohost_dyndns_domain(domain): + if is_yunohost_dyndns_domain(domain): + if ((subscribe==None) == (no_subscribe==False)): + raise YunohostValidationError("domain_dyndns_instruction_unclear") + # Check if the domain is available... try: - available = _dyndns_available(domain) + _dyndns_available(domain) # If an exception is thrown, most likely we don't have internet # connectivity or something. Assume that this domain isn't manageable # and inform the user that we could not contact the dyndns host server. @@ -241,14 +246,7 @@ def tools_postinstall( logger.warning( m18n.n("dyndns_provider_unreachable", provider="dyndns.yunohost.org") ) - - if available: - dyndns = True - # If not, abort the postinstall - else: raise YunohostValidationError("dyndns_unavailable", domain=domain) - else: - dyndns = False if os.system("iptables -V >/dev/null 2>/dev/null") != 0: raise YunohostValidationError( @@ -260,7 +258,7 @@ def tools_postinstall( logger.info(m18n.n("yunohost_installing")) # New domain config - domain_add(domain, dyndns) + domain_add(domain, subscribe=subscribe,no_subscribe=no_subscribe) domain_main_domain(domain) # Update LDAP admin and create home dir From 01dfb778e9bd8b29b2980dcc6ac9d75ed6219dbf Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 6 Jul 2022 09:32:25 +0200 Subject: [PATCH 0030/1116] Added domain_dyndns_instruction_unclear --- locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/locales/en.json b/locales/en.json index f5a38709a..8e3d5c2b6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -339,6 +339,7 @@ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", + "domain_dyndns_instruction_unclear": "The --subscribe and --no-subscribe options are not compatible", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", From 940af74c2d58fa9a0b055c7c6cbb4dfdd4202e03 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 6 Jul 2022 15:52:27 +0200 Subject: [PATCH 0031/1116] `yunohost domain dns push` now accepts an --auto option Domains can be configured to be auto-pushed by a cron job --- hooks/conf_regen/01-yunohost | 5 +++-- locales/en.json | 2 ++ share/actionsmap.yml | 10 ++++++++-- share/config_domain.toml | 7 +++++++ src/dns.py | 9 +++++---- src/domain.py | 8 +++++--- src/dyndns.py | 4 ++++ 7 files changed, 34 insertions(+), 11 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 29da2b183..55accc4f4 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -116,8 +116,9 @@ SHELL=/bin/bash # - (sleep random 60 is here to spread requests over a 1-min window) # - if ip.yunohost.org answers ping (basic check to validate that we're connected to the internet and yunohost infra aint down) # - and if lock ain't already taken by another command -# - trigger yunohost dyndns update -*/10 * * * * root : YunoHost DynDNS update ; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || yunohost domain list --exclude-subdomains --output json | jq --raw-output '.domains[]' | grep -E "\.(noho\.st|nohost\.me|ynh\.fr)$" | xargs -I {} yunohost domain dns push "{}" >> /dev/null +# - check if some domains are flagged as autopush +# - trigger yunohost domain dns push --auto +*/10 * * * * root : YunoHost DynDNS update ; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || !(grep -nR "autopush: 1" /etc/yunohost/domains/*.yml > /dev/null) || yunohost domain dns push --auto >> /dev/null EOF else # (Delete cron if no dyndns domain found) diff --git a/locales/en.json b/locales/en.json index 8e3d5c2b6..d58790ba2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -312,6 +312,8 @@ "domain_config_auth_token": "Authentication token", "domain_config_default_app": "Default app", "domain_config_features_disclaimer": "So far, enabling/disabling mail or XMPP features only impact the recommended and automatic DNS configuration, not system configurations!", + "domain_config_autopush": "Auto-push", + "domain_config_autopush_help": "Automatically update the domain's record", "domain_config_mail_in": "Incoming emails", "domain_config_mail_out": "Outgoing emails", "domain_config_xmpp": "Instant messaging (XMPP)", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index d69a35f1f..17b4c1f96 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -443,6 +443,9 @@ domain: --exclude-subdomains: help: Filter out domains that are obviously subdomains of other declared domains action: store_true + --auto-push: + help: Only display domains that are pushed automatically + action: store_true ### domain_add() add: @@ -689,8 +692,8 @@ domain: action_help: Push DNS records to registrar api: POST /domains//dns/push arguments: - domain: - help: Domain name to push DNS conf for + domains: + help: Domain names to push DNS conf for nargs: "*" extra: pattern: *pattern_domain @@ -704,6 +707,9 @@ domain: --purge: help: Delete all records action: store_true + --auto: + help: Push only domains that should be pushed automatically + action: store_true cert: subcategory_help: Manage domain certificates diff --git a/share/config_domain.toml b/share/config_domain.toml index 65e755365..ba0706749 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -46,6 +46,13 @@ i18n = "domain_config" default = 0 [dns] + + [dns.zone] + + [dns.zone.autopush] + type = "boolean" + default = 0 + help = "" [dns.registrar] optional = true diff --git a/src/dns.py b/src/dns.py index 0a7ce7ea2..8ba46011e 100644 --- a/src/dns.py +++ b/src/dns.py @@ -623,10 +623,11 @@ def _get_registar_settings(domain): @is_unit_operation() -def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False): - # If we provide only a domain as an argument - if isinstance(domains, str): - domains = [domains] +def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False, auto=False): + if auto: + domains = domain_list(exclude_subdomains=True,auto_push=True)["domains"] + elif len(domains)==0: + domains = domain_list(exclude_subdomains=True)["domains"] error_domains = [] for domain in domains: try: diff --git a/src/domain.py b/src/domain.py index f9597b813..df40577da 100644 --- a/src/domain.py +++ b/src/domain.py @@ -52,7 +52,7 @@ DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" domain_list_cache: Dict[str, Any] = {} -def domain_list(exclude_subdomains=False): +def domain_list(exclude_subdomains=False,auto_push=False): """ List domains @@ -78,6 +78,8 @@ def domain_list(exclude_subdomains=False): parent_domain = domain.split(".", 1)[1] if parent_domain in result: continue + if auto_push and not domain_config_get(domain, key="dns.zone.autopush"): + continue result_list.append(domain) @@ -611,7 +613,7 @@ def domain_dns_suggest(domain): return domain_dns_suggest(domain) -def domain_dns_push(domain, dry_run=None, force=None, purge=None): +def domain_dns_push(domains, dry_run=None, force=None, purge=None, auto=False): from yunohost.dns import domain_dns_push - return domain_dns_push(domain, dry_run=dry_run, force=force, purge=purge) + return domain_dns_push(domains, dry_run=dry_run, force=force, purge=purge, auto=auto) diff --git a/src/dyndns.py b/src/dyndns.py index f5531d518..4ddbf7396 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -170,6 +170,10 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): error = f'Server error, code: {r.status_code}. (Message: "{r.text}")' raise YunohostError("dyndns_registration_failed", error=error) + # Set the domain's config to autopush + from yunohost.domain import domain_config_set + domain_config_set(domain,key="dns.zone.autopush",value=1) + # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns regen_conf(["yunohost"]) From 9e44b33401f83ba5d720c4e62e2bf89e254768e9 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 6 Jul 2022 15:57:17 +0200 Subject: [PATCH 0032/1116] Clarification --- src/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index 4ddbf7396..070090d7f 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -82,7 +82,7 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): Keyword argument: domain -- Full domain to subscribe with - key -- Public DNS key + key -- TSIG Shared DNS key password -- Password that will be used to delete the domain """ From 06db6f7e0430847f1bbdd4bcf9e6fd2ae4ecbfa9 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 8 Jul 2022 09:21:08 +0200 Subject: [PATCH 0033/1116] Clearer locales --- locales/en.json | 3 ++- src/domain.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index d58790ba2..b8d22bbfa 100644 --- a/locales/en.json +++ b/locales/en.json @@ -341,7 +341,8 @@ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", - "domain_dyndns_instruction_unclear": "The --subscribe and --no-subscribe options are not compatible", + "domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --no-subscribe", + "domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --no-unsubscribe", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", diff --git a/src/domain.py b/src/domain.py index df40577da..68726f4f2 100644 --- a/src/domain.py +++ b/src/domain.py @@ -314,7 +314,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu dyndns = is_yunohost_dyndns_domain(domain) if dyndns: if ((unsubscribe==None) == (no_unsubscribe==False)): - raise YunohostValidationError("domain_dyndns_instruction_unclear") + raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") operation_logger.start() From 0b5c96e2495c6155f3a12ec56a7c949435f64742 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Mon, 11 Jul 2022 15:16:33 +0200 Subject: [PATCH 0034/1116] Added an API endpoint to check if a domain is a DynDNS one --- share/actionsmap.yml | 10 ++++++++++ src/domain.py | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 17b4c1f96..fa620b0b6 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -585,6 +585,16 @@ domain: pattern: *pattern_domain path: help: The path to check (e.g. /coffee) + + ### domain_isdyndns() + isdyndns: + action_help: Check if a domain is a dyndns one + api: GET /domain//isdyndns + arguments: + domain: + help: The domain to test (e.g. your.domain.tld) + extra: + pattern: *pattern_domain subcategories: dyndns: diff --git a/src/domain.py b/src/domain.py index 68726f4f2..750224da0 100644 --- a/src/domain.py +++ b/src/domain.py @@ -458,6 +458,14 @@ def domain_url_available(domain, path): return len(_get_conflicting_apps(domain, path)) == 0 +def domain_isdyndns(domain): + """ + Returns if a domain is a DynDNS one ( used via the web API ) + + Arguments: + domain -- the domain to check + """ + return is_yunohost_dyndns_domain(domain) def _get_maindomain(): with open("/etc/yunohost/current_host", "r") as f: From ba061a49e495154b8f5386c55c766fd87f153ced Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Mon, 11 Jul 2022 15:48:30 +0200 Subject: [PATCH 0035/1116] Added a --full option to `domain list` --- share/actionsmap.yml | 13 ++++--------- src/domain.py | 16 ++++++---------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index fa620b0b6..b4b8955f4 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -446,6 +446,10 @@ domain: --auto-push: help: Only display domains that are pushed automatically action: store_true + -f: + full: --full + action: store_true + help: Display more information ### domain_add() add: @@ -586,15 +590,6 @@ domain: path: help: The path to check (e.g. /coffee) - ### domain_isdyndns() - isdyndns: - action_help: Check if a domain is a dyndns one - api: GET /domain//isdyndns - arguments: - domain: - help: The domain to test (e.g. your.domain.tld) - extra: - pattern: *pattern_domain subcategories: dyndns: diff --git a/src/domain.py b/src/domain.py index 750224da0..0378cca51 100644 --- a/src/domain.py +++ b/src/domain.py @@ -52,7 +52,7 @@ DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" domain_list_cache: Dict[str, Any] = {} -def domain_list(exclude_subdomains=False,auto_push=False): +def domain_list(exclude_subdomains=False,auto_push=False,full=False): """ List domains @@ -97,6 +97,11 @@ def domain_list(exclude_subdomains=False,auto_push=False): if exclude_subdomains: return {"domains": result_list, "main": _get_maindomain()} + if full: + for i in range(len(result_list)): + domain = result_list[i] + result_list[i] = {'name':domain,'isdyndns': is_yunohost_dyndns_domain(domain)} + domain_list_cache = {"domains": result_list, "main": _get_maindomain()} return domain_list_cache @@ -458,15 +463,6 @@ def domain_url_available(domain, path): return len(_get_conflicting_apps(domain, path)) == 0 -def domain_isdyndns(domain): - """ - Returns if a domain is a DynDNS one ( used via the web API ) - - Arguments: - domain -- the domain to check - """ - return is_yunohost_dyndns_domain(domain) - def _get_maindomain(): with open("/etc/yunohost/current_host", "r") as f: maindomain = f.readline().rstrip() From a2a1eefbed051587bb08dbfda71de7d925cf17f8 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Mon, 11 Jul 2022 17:11:49 +0200 Subject: [PATCH 0036/1116] Small fix --- share/actionsmap.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index b4b8955f4..1155e6697 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -446,8 +446,7 @@ domain: --auto-push: help: Only display domains that are pushed automatically action: store_true - -f: - full: --full + --full: action: store_true help: Display more information From f67eaef90bc3950290eb20152e8b37193e1cd41c Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Tue, 12 Jul 2022 10:45:35 +0200 Subject: [PATCH 0037/1116] Ignore cache if "full" is specified --- src/domain.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/domain.py b/src/domain.py index 0378cca51..b7ab302b8 100644 --- a/src/domain.py +++ b/src/domain.py @@ -61,7 +61,7 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): """ global domain_list_cache - if not exclude_subdomains and domain_list_cache: + if not (exclude_subdomains or full) and domain_list_cache: return domain_list_cache from yunohost.utils.ldap import _get_ldap_interface @@ -93,7 +93,6 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): result_list = sorted(result_list, key=cmp_domain) - # Don't cache answer if using exclude_subdomains if exclude_subdomains: return {"domains": result_list, "main": _get_maindomain()} @@ -102,8 +101,13 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): domain = result_list[i] result_list[i] = {'name':domain,'isdyndns': is_yunohost_dyndns_domain(domain)} - domain_list_cache = {"domains": result_list, "main": _get_maindomain()} - return domain_list_cache + result = {"domains": result_list, "main": _get_maindomain()} + + # Cache answer only if not using exclude_subdomains or full + if not (full or exclude_subdomains): + domain_list_cache = result + + return result def _assert_domain_exists(domain): From 731f07817b4835a8f2f7e983fb5d3d2321fa740f Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 13 Jul 2022 11:03:16 +0200 Subject: [PATCH 0038/1116] Redact domain passwords in logs --- src/domain.py | 6 ++++++ src/dyndns.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/domain.py b/src/domain.py index b7ab302b8..a107c7635 100644 --- a/src/domain.py +++ b/src/domain.py @@ -158,6 +158,9 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): from yunohost.utils.ldap import _get_ldap_interface from yunohost.certificate import _certificate_install_selfsigned + if subscribe!=0 and subscribe!=None: + operation_logger.data_to_redact.append(subscribe) + if domain.startswith("xmpp-upload."): raise YunohostValidationError("domain_cannot_add_xmpp_upload") @@ -258,6 +261,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface + + if unsubscribe!=0 and unsubscribe!=None: + operation_logger.data_to_redact.append(unsubscribe) # the 'force' here is related to the exception happening in domain_add ... # we don't want to check the domain exists because the ldap add may have diff --git a/src/dyndns.py b/src/dyndns.py index 070090d7f..0baa1d428 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -95,6 +95,7 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): password = Moulinette.prompt( m18n.n("ask_password"), is_password=True, confirm=True ) + operation_logger.data_to_redact.append(password) assert_password_is_strong_enough("admin", password) if _guess_current_dyndns_domain() != (None, None): @@ -207,6 +208,7 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): password = Moulinette.prompt( m18n.n("ask_password"), is_password=True ) + operation_logger.data_to_redact.append(password) assert_password_is_strong_enough("admin", password) operation_logger.start() From 0084ce757c6937449c7f8e2e177a995710089b80 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 13 Jul 2022 11:34:04 +0200 Subject: [PATCH 0039/1116] Don't ask for the (un)subscribe options if this is a sub-subdomain --- src/domain.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/domain.py b/src/domain.py index a107c7635..d6a3aa095 100644 --- a/src/domain.py +++ b/src/domain.py @@ -93,9 +93,6 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): result_list = sorted(result_list, key=cmp_domain) - if exclude_subdomains: - return {"domains": result_list, "main": _get_maindomain()} - if full: for i in range(len(result_list)): domain = result_list[i] @@ -178,8 +175,8 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): # Non-latin characters (e.g. café.com => xn--caf-dma.com) domain = domain.encode("idna").decode("utf-8") - # DynDNS domain - dyndns = is_yunohost_dyndns_domain(domain) + # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 if dyndns: if ((subscribe==None) == (no_subscribe==False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") @@ -325,8 +322,8 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu apps="\n".join([x[1] for x in apps_on_that_domain]), ) - # DynDNS domain - dyndns = is_yunohost_dyndns_domain(domain) + # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 if dyndns: if ((unsubscribe==None) == (no_unsubscribe==False)): raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") From 02103c07a33d6242d6852b5cbbffd8c962e95891 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Wed, 13 Jul 2022 11:50:20 +0200 Subject: [PATCH 0040/1116] Don't flag sub-DynDNS domains as subscribed --- src/domain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain.py b/src/domain.py index d6a3aa095..b1544d46a 100644 --- a/src/domain.py +++ b/src/domain.py @@ -96,7 +96,8 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): if full: for i in range(len(result_list)): domain = result_list[i] - result_list[i] = {'name':domain,'isdyndns': is_yunohost_dyndns_domain(domain)} + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 + result_list[i] = {'name':domain,'isdyndns': dyndns} result = {"domains": result_list, "main": _get_maindomain()} From f015767f508d9ab2ba26216cb36ffb8981c24568 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 13:21:16 +0200 Subject: [PATCH 0041/1116] Added some tests for subscribing and unsubscribing --- src/tests/test_domains.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 95a33e0ba..6aab9f241 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -1,7 +1,9 @@ import pytest import os +import random from moulinette.core import MoulinetteError +from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.domain import ( @@ -16,6 +18,8 @@ from yunohost.domain import ( ) TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"] +TEST_DYNDNS_DOMAIN = "".join(chr(random.randint(ord("a"),ord("z"))) for x in range(15))+random.choice([".noho.st",".ynh.fr",".nohost.me"]) +TEST_DYNDNS_PASSWORD = "astrongandcomplicatedpassphrasethatisverysecure" def setup_function(function): @@ -35,9 +39,9 @@ def setup_function(function): # Clear other domains for domain in domains: - if domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]: + if (domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]) and domain != TEST_DYNDNS_DOMAIN: # Clean domains not used for testing - domain_remove(domain) + domain_remove(domain,no_unsubscribe=is_yunohost_dyndns_domain(domain)) elif domain in TEST_DOMAINS: # Reset settings if any os.system(f"rm -rf {DOMAIN_SETTINGS_DIR}/{domain}.yml") @@ -67,6 +71,12 @@ def test_domain_add(): assert TEST_DOMAINS[2] in domain_list()["domains"] +def test_domain_add_subscribe(): + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + domain_add(TEST_DYNDNS_DOMAIN,subscribe=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + + def test_domain_add_existing_domain(): with pytest.raises(MoulinetteError): assert TEST_DOMAINS[1] in domain_list()["domains"] @@ -79,6 +89,12 @@ def test_domain_remove(): assert TEST_DOMAINS[1] not in domain_list()["domains"] +def test_domain_remove_unsubscribe(): + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + domain_remove(TEST_DYNDNS_DOMAIN,unsubscribe=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + + def test_main_domain(): current_main_domain = _get_maindomain() assert domain_main_domain()["current_main_domain"] == current_main_domain From 863843a1cf21919a72f286dc875930c0100bcffd Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 14:26:21 +0200 Subject: [PATCH 0042/1116] The maximum number of subscribed DynDNS domains is configurable --- src/domain.py | 4 +- src/dyndns.py | 107 ++++++++++++++++++++++---------------------------- 2 files changed, 50 insertions(+), 61 deletions(-) diff --git a/src/domain.py b/src/domain.py index b1544d46a..bc04dd523 100644 --- a/src/domain.py +++ b/src/domain.py @@ -182,10 +182,10 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): if ((subscribe==None) == (no_subscribe==False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") - from yunohost.dyndns import _guess_current_dyndns_domain + from yunohost.dyndns import is_subscribing_allowed # Do not allow to subscribe to multiple dyndns domains... - if _guess_current_dyndns_domain() != (None, None): + if not is_subscribing_allowed(): raise YunohostValidationError("domain_dyndns_already_subscribed") operation_logger.start() diff --git a/src/dyndns.py b/src/dyndns.py index 0baa1d428..a12ef3355 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -48,6 +48,16 @@ logger = getActionLogger("yunohost.dyndns") DYNDNS_PROVIDER = "dyndns.yunohost.org" DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"] +MAX_DYNDNS_DOMAINS = 1 + +def is_subscribing_allowed(): + """ + Check if the limit of subscribed DynDNS domains has been reached + + Returns: + True if the limit is not reached, False otherwise + """ + return len(glob.glob("/etc/yunohost/dyndns/*.key"))[^\s\+]+)\.\+165.+\.key$") - - # Retrieve the first registered domain - paths = list(glob.iglob("/etc/yunohost/dyndns/K*.key")) - for path in paths: - match = DYNDNS_KEY_REGEX.match(path) - if not match: - continue - _domain = match.group("domain") - - # Verify if domain is registered (i.e., if it's available, skip - # current domain beause that's not the one we want to update..) - # If there's only 1 such key found, then avoid doing the request - # for nothing (that's very probably the one we want to find ...) - if len(paths) > 1 and _dyndns_available(_domain): - continue - else: - return (_domain, path) - - return (None, None) From eb3c3916242e402e7622534530958f069541e42f Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 14:33:11 +0200 Subject: [PATCH 0043/1116] Removed useless argument --- .gitlab/ci/install.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/install.gitlab-ci.yml b/.gitlab/ci/install.gitlab-ci.yml index e2662e9e2..335e07eb6 100644 --- a/.gitlab/ci/install.gitlab-ci.yml +++ b/.gitlab/ci/install.gitlab-ci.yml @@ -26,4 +26,4 @@ install-postinstall: script: - apt-get update -o Acquire::Retries=3 - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ./$YNH_BUILD_DIR/*.deb - - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace + - yunohost tools postinstall -d domain.tld -p the_password --force-diskspace From 5f2785c6c931e4990a2859125922451f2691cc92 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 15:11:45 +0200 Subject: [PATCH 0044/1116] Pleasing the linter --- src/dns.py | 19 ++++++++--------- src/domain.py | 36 +++++++++++++++++--------------- src/dyndns.py | 43 ++++++++++++++++++++++----------------- src/tests/test_domains.py | 8 ++++---- src/tools.py | 4 ++-- src/utils/config.py | 1 + 6 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/dns.py b/src/dns.py index 8ba46011e..9b27c18af 100644 --- a/src/dns.py +++ b/src/dns.py @@ -47,7 +47,6 @@ from yunohost.utils.error import YunohostValidationError, YunohostError from yunohost.utils.network import get_public_ip from yunohost.log import is_unit_operation from yunohost.hook import hook_callback -from yunohost.dyndns import dyndns_update logger = getActionLogger("yunohost.domain") @@ -625,18 +624,19 @@ def _get_registar_settings(domain): @is_unit_operation() def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False, auto=False): if auto: - domains = domain_list(exclude_subdomains=True,auto_push=True)["domains"] - elif len(domains)==0: - domains = domain_list(exclude_subdomains=True)["domains"] + domains = domain_list(exclude_subdomains=True, auto_push=True)["domains"] + elif len(domains) == 0: + domains = domain_list(exclude_subdomains=True)["domains"] error_domains = [] for domain in domains: try: - domain_dns_push_unique(domain,dry_run=dry_run,force=force,purge=purge) + domain_dns_push_unique(domain, dry_run=dry_run, force=force, purge=purge) except YunohostError as e: - logger.error(m18n.n("domain_dns_push_failed_domain",domain=domain,error=str(e))) + logger.error(m18n.n("domain_dns_push_failed_domain", domain=domain, error=str(e))) error_domains.append(domain) - if len(error_domains)>0: - raise YunohostError("domain_dns_push_failed_domains",domains=', '.join(error_domains)) + if len(error_domains) > 0: + raise YunohostError("domain_dns_push_failed_domains", domains=', '.join(error_domains)) + @is_unit_operation() def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, purge=False): @@ -660,9 +660,8 @@ def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, # FIXME: in the future, properly unify this with yunohost dyndns update if registrar == "yunohost": - #logger.info(m18n.n("domain_dns_registrar_yunohost")) from yunohost.dyndns import dyndns_update - dyndns_update(domain=domain,force=force) + dyndns_update(domain=domain, force=force) return {} if registrar == "parent_domain": diff --git a/src/domain.py b/src/domain.py index bc04dd523..4c4ed3472 100644 --- a/src/domain.py +++ b/src/domain.py @@ -52,7 +52,7 @@ DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" domain_list_cache: Dict[str, Any] = {} -def domain_list(exclude_subdomains=False,auto_push=False,full=False): +def domain_list(exclude_subdomains=False, auto_push=False, full=False): """ List domains @@ -96,11 +96,11 @@ def domain_list(exclude_subdomains=False,auto_push=False,full=False): if full: for i in range(len(result_list)): domain = result_list[i] - dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 - result_list[i] = {'name':domain,'isdyndns': dyndns} + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 + result_list[i] = {'name': domain, 'isdyndns': dyndns} result = {"domains": result_list, "main": _get_maindomain()} - + # Cache answer only if not using exclude_subdomains or full if not (full or exclude_subdomains): domain_list_cache = result @@ -156,7 +156,7 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): from yunohost.utils.ldap import _get_ldap_interface from yunohost.certificate import _certificate_install_selfsigned - if subscribe!=0 and subscribe!=None: + if subscribe != 0 and subscribe is not None: operation_logger.data_to_redact.append(subscribe) if domain.startswith("xmpp-upload."): @@ -177,9 +177,9 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): domain = domain.encode("idna").decode("utf-8") # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) - dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((subscribe==None) == (no_subscribe==False)): + if ((subscribe is None) == (no_subscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") from yunohost.dyndns import is_subscribing_allowed @@ -194,7 +194,7 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): if dyndns and not no_subscribe: # Actually subscribe - domain_dyndns_subscribe(domain=domain,password=subscribe) + domain_dyndns_subscribe(domain=domain, password=subscribe) _certificate_install_selfsigned([domain], True) @@ -244,7 +244,7 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): @is_unit_operation() -def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsubscribe=None,no_unsubscribe=False): +def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsubscribe=None, no_unsubscribe=False): """ Delete domains @@ -259,8 +259,8 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface - - if unsubscribe!=0 and unsubscribe!=None: + + if unsubscribe != 0 and unsubscribe is not None: operation_logger.data_to_redact.append(unsubscribe) # the 'force' here is related to the exception happening in domain_add ... @@ -322,16 +322,16 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu "domain_uninstall_app_first", apps="\n".join([x[1] for x in apps_on_that_domain]), ) - + # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) - dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split("."))==3 + dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((unsubscribe==None) == (no_unsubscribe==False)): + if ((unsubscribe is None) == (no_unsubscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") operation_logger.start() - if not dyndns and (unsubscribe!=None or no_unsubscribe!=False): + if not dyndns and ((unsubscribe is not None) or (no_unsubscribe is not False)): logger.warning("This domain is not a DynDNS one, no need for the --unsubscribe or --no-unsubscribe option") ldap = _get_ldap_interface() @@ -381,7 +381,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu # If a password is provided, delete the DynDNS record if dyndns and not no_unsubscribe: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain,password=unsubscribe) + domain_dyndns_unsubscribe(domain=domain, password=unsubscribe) logger.success(m18n.n("domain_deleted")) @@ -394,6 +394,7 @@ def domain_dyndns_subscribe(**kwargs): dyndns_subscribe(**kwargs) + def domain_dyndns_unsubscribe(**kwargs): """ Unsubscribe from a DynDNS domain @@ -402,6 +403,7 @@ def domain_dyndns_unsubscribe(**kwargs): dyndns_unsubscribe(**kwargs) + def domain_dyndns_list(): """ Returns all currently subscribed DynDNS domains @@ -410,6 +412,7 @@ def domain_dyndns_list(): return dyndns_list() + def domain_dyndns_update(**kwargs): """ Update a DynDNS domain @@ -471,6 +474,7 @@ def domain_url_available(domain, path): return len(_get_conflicting_apps(domain, path)) == 0 + def _get_maindomain(): with open("/etc/yunohost/current_host", "r") as f: maindomain = f.readline().rstrip() diff --git a/src/dyndns.py b/src/dyndns.py index a12ef3355..a324b35a5 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -24,7 +24,6 @@ Subscribe and Update DynDNS Hosts """ import os -import re import json import glob import base64 @@ -50,6 +49,7 @@ DYNDNS_PROVIDER = "dyndns.yunohost.org" DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"] MAX_DYNDNS_DOMAINS = 1 + def is_subscribing_allowed(): """ Check if the limit of subscribed DynDNS domains has been reached @@ -57,7 +57,7 @@ def is_subscribing_allowed(): Returns: True if the limit is not reached, False otherwise """ - return len(glob.glob("/etc/yunohost/dyndns/*.key")) Date: Fri, 15 Jul 2022 15:29:04 +0200 Subject: [PATCH 0045/1116] Let the dynette cool down --- src/tests/test_domains.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 360a3b81f..cbdd412a7 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -1,5 +1,6 @@ import pytest import os +import time import random from moulinette.core import MoulinetteError @@ -18,7 +19,7 @@ from yunohost.domain import ( ) TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"] -TEST_DYNDNS_DOMAIN = "".join(chr(random.randint(ord("a"), ord("z"))) for x in range(15)) + random.choice([".noho.st", ".ynh.fr", ".nohost.me"]) +TEST_DYNDNS_DOMAIN = "ci-test-" + "".join(chr(random.randint(ord("a"), ord("z"))) for x in range(12)) + random.choice([".noho.st", ".ynh.fr", ".nohost.me"]) TEST_DYNDNS_PASSWORD = "astrongandcomplicatedpassphrasethatisverysecure" @@ -72,6 +73,8 @@ def test_domain_add(): def test_domain_add_subscribe(): + + time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] domain_add(TEST_DYNDNS_DOMAIN, subscribe=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] @@ -90,6 +93,8 @@ def test_domain_remove(): def test_domain_remove_unsubscribe(): + + time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] domain_remove(TEST_DYNDNS_DOMAIN, unsubscribe=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] From 4f303de7a48107b1524918f9b39227009ae5ab83 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 15:56:33 +0200 Subject: [PATCH 0046/1116] Removed the useless argument `key` from dyndns_subscribe --- share/actionsmap.yml | 6 ------ src/dyndns.py | 36 +++++++++++++++++------------------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 1155e6697..e3d315996 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -603,9 +603,6 @@ domain: help: Full domain to subscribe with extra: pattern: *pattern_domain - -k: - full: --key - help: Public DNS key -p: full: --password nargs: "?" @@ -1480,9 +1477,6 @@ dyndns: help: Full domain to subscribe with ( deprecated, use 'yunohost domain dyndns subscribe' instead ) extra: pattern: *pattern_domain - -k: - full: --key - help: Public DNS key -p: full: --password nargs: "?" diff --git a/src/dyndns.py b/src/dyndns.py index a324b35a5..6b64c1e78 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -86,13 +86,12 @@ def _dyndns_available(domain): @is_unit_operation() -def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): +def dyndns_subscribe(operation_logger, domain=None, password=None): """ Subscribe to a DynDNS service Keyword argument: domain -- Full domain to subscribe with - key -- TSIG Shared DNS key password -- Password that will be used to delete the domain """ @@ -133,29 +132,28 @@ def dyndns_subscribe(operation_logger, domain=None, key=None, password=None): # '1234' is idk? doesnt matter, but the old format contained a number here... key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" - if key is None: - if not os.path.exists("/etc/yunohost/dyndns"): - os.makedirs("/etc/yunohost/dyndns") + if not os.path.exists("/etc/yunohost/dyndns"): + os.makedirs("/etc/yunohost/dyndns") - logger.debug(m18n.n("dyndns_key_generating")) + logger.debug(m18n.n("dyndns_key_generating")) - # Here, we emulate the behavior of the old 'dnssec-keygen' utility - # which since bullseye was replaced by ddns-keygen which is now - # in the bind9 package ... but installing bind9 will conflict with dnsmasq - # and is just madness just to have access to a tsig keygen utility -.- + # Here, we emulate the behavior of the old 'dnssec-keygen' utility + # which since bullseye was replaced by ddns-keygen which is now + # in the bind9 package ... but installing bind9 will conflict with dnsmasq + # and is just madness just to have access to a tsig keygen utility -.- - # Use 512 // 8 = 64 bytes for hmac-sha512 (c.f. https://git.hactrn.net/sra/tsig-keygen/src/master/tsig-keygen.py) - secret = base64.b64encode(os.urandom(512 // 8)).decode("ascii") + # Use 512 // 8 = 64 bytes for hmac-sha512 (c.f. https://git.hactrn.net/sra/tsig-keygen/src/master/tsig-keygen.py) + secret = base64.b64encode(os.urandom(512 // 8)).decode("ascii") - # Idk why but the secret is split in two parts, with the first one - # being 57-long char ... probably some DNS format - secret = f"{secret[:56]} {secret[56:]}" + # Idk why but the secret is split in two parts, with the first one + # being 57-long char ... probably some DNS format + secret = f"{secret[:56]} {secret[56:]}" - key_content = f"{domain}. IN KEY 0 3 165 {secret}" - write_to_file(key_file, key_content) + key_content = f"{domain}. IN KEY 0 3 165 {secret}" + write_to_file(key_file, key_content) - chmod("/etc/yunohost/dyndns", 0o600, recursive=True) - chown("/etc/yunohost/dyndns", "root", recursive=True) + chmod("/etc/yunohost/dyndns", 0o600, recursive=True) + chown("/etc/yunohost/dyndns", "root", recursive=True) import requests # lazy loading this module for performance reasons From bbc6dcc50b5101d8fdab04c91f38a18b4dde9966 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 16:30:57 +0200 Subject: [PATCH 0047/1116] Better logging for `domain dns push --auto` --- src/dns.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dns.py b/src/dns.py index 9b27c18af..44547d412 100644 --- a/src/dns.py +++ b/src/dns.py @@ -621,8 +621,7 @@ def _get_registar_settings(domain): return registrar, settings -@is_unit_operation() -def domain_dns_push(operation_logger, domains, dry_run=False, force=False, purge=False, auto=False): +def domain_dns_push(domains, dry_run=False, force=False, purge=False, auto=False): if auto: domains = domain_list(exclude_subdomains=True, auto_push=True)["domains"] elif len(domains) == 0: From e21c114b70202ab518f3b57c49a7a024e6d16961 Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 16:54:53 +0200 Subject: [PATCH 0048/1116] Better log redacting --- src/domain.py | 4 ++-- src/dyndns.py | 2 -- src/tools.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/domain.py b/src/domain.py index 4c4ed3472..f49f96e46 100644 --- a/src/domain.py +++ b/src/domain.py @@ -140,7 +140,7 @@ def _get_parent_domain_of(domain): return _get_parent_domain_of(parent_domain) -@is_unit_operation() +@is_unit_operation(exclude=["subscribe"]) def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): """ Create a custom domain @@ -243,7 +243,7 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): logger.success(m18n.n("domain_created")) -@is_unit_operation() +@is_unit_operation(exclude=["unsubscribe"]) def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsubscribe=None, no_unsubscribe=False): """ Delete domains diff --git a/src/dyndns.py b/src/dyndns.py index 6b64c1e78..9fd25442c 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -106,7 +106,6 @@ def dyndns_subscribe(operation_logger, domain=None, password=None): is_password=True, confirm=True ) - operation_logger.data_to_redact.append(password) assert_password_is_strong_enough("admin", password) if not is_subscribing_allowed(): @@ -218,7 +217,6 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): m18n.n("ask_password"), is_password=True ) - operation_logger.data_to_redact.append(password) assert_password_is_strong_enough("admin", password) operation_logger.start() diff --git a/src/tools.py b/src/tools.py index b77279208..032bbea9f 100644 --- a/src/tools.py +++ b/src/tools.py @@ -180,12 +180,11 @@ def _detect_virt(): return out.split()[0] -@is_unit_operation() +@is_unit_operation(exclude=["subscribe","password"]) def tools_postinstall( operation_logger, domain, password, - ignore_dyndns=False, subscribe=None, no_subscribe=False, force_password=False, From 129f5cce9537fc9cbbbdd804091f1cdb662983ff Mon Sep 17 00:00:00 2001 From: "theo@manjaro" Date: Fri, 15 Jul 2022 16:57:12 +0200 Subject: [PATCH 0049/1116] Linter fixes --- src/tests/test_domains.py | 6 +++--- src/tools.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index cbdd412a7..272f5bb4d 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -73,8 +73,8 @@ def test_domain_add(): def test_domain_add_subscribe(): - - time.sleep(35) # Dynette blocks requests that happen too frequently + + time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] domain_add(TEST_DYNDNS_DOMAIN, subscribe=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] @@ -94,7 +94,7 @@ def test_domain_remove(): def test_domain_remove_unsubscribe(): - time.sleep(35) # Dynette blocks requests that happen too frequently + time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] domain_remove(TEST_DYNDNS_DOMAIN, unsubscribe=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] diff --git a/src/tools.py b/src/tools.py index 032bbea9f..6dcc262ad 100644 --- a/src/tools.py +++ b/src/tools.py @@ -180,7 +180,7 @@ def _detect_virt(): return out.split()[0] -@is_unit_operation(exclude=["subscribe","password"]) +@is_unit_operation(exclude=["subscribe", "password"]) def tools_postinstall( operation_logger, domain, From dc5fbd5555aa1e59e97a228385da28fcab639996 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 18 Nov 2022 22:56:10 +0100 Subject: [PATCH 0050/1116] Fix OCSP stapling ... but using Google resolver :| --- conf/nginx/server.tpl.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index d5b1d3bef..183cce8b8 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -54,7 +54,7 @@ server { ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - resolver 127.0.0.1 127.0.1.1 valid=300s; + resolver 8.8.8.8 valid=300s; resolver_timeout 5s; {% endif %} @@ -110,7 +110,7 @@ server { ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - resolver 127.0.0.1 127.0.1.1 valid=300s; + resolver 8.8.8.8 valid=300s; resolver_timeout 5s; {% endif %} From a21567b27dda8b6b11f2fa683f41221c65b83a0e Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 8 Jan 2023 00:35:34 +0100 Subject: [PATCH 0051/1116] [enh] Semantic --- locales/en.json | 1 + share/actionsmap.yml | 9 +++----- src/domain.py | 48 ++++++++++++++++++++++++--------------- src/tests/test_domains.py | 2 +- src/tools.py | 8 +++---- 5 files changed, 39 insertions(+), 29 deletions(-) diff --git a/locales/en.json b/locales/en.json index b8d22bbfa..432462708 100644 --- a/locales/en.json +++ b/locales/en.json @@ -73,6 +73,7 @@ "ask_new_domain": "New domain", "ask_new_path": "New path", "ask_password": "Password", + "ask_dyndns_recovery_password": "DynDNS recovey password", "ask_user_domain": "Domain to use for the user's email address and XMPP account", "backup_abstract_method": "This backup method has yet to be implemented", "backup_actually_backuping": "Creating a backup archive from the collected files...", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index e3d315996..e437a812b 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -463,8 +463,7 @@ domain: full: --no-subscribe help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true - -s: - full: --subscribe + --dyndns-password-recovery: metavar: PASSWORD nargs: "?" const: 0 @@ -494,8 +493,7 @@ domain: full: --no-unsubscribe help: If removing a DynDNS domain, only remove the domain, without unsubscribing from the DynDNS service action: store_true - -u: - full: --unsubscribe + --dyndns-password-recovery: metavar: PASSWORD nargs: "?" const: 0 @@ -1582,8 +1580,7 @@ tools: full: --no-subscribe help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true - -s: - full: --subscribe + --dyndns-password-recovery: metavar: PASSWORD nargs: "?" const: 0 diff --git a/src/domain.py b/src/domain.py index f49f96e46..f6b99b5cb 100644 --- a/src/domain.py +++ b/src/domain.py @@ -140,24 +140,25 @@ def _get_parent_domain_of(domain): return _get_parent_domain_of(parent_domain) -@is_unit_operation(exclude=["subscribe"]) -def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): +@is_unit_operation(exclude=["dyndns_password_recovery"]) +def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subscribe=False): """ Create a custom domain Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS - subscribe -- Password used to later unsubscribe from DynDNS + dyndns_password_recovery -- Password used to later unsubscribe from DynDNS no_unsubscribe -- If we want to just add the DynDNS domain to the list, without subscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf from yunohost.utils.ldap import _get_ldap_interface + from yunohost.utils.password import assert_password_is_strong_enough from yunohost.certificate import _certificate_install_selfsigned - if subscribe != 0 and subscribe is not None: - operation_logger.data_to_redact.append(subscribe) + if dyndns_password_recovery != 0 and dyndns_password_recovery is not None: + operation_logger.data_to_redact.append(dyndns_password_recovery) if domain.startswith("xmpp-upload."): raise YunohostValidationError("domain_cannot_add_xmpp_upload") @@ -179,7 +180,18 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((subscribe is None) == (no_subscribe is False)): + if not no_subscribe and not dyndns_password_recovery: + if Moulinette.interface.type == "api": + raise YunohostValidationError("domain_dyndns_missing_password") + else: + dyndns_password_recovery = Moulinette.prompt( + m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True + ) + + # Ensure sufficiently complex password + assert_password_is_strong_enough("admin", dyndns_password_recovery) + + if ((dyndns_password_recovery is None) == (no_subscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") from yunohost.dyndns import is_subscribing_allowed @@ -189,12 +201,12 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): raise YunohostValidationError("domain_dyndns_already_subscribed") operation_logger.start() - if not dyndns and (subscribe is not None or no_subscribe): - logger.warning("This domain is not a DynDNS one, no need for the --subscribe or --no-subscribe option") + if not dyndns and (dyndns_password_recovery is not None or no_subscribe): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns-password-recovery or --no-subscribe option") if dyndns and not no_subscribe: # Actually subscribe - domain_dyndns_subscribe(domain=domain, password=subscribe) + domain_dyndns_subscribe(domain=domain, password=dyndns_password_recovery) _certificate_install_selfsigned([domain], True) @@ -243,8 +255,8 @@ def domain_add(operation_logger, domain, subscribe=None, no_subscribe=False): logger.success(m18n.n("domain_created")) -@is_unit_operation(exclude=["unsubscribe"]) -def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsubscribe=None, no_unsubscribe=False): +@is_unit_operation(exclude=["dyndns_password_recovery"]) +def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_password_recovery=None, no_unsubscribe=False): """ Delete domains @@ -253,15 +265,15 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu remove_apps -- Remove applications installed on the domain force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified - unsubscribe -- Recovery password used at the creation of the DynDNS domain + dyndns_password_recovery -- Recovery password used at the creation of the DynDNS domain no_unsubscribe -- If we just remove the DynDNS domain, without unsubscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface - if unsubscribe != 0 and unsubscribe is not None: - operation_logger.data_to_redact.append(unsubscribe) + if dyndns_password_recovery != 0 and dyndns_password_recovery is not None: + operation_logger.data_to_redact.append(dyndns_password_recovery) # the 'force' here is related to the exception happening in domain_add ... # we don't want to check the domain exists because the ldap add may have @@ -326,13 +338,13 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((unsubscribe is None) == (no_unsubscribe is False)): + if ((dyndns_password_recovery is None) == (no_unsubscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") operation_logger.start() - if not dyndns and ((unsubscribe is not None) or (no_unsubscribe is not False)): - logger.warning("This domain is not a DynDNS one, no need for the --unsubscribe or --no-unsubscribe option") + if not dyndns and ((dyndns_password_recovery is not None) or (no_unsubscribe is not False)): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns_password_recovery or --no-unsubscribe option") ldap = _get_ldap_interface() try: @@ -381,7 +393,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, unsu # If a password is provided, delete the DynDNS record if dyndns and not no_unsubscribe: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain, password=unsubscribe) + domain_dyndns_unsubscribe(domain=domain, password=dyndns_password_recovery) logger.success(m18n.n("domain_deleted")) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 272f5bb4d..e09c3534b 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -76,7 +76,7 @@ def test_domain_add_subscribe(): time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] - domain_add(TEST_DYNDNS_DOMAIN, subscribe=TEST_DYNDNS_PASSWORD) + domain_add(TEST_DYNDNS_DOMAIN, dyndns_password_recovery=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] diff --git a/src/tools.py b/src/tools.py index 6dcc262ad..21262c64b 100644 --- a/src/tools.py +++ b/src/tools.py @@ -180,12 +180,12 @@ def _detect_virt(): return out.split()[0] -@is_unit_operation(exclude=["subscribe", "password"]) +@is_unit_operation(exclude=["dyndns_password_recovery", "password"]) def tools_postinstall( operation_logger, domain, password, - subscribe=None, + dyndns_password_recovery=None, no_subscribe=False, force_password=False, force_diskspace=False, @@ -232,7 +232,7 @@ def tools_postinstall( # If this is a nohost.me/noho.st, actually check for availability if is_yunohost_dyndns_domain(domain): - if ((subscribe is None) == (no_subscribe is False)): + if ((dyndns_password_recovery is None) == (no_subscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") # Check if the domain is available... @@ -257,7 +257,7 @@ def tools_postinstall( logger.info(m18n.n("yunohost_installing")) # New domain config - domain_add(domain, subscribe=subscribe, no_subscribe=no_subscribe) + domain_add(domain, dyndns_password_recovery=dyndns_password_recovery, no_subscribe=no_subscribe) domain_main_domain(domain) # Update LDAP admin and create home dir From 1e0fe76672e9d72ee1e60e5c58f8cf2e8c7810b5 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 10 Jan 2023 10:25:30 +0100 Subject: [PATCH 0052/1116] [fix] Test --- src/tests/test_domains.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index e09c3534b..b221d688e 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -96,7 +96,7 @@ def test_domain_remove_unsubscribe(): time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] - domain_remove(TEST_DYNDNS_DOMAIN, unsubscribe=TEST_DYNDNS_PASSWORD) + domain_remove(TEST_DYNDNS_DOMAIN, dyndns_password_recovery=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] From f0751aff17c65d4240591c6c4c658aee994208ff Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 27 Sep 2019 22:55:12 +0200 Subject: [PATCH 0053/1116] Allow system users to auth on the mail stack and send emails --- conf/dovecot/dovecot.conf | 12 ++++++++++++ conf/postfix/main.cf | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index e614c3796..e4ada80ab 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -38,16 +38,28 @@ ssl_prefer_server_ciphers = no ############################################################################### +# Regular Yunohost accounts passdb { args = /etc/dovecot/dovecot-ldap.conf driver = ldap } +# Internally, allow authentication from system user +# who might want to send emails (e.g. from apps) +passdb { + override_fields = allow_nets=127.0.0.1/32,::1/64 + driver = pam +} + userdb { args = /etc/dovecot/dovecot-ldap.conf driver = ldap } +userdb { + driver = passwd +} + protocol imap { imap_client_workarounds = mail_plugins = $mail_plugins imap_quota antispam diff --git a/conf/postfix/main.cf b/conf/postfix/main.cf index 19b40aefb..4d02fc5c0 100644 --- a/conf/postfix/main.cf +++ b/conf/postfix/main.cf @@ -107,7 +107,10 @@ virtual_alias_domains = virtual_minimum_uid = 100 virtual_uid_maps = static:vmail virtual_gid_maps = static:mail -smtpd_sender_login_maps= ldap:/etc/postfix/ldap-accounts.cf +smtpd_sender_login_maps= + ldap:/etc/postfix/ldap-accounts.cf, # Regular Yunohost accounts + hash:/etc/postfix/sender_login_maps # Extra maps for system users who need to send emails + # Dovecot LDA virtual_transport = dovecot From c48d9ec483241dc45aa3f2888c76226f55c8005f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 28 Feb 2023 17:56:49 +0100 Subject: [PATCH 0054/1116] appsv2/mail: add new 'allow_email' flag on app system users that will autogenerate a passwd-like file to be used by dovecot + map for postfix --- conf/dovecot/dovecot.conf | 12 +++++----- conf/postfix/main.cf | 6 +++-- hooks/conf_regen/19-postfix | 2 ++ hooks/conf_regen/25-dovecot | 2 ++ src/app.py | 45 +++++++++++++++++++++++++++++++++++++ src/utils/resources.py | 31 +++++++++++++++++++++++-- 6 files changed, 88 insertions(+), 10 deletions(-) diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index e4ada80ab..152f4c01c 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -44,20 +44,20 @@ passdb { driver = ldap } -# Internally, allow authentication from system user -# who might want to send emails (e.g. from apps) +# Internally, allow authentication from apps system user who have "enable_email = true" passdb { - override_fields = allow_nets=127.0.0.1/32,::1/64 - driver = pam + driver = passwd-file + args = /etc/dovecot/app-senders-passwd } userdb { - args = /etc/dovecot/dovecot-ldap.conf driver = ldap + args = /etc/dovecot/dovecot-ldap.conf } userdb { - driver = passwd + driver = passwd-file + args = /etc/dovecot/app-senders-passwd } protocol imap { diff --git a/conf/postfix/main.cf b/conf/postfix/main.cf index 4d02fc5c0..e30ca0874 100644 --- a/conf/postfix/main.cf +++ b/conf/postfix/main.cf @@ -108,8 +108,10 @@ virtual_minimum_uid = 100 virtual_uid_maps = static:vmail virtual_gid_maps = static:mail smtpd_sender_login_maps= - ldap:/etc/postfix/ldap-accounts.cf, # Regular Yunohost accounts - hash:/etc/postfix/sender_login_maps # Extra maps for system users who need to send emails + # Regular Yunohost accounts + ldap:/etc/postfix/ldap-accounts.cf, + # Extra maps for app system users who need to send emails + hash:/etc/postfix/app_senders_login_maps # Dovecot LDA diff --git a/hooks/conf_regen/19-postfix b/hooks/conf_regen/19-postfix index 3a2aead5d..d6ddcb5ee 100755 --- a/hooks/conf_regen/19-postfix +++ b/hooks/conf_regen/19-postfix @@ -80,6 +80,8 @@ do_post_regen() { postmap -F hash:/etc/postfix/sni + python3 -c 'from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix as r; r(only="postfix")' + [[ -z "$regen_conf_files" ]] \ || { systemctl restart postfix && systemctl restart postsrsd; } diff --git a/hooks/conf_regen/25-dovecot b/hooks/conf_regen/25-dovecot index 49ff0c9ba..54b4e5d37 100755 --- a/hooks/conf_regen/25-dovecot +++ b/hooks/conf_regen/25-dovecot @@ -53,6 +53,8 @@ do_post_regen() { chown root:mail /var/mail chmod 1775 /var/mail + python3 -c 'from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix as r; r(only="dovecot")' + [ -z "$regen_conf_files" ] && exit 0 # compile sieve script diff --git a/src/app.py b/src/app.py index 17ebe96ca..88d79e750 100644 --- a/src/app.py +++ b/src/app.py @@ -1582,6 +1582,9 @@ def app_setting(app, key, value=None, delete=False): if delete: if key in app_settings: del app_settings[key] + else: + # Don't call _set_app_settings to avoid unecessary writes... + return # SET else: @@ -3148,3 +3151,45 @@ def _ask_confirmation( if not answer: raise YunohostError("aborting") + + +def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): + + dovecot = True if only in [None, "dovecot"] else False + postfix = True if only in [None, "postfix"] else False + + from yunohost.user import _hash_user_password + + postfix_map = [] + dovecot_passwd = [] + for app in _installed_apps(): + + settings = _get_app_settings(app) + + if "domain" not in settings or "mail_pwd" not in settings: + continue + + if dovecot: + hashed_password = _hash_user_password(settings["mail_pwd"]) + dovecot_passwd.append(f"{app}:{hashed_password}::::::allow_nets=127.0.0.1/24") + if postfix: + postfix_map.append(f"{app}@{settings['domain']} {app}") + + if dovecot: + app_senders_passwd = "/etc/dovecot/app-senders-passwd" + content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!" + content += '\n' + '\n'.join(dovecot_passwd) + write_to_file(app_senders_passwd, content) + chmod(app_senders_passwd, 0o440) + chown(app_senders_passwd, "root", "dovecot") + + if postfix: + app_senders_map = "/etc/postfix/app_senders_login_maps" + content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!" + content += '\n' + '\n'.join(postfix_map) + write_to_file(app_senders_map, content) + chmod(app_senders_map, 0o440) + chown(app_senders_map, "postfix", "root") + os.system(f"postmap {app_senders_map} 2>/dev/null") + chmod(app_senders_map + ".db", 0o640) + chown(app_senders_map + ".db", "postfix", "root") diff --git a/src/utils/resources.py b/src/utils/resources.py index cff6c6b19..fe8e33b47 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -24,6 +24,7 @@ import tempfile from typing import Dict, Any, List from moulinette import m18n +from moulinette.utils.text import random_ascii from moulinette.utils.process import check_output from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import mkdir, chown, chmod, write_to_file @@ -473,6 +474,7 @@ class SystemuserAppResource(AppResource): default_properties: Dict[str, Any] = { "allow_ssh": False, "allow_sftp": False, + "allow_email": False, "home": "/var/www/__APP__", } @@ -480,9 +482,13 @@ class SystemuserAppResource(AppResource): allow_ssh: bool = False allow_sftp: bool = False + allow_email: bool = False home: str = "" def provision_or_update(self, context: Dict = {}): + + from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix + # FIXME : validate that no yunohost user exists with that name? # and/or that no system user exists during install ? @@ -527,7 +533,25 @@ class SystemuserAppResource(AppResource): f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd" ) + # Update mail-related stuff + if self.allow_email: + mail_pwd = self.get_setting("mail_pwd") + if not mail_pwd: + mail_pwd = random_ascii(24) + self.set_setting("mail_pwd", mail_pwd) + + regen_mail_app_user_config_for_dovecot_and_postfix() + else: + self.delete_setting("mail_pwd") + if os.system(f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps") == 0 \ + or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") == 0: + regen_mail_app_user_config_for_dovecot_and_postfix() + + def deprovision(self, context: Dict = {}): + + from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix + if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: os.system(f"deluser {self.app} >/dev/null") if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: @@ -542,6 +566,11 @@ class SystemuserAppResource(AppResource): f"Failed to delete system user for {self.app}", raw_msg=True ) + self.delete_setting("mail_pwd") + if os.system(f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps") == 0 \ + or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") == 0: + regen_mail_app_user_config_for_dovecot_and_postfix() + # FIXME : better logging and error handling, add stdout/stderr from the deluser/delgroup commands... @@ -1060,8 +1089,6 @@ class DatabaseAppResource(AppResource): self.set_setting("db_pwd", db_pwd) if not db_pwd: - from moulinette.utils.text import random_ascii - db_pwd = random_ascii(24) self.set_setting("db_pwd", db_pwd) From 8188c2816759bec61b24c5807272640e54fdd68b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 28 Feb 2023 18:10:27 +0100 Subject: [PATCH 0055/1116] appsv2: add documentation for previously introduced allow_email flag --- src/utils/resources.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/resources.py b/src/utils/resources.py index fe8e33b47..4e1907ab4 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -451,6 +451,7 @@ class SystemuserAppResource(AppResource): ##### Properties: - `allow_ssh`: (default: False) Adds the user to the ssh.app group, allowing SSH connection via this user - `allow_sftp`: (default: False) Adds the user to the sftp.app group, allowing SFTP connection via this user + - `allow_email`: (default: False) Enable authentication on the mail stack for the system user and send mail using `__APP__@__DOMAIN__`. A `mail_pwd` setting is automatically defined (similar to `db_pwd` for databases). You can then configure the app to use `__APP__` and `__MAIL_PWD__` as SMTP credentials (with host 127.0.0.1) - `home`: (default: `/var/www/__APP__`) Defines the home property for this user. NB: unfortunately you can't simply use `__INSTALL_DIR__` or `__DATA_DIR__` for now ##### Provision/Update: From 22681a4f241c074feaa628b5c8d28716d65812e9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 28 Feb 2023 18:50:31 +0100 Subject: [PATCH 0056/1116] Kill the old 'unprotected/protected/skipped' setting hell --- helpers/setting | 18 ++------ src/app.py | 115 ------------------------------------------------ 2 files changed, 3 insertions(+), 130 deletions(-) diff --git a/helpers/setting b/helpers/setting index a2cf3a93d..a89f72091 100644 --- a/helpers/setting +++ b/helpers/setting @@ -18,11 +18,7 @@ ynh_app_setting_get() { ynh_handle_getopts_args "$@" app="${app:-$_globalapp}" - if [[ $key =~ (unprotected|protected|skipped)_ ]]; then - yunohost app setting $app $key - else - ynh_app_setting "get" "$app" "$key" - fi + ynh_app_setting "get" "$app" "$key" } # Set an application setting @@ -45,11 +41,7 @@ ynh_app_setting_set() { ynh_handle_getopts_args "$@" app="${app:-$_globalapp}" - if [[ $key =~ (unprotected|protected|skipped)_ ]]; then - yunohost app setting $app $key -v $value - else - ynh_app_setting "set" "$app" "$key" "$value" - fi + ynh_app_setting "set" "$app" "$key" "$value" } # Delete an application setting @@ -70,11 +62,7 @@ ynh_app_setting_delete() { ynh_handle_getopts_args "$@" app="${app:-$_globalapp}" - if [[ "$key" =~ (unprotected|skipped|protected)_ ]]; then - yunohost app setting $app $key -d - else - ynh_app_setting "delete" "$app" "$key" - fi + ynh_app_setting "delete" "$app" "$key" } # Small "hard-coded" interface to avoid calling "yunohost app" directly each diff --git a/src/app.py b/src/app.py index f17c46929..65402a024 100644 --- a/src/app.py +++ b/src/app.py @@ -1492,119 +1492,6 @@ def app_setting(app, key, value=None, delete=False): """ app_settings = _get_app_settings(app) or {} - # - # Legacy permission setting management - # (unprotected, protected, skipped_uri/regex) - # - - is_legacy_permission_setting = any( - key.startswith(word + "_") for word in ["unprotected", "protected", "skipped"] - ) - - if is_legacy_permission_setting: - from yunohost.permission import ( - user_permission_list, - user_permission_update, - permission_create, - permission_delete, - permission_url, - ) - - permissions = user_permission_list(full=True, apps=[app])["permissions"] - key_ = key.split("_")[0] - permission_name = f"{app}.legacy_{key_}_uris" - permission = permissions.get(permission_name) - - # GET - if value is None and not delete: - return ( - ",".join(permission.get("uris", []) + permission["additional_urls"]) - if permission - else None - ) - - # DELETE - if delete: - # If 'is_public' setting still exists, we interpret this as - # coming from a legacy app (because new apps shouldn't manage the - # is_public state themselves anymore...) - # - # In that case, we interpret the request for "deleting - # unprotected/skipped" setting as willing to make the app - # private - if ( - "is_public" in app_settings - and "visitors" in permissions[app + ".main"]["allowed"] - ): - if key.startswith("unprotected_") or key.startswith("skipped_"): - user_permission_update(app + ".main", remove="visitors") - - if permission: - permission_delete(permission_name) - - # SET - else: - urls = value - # If the request is about the root of the app (/), ( = the vast majority of cases) - # we interpret this as a change for the main permission - # (i.e. allowing/disallowing visitors) - if urls == "/": - if key.startswith("unprotected_") or key.startswith("skipped_"): - permission_url(app + ".main", url="/", sync_perm=False) - user_permission_update(app + ".main", add="visitors") - else: - user_permission_update(app + ".main", remove="visitors") - else: - urls = urls.split(",") - if key.endswith("_regex"): - urls = ["re:" + url for url in urls] - - if permission: - # In case of new regex, save the urls, to add a new time in the additional_urls - # In case of new urls, we do the same thing but inversed - if key.endswith("_regex"): - # List of urls to save - current_urls_or_regex = [ - url - for url in permission["additional_urls"] - if not url.startswith("re:") - ] - else: - # List of regex to save - current_urls_or_regex = [ - url - for url in permission["additional_urls"] - if url.startswith("re:") - ] - - new_urls = urls + current_urls_or_regex - # We need to clear urls because in the old setting the new setting override the old one and dont just add some urls - permission_url(permission_name, clear_urls=True, sync_perm=False) - permission_url(permission_name, add_url=new_urls) - else: - from yunohost.utils.legacy import legacy_permission_label - - # Let's create a "special" permission for the legacy settings - permission_create( - permission=permission_name, - # FIXME find a way to limit to only the user allowed to the main permission - allowed=["all_users"] - if key.startswith("protected_") - else ["all_users", "visitors"], - url=None, - additional_urls=urls, - auth_header=not key.startswith("skipped_"), - label=legacy_permission_label(app, key.split("_")[0]), - show_tile=False, - protected=True, - ) - - return - - # - # Regular setting management - # - # GET if value is None and not delete: return app_settings.get(key, None) @@ -1616,8 +1503,6 @@ def app_setting(app, key, value=None, delete=False): # SET else: - if key in ["redirected_urls", "redirected_regex"]: - value = yaml.safe_load(value) app_settings[key] = value _set_app_settings(app, app_settings) From 04eadd715c8723e43113461b92c7493a76760e02 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 28 Feb 2023 18:55:47 +0100 Subject: [PATCH 0057/1116] Kill old --package option and .ini for PHP FPM config --- helpers/php | 71 ++--------------------------------------------------- 1 file changed, 2 insertions(+), 69 deletions(-) diff --git a/helpers/php b/helpers/php index 417dbbc61..1b28b32f7 100644 --- a/helpers/php +++ b/helpers/php @@ -15,7 +15,7 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} # # ----------------------------------------------------------------------------- # -# usage 2: ynh_add_fpm_config [--phpversion=7.X] --usage=usage --footprint=footprint [--package=packages] [--dedicated_service] +# usage 2: ynh_add_fpm_config [--phpversion=7.X] --usage=usage --footprint=footprint [--dedicated_service] # | arg: -v, --phpversion= - Version of PHP to use. # | arg: -f, --footprint= - Memory footprint of the service (low/medium/high). # low - Less than 20 MB of RAM by pool. @@ -30,7 +30,6 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} # medium - Low usage, few people or/and publicly accessible. # high - High usage, frequently visited website. # -# | arg: -p, --package= - Additionnal PHP packages to install for a specific version of PHP # | arg: -d, --dedicated_service - Use a dedicated PHP-FPM service instead of the common one. # # @@ -60,16 +59,14 @@ ynh_add_fpm_config() { local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. local legacy_args=vtufpd - local -A args_array=([v]=phpversion= [t]=use_template [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service) + local -A args_array=([v]=phpversion= [t]=use_template [u]=usage= [f]=footprint= [d]=dedicated_service) local phpversion local use_template local usage local footprint - local package local dedicated_service # Manage arguments with getopts ynh_handle_getopts_args "$@" - package=${package:-} # The default behaviour is to use the template. use_template="${use_template:-1}" @@ -103,13 +100,6 @@ ynh_add_fpm_config() { fi fi - # Legacy args (packager should just list their php dependency as regular apt dependencies... - if [ -n "$package" ]; then - # Install the additionnal packages from the default repository - ynh_print_warn --message "Argument --package of ynh_add_fpm_config is deprecated and to be removed in the future" - ynh_install_app_dependencies "$package" - fi - if [ $dedicated_service -eq 1 ]; then local fpm_service="${app}-phpfpm" local fpm_config_dir="/etc/php/$phpversion/dedicated-fpm" @@ -197,11 +187,6 @@ pm.process_idle_timeout = 10s local finalphpconf="$fpm_config_dir/pool.d/$app.conf" ynh_add_config --template="$phpfpm_path" --destination="$finalphpconf" - 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="php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini" - fi - if [ $dedicated_service -eq 1 ]; then # Create a dedicated php-fpm.conf for the service local globalphpconf=$fpm_config_dir/php-fpm-$app.conf @@ -272,9 +257,6 @@ ynh_remove_fpm_config() { fi ynh_secure_remove --file="$fpm_config_dir/pool.d/$app.conf" - if [ -e $fpm_config_dir/conf.d/20-$app.ini ]; then - ynh_secure_remove --file="$fpm_config_dir/conf.d/20-$app.ini" - fi if [ $dedicated_service -eq 1 ]; then # Remove the dedicated service PHP-FPM service for the app @@ -286,55 +268,6 @@ ynh_remove_fpm_config() { elif ynh_package_is_installed --package="php${phpversion}-fpm"; then ynh_systemd_action --service_name=$fpm_service --action=reload fi - - # If the PHP version used is not the default version for YunoHost - # The second part with YNH_APP_PURGE is an ugly hack to guess that we're inside the remove script - # (we don't actually care about its value, we just check its not empty hence it exists) - if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] && [ -n "${YNH_APP_PURGE:-}" ] && dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then - # Remove app dependencies ... but ideally should happen via an explicit call from packager - ynh_remove_app_dependencies - fi -} - -# Install another version of PHP. -# -# [internal] -# -# Legacy, to be remove on bullseye -# -# usage: ynh_install_php --phpversion=phpversion [--package=packages] -# | arg: -v, --phpversion= - Version of PHP to install. -# | arg: -p, --package= - Additionnal PHP packages to install -# -# Requires YunoHost version 3.8.1 or higher. -ynh_install_php() { - # Declare an array to define the options of this helper. - local legacy_args=vp - local -A args_array=([v]=phpversion= [p]=package=) - local phpversion - local package - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - package=${package:-} - - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ]; then - ynh_die --message="Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION" - fi - - ynh_install_app_dependencies "$package" -} - -# Remove the specific version of PHP used by the app. -# -# [internal] -# -# Legacy, to be remove on bullseye -# -# usage: ynh_remove_php -# -# Requires YunoHost version 3.8.1 or higher. -ynh_remove_php () { - ynh_remove_app_dependencies } # Define the values to configure PHP-FPM From d67e23167897ca3307e2dad89dda86fc824a1514 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 14:48:44 +0200 Subject: [PATCH 0058/1116] dydns-password-recovery -> dyndns-recovery-password --- share/actionsmap.yml | 6 +++--- src/domain.py | 42 +++++++++++++++++++-------------------- src/tests/test_domains.py | 4 ++-- src/tools.py | 8 ++++---- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 6e60655d0..294d00881 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -510,7 +510,7 @@ domain: full: --no-subscribe help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true - --dyndns-password-recovery: + --dyndns-recovery-password: metavar: PASSWORD nargs: "?" const: 0 @@ -540,7 +540,7 @@ domain: full: --no-unsubscribe help: If removing a DynDNS domain, only remove the domain, without unsubscribing from the DynDNS service action: store_true - --dyndns-password-recovery: + --dyndns-recovery-password: metavar: PASSWORD nargs: "?" const: 0 @@ -1692,7 +1692,7 @@ tools: full: --no-subscribe help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true - --dyndns-password-recovery: + --dyndns-recovery-password: metavar: PASSWORD nargs: "?" const: 0 diff --git a/src/domain.py b/src/domain.py index 67f39aa71..4301f9ab1 100644 --- a/src/domain.py +++ b/src/domain.py @@ -213,15 +213,15 @@ def _get_parent_domain_of(domain, return_self=False, topest=False): return domain if return_self else None -@is_unit_operation(exclude=["dyndns_password_recovery"]) -def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subscribe=False): +@is_unit_operation(exclude=["dyndns_recovery_password"]) +def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subscribe=False): """ Create a custom domain Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS - dyndns_password_recovery -- Password used to later unsubscribe from DynDNS + dyndns_recovery_password -- Password used to later unsubscribe from DynDNS no_unsubscribe -- If we want to just add the DynDNS domain to the list, without subscribing """ from yunohost.hook import hook_callback @@ -230,8 +230,8 @@ def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subsc from yunohost.utils.password import assert_password_is_strong_enough from yunohost.certificate import _certificate_install_selfsigned - if dyndns_password_recovery != 0 and dyndns_password_recovery is not None: - operation_logger.data_to_redact.append(dyndns_password_recovery) + if dyndns_recovery_password != 0 and dyndns_recovery_password is not None: + operation_logger.data_to_redact.append(dyndns_recovery_password) if domain.startswith("xmpp-upload."): raise YunohostValidationError("domain_cannot_add_xmpp_upload") @@ -256,18 +256,18 @@ def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subsc # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if not no_subscribe and not dyndns_password_recovery: + if not no_subscribe and not dyndns_recovery_password: if Moulinette.interface.type == "api": raise YunohostValidationError("domain_dyndns_missing_password") else: - dyndns_password_recovery = Moulinette.prompt( + dyndns_recovery_password = Moulinette.prompt( m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True ) # Ensure sufficiently complex password - assert_password_is_strong_enough("admin", dyndns_password_recovery) + assert_password_is_strong_enough("admin", dyndns_recovery_password) - if ((dyndns_password_recovery is None) == (no_subscribe is False)): + if ((dyndns_recovery_password is None) == (no_subscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") from yunohost.dyndns import is_subscribing_allowed @@ -277,12 +277,12 @@ def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subsc raise YunohostValidationError("domain_dyndns_already_subscribed") operation_logger.start() - if not dyndns and (dyndns_password_recovery is not None or no_subscribe): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns-password-recovery or --no-subscribe option") + if not dyndns and (dyndns_recovery_password is not None or no_subscribe): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns-recovery-password or --no-subscribe option") if dyndns and not no_subscribe: # Actually subscribe - domain_dyndns_subscribe(domain=domain, password=dyndns_password_recovery) + domain_dyndns_subscribe(domain=domain, password=dyndns_recovery_password) _certificate_install_selfsigned([domain], True) @@ -331,8 +331,8 @@ def domain_add(operation_logger, domain, dyndns_password_recovery=None, no_subsc logger.success(m18n.n("domain_created")) -@is_unit_operation(exclude=["dyndns_password_recovery"]) -def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_password_recovery=None, no_unsubscribe=False): +@is_unit_operation(exclude=["dyndns_recovery_password"]) +def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_recovery_password=None, no_unsubscribe=False): """ Delete domains @@ -341,15 +341,15 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd remove_apps -- Remove applications installed on the domain force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified - dyndns_password_recovery -- Recovery password used at the creation of the DynDNS domain + dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain no_unsubscribe -- If we just remove the DynDNS domain, without unsubscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface - if dyndns_password_recovery != 0 and dyndns_password_recovery is not None: - operation_logger.data_to_redact.append(dyndns_password_recovery) + if dyndns_recovery_password != 0 and dyndns_recovery_password is not None: + operation_logger.data_to_redact.append(dyndns_recovery_password) # the 'force' here is related to the exception happening in domain_add ... # we don't want to check the domain exists because the ldap add may have @@ -414,13 +414,13 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((dyndns_password_recovery is None) == (no_unsubscribe is False)): + if ((dyndns_recovery_password is None) == (no_unsubscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") operation_logger.start() - if not dyndns and ((dyndns_password_recovery is not None) or (no_unsubscribe is not False)): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns_password_recovery or --no-unsubscribe option") + if not dyndns and ((dyndns_recovery_password is not None) or (no_unsubscribe is not False)): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns_recovery_password or --no-unsubscribe option") ldap = _get_ldap_interface() try: @@ -469,7 +469,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd # If a password is provided, delete the DynDNS record if dyndns and not no_unsubscribe: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain, password=dyndns_password_recovery) + domain_dyndns_unsubscribe(domain=domain, password=dyndns_recovery_password) logger.success(m18n.n("domain_deleted")) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index b221d688e..43c04bee6 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -76,7 +76,7 @@ def test_domain_add_subscribe(): time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] - domain_add(TEST_DYNDNS_DOMAIN, dyndns_password_recovery=TEST_DYNDNS_PASSWORD) + domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] @@ -96,7 +96,7 @@ def test_domain_remove_unsubscribe(): time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] - domain_remove(TEST_DYNDNS_DOMAIN, dyndns_password_recovery=TEST_DYNDNS_PASSWORD) + domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] diff --git a/src/tools.py b/src/tools.py index 02b4f58d1..512986ff9 100644 --- a/src/tools.py +++ b/src/tools.py @@ -144,14 +144,14 @@ def _set_hostname(hostname, pretty_hostname=None): logger.debug(out) -@is_unit_operation(exclude=["dyndns_password_recovery", "password"]) +@is_unit_operation(exclude=["dyndns_recovery_password", "password"]) def tools_postinstall( operation_logger, domain, username, fullname, password, - dyndns_password_recovery=None, + dyndns_recovery_password=None, no_subscribe=False, force_diskspace=False, ): @@ -194,7 +194,7 @@ def tools_postinstall( # If this is a nohost.me/noho.st, actually check for availability if is_yunohost_dyndns_domain(domain): - if ((dyndns_password_recovery is None) == (no_subscribe is False)): + if ((dyndns_recovery_password is None) == (no_subscribe is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") # Check if the domain is available... @@ -219,7 +219,7 @@ def tools_postinstall( logger.info(m18n.n("yunohost_installing")) # New domain config - domain_add(domain, dyndns_password_recovery=dyndns_password_recovery, no_subscribe=no_subscribe) + domain_add(domain, dyndns_recovery_password=dyndns_recovery_password, no_subscribe=no_subscribe) domain_main_domain(domain) user_create(username, domain, password, admin=True, fullname=fullname) From 81360723cc6284fa61e3668a844d412346322c23 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 15:17:41 +0200 Subject: [PATCH 0059/1116] dyndns: Misc semantic tweaks... --- locales/en.json | 4 ++-- share/actionsmap.yml | 13 +++++-------- src/domain.py | 26 +++++++++++++------------- src/dyndns.py | 26 +++++++++++++------------- src/tools.py | 7 ++++--- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/locales/en.json b/locales/en.json index 21ffdfdc2..8c6636322 100644 --- a/locales/en.json +++ b/locales/en.json @@ -375,8 +375,8 @@ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", - "domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --no-subscribe", - "domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --no-unsubscribe", + "domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --ignore-dyndns", + "domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --ignore-dyndns", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 294d00881..3124f3105 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -506,8 +506,7 @@ domain: help: Domain name to add extra: pattern: *pattern_domain - -n: - full: --no-subscribe + --ignore-dyndns: help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true --dyndns-recovery-password: @@ -536,8 +535,7 @@ domain: full: --force help: Do not ask confirmation to remove apps action: store_true - -n: - full: --no-unsubscribe + --ignore-dyndns: help: If removing a DynDNS domain, only remove the domain, without unsubscribing from the DynDNS service action: store_true --dyndns-recovery-password: @@ -662,7 +660,7 @@ domain: extra: pattern: *pattern_domain -p: - full: --password + full: --recovery-password nargs: "?" const: 0 help: Password used to later delete the domain @@ -681,7 +679,7 @@ domain: pattern: *pattern_domain required: True -p: - full: --password + full: --recovery-password nargs: "?" const: 0 help: Password used to delete the domain @@ -1688,8 +1686,7 @@ tools: pattern: *pattern_password required: True comment: good_practices_about_admin_password - -n: - full: --no-subscribe + --ingnore-dyndns: help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true --dyndns-recovery-password: diff --git a/src/domain.py b/src/domain.py index 4301f9ab1..020a707c7 100644 --- a/src/domain.py +++ b/src/domain.py @@ -214,7 +214,7 @@ def _get_parent_domain_of(domain, return_self=False, topest=False): @is_unit_operation(exclude=["dyndns_recovery_password"]) -def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subscribe=False): +def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_dyndns=False): """ Create a custom domain @@ -222,7 +222,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subsc domain -- Domain name to add dyndns -- Subscribe to DynDNS dyndns_recovery_password -- Password used to later unsubscribe from DynDNS - no_unsubscribe -- If we want to just add the DynDNS domain to the list, without subscribing + ignore_dyndns -- If we want to just add the DynDNS domain to the list, without subscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf @@ -256,7 +256,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subsc # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if not no_subscribe and not dyndns_recovery_password: + if not ignore_dyndns and not dyndns_recovery_password: if Moulinette.interface.type == "api": raise YunohostValidationError("domain_dyndns_missing_password") else: @@ -267,7 +267,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subsc # Ensure sufficiently complex password assert_password_is_strong_enough("admin", dyndns_recovery_password) - if ((dyndns_recovery_password is None) == (no_subscribe is False)): + if ((dyndns_recovery_password is None) == (ignore_dyndns is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear") from yunohost.dyndns import is_subscribing_allowed @@ -277,10 +277,10 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subsc raise YunohostValidationError("domain_dyndns_already_subscribed") operation_logger.start() - if not dyndns and (dyndns_recovery_password is not None or no_subscribe): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns-recovery-password or --no-subscribe option") + if not dyndns and (dyndns_recovery_password is not None or ignore_dyndns): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns-recovery-password or --ignore-dyndns option") - if dyndns and not no_subscribe: + if dyndns and not ignore_dyndns: # Actually subscribe domain_dyndns_subscribe(domain=domain, password=dyndns_recovery_password) @@ -332,7 +332,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, no_subsc @is_unit_operation(exclude=["dyndns_recovery_password"]) -def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_recovery_password=None, no_unsubscribe=False): +def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_recovery_password=None, ignore_dyndns=False): """ Delete domains @@ -342,7 +342,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain - no_unsubscribe -- If we just remove the DynDNS domain, without unsubscribing + ignore_dyndns -- If we just remove the DynDNS domain, without unsubscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove @@ -414,13 +414,13 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if ((dyndns_recovery_password is None) == (no_unsubscribe is False)): + if ((dyndns_recovery_password is None) == (ignore_dyndns is False)): raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") operation_logger.start() - if not dyndns and ((dyndns_recovery_password is not None) or (no_unsubscribe is not False)): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns_recovery_password or --no-unsubscribe option") + if not dyndns and ((dyndns_recovery_password is not None) or (ignore_dyndns is not False)): + logger.warning("This domain is not a DynDNS one, no need for the --dyndns_recovery_password or --ignore-dyndns option") ldap = _get_ldap_interface() try: @@ -467,7 +467,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd hook_callback("post_domain_remove", args=[domain]) # If a password is provided, delete the DynDNS record - if dyndns and not no_unsubscribe: + if dyndns and not ignore_dyndns: # Actually unsubscribe domain_dyndns_unsubscribe(domain=domain, password=dyndns_recovery_password) diff --git a/src/dyndns.py b/src/dyndns.py index d1049e756..caeef9459 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -79,27 +79,27 @@ def _dyndns_available(domain): @is_unit_operation() -def dyndns_subscribe(operation_logger, domain=None, password=None): +def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): """ Subscribe to a DynDNS service Keyword argument: domain -- Full domain to subscribe with - password -- Password that will be used to delete the domain + recovery_password -- Password that will be used to delete the domain """ - if password is None: + if recovery_password is None: logger.warning(m18n.n('dyndns_no_recovery_password')) else: from yunohost.utils.password import assert_password_is_strong_enough # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and password == 0: - password = Moulinette.prompt( + if Moulinette.interface.type == "cli" and recovery_password == 0: + recovery_password = Moulinette.prompt( m18n.n("ask_password"), is_password=True, confirm=True ) - assert_password_is_strong_enough("admin", password) + assert_password_is_strong_enough("admin", recovery_password) if not is_subscribing_allowed(): raise YunohostValidationError("domain_dyndns_already_subscribed") @@ -155,7 +155,7 @@ def dyndns_subscribe(operation_logger, domain=None, password=None): b64encoded_key = base64.b64encode(secret.encode()).decode() data = {"subdomain": domain} if password is not None: - data["recovery_password"] = hashlib.sha256((domain + ":" + password.strip()).encode('utf-8')).hexdigest() + data["recovery_password"] = hashlib.sha256((domain + ":" + recovery_password.strip()).encode('utf-8')).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", data=data, @@ -193,24 +193,24 @@ def dyndns_subscribe(operation_logger, domain=None, password=None): @is_unit_operation() -def dyndns_unsubscribe(operation_logger, domain, password=None): +def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): """ Unsubscribe from a DynDNS service Keyword argument: domain -- Full domain to unsubscribe with - password -- Password that is used to delete the domain ( defined when subscribing ) + recovery_password -- Password that is used to delete the domain ( defined when subscribing ) """ from yunohost.utils.password import assert_password_is_strong_enough # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and not password: - password = Moulinette.prompt( + if Moulinette.interface.type == "cli" and not recovery_password: + recovery_password = Moulinette.prompt( m18n.n("ask_password"), is_password=True ) - assert_password_is_strong_enough("admin", password) + assert_password_is_strong_enough("admin", recovery_password) operation_logger.start() @@ -222,7 +222,7 @@ def dyndns_unsubscribe(operation_logger, domain, password=None): # Send delete request try: - secret = str(domain) + ":" + str(password).strip() + secret = str(domain) + ":" + str(recovery_password).strip() r = requests.delete( f"https://{DYNDNS_PROVIDER}/domains/{domain}", data={"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, diff --git a/src/tools.py b/src/tools.py index 512986ff9..c0da7a37b 100644 --- a/src/tools.py +++ b/src/tools.py @@ -152,7 +152,7 @@ def tools_postinstall( fullname, password, dyndns_recovery_password=None, - no_subscribe=False, + ignore_dyndns=False, force_diskspace=False, ): @@ -194,7 +194,8 @@ def tools_postinstall( # If this is a nohost.me/noho.st, actually check for availability if is_yunohost_dyndns_domain(domain): - if ((dyndns_recovery_password is None) == (no_subscribe is False)): + + if (bool(dyndns_recovery_password), ignore_dyndns) in [(True, True), (False, False)]: raise YunohostValidationError("domain_dyndns_instruction_unclear") # Check if the domain is available... @@ -219,7 +220,7 @@ def tools_postinstall( logger.info(m18n.n("yunohost_installing")) # New domain config - domain_add(domain, dyndns_recovery_password=dyndns_recovery_password, no_subscribe=no_subscribe) + domain_add(domain, dyndns_recovery_password=dyndns_recovery_password, ignore_dyndns=ignore_dyndns) domain_main_domain(domain) user_create(username, domain, password, admin=True, fullname=fullname) From 789b1b2af93e330e7a3f0bed51cc98979f46869d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 15:49:52 +0200 Subject: [PATCH 0060/1116] dyndns: revert changes regarding auto-push ... it's not complete, and the PR about dyndns recovery password is already too complex... --- hooks/conf_regen/01-yunohost | 5 ++--- locales/ca.json | 2 +- locales/de.json | 4 ++-- locales/en.json | 8 ++------ locales/es.json | 4 ++-- locales/eu.json | 4 ++-- locales/fa.json | 2 +- locales/fr.json | 4 ++-- locales/gl.json | 4 ++-- locales/it.json | 4 ++-- locales/uk.json | 4 ++-- locales/zh_Hans.json | 2 +- share/actionsmap.yml | 29 +++-------------------------- share/config_domain.toml | 7 ------- src/dns.py | 18 +----------------- src/domain.py | 5 ++--- src/dyndns.py | 6 +----- src/tools.py | 10 +++++----- 18 files changed, 33 insertions(+), 89 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index a3fd13687..51022a4e5 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -116,9 +116,8 @@ SHELL=/bin/bash # - (sleep random 60 is here to spread requests over a 1-min window) # - if ip.yunohost.org answers ping (basic check to validate that we're connected to the internet and yunohost infra aint down) # - and if lock ain't already taken by another command -# - check if some domains are flagged as autopush -# - trigger yunohost domain dns push --auto -*/10 * * * * root : YunoHost DynDNS update ; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || !(grep -nR "autopush: 1" /etc/yunohost/domains/*.yml > /dev/null) || yunohost domain dns push --auto >> /dev/null +# - trigger yunohost dyndns update +*/10 * * * * root : YunoHost DynDNS update; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ip.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || yunohost dyndns update >> /dev/null EOF else # (Delete cron if no dyndns domain found) diff --git a/locales/ca.json b/locales/ca.json index 808354264..106d0af89 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -506,7 +506,7 @@ "diagnosis_swap_tip": "Vigileu i tingueu en compte que els servidor està allotjant memòria d'intercanvi en una targeta SD o en l'emmagatzematge SSD, això pot reduir dràsticament l'esperança de vida del dispositiu.", "restore_already_installed_apps": "No s'han pogut restaurar les següents aplicacions perquè ja estan instal·lades: {apps}", "app_packaging_format_not_supported": "No es pot instal·lar aquesta aplicació ja que el format del paquet no és compatible amb la versió de YunoHost del sistema. Hauríeu de considerar actualitzar el sistema.", - "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost dyndns update --force.", "regenconf_need_to_explicitly_specify_ssh": "La configuració ssh ha estat modificada manualment, però heu d'especificar explícitament la categoria «ssh» amb --force per fer realment els canvis.", "unknown_main_domain_path": "Domini o ruta desconeguda per a «{app}». Heu d'especificar un domini i una ruta per a poder especificar una URL per al permís.", "show_tile_cant_be_enabled_for_regex": "No podeu activar «show_title» ara, perquè la URL per al permís «{permission}» és una expressió regular", diff --git a/locales/de.json b/locales/de.json index 4a1db2961..5baa41687 100644 --- a/locales/de.json +++ b/locales/de.json @@ -290,7 +290,7 @@ "diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.", "diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!", "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden", - "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost domain dns push DOMAIN --force ein Update erzwingen.", + "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost dyndns update --force ein Update erzwingen.", "diagnosis_dns_point_to_doc": "Bitte schauen Sie in der Dokumentation unter https://yunohost.org/dns_config nach, wenn Sie Hilfe bei der Konfiguration der DNS-Einträge benötigen.", "diagnosis_dns_discrepancy": "Der folgende DNS Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}", "diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration solltest du einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}", @@ -612,7 +612,7 @@ "log_app_config_set": "Konfiguration auf die Applikation '{}' anwenden", "log_user_import": "Konten importieren", "diagnosis_high_number_auth_failures": "In letzter Zeit gab es eine verdächtig hohe Anzahl von Authentifizierungsfehlern. Stelle sicher, dass fail2ban läuft und korrekt konfiguriert ist, oder verwende einen benutzerdefinierten Port für SSH, wie unter https://yunohost.org/security beschrieben.", - "domain_dns_registrar_yunohost": "Dies ist eine nohost.me / nohost.st / ynh.fr Domäne, ihre DNS-Konfiguration wird daher automatisch von YunoHost ohne weitere Konfiguration übernommen. (siehe Befehl 'yunohost domain dns push DOMAIN')", + "domain_dns_registrar_yunohost": "Dies ist eine nohost.me / nohost.st / ynh.fr Domäne, ihre DNS-Konfiguration wird daher automatisch von YunoHost ohne weitere Konfiguration übernommen. (siehe Befehl 'yunohost dyndns update')", "domain_config_auth_entrypoint": "API-Einstiegspunkt", "domain_config_auth_application_key": "Anwendungsschlüssel", "domain_config_auth_application_secret": "Geheimer Anwendungsschlüssel", diff --git a/locales/en.json b/locales/en.json index 8c6636322..6065c75d8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -208,7 +208,7 @@ "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}", "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help configuring DNS records.", "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", - "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force.", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", "diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains", "diagnosis_domain_expiration_not_found_details": "The WHOIS information for domain {domain} doesn't seem to contain the information about the expiration date?", @@ -333,8 +333,6 @@ "domain_config_auth_key": "Authentication key", "domain_config_auth_secret": "Authentication secret", "domain_config_auth_token": "Authentication token", - "domain_config_autopush": "Auto-push", - "domain_config_autopush_help": "Automatically update the domain's record", "domain_config_cert_install": "Install Let's Encrypt certificate", "domain_config_cert_issuer": "Certification authority", "domain_config_cert_no_checks": "Ignore diagnosis checks", @@ -359,8 +357,6 @@ "domain_dns_conf_special_use_tld": "This domain is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", "domain_dns_push_already_up_to_date": "Records already up to date, nothing to do.", "domain_dns_push_failed": "Updating the DNS records failed miserably.", - "domain_dns_push_failed_domain": "Updating the DNS records for {domain} failed : {error}", - "domain_dns_push_failed_domains": "Updating the DNS records for {domains} failed.", "domain_dns_push_failed_to_authenticate": "Failed to authenticate on registrar's API for domain '{domain}'. Most probably the credentials are incorrect? (Error: {error})", "domain_dns_push_failed_to_list": "Failed to list current records using the registrar's API: {error}", "domain_dns_push_managed_in_parent_domain": "The automatic DNS configuration feature is managed in the parent domain {parent_domain}.", @@ -373,7 +369,7 @@ "domain_dns_registrar_managed_in_parent_domain": "This domain is a subdomain of {parent_domain_link}. DNS registrar configuration should be managed in {parent_domain}'s configuration panel.", "domain_dns_registrar_not_supported": "YunoHost could not automatically detect the registrar handling this domain. You should manually configure your DNS records following the documentation at https://yunohost.org/dns.", "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", - "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost domain dns push DOMAIN' command)", + "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", "domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --ignore-dyndns", "domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --ignore-dyndns", diff --git a/locales/es.json b/locales/es.json index bfb111f26..8637c3da8 100644 --- a/locales/es.json +++ b/locales/es.json @@ -467,7 +467,7 @@ "diagnosis_domain_expiration_not_found_details": "¿Parece que la información de WHOIS para el dominio {domain} no contiene información sobre la fecha de expiración?", "diagnosis_domain_not_found_details": "¡El dominio {domain} no existe en la base de datos WHOIS o ha expirado!", "diagnosis_domain_expiration_not_found": "No se pudo revisar la fecha de expiración para algunos dominios", - "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por YunoHost. Si no es el caso, puedes intentar forzar una actualización mediante yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por YunoHost. Si no es el caso, puedes intentar forzar una actualización mediante yunohost dyndns update --force.", "diagnosis_ip_local": "IP Local: {local}", "diagnosis_ip_no_ipv6_tip": "Tener IPv6 funcionando no es obligatorio para que su servidor funcione, pero es mejor para la salud del Internet en general. IPv6 debería ser configurado automáticamente por el sistema o su proveedor si está disponible. De otra manera, es posible que tenga que configurar varias cosas manualmente, tal y como se explica en esta documentación https://yunohost.org/#/ipv6. Si no puede habilitar IPv6 o si parece demasiado técnico, puede ignorar esta advertencia con toda seguridad.", "diagnosis_display_tip": "Para ver los problemas encontrados, puede ir a la sección de diagnóstico del webadmin, o ejecutar 'yunohost diagnosis show --issues --human-readable' en la línea de comandos.", @@ -589,7 +589,7 @@ "domain_config_auth_application_key": "LLave de Aplicación", "domain_dns_registrar_supported": "YunoHost detectó automáticamente que este dominio es manejado por el registrador **{registrar}**. Si lo desea, YunoHost configurará automáticamente esta zona DNS, si le proporciona las credenciales de API adecuadas. Puede encontrar documentación sobre cómo obtener sus credenciales de API en esta página: https://yunohost.org/registar_api_{registrar}. (También puede configurar manualmente sus registros DNS siguiendo la documentación en https://yunohost.org/dns)", "domain_dns_registrar_managed_in_parent_domain": "Este dominio es un subdominio de {parent_domain_link}. La configuración del registrador de DNS debe administrarse en el panel de configuración de {parent_domain}.", - "domain_dns_registrar_yunohost": "Este dominio es un nohost.me / nohost.st / ynh.fr y, por lo tanto, YunoHost maneja automáticamente su configuración de DNS sin ninguna configuración adicional. (vea el comando 'yunohost domain dns push DOMAIN')", + "domain_dns_registrar_yunohost": "Este dominio es un nohost.me / nohost.st / ynh.fr y, por lo tanto, YunoHost maneja automáticamente su configuración de DNS sin ninguna configuración adicional. (vea el comando 'yunohost dyndns update')", "domain_dns_registrar_not_supported": "YunoHost no pudo detectar automáticamente el registrador que maneja este dominio. Debe configurar manualmente sus registros DNS siguiendo la documentación en https://yunohost.org/dns.", "migration_ldap_backup_before_migration": "Creación de una copia de seguridad de la base de datos LDAP y la configuración de las aplicaciones antes de la migración real.", "invalid_number": "Debe ser un miembro", diff --git a/locales/eu.json b/locales/eu.json index 47ff773da..d58289bf4 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -154,7 +154,7 @@ "certmanager_domain_not_diagnosed_yet": "Oraindik ez dago {domain} domeinurako diagnostikorik. Berrabiarazi diagnostikoak 'DNS balioak' eta 'Web' ataletarako diagnostikoen gunean Let's Encrypt ziurtagirirako prest ote dagoen egiaztatzeko. (Edo zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztatzea desgaitzeko.)", "diagnosis_domain_expiration_warning": "Domeinu batzuk iraungitzear daude!", "app_packaging_format_not_supported": "Aplikazio hau ezin da instalatu YunoHostek ez duelako paketea ezagutzen. Sistema eguneratzea hausnartu beharko zenuke ziur asko.", - "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost domain dns push DOMAIN --force erabiliz.", + "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost dyndns update --force erabiliz.", "app_manifest_install_ask_path": "Aukeratu aplikazio hau instalatzeko URLaren bidea (domeinuaren atzeko aldean)", "app_manifest_install_ask_admin": "Aukeratu administrari bat aplikazio honetarako", "app_manifest_install_ask_password": "Aukeratu administrazio-pasahitz bat aplikazio honetarako", @@ -316,7 +316,7 @@ "domain_dns_push_not_applicable": "Ezin da {domain} domeinurako DNS konfigurazio automatiko funtzioa erabili. DNS erregistroak eskuz ezarri beharko zenituzke gidaorriei erreparatuz: https://yunohost.org/dns_config.", "domain_dns_push_managed_in_parent_domain": "DNS ezarpenak automatikoki konfiguratzeko funtzioa {parent_domain} domeinu nagusian kudeatzen da.", "domain_dns_registrar_managed_in_parent_domain": "Domeinu hau {parent_domain_link}(r)en azpidomeinua da. DNS ezarpenak {parent_domain}(r)en konfigurazio atalean kudeatu behar dira.", - "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost domain dns push DOMAIN' komandoa)", + "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost dyndns update' komandoa)", "domain_dns_registrar_not_supported": "YunoHostek ezin izan du domeinu honen erregistro-enpresa automatikoki antzeman. Eskuz konfiguratu beharko dituzu DNS ezarpenak gidalerroei erreparatuz: https://yunohost.org/dns.", "domain_dns_registrar_experimental": "Oraingoz, YunoHosten kideek ez dute **{registrar}** erregistro-enpresaren APIa nahi beste probatu eta aztertu. Funtzioa **oso esperimentala** da — kontuz!", "domain_config_mail_in": "Jasotako mezuak", diff --git a/locales/fa.json b/locales/fa.json index afa86b13b..92e05bdad 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -28,7 +28,7 @@ "diagnosis_domain_not_found_details": "دامنه {domain} در پایگاه داده WHOIS وجود ندارد یا منقضی شده است!", "diagnosis_domain_expiration_not_found": "بررسی تاریخ انقضا برخی از دامنه ها امکان پذیر نیست", "diagnosis_dns_specialusedomain": "دامنه {domain} بر اساس یک دامنه سطح بالا (TLD) مخصوص استفاده است و بنابراین انتظار نمی رود که دارای سوابق DNS واقعی باشد.", - "diagnosis_dns_try_dyndns_update_force": "پیکربندی DNS این دامنه باید به طور خودکار توسط YunoHost مدیریت شود. اگر اینطور نیست ، می توانید سعی کنید به زور یک به روز رسانی را با استفاده از yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "پیکربندی DNS این دامنه باید به طور خودکار توسط YunoHost مدیریت شود. اگر اینطور نیست ، می توانید سعی کنید به زور یک به روز رسانی را با استفاده از yunohost dyndns update --force.", "diagnosis_dns_point_to_doc": "لطفاً اسناد را در https://yunohost.org/dns_config برسی و مطالعه کنید، اگر در مورد پیکربندی سوابق DNS به کمک نیاز دارید.", "diagnosis_dns_discrepancy": "به نظر می رسد پرونده DNS زیر از پیکربندی توصیه شده پیروی نمی کند:
نوع: {type}
نام: {name}
ارزش فعلی: {current}
مقدار مورد انتظار: {value}", "diagnosis_dns_missing_record": "با توجه به پیکربندی DNS توصیه شده ، باید یک رکورد DNS با اطلاعات زیر اضافه کنید.
نوع: {type}
نام: {name}
ارزش: {value}", diff --git a/locales/fr.json b/locales/fr.json index 9affb5869..959ef1a8d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -510,7 +510,7 @@ "diagnosis_swap_tip": "Soyez averti et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire considérablement l'espérance de vie de celui-ci.", "restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}", "regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.", - "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost dyndns update --force.", "app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de YunoHost. Vous devriez probablement envisager de mettre à jour votre système.", "global_settings_setting_backup_compress_tar_archives": "Compresser les archives de backup", "diagnosis_processes_killed_by_oom_reaper": "Certains processus ont été récemment arrêtés par le système car il manquait de mémoire. Ceci est typiquement symptomatique d'un manque de mémoire sur le système ou d'un processus consommant trop de mémoire. Liste des processus arrêtés :\n{kills_summary}", @@ -598,7 +598,7 @@ "service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}", "domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.", "domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.", - "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost domain dns push DOMAIN')", + "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost dyndns update')", "domain_dns_registrar_supported": "YunoHost a détecté automatiquement que ce domaine est géré par le registrar **{registrar}**. Si vous le souhaitez, YunoHost configurera automatiquement cette zone DNS, si vous lui fournissez les identifiants API appropriés. Vous pouvez trouver de la documentation sur la façon d'obtenir vos identifiants API sur cette page : https://yunohost.org/registar_api_{registrar}. (Vous pouvez également configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns )", "domain_config_features_disclaimer": "Jusqu'à présent, l'activation/désactivation des fonctionnalités de messagerie ou XMPP n'a d'impact que sur la configuration DNS recommandée et automatique, et non sur les configurations système !", "domain_dns_push_managed_in_parent_domain": "La fonctionnalité de configuration DNS automatique est gérée dans le domaine parent {parent_domain}.", diff --git a/locales/gl.json b/locales/gl.json index beaeec801..61af0b672 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -185,7 +185,7 @@ "diagnosis_domain_expiration_not_found_details": "A información WHOIS para o dominio {domain} non semella conter información acerca da data de caducidade?", "diagnosis_domain_not_found_details": "O dominio {domain} non existe na base de datos de WHOIS ou está caducado!", "diagnosis_domain_expiration_not_found": "Non se puido comprobar a data de caducidade para algúns dominios", - "diagnosis_dns_try_dyndns_update_force": "A xestión DNS deste dominio debería estar xestionada directamente por YunoHost. Se non fose o caso, podes intentar forzar unha actualización executando yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "A xestión DNS deste dominio debería estar xestionada directamente por YunoHost. Se non fose o caso, podes intentar forzar unha actualización executando yunohost dyndns update --force.", "diagnosis_swap_ok": "O sistema ten {total} de swap!", "diagnosis_swap_notsomuch": "O sistema só ten {total} de swap. Deberías considerar ter polo menos {recommended} para evitar situacións onde o sistema esgote a memoria.", "diagnosis_swap_none": "O sistema non ten partición swap. Deberías considerar engadir polo menos {recommended} de swap para evitar situación onde o sistema esgote a memoria.", @@ -615,7 +615,7 @@ "domain_config_auth_consumer_key": "Chave consumidora", "log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'", "other_available_options": "... e outras {n} opcións dispoñibles non mostradas", - "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost domain dns push DOMAIN')", + "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost dyndns update')", "domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registrar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registrar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )", "domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.", "domain_config_auth_token": "Token de autenticación", diff --git a/locales/it.json b/locales/it.json index 888b6cc62..9bb923c2a 100644 --- a/locales/it.json +++ b/locales/it.json @@ -299,7 +299,7 @@ "diagnosis_domain_expiration_not_found_details": "Le informazioni WHOIS per il dominio {domain} non sembrano contenere la data di scadenza, giusto?", "diagnosis_domain_not_found_details": "Il dominio {domain} non esiste nel database WHOIS o è scaduto!", "diagnosis_domain_expiration_not_found": "Non riesco a controllare la data di scadenza di alcuni domini", - "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da YunoHost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da YunoHost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost dyndns update --force.", "diagnosis_dns_point_to_doc": "Controlla la documentazione a https://yunohost.org/dns_config se hai bisogno di aiuto nel configurare i record DNS.", "diagnosis_dns_discrepancy": "Il record DNS non sembra seguire la configurazione DNS raccomandata:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", "diagnosis_dns_missing_record": "Stando alla configurazione DNS raccomandata, dovresti aggiungere un record DNS con le seguenti informazioni.
Type: {type}
Name: {name}
Value: {value}", @@ -608,7 +608,7 @@ "diagnosis_description_apps": "Applicazioni", "domain_registrar_is_not_configured": "Il registrar non è ancora configurato per il dominio {domain}.", "domain_dns_registrar_managed_in_parent_domain": "Questo dominio è un sotto-dominio di {parent_domain_link}. La configurazione del registrar DNS dovrebbe essere gestita dal pannello di configurazione di {parent_domain}.", - "domain_dns_registrar_yunohost": "Questo dominio è un nohost.me / nohost.st / ynh.fr, perciò la sua configurazione DNS è gestita automaticamente da YunoHost, senza alcuna ulteriore configurazione. (vedi il comando yunohost domain dns push DOMAIN)", + "domain_dns_registrar_yunohost": "Questo dominio è un nohost.me / nohost.st / ynh.fr, perciò la sua configurazione DNS è gestita automaticamente da YunoHost, senza alcuna ulteriore configurazione. (vedi il comando yunohost dyndns update)", "domain_dns_push_success": "Record DNS aggiornati!", "domain_dns_push_failed": "L’aggiornamento dei record DNS è miseramente fallito.", "domain_dns_push_partial_failure": "Record DNS parzialmente aggiornati: alcuni segnali/errori sono stati riportati.", diff --git a/locales/uk.json b/locales/uk.json index 2b168b65e..281f2dba7 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -409,7 +409,7 @@ "diagnosis_domain_not_found_details": "Домен {domain} не існує в базі даних WHOIS або строк його дії сплив!", "diagnosis_domain_expiration_not_found": "Неможливо перевірити строк дії деяких доменів", "diagnosis_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) такого як .local або .test і тому не очікується, що у нього будуть актуальні записи DNS.", - "diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди yunohost domain dns push DOMAIN --force.", + "diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди yunohost dyndns update --force.", "diagnosis_dns_point_to_doc": "Якщо вам потрібна допомога з налаштування DNS-записів, зверніться до документації на сайті https://yunohost.org/dns_config.", "diagnosis_dns_discrepancy": "Наступний запис DNS, схоже, не відповідає рекомендованій конфігурації:
Тип: {type}
Назва: {name}
Поточне значення: {current}
Очікуване значення: {value}", "diagnosis_dns_missing_record": "Згідно рекомендованої конфігурації DNS, ви повинні додати запис DNS з наступними відомостями.
Тип: {type}
Назва: {name}
Значення: {value}", @@ -599,7 +599,7 @@ "diagnosis_http_special_use_tld": "Домен {domain} базується на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він буде відкритий за межами локальної мережі.", "domain_dns_push_managed_in_parent_domain": "Функцією автоконфігурації DNS керує батьківський домен {parent_domain}.", "domain_dns_registrar_managed_in_parent_domain": "Цей домен є піддоменом {parent_domain_link}. Конфігурацією реєстратора DNS слід керувати на панелі конфігурації {parent_domain}.", - "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost domain dns push DOMAIN')", + "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost dyndns update')", "domain_dns_conf_special_use_tld": "Цей домен засновано на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він матиме актуальні записи DNS.", "domain_dns_registrar_supported": "YunoHost автоматично визначив, що цей домен обслуговується реєстратором **{registrar}**. Якщо ви хочете, YunoHost автоматично налаштує цю DNS-зону, якщо ви надасте йому відповідні облікові дані API. Ви можете знайти документацію про те, як отримати реєстраційні дані API на цій сторінці: https://yunohost.org/registar_api_{registrar}. (Ви також можете вручну налаштувати свої DNS-записи, дотримуючись документації на https://yunohost.org/dns)", "domain_dns_registrar_experimental": "Поки що інтерфейс з API **{registrar}** не був належним чином протестований і перевірений спільнотою YunoHost. Підтримка є **дуже експериментальною** - будьте обережні!", diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index b31d88217..8aecbbce3 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -449,7 +449,7 @@ "diagnosis_diskusage_low": "存储器{mountpoint}(在设备{device}上)只有{free} ({free_percent}%) 的空间。({free_percent}%)的剩余空间(在{total}中)。要小心。", "diagnosis_diskusage_verylow": "存储器{mountpoint}(在设备{device}上)仅剩余{free} ({free_percent}%) (剩余{total})个空间。您应该真正考虑清理一些空间!", "diagnosis_services_bad_status_tip": "你可以尝试重新启动服务,如果没有效果,可以看看webadmin中的服务日志(从命令行,你可以用yunohost service restart {service}yunohost service log {service})来做。", - "diagnosis_dns_try_dyndns_update_force": "该域的DNS配置应由YunoHost自动管理,如果不是这种情况,您可以尝试使用 yunohost domain dns push DOMAIN --force强制进行更新。", + "diagnosis_dns_try_dyndns_update_force": "该域的DNS配置应由YunoHost自动管理,如果不是这种情况,您可以尝试使用 yunohost dyndns update --force强制进行更新。", "diagnosis_dns_point_to_doc": "如果您需要有关配置DNS记录的帮助,请查看 https://yunohost.org/dns_config 上的文档。", "diagnosis_dns_discrepancy": "以下DNS记录似乎未遵循建议的配置:
类型: {type}
名称: {name}
代码> 当前值: {current}期望值: {value}", "log_backup_create": "创建备份档案", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 3124f3105..9fe077c23 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -751,9 +751,8 @@ domain: action_help: Push DNS records to registrar api: POST /domains//dns/push arguments: - domains: - help: Domain names to push DNS conf for - nargs: "*" + domain: + help: Domain name to push DNS conf for extra: pattern: *pattern_domain -d: @@ -766,9 +765,6 @@ domain: --purge: help: Delete all records action: store_true - --auto: - help: Push only domains that should be pushed automatically - action: store_true cert: subcategory_help: Manage domain certificates @@ -1572,7 +1568,7 @@ dyndns: extra: pattern: *pattern_domain -p: - full: --password + full: --recovery-password nargs: "?" const: 0 help: Password used to later delete the domain @@ -1580,25 +1576,6 @@ dyndns: pattern: *pattern_password comment: dyndns_added_password - ### dyndns_unsubscribe() - unsubscribe: - action_help: Unsubscribe to a DynDNS service ( deprecated, use 'yunohost domain dyndns unsubscribe' instead ) - deprecated: true - arguments: - -d: - full: --domain - help: Full domain to unsubscribe with - extra: - pattern: *pattern_domain - required: True - -p: - full: --password - nargs: "?" - const: 0 - help: Password used to delete the domain - extra: - pattern: *pattern_password - ### dyndns_update() update: action_help: Update IP on DynDNS platform ( deprecated, use 'yunohost domain dns push DOMAIN' instead ) diff --git a/share/config_domain.toml b/share/config_domain.toml index 0aae4df26..b1ec436c5 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -33,13 +33,6 @@ name = "Features" [dns] name = "DNS" - [dns.zone] - - [dns.zone.autopush] - type = "boolean" - default = 0 - help = "" - [dns.registrar] # This part is automatically generated in DomainConfigPanel diff --git a/src/dns.py b/src/dns.py index 085f47471..296ecfaaa 100644 --- a/src/dns.py +++ b/src/dns.py @@ -618,24 +618,8 @@ def _get_registar_settings(domain): return registrar, settings -def domain_dns_push(domains, dry_run=False, force=False, purge=False, auto=False): - if auto: - domains = domain_list(exclude_subdomains=True, auto_push=True)["domains"] - elif len(domains) == 0: - domains = domain_list(exclude_subdomains=True)["domains"] - error_domains = [] - for domain in domains: - try: - domain_dns_push_unique(domain, dry_run=dry_run, force=force, purge=purge) - except YunohostError as e: - logger.error(m18n.n("domain_dns_push_failed_domain", domain=domain, error=str(e))) - error_domains.append(domain) - if len(error_domains) > 0: - raise YunohostError("domain_dns_push_failed_domains", domains=', '.join(error_domains)) - - @is_unit_operation() -def domain_dns_push_unique(operation_logger, domain, dry_run=False, force=False, purge=False): +def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge=False): """ Send DNS records to the previously-configured registrar of the domain. """ diff --git a/src/domain.py b/src/domain.py index 020a707c7..9dc884177 100644 --- a/src/domain.py +++ b/src/domain.py @@ -116,7 +116,6 @@ def domain_list(exclude_subdomains=False, tree=False, features=[]): domains_filtered = [] for domain in domains: config = domain_config_get(domain, key="feature", export=True) - config += domain_config_get(domain, key="dns.zone", export=True) if any(config.get(feature) == 1 for feature in features): domains_filtered.append(domain) domains = domains_filtered @@ -791,7 +790,7 @@ def domain_dns_suggest(domain): return domain_dns_suggest(domain) -def domain_dns_push(domains, dry_run=None, force=None, purge=None, auto=False): +def domain_dns_push(domain, dry_run, force, purge): from yunohost.dns import domain_dns_push - return domain_dns_push(domains, dry_run=dry_run, force=force, purge=purge, auto=auto) + return domain_dns_push(domain, dry_run, force, purge) diff --git a/src/dyndns.py b/src/dyndns.py index caeef9459..2f83038cc 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -172,10 +172,6 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): error = f'Server error, code: {r.status_code}. (Message: "{r.text}")' raise YunohostError("dyndns_registration_failed", error=error) - # Set the domain's config to autopush - from yunohost.domain import domain_config_set - domain_config_set(domain, key="dns.zone.autopush", value=1) - # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns regen_conf(["yunohost"]) @@ -183,7 +179,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): # Add some dyndns update in 2 and 4 minutes from now such that user should # not have to wait 10ish minutes for the conf to propagate cmd = ( - f"at -M now + {{t}} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost domain dns push {domain}'\"" + "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" ) # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) diff --git a/src/tools.py b/src/tools.py index c0da7a37b..79f10bc8c 100644 --- a/src/tools.py +++ b/src/tools.py @@ -200,15 +200,15 @@ def tools_postinstall( # Check if the domain is available... try: - _dyndns_available(domain) + available = _dyndns_available(domain) # If an exception is thrown, most likely we don't have internet # connectivity or something. Assume that this domain isn't manageable # and inform the user that we could not contact the dyndns host server. except Exception: - logger.warning( - m18n.n("dyndns_provider_unreachable", provider="dyndns.yunohost.org") - ) - raise YunohostValidationError("dyndns_unavailable", domain=domain) + raise YunohostValidationError("dyndns_provider_unreachable", provider="dyndns.yunohost.org") + else: + if not available: + raise YunohostValidationError("dyndns_unavailable", domain=domain) if os.system("iptables -V >/dev/null 2>/dev/null") != 0: raise YunohostValidationError( From e2da51b9a367fb7f4e8f7d1b39a081d19c58427b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 18:43:27 +0200 Subject: [PATCH 0061/1116] dyndns: various tweaks to simplify the code, improve UX ... --- locales/en.json | 8 ++-- share/actionsmap.yml | 2 +- src/domain.py | 42 +++++------------- src/dyndns.py | 102 +++++++++++++++++++------------------------ src/tools.py | 7 +-- 5 files changed, 62 insertions(+), 99 deletions(-) diff --git a/locales/en.json b/locales/en.json index 83ee34052..b3a1725e8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -90,7 +90,8 @@ "ask_new_domain": "New domain", "ask_new_path": "New path", "ask_password": "Password", - "ask_dyndns_recovery_password": "DynDNS recovey password", + "ask_dyndns_recovery_password_explain": "Please pick a recovery password for your DynDNS domain, in case you need to reset it later.", + "ask_dyndns_recovery_password": "DynDNS recovey passwory", "ask_user_domain": "Domain to use for the user's email address and XMPP account", "backup_abstract_method": "This backup method has yet to be implemented", "backup_actually_backuping": "Creating a backup archive from the collected files...", @@ -383,8 +384,6 @@ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", - "domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --ignore-dyndns", - "domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --ignore-dyndns", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", @@ -400,7 +399,6 @@ "dyndns_domain_not_provided": "DynDNS provider {provider} cannot provide domain {domain}.", "dyndns_ip_update_failed": "Could not update IP address to DynDNS", "dyndns_ip_updated": "Updated your IP on DynDNS", - "dyndns_key_generating": "Generating DNS key... It may take a while.", "dyndns_key_not_found": "DNS key not found for the domain", "dyndns_no_domain_registered": "No domain registered with DynDNS", "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", @@ -772,4 +770,4 @@ "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." -} \ No newline at end of file +} diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 412297440..e11073afc 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -493,7 +493,7 @@ domain: help: Display domains as a tree action: store_true --features: - help: List only domains with features enabled (xmpp, mail_in, mail_out, auto_push) + help: List only domains with features enabled (xmpp, mail_in, mail_out) nargs: "*" ### domain_info() diff --git a/src/domain.py b/src/domain.py index 35730483b..8d2758ab2 100644 --- a/src/domain.py +++ b/src/domain.py @@ -228,7 +228,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_d from yunohost.utils.password import assert_password_is_strong_enough from yunohost.certificate import _certificate_install_selfsigned - if dyndns_recovery_password != 0 and dyndns_recovery_password is not None: + if dyndns_recovery_password: operation_logger.data_to_redact.append(dyndns_recovery_password) if domain.startswith("xmpp-upload."): @@ -252,35 +252,19 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_d domain = domain.encode("idna").decode("utf-8") # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) - dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 + dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 if dyndns: - if not ignore_dyndns and not dyndns_recovery_password: - if Moulinette.interface.type == "api": - raise YunohostValidationError("domain_dyndns_missing_password") - else: - dyndns_recovery_password = Moulinette.prompt( - m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True - ) - - # Ensure sufficiently complex password - assert_password_is_strong_enough("admin", dyndns_recovery_password) - - if ((dyndns_recovery_password is None) == (ignore_dyndns is False)): - raise YunohostValidationError("domain_dyndns_instruction_unclear") - from yunohost.dyndns import is_subscribing_allowed - # Do not allow to subscribe to multiple dyndns domains... if not is_subscribing_allowed(): raise YunohostValidationError("domain_dyndns_already_subscribed") + if dyndns_recovery_password: + assert_password_is_strong_enough("admin", dyndns_recovery_password) operation_logger.start() - if not dyndns and (dyndns_recovery_password is not None or ignore_dyndns): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns-recovery-password or --ignore-dyndns option") - if dyndns and not ignore_dyndns: - # Actually subscribe - domain_dyndns_subscribe(domain=domain, password=dyndns_recovery_password) + if dyndns: + domain_dyndns_subscribe(domain=domain, recovery_password=dyndns_recovery_password) _certificate_install_selfsigned([domain], True) @@ -346,7 +330,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface - if dyndns_recovery_password != 0 and dyndns_recovery_password is not None: + if dyndns_recovery_password: operation_logger.data_to_redact.append(dyndns_recovery_password) # the 'force' here is related to the exception happening in domain_add ... @@ -410,16 +394,10 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd ) # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) - dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 - if dyndns: - if ((dyndns_recovery_password is None) == (ignore_dyndns is False)): - raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe") + dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 operation_logger.start() - if not dyndns and ((dyndns_recovery_password is not None) or (ignore_dyndns is not False)): - logger.warning("This domain is not a DynDNS one, no need for the --dyndns_recovery_password or --ignore-dyndns option") - ldap = _get_ldap_interface() try: ldap.remove("virtualdomain=" + domain + ",ou=domains") @@ -465,9 +443,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd hook_callback("post_domain_remove", args=[domain]) # If a password is provided, delete the DynDNS record - if dyndns and not ignore_dyndns: + if dyndns: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain, password=dyndns_recovery_password) + domain_dyndns_unsubscribe(domain=domain, recovery_password=dyndns_recovery_password) logger.success(m18n.n("domain_deleted")) diff --git a/src/dyndns.py b/src/dyndns.py index 3c9788af7..2da3de212 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -50,7 +50,7 @@ def is_subscribing_allowed(): Returns: True if the limit is not reached, False otherwise """ - return len(glob.glob("/etc/yunohost/dyndns/*.key")) < MAX_DYNDNS_DOMAINS + return len(dyndns_list()["domains"]) < MAX_DYNDNS_DOMAINS def _dyndns_available(domain): @@ -78,7 +78,7 @@ def _dyndns_available(domain): return r == f"Domain {domain} is available" -@is_unit_operation() +@is_unit_operation(exclude=["recovery_password"]) def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): """ Subscribe to a DynDNS service @@ -88,26 +88,6 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): recovery_password -- Password that will be used to delete the domain """ - if recovery_password is None: - logger.warning(m18n.n('dyndns_no_recovery_password')) - else: - from yunohost.utils.password import assert_password_is_strong_enough - # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and recovery_password == 0: - recovery_password = Moulinette.prompt( - m18n.n("ask_password"), - is_password=True, - confirm=True - ) - assert_password_is_strong_enough("admin", recovery_password) - - if not is_subscribing_allowed(): - raise YunohostValidationError("domain_dyndns_already_subscribed") - - if domain is None: - domain = _get_maindomain() - operation_logger.related_to.append(("domain", domain)) - # Verify if domain is provided by subscribe_host if not is_yunohost_dyndns_domain(domain): raise YunohostValidationError( @@ -118,6 +98,30 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): if not _dyndns_available(domain): raise YunohostValidationError("dyndns_unavailable", domain=domain) + # Check adding another dyndns domain is still allowed + if not is_subscribing_allowed(): + raise YunohostValidationError("domain_dyndns_already_subscribed") + + # Prompt for a password if running in CLI and no password provided + if not recovery_password and Moulinette.interface.type == "cli": + logger.warning(m18n.n("ask_dyndns_recovery_password_explain")) + recovery_password = Moulinette.prompt( + m18n.n("ask_dyndns_recovery_password"), + is_password=True, + confirm=True + ) + elif not recovery_password: + logger.warning(m18n.n("dyndns_no_recovery_password")) + + if recovery_password: + from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough("admin", recovery_password) + operation_logger.data_to_redact.append(recovery_password) + + if domain is None: + domain = _get_maindomain() + operation_logger.related_to.append(("domain", domain)) + operation_logger.start() # '165' is the convention identifier for hmac-sha512 algorithm @@ -127,8 +131,6 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): if not os.path.exists("/etc/yunohost/dyndns"): os.makedirs("/etc/yunohost/dyndns") - logger.debug(m18n.n("dyndns_key_generating")) - # Here, we emulate the behavior of the old 'dnssec-keygen' utility # which since bullseye was replaced by ddns-keygen which is now # in the bind9 package ... but installing bind9 will conflict with dnsmasq @@ -154,7 +156,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): # Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever... b64encoded_key = base64.b64encode(secret.encode()).decode() data = {"subdomain": domain} - if password is not None: + if recovery_password: data["recovery_password"] = hashlib.sha256((domain + ":" + recovery_password.strip()).encode('utf-8')).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", @@ -188,7 +190,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): logger.success(m18n.n("dyndns_registered")) -@is_unit_operation() +@is_unit_operation(exclude=["recovery_password"]) def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): """ Unsubscribe from a DynDNS service @@ -198,24 +200,19 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): recovery_password -- Password that is used to delete the domain ( defined when subscribing ) """ - from yunohost.utils.password import assert_password_is_strong_enough + import requests # lazy loading this module for performance reasons + + # FIXME : it should be possible to unsubscribe the domain just using the key file ... # Ensure sufficiently complex password if Moulinette.interface.type == "cli" and not recovery_password: recovery_password = Moulinette.prompt( - m18n.n("ask_password"), + m18n.n("ask_dyndns_recovery_password"), is_password=True ) - assert_password_is_strong_enough("admin", recovery_password) operation_logger.start() - # '165' is the convention identifier for hmac-sha512 algorithm - # '1234' is idk? doesnt matter, but the old format contained a number here... - key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" - - import requests # lazy loading this module for performance reasons - # Send delete request try: secret = str(domain) + ":" + str(recovery_password).strip() @@ -228,30 +225,30 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): raise YunohostError("dyndns_unregistration_failed", error=str(e)) if r.status_code == 200: # Deletion was successful - rm(key_file, force=True) + for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key"): + rm(key_file, force=True) # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns regen_conf(["yunohost"]) - - logger.success(m18n.n("dyndns_unregistered")) elif r.status_code == 403: # Wrong password raise YunohostError("dyndns_unsubscribe_wrong_password") elif r.status_code == 404: # Invalid domain raise YunohostError("dyndns_unsubscribe_wrong_domain") + logger.success(m18n.n("dyndns_unregistered")) + def dyndns_list(): """ Returns all currently subscribed DynDNS domains ( deduced from the key files ) """ - files = glob.glob("/etc/yunohost/dyndns/K*key") - # Get the domain names - for i in range(len(files)): - files[i] = files[i].split(".+", 1)[0] - files[i] = files[i].split("/etc/yunohost/dyndns/K")[1] + from yunohost.domain import domain_list - return {"domains": files} + domains = domain_list(exclude_subdomains=True)["domains"] + dyndns_domains = [d for d in domains if is_yunohost_dyndns_domain(d) and glob.glob(f"/etc/yunohost/dyndns/K{d}.+*.key")] + + return {"domains": dyndns_domains} @is_unit_operation() @@ -277,21 +274,14 @@ def dyndns_update( # If domain is not given, update all DynDNS domains if domain is None: - from yunohost.domain import domain_list + dyndns_domains = dyndns_list()["domains"] - domains = domain_list(exclude_subdomains=True, auto_push=True)["domains"] - pushed = 0 - for d in domains: - if is_yunohost_dyndns_domain(d): - dyndns_update(d, force=force, dry_run=dry_run) - pushed += 1 - if pushed == 0: + if not dyndns_domains: raise YunohostValidationError("dyndns_no_domain_registered") - return - elif type(domain).__name__ in ["list", "tuple"]: - for d in domain: - dyndns_update(d, force=force, dry_run=dry_run) + for domain in dyndns_domains: + dyndns_update(domain, force=force, dry_run=dry_run) + return # If key is not given, pick the first file we find with the domain given diff --git a/src/tools.py b/src/tools.py index 33cccd729..af6a2e61a 100644 --- a/src/tools.py +++ b/src/tools.py @@ -197,11 +197,8 @@ def tools_postinstall( assert_password_is_strong_enough("admin", password) # If this is a nohost.me/noho.st, actually check for availability - if not ignore_dyndns and is_yunohost_dyndns_domain(domain): - - if (bool(dyndns_recovery_password), ignore_dyndns) in [(True, True), (False, False)]: - raise YunohostValidationError("domain_dyndns_instruction_unclear") - + dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) + if dyndns: # Check if the domain is available... try: available = _dyndns_available(domain) From cbef40798c2843777a43db4b3b328e1d01abfcda Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 19:40:30 +0200 Subject: [PATCH 0062/1116] dyndns: be able to unsubscribe using the key + domain and i18n string consistency --- locales/en.json | 19 +++++++++-------- share/actionsmap.yml | 10 ++++----- src/dyndns.py | 50 +++++++++++++++++++++++++++++--------------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/locales/en.json b/locales/en.json index b3a1725e8..81d0b8a3e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -91,7 +91,8 @@ "ask_new_path": "New path", "ask_password": "Password", "ask_dyndns_recovery_password_explain": "Please pick a recovery password for your DynDNS domain, in case you need to reset it later.", - "ask_dyndns_recovery_password": "DynDNS recovey passwory", + "ask_dyndns_recovery_password": "DynDNS recovery password", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Please enter the recovery password for this DynDNS domain.", "ask_user_domain": "Domain to use for the user's email address and XMPP account", "backup_abstract_method": "This backup method has yet to be implemented", "backup_actually_backuping": "Creating a backup archive from the collected files...", @@ -401,15 +402,15 @@ "dyndns_ip_updated": "Updated your IP on DynDNS", "dyndns_key_not_found": "DNS key not found for the domain", "dyndns_no_domain_registered": "No domain registered with DynDNS", - "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", - "dyndns_added_password": "Remember your recovery password, you can use it to delete this domain record.", + "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", + "dyndns_added_password": "Remember your recovery password, you can use it to delete this domain record.", "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", - "dyndns_registered": "DynDNS domain registered", - "dyndns_registration_failed": "Could not register DynDNS domain: {error}", - "dyndns_unregistration_failed": "Could not unregister DynDNS domain: {error}", - "dyndns_unregistered": "DynDNS domain successfully unregistered", - "dyndns_unsubscribe_wrong_password": "Invalid password", - "dyndns_unsubscribe_wrong_domain": "Domain is not registered", + "dyndns_subscribed": "DynDNS domain subscribed", + "dyndns_subscribe_failed": "Could not subscribe DynDNS domain: {error}", + "dyndns_unsubscribe_failed": "Could not unsubscribe DynDNS domain: {error}", + "dyndns_unsubscribed": "DynDNS domain unsubscribed", + "dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials", + "dyndns_unsubscribe_already_unsubscribed": "Domain is already unsubscribed", "dyndns_unavailable": "The domain '{domain}' is unavailable.", "extracting": "Extracting...", "field_invalid": "Invalid field '{}'", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index e11073afc..b229be84a 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -663,9 +663,8 @@ domain: subscribe: action_help: Subscribe to a DynDNS service arguments: - -d: - full: --domain - help: Full domain to subscribe with + domain: + help: Domain to subscribe to the DynDNS service extra: pattern: *pattern_domain -p: @@ -681,9 +680,8 @@ domain: unsubscribe: action_help: Unsubscribe from a DynDNS service arguments: - -d: - full: --domain - help: Full domain to unsubscribe with + domain: + help: Domain to unsubscribe from the DynDNS service extra: pattern: *pattern_domain required: True diff --git a/src/dyndns.py b/src/dyndns.py index 2da3de212..4ed730ecc 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -165,14 +165,14 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): ) except Exception as e: rm(key_file, force=True) - raise YunohostError("dyndns_registration_failed", error=str(e)) + raise YunohostError("dyndns_subscribe_failed", error=str(e)) if r.status_code != 201: rm(key_file, force=True) try: error = json.loads(r.text)["error"] except Exception: error = f'Server error, code: {r.status_code}. (Message: "{r.text}")' - raise YunohostError("dyndns_registration_failed", error=error) + raise YunohostError("dyndns_subscribe_failed", error=error) # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns @@ -187,7 +187,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) subprocess.check_call(["bash", "-c", cmd.format(t="4 min")]) - logger.success(m18n.n("dyndns_registered")) + logger.success(m18n.n("dyndns_subscribed")) @is_unit_operation(exclude=["recovery_password"]) @@ -202,23 +202,37 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): import requests # lazy loading this module for performance reasons - # FIXME : it should be possible to unsubscribe the domain just using the key file ... + # Unsubscribe the domain using the key if available + keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") + if keys: + key = keys[0] + with open(key) as f: + key = f.readline().strip().split(" ", 6)[-1] + base64key = base64.b64encode(key.encode()).decode() + credential = {"key": base64key} + else: + # Ensure sufficiently complex password + if Moulinette.interface.type == "cli" and not recovery_password: + logger.warning(m18n.n("ask_dyndns_recovery_password_explain_during_unsubscribe")) + recovery_password = Moulinette.prompt( + m18n.n("ask_dyndns_recovery_password"), + is_password=True + ) - # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and not recovery_password: - recovery_password = Moulinette.prompt( - m18n.n("ask_dyndns_recovery_password"), - is_password=True - ) + if not recovery_password: + logger.error(f"Cannot unsubscribe the domain {domain}: no credential provided") + return + + secret = str(domain) + ":" + str(recovery_password).strip() + credential = {"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()} operation_logger.start() # Send delete request try: - secret = str(domain) + ":" + str(recovery_password).strip() r = requests.delete( f"https://{DYNDNS_PROVIDER}/domains/{domain}", - data={"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, + data=credential, timeout=30, ) except Exception as e: @@ -230,12 +244,14 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns regen_conf(["yunohost"]) - elif r.status_code == 403: # Wrong password - raise YunohostError("dyndns_unsubscribe_wrong_password") - elif r.status_code == 404: # Invalid domain - raise YunohostError("dyndns_unsubscribe_wrong_domain") + elif r.status_code == 403: + raise YunohostError("dyndns_unsubscribe_denied") + elif r.status_code == 409: + raise YunohostError("dyndns_unsubscribe_already_unsubscribed") + else: + raise YunohostError("dyndns_unsubscribe_failed", error=f"The server returned code {r.status_code}") - logger.success(m18n.n("dyndns_unregistered")) + logger.success(m18n.n("dyndns_unsubscribed")) def dyndns_list(): From 59a2c96921fe7d3b2083e1a1ae086bcc0edc3e90 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 19:49:28 +0200 Subject: [PATCH 0063/1116] dyndns: remove this 'comment' thing from the actionsmap, it's being displayed even for non-dyndns domains... --- locales/en.json | 1 - share/actionsmap.yml | 4 ---- 2 files changed, 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index 81d0b8a3e..e25911435 100644 --- a/locales/en.json +++ b/locales/en.json @@ -403,7 +403,6 @@ "dyndns_key_not_found": "DNS key not found for the domain", "dyndns_no_domain_registered": "No domain registered with DynDNS", "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", - "dyndns_added_password": "Remember your recovery password, you can use it to delete this domain record.", "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", "dyndns_subscribed": "DynDNS domain subscribed", "dyndns_subscribe_failed": "Could not subscribe DynDNS domain: {error}", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index b229be84a..9132a8545 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -525,7 +525,6 @@ domain: help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain extra: pattern: *pattern_password - comment: dyndns_added_password ### domain_remove() remove: @@ -674,7 +673,6 @@ domain: help: Password used to later delete the domain extra: pattern: *pattern_password - comment: dyndns_added_password ### domain_dyndns_unsubscribe() unsubscribe: @@ -1585,7 +1583,6 @@ dyndns: help: Password used to later delete the domain extra: pattern: *pattern_password - comment: dyndns_added_password ### dyndns_update() update: @@ -1684,7 +1681,6 @@ tools: help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain extra: pattern: *pattern_password - comment: dyndns_added_password --force-diskspace: help: Use this if you really want to install YunoHost on a setup with less than 10 GB on the root filesystem action: store_true From 58614add7905dc6ca361ee79aad66e9d0296c498 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 20:22:40 +0200 Subject: [PATCH 0064/1116] dyndns: add a 'set-recovery-password' command to set the recovery password using only the key --- locales/en.json | 5 +++++ share/actionsmap.yml | 20 ++++++++++++++++++-- src/domain.py | 21 +++++++++++++++------ src/dyndns.py | 45 ++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/locales/en.json b/locales/en.json index e25911435..bb0976fff 100644 --- a/locales/en.json +++ b/locales/en.json @@ -410,6 +410,11 @@ "dyndns_unsubscribed": "DynDNS domain unsubscribed", "dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials", "dyndns_unsubscribe_already_unsubscribed": "Domain is already unsubscribed", + "dyndns_set_recovery_password_denied": "Failed to set recovery password: invalid key", + "dyndns_set_recovery_password_unknown_domain": "Failed to set recovery password: domain not registered", + "dyndns_set_recovery_password_invalid_password": "Failed to set recovery password: password is not strong enough", + "dyndns_set_recovery_password_failed": "Failed to set recovery password: {error}", + "dyndns_set_recovery_password_success": "Recovery password set!", "dyndns_unavailable": "The domain '{domain}' is unavailable.", "extracting": "Extracting...", "field_invalid": "Invalid field '{}'", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 9132a8545..689c3da86 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -670,7 +670,7 @@ domain: full: --recovery-password nargs: "?" const: 0 - help: Password used to later delete the domain + help: Password used to later recover the domain if needed extra: pattern: *pattern_password @@ -687,7 +687,7 @@ domain: full: --recovery-password nargs: "?" const: 0 - help: Password used to delete the domain + help: Recovery password used to delete the domain extra: pattern: *pattern_password @@ -695,6 +695,22 @@ domain: list: action_help: List all subscribed DynDNS domains + ### domain_dyndns_set_recovery_password() + set-recovery-password: + action_help: Set recovery password + arguments: + domain: + help: Domain to set recovery password for + extra: + pattern: *pattern_domain + required: True + -p: + full: --recovery-password + help: The new recovery password + extra: + password: ask_dyndns_recovery_password + pattern: *pattern_password + config: subcategory_help: Domain settings actions: diff --git a/src/domain.py b/src/domain.py index 8d2758ab2..424b461e8 100644 --- a/src/domain.py +++ b/src/domain.py @@ -450,22 +450,22 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd logger.success(m18n.n("domain_deleted")) -def domain_dyndns_subscribe(**kwargs): +def domain_dyndns_subscribe(*args, **kwargs): """ Subscribe to a DynDNS domain """ from yunohost.dyndns import dyndns_subscribe - dyndns_subscribe(**kwargs) + dyndns_subscribe(*args, **kwargs) -def domain_dyndns_unsubscribe(**kwargs): +def domain_dyndns_unsubscribe(*args, **kwargs): """ Unsubscribe from a DynDNS domain """ from yunohost.dyndns import dyndns_unsubscribe - dyndns_unsubscribe(**kwargs) + dyndns_unsubscribe(*args, **kwargs) def domain_dyndns_list(): @@ -477,13 +477,22 @@ def domain_dyndns_list(): return dyndns_list() -def domain_dyndns_update(**kwargs): +def domain_dyndns_update(*args, **kwargs): """ Update a DynDNS domain """ from yunohost.dyndns import dyndns_update - dyndns_update(**kwargs) + dyndns_update(*args, **kwargs) + + +def domain_dyndns_set_recovery_password(*args, **kwargs): + """ + Set a recovery password for an already registered dyndns domain + """ + from yunohost.dyndns import dyndns_set_recovery_password + + dyndns_set_recovery_password(*args, **kwargs) @is_unit_operation() diff --git a/src/dyndns.py b/src/dyndns.py index 4ed730ecc..dca4e9c77 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -110,7 +110,8 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): is_password=True, confirm=True ) - elif not recovery_password: + + if not recovery_password: logger.warning(m18n.n("dyndns_no_recovery_password")) if recovery_password: @@ -210,8 +211,8 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): key = f.readline().strip().split(" ", 6)[-1] base64key = base64.b64encode(key.encode()).decode() credential = {"key": base64key} + # Otherwise, ask for the recovery password else: - # Ensure sufficiently complex password if Moulinette.interface.type == "cli" and not recovery_password: logger.warning(m18n.n("ask_dyndns_recovery_password_explain_during_unsubscribe")) recovery_password = Moulinette.prompt( @@ -254,6 +255,46 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): logger.success(m18n.n("dyndns_unsubscribed")) +def dyndns_set_recovery_password(domain, recovery_password): + + keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") + + if not keys: + raise YunohostValidationError("dyndns_key_not_found") + + from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough("admin", recovery_password) + secret = str(domain) + ":" + str(recovery_password).strip() + + key = keys[0] + with open(key) as f: + key = f.readline().strip().split(" ", 6)[-1] + base64key = base64.b64encode(key.encode()).decode() + + import requests # lazy loading this module for performance reasons + + # Send delete request + try: + r = requests.put( + f"https://{DYNDNS_PROVIDER}/domains/{domain}/recovery_password", + data={"key": base64key, "recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, + timeout=30, + ) + except Exception as e: + raise YunohostError("dyndns_set_recovery_password_failed", error=str(e)) + + if r.status_code == 200: + logger.success(m18n.n("dyndns_set_recovery_password_success")) + elif r.status_code == 403: + raise YunohostError("dyndns_set_recovery_password_denied") + elif r.status_code == 404: + raise YunohostError("dyndns_set_recovery_password_unknown_domain") + elif r.status_code == 409: + raise YunohostError("dyndns_set_recovery_password_invalid_password") + else: + raise YunohostError("dyndns_set_recovery_password_failed", error=f"The server returned code {r.status_code}") + + def dyndns_list(): """ Returns all currently subscribed DynDNS domains ( deduced from the key files ) From 91497afbfeb0713ca3170e4935bb5f52c16d24a3 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 12 Apr 2023 13:04:55 +0200 Subject: [PATCH 0065/1116] form: move option asking+prompt in external function --- src/tests/test_questions.py | 2 +- src/utils/form.py | 276 ++++++++++++++++++------------------ 2 files changed, 142 insertions(+), 136 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 190eb0cba..7ada38a1c 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -445,7 +445,7 @@ class BaseTest: assert option.name == id_ assert option.ask == {"en": id_} assert option.readonly is (True if is_special_readonly_option else False) - assert option.visible is None + assert option.visible is True # assert option.bind is None if is_special_readonly_option: diff --git a/src/utils/form.py b/src/utils/form.py index 12c3249c3..701632c30 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -35,6 +35,7 @@ from yunohost.utils.i18n import _value_for_locale logger = getActionLogger("yunohost.form") +Context = dict[str, Any] # ╭───────────────────────────────────────────────────────╮ # │ ┌─╴╷ ╷╭─┐╷ │ @@ -200,16 +201,14 @@ class BaseOption: def __init__( self, question: Dict[str, Any], - context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {}, ): self.name = question["name"] - self.context = context self.hooks = hooks self.type = question.get("type", "string") self.default = question.get("default", None) self.optional = question.get("optional", False) - self.visible = question.get("visible", None) + self.visible = question.get("visible", True) self.readonly = question.get("readonly", False) # Don't restrict choices if there's none specified self.choices = question.get("choices", None) @@ -241,75 +240,11 @@ class BaseOption: value = value.strip() return value - def ask_if_needed(self): - if self.visible and not evaluate_simple_js_expression( - self.visible, context=self.context - ): - # FIXME There could be several use case if the question is not displayed: - # - we doesn't want to give a specific value - # - we want to keep the previous value - # - we want the default value - self.value = self.values[self.name] = None - return self.values + def is_visible(self, context: Context) -> bool: + if isinstance(self.visible, bool): + return self.visible - for i in range(5): - # Display question if no value filled or if it's a readonly message - if Moulinette.interface.type == "cli" and os.isatty(1): - text_for_user_input_in_cli = self._format_text_for_user_input_in_cli() - if self.readonly: - Moulinette.display(text_for_user_input_in_cli) - self.value = self.values[self.name] = self.current_value - return self.values - elif self.value is None: - self._prompt(text_for_user_input_in_cli) - - # Apply default value - class_default = getattr(self, "default_value", None) - if self.value in [None, ""] and ( - self.default is not None or class_default is not None - ): - self.value = class_default if self.default is None else self.default - - try: - # Normalize and validate - self.value = self.normalize(self.value, self) - self._value_pre_validator() - except YunohostValidationError as e: - # If in interactive cli, re-ask the current question - if i < 4 and Moulinette.interface.type == "cli" and os.isatty(1): - logger.error(str(e)) - self.value = None - continue - - # Otherwise raise the ValidationError - raise - - break - - self.value = self.values[self.name] = self._value_post_validator() - - # Search for post actions in hooks - post_hook = f"post_ask__{self.name}" - if post_hook in self.hooks: - self.values.update(self.hooks[post_hook](self)) - - return self.values - - def _prompt(self, text): - prefill = "" - if self.current_value is not None: - prefill = self.humanize(self.current_value, self) - elif self.default is not None: - prefill = self.humanize(self.default, self) - self.value = Moulinette.prompt( - message=text, - is_password=self.hide_user_input_in_prompt, - confirm=False, - prefill=prefill, - is_multiline=(self.type == "text"), - autocomplete=self.choices or [], - help=_value_for_locale(self.help), - ) + return evaluate_simple_js_expression(self.visible, context=context) def _format_text_for_user_input_in_cli(self): text_for_user_input_in_cli = _value_for_locale(self.ask) @@ -396,9 +331,9 @@ class DisplayTextOption(BaseOption): argument_type = "display_text" def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + self, question, hooks: Dict[str, Callable] = {} ): - super().__init__(question, context, hooks) + super().__init__(question, hooks) self.optional = True self.readonly = True @@ -424,13 +359,19 @@ class DisplayTextOption(BaseOption): class ButtonOption(BaseOption): argument_type = "button" - enabled = None + enabled = True def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + self, question, hooks: Dict[str, Callable] = {} ): - super().__init__(question, context, hooks) - self.enabled = question.get("enabled", None) + super().__init__(question, hooks) + self.enabled = question.get("enabled", True) + + def is_enabled(self, context: Context) -> bool: + if isinstance(self.enabled, bool): + return self.enabled + + return evaluate_simple_js_expression(self.enabled, context=context) # ╭───────────────────────────────────────────────────────╮ @@ -452,10 +393,8 @@ class PasswordOption(BaseOption): default_value = "" forbidden_chars = "{}" - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) self.redact = True if self.default is not None: raise YunohostValidationError( @@ -491,10 +430,8 @@ class NumberOption(BaseOption): argument_type = "number" default_value = None - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) self.min = question.get("min", None) self.max = question.get("max", None) self.step = question.get("step", None) @@ -549,10 +486,8 @@ class BooleanOption(BaseOption): yes_answers = ["1", "yes", "y", "true", "t", "on"] no_answers = ["0", "no", "n", "false", "f", "off"] - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) self.yes = question.get("yes", 1) self.no = question.get("no", 0) if self.default is None: @@ -716,10 +651,8 @@ class FileOption(BaseOption): argument_type = "file" upload_dirs: List[str] = [] - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) self.accept = question.get("accept", "") @classmethod @@ -830,15 +763,16 @@ class TagsOption(BaseOption): return super()._value_post_validator() +# ─ ENTITIES ────────────────────────────────────────────── + + class DomainOption(BaseOption): argument_type = "domain" - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): + def __init__(self, question, hooks: Dict[str, Callable] = {}): from yunohost.domain import domain_list, _get_maindomain - super().__init__(question, context, hooks) + super().__init__(question, hooks) if self.default is None: self.default = _get_maindomain() @@ -864,12 +798,10 @@ class DomainOption(BaseOption): class AppOption(BaseOption): argument_type = "app" - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): + def __init__(self, question, hooks: Dict[str, Callable] = {}): from yunohost.app import app_list - super().__init__(question, context, hooks) + super().__init__(question, hooks) apps = app_list(full=True)["apps"] @@ -891,13 +823,11 @@ class AppOption(BaseOption): class UserOption(BaseOption): argument_type = "user" - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): + def __init__(self, question, hooks: Dict[str, Callable] = {}): from yunohost.user import user_list, user_info from yunohost.domain import _get_maindomain - super().__init__(question, context, hooks) + super().__init__(question, hooks) self.choices = { username: f"{infos['fullname']} ({infos['mail']})" @@ -924,12 +854,10 @@ class UserOption(BaseOption): class GroupOption(BaseOption): argument_type = "group" - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): + def __init__(self, question, hooks: Dict[str, Callable] = {}): from yunohost.user import user_group_list - super().__init__(question, context) + super().__init__(question) self.choices = list( user_group_list(short=True, include_primary_groups=False)["groups"] @@ -981,12 +909,111 @@ OPTIONS = { # ╰───────────────────────────────────────────────────────╯ +def prompt_or_validate_form( + raw_options: dict[str, Any], + prefilled_answers: dict[str, Any] = {}, + context: Context = {}, + hooks: dict[str, Callable[[], None]] = {}, +) -> list[BaseOption]: + options = [] + answers = {**prefilled_answers} + + for name, raw_option in raw_options.items(): + raw_option["name"] = name + raw_option["value"] = answers.get(name) + question_class = OPTIONS[raw_option.get("type", "string")] + option = question_class(raw_option, hooks=hooks) + + interactive = Moulinette.interface.type == "cli" and os.isatty(1) + + if isinstance(option, ButtonOption): + if option.is_enabled(context): + continue + else: + raise YunohostValidationError( + "config_action_disabled", + action=option.name, + help=_value_for_locale(option.help), + ) + + if option.is_visible(context): + for i in range(5): + # Display question if no value filled or if it's a readonly message + if interactive: + text_for_user_input_in_cli = ( + option._format_text_for_user_input_in_cli() + ) + if option.readonly: + Moulinette.display(text_for_user_input_in_cli) + option.value = option.current_value + break + elif option.value is None: + prefill = "" + if option.current_value is not None: + prefill = option.humanize(option.current_value, option) + elif option.default is not None: + prefill = option.humanize(option.default, option) + option.value = Moulinette.prompt( + message=text_for_user_input_in_cli, + is_password=option.hide_user_input_in_prompt, + confirm=False, + prefill=prefill, + is_multiline=(option.type == "text"), + autocomplete=option.choices or [], + help=_value_for_locale(option.help), + ) + + # Apply default value + class_default = getattr(option, "default_value", None) + if option.value in [None, ""] and ( + option.default is not None or class_default is not None + ): + option.value = ( + class_default if option.default is None else option.default + ) + + try: + # Normalize and validate + option.value = option.normalize(option.value, option) + option._value_pre_validator() + except YunohostValidationError as e: + # If in interactive cli, re-ask the current question + if i < 4 and interactive: + logger.error(str(e)) + option.value = None + continue + + # Otherwise raise the ValidationError + raise + + break + + option.value = option.values[option.name] = option._value_post_validator() + + # Search for post actions in hooks + post_hook = f"post_ask__{option.name}" + if post_hook in option.hooks: + option.values.update(option.hooks[post_hook](option)) + else: + # FIXME There could be several use case if the question is not displayed: + # - we doesn't want to give a specific value + # - we want to keep the previous value + # - we want the default value + option.value = option.values[option.name] = None + + answers.update(option.values) + context.update(option.values) + options.append(option) + + return options + + def ask_questions_and_parse_answers( - raw_questions: Dict, + raw_options: dict[str, Any], prefilled_answers: Union[str, Mapping[str, Any]] = {}, current_values: Mapping[str, Any] = {}, hooks: Dict[str, Callable[[], None]] = {}, -) -> List[BaseOption]: +) -> list[BaseOption]: """Parse arguments store in either manifest.json or actions.json or from a config panel against the user answers when they are present. @@ -1013,31 +1040,10 @@ def ask_questions_and_parse_answers( answers = {} context = {**current_values, **answers} - out = [] - for name, raw_question in raw_questions.items(): - raw_question["name"] = name - question_class = OPTIONS[raw_question.get("type", "string")] - raw_question["value"] = answers.get(name) - question = question_class(raw_question, context=context, hooks=hooks) - if question.type == "button": - if question.enabled is None or evaluate_simple_js_expression( # type: ignore - question.enabled, context=context # type: ignore - ): # type: ignore - continue - else: - raise YunohostValidationError( - "config_action_disabled", - action=question.name, - help=_value_for_locale(question.help), - ) - - new_values = question.ask_if_needed() - answers.update(new_values) - context.update(new_values) - out.append(question) - - return out + return prompt_or_validate_form( + raw_options, prefilled_answers=answers, context=context, hooks=hooks + ) def hydrate_questions_with_choices(raw_questions: List) -> List: From 4261317e49cc2dff36028077224006469ad8de4b Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 12 Apr 2023 20:48:56 +0200 Subject: [PATCH 0066/1116] form: separate BaseOption into BaseReadonlyOption + BaseInputOption --- src/tests/test_questions.py | 7 +- src/utils/form.py | 191 ++++++++++++++++++++---------------- 2 files changed, 109 insertions(+), 89 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 7ada38a1c..706645f9b 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -17,6 +17,8 @@ from yunohost import app, domain, user from yunohost.utils.form import ( OPTIONS, ask_questions_and_parse_answers, + BaseInputOption, + BaseReadonlyOption, DisplayTextOption, PasswordOption, DomainOption, @@ -377,8 +379,7 @@ def _fill_or_prompt_one_option(raw_option, intake): answers = {id_: intake} if intake is not None else {} option = ask_questions_and_parse_answers(options, answers)[0] - - return (option, option.value) + return (option, option.value if isinstance(option, BaseInputOption) else None) def _test_value_is_expected_output(value, expected_output): @@ -438,7 +439,7 @@ class BaseTest: id_ = raw_option["id"] option, value = _fill_or_prompt_one_option(raw_option, None) - is_special_readonly_option = isinstance(option, DisplayTextOption) + is_special_readonly_option = isinstance(option, BaseReadonlyOption) assert isinstance(option, OPTIONS[raw_option["type"]]) assert option.type == raw_option["type"] diff --git a/src/utils/form.py b/src/utils/form.py index 701632c30..0da3f892d 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -195,9 +195,6 @@ def evaluate_simple_js_expression(expr, context={}): class BaseOption: - hide_user_input_in_prompt = False - pattern: Optional[Dict] = None - def __init__( self, question: Dict[str, Any], @@ -206,16 +203,101 @@ class BaseOption: self.name = question["name"] self.hooks = hooks self.type = question.get("type", "string") - self.default = question.get("default", None) - self.optional = question.get("optional", False) self.visible = question.get("visible", True) self.readonly = question.get("readonly", False) - # Don't restrict choices if there's none specified - self.choices = question.get("choices", None) - self.pattern = question.get("pattern", self.pattern) self.ask = question.get("ask", self.name) if not isinstance(self.ask, dict): self.ask = {"en": self.ask} + + def is_visible(self, context: Context) -> bool: + if isinstance(self.visible, bool): + return self.visible + + return evaluate_simple_js_expression(self.visible, context=context) + + def _format_text_for_user_input_in_cli(self) -> str: + return _value_for_locale(self.ask) + + +# ╭───────────────────────────────────────────────────────╮ +# │ DISPLAY OPTIONS │ +# ╰───────────────────────────────────────────────────────╯ + + +class BaseReadonlyOption(BaseOption): + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) + self.readonly = True + + +class DisplayTextOption(BaseReadonlyOption): + argument_type = "display_text" + + +class MarkdownOption(BaseReadonlyOption): + argument_type = "markdown" + + +class AlertOption(BaseReadonlyOption): + argument_type = "alert" + + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) + self.style = question.get("style", "info") + + def _format_text_for_user_input_in_cli(self) -> str: + text = _value_for_locale(self.ask) + + if self.style in ["success", "info", "warning", "danger"]: + color = { + "success": "green", + "info": "cyan", + "warning": "yellow", + "danger": "red", + } + prompt = m18n.g(self.style) if self.style != "danger" else m18n.n("danger") + return colorize(prompt, color[self.style]) + f" {text}" + else: + return text + + +class ButtonOption(BaseReadonlyOption): + argument_type = "button" + enabled = True + + def __init__(self, question, hooks: Dict[str, Callable] = {}): + super().__init__(question, hooks) + self.help = question.get("help") + self.style = question.get("style", "success") + self.enabled = question.get("enabled", True) + + def is_enabled(self, context: Context) -> bool: + if isinstance(self.enabled, bool): + return self.enabled + + return evaluate_simple_js_expression(self.enabled, context=context) + + +# ╭───────────────────────────────────────────────────────╮ +# │ INPUT OPTIONS │ +# ╰───────────────────────────────────────────────────────╯ + + +class BaseInputOption(BaseOption): + hide_user_input_in_prompt = False + pattern: Optional[Dict] = None + + def __init__( + self, + question: Dict[str, Any], + hooks: Dict[str, Callable] = {}, + ): + super().__init__(question, hooks) + self.default = question.get("default", None) + self.optional = question.get("optional", False) + # Don't restrict choices if there's none specified + self.choices = question.get("choices", None) + self.pattern = question.get("pattern", self.pattern) self.help = question.get("help") self.redact = question.get("redact", False) self.filter = question.get("filter", None) @@ -240,14 +322,8 @@ class BaseOption: value = value.strip() return value - def is_visible(self, context: Context) -> bool: - if isinstance(self.visible, bool): - return self.visible - - return evaluate_simple_js_expression(self.visible, context=context) - - def _format_text_for_user_input_in_cli(self): - text_for_user_input_in_cli = _value_for_locale(self.ask) + def _format_text_for_user_input_in_cli(self) -> str: + text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() if self.readonly: text_for_user_input_in_cli = colorize(text_for_user_input_in_cli, "purple") @@ -322,72 +398,15 @@ class BaseOption: return self.value -# ╭───────────────────────────────────────────────────────╮ -# │ DISPLAY OPTIONS │ -# ╰───────────────────────────────────────────────────────╯ - - -class DisplayTextOption(BaseOption): - argument_type = "display_text" - - def __init__( - self, question, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, hooks) - - self.optional = True - self.readonly = True - self.style = question.get( - "style", "info" if question["type"] == "alert" else "" - ) - - def _format_text_for_user_input_in_cli(self): - text = _value_for_locale(self.ask) - - if self.style in ["success", "info", "warning", "danger"]: - color = { - "success": "green", - "info": "cyan", - "warning": "yellow", - "danger": "red", - } - prompt = m18n.g(self.style) if self.style != "danger" else m18n.n("danger") - return colorize(prompt, color[self.style]) + f" {text}" - else: - return text - - -class ButtonOption(BaseOption): - argument_type = "button" - enabled = True - - def __init__( - self, question, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, hooks) - self.enabled = question.get("enabled", True) - - def is_enabled(self, context: Context) -> bool: - if isinstance(self.enabled, bool): - return self.enabled - - return evaluate_simple_js_expression(self.enabled, context=context) - - -# ╭───────────────────────────────────────────────────────╮ -# │ INPUT OPTIONS │ -# ╰───────────────────────────────────────────────────────╯ - - # ─ STRINGS ─────────────────────────────────────────────── -class StringOption(BaseOption): +class StringOption(BaseInputOption): argument_type = "string" default_value = "" -class PasswordOption(BaseOption): +class PasswordOption(BaseInputOption): hide_user_input_in_prompt = True argument_type = "password" default_value = "" @@ -426,7 +445,7 @@ class ColorOption(StringOption): # ─ NUMERIC ─────────────────────────────────────────────── -class NumberOption(BaseOption): +class NumberOption(BaseInputOption): argument_type = "number" default_value = None @@ -480,7 +499,7 @@ class NumberOption(BaseOption): # ─ BOOLEAN ─────────────────────────────────────────────── -class BooleanOption(BaseOption): +class BooleanOption(BaseInputOption): argument_type = "boolean" default_value = 0 yes_answers = ["1", "yes", "y", "true", "t", "on"] @@ -606,7 +625,7 @@ class EmailOption(StringOption): } -class WebPathOption(BaseOption): +class WebPathOption(BaseInputOption): argument_type = "path" default_value = "" @@ -647,7 +666,7 @@ class URLOption(StringOption): # ─ FILE ────────────────────────────────────────────────── -class FileOption(BaseOption): +class FileOption(BaseInputOption): argument_type = "file" upload_dirs: List[str] = [] @@ -713,7 +732,7 @@ class FileOption(BaseOption): # ─ CHOICES ─────────────────────────────────────────────── -class TagsOption(BaseOption): +class TagsOption(BaseInputOption): argument_type = "tags" default_value = "" @@ -766,7 +785,7 @@ class TagsOption(BaseOption): # ─ ENTITIES ────────────────────────────────────────────── -class DomainOption(BaseOption): +class DomainOption(BaseInputOption): argument_type = "domain" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -795,7 +814,7 @@ class DomainOption(BaseOption): return value -class AppOption(BaseOption): +class AppOption(BaseInputOption): argument_type = "app" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -820,7 +839,7 @@ class AppOption(BaseOption): self.choices.update({app["id"]: _app_display(app) for app in apps}) -class UserOption(BaseOption): +class UserOption(BaseInputOption): argument_type = "user" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -851,7 +870,7 @@ class UserOption(BaseOption): break -class GroupOption(BaseOption): +class GroupOption(BaseInputOption): argument_type = "group" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -877,8 +896,8 @@ class GroupOption(BaseOption): OPTIONS = { "display_text": DisplayTextOption, - "markdown": DisplayTextOption, - "alert": DisplayTextOption, + "markdown": MarkdownOption, + "alert": AlertOption, "button": ButtonOption, "string": StringOption, "text": StringOption, From 9e8e0497dd286ee4a0992d005bc893ea57e6ef38 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 12 Apr 2023 21:01:25 +0200 Subject: [PATCH 0067/1116] form: fix readonly prompting + + choices + tests --- src/tests/test_questions.py | 68 ++++++------------------ src/utils/configpanel.py | 10 ---- src/utils/form.py | 100 ++++++++++++++++++++++-------------- 3 files changed, 78 insertions(+), 100 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 706645f9b..9e7be5db4 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -662,9 +662,7 @@ class TestString(BaseTest): (" ##value \n \tvalue\n ", "##value \n \tvalue"), ], reason=r"should fail or without `\n`?"), # readonly - *xfail(scenarios=[ - ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), - ], reason="Should not be overwritten"), + ("overwrite", "expected value", {"readonly": True, "current_value": "expected value"}), ] # fmt: on @@ -701,9 +699,7 @@ class TestText(BaseTest): (r" ##value \n \tvalue\n ", r"##value \n \tvalue\n"), ], reason="Should not be stripped"), # readonly - *xfail(scenarios=[ - ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), - ], reason="Should not be overwritten"), + ("overwrite", "expected value", {"readonly": True, "current_value": "expected value"}), ] # fmt: on @@ -737,9 +733,7 @@ class TestPassword(BaseTest): ("secret", FAIL), *[("supersecret" + char, FAIL) for char in PasswordOption.forbidden_chars], # FIXME maybe add ` \n` to the list? # readonly - *xpass(scenarios=[ - ("s3cr3t!!", "s3cr3t!!", {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + ("s3cr3t!!", YunohostError, {"readonly": True, "current_value": "isforbidden"}), # readonly is forbidden ] # fmt: on @@ -780,9 +774,7 @@ class TestColor(BaseTest): ("yellow", "#ffff00"), ], reason="Should work with pydantic"), # readonly - *xfail(scenarios=[ - ("#ffff00", "#fe100", {"readonly": True, "default": "#fe100"}), - ], reason="Should not be overwritten"), + ("#ffff00", "#fe100", {"readonly": True, "current_value": "#fe100"}), ] # fmt: on @@ -824,9 +816,7 @@ class TestNumber(BaseTest): (-10, -10, {"default": 10}), (-10, -10, {"default": 10, "optional": True}), # readonly - *xfail(scenarios=[ - (1337, 10000, {"readonly": True, "default": 10000}), - ], reason="Should not be overwritten"), + (1337, 10000, {"readonly": True, "current_value": 10000}), ] # fmt: on # FIXME should `step` be some kind of "multiple of"? @@ -891,9 +881,7 @@ class TestBoolean(BaseTest): "scenarios": all_fails("", "y", "n", error=AssertionError), }, # readonly - *xfail(scenarios=[ - (1, 0, {"readonly": True, "default": 0}), - ], reason="Should not be overwritten"), + (1, 0, {"readonly": True, "current_value": 0}), ] @@ -931,9 +919,7 @@ class TestDate(BaseTest): ("12-01-10", FAIL), ("2022-02-29", FAIL), # readonly - *xfail(scenarios=[ - ("2070-12-31", "2024-02-29", {"readonly": True, "default": "2024-02-29"}), - ], reason="Should not be overwritten"), + ("2070-12-31", "2024-02-29", {"readonly": True, "current_value": "2024-02-29"}), ] # fmt: on @@ -966,9 +952,7 @@ class TestTime(BaseTest): ("23:1", FAIL), ("23:005", FAIL), # readonly - *xfail(scenarios=[ - ("00:00", "08:00", {"readonly": True, "default": "08:00"}), - ], reason="Should not be overwritten"), + ("00:00", "08:00", {"readonly": True, "current_value": "08:00"}), ] # fmt: on @@ -992,9 +976,7 @@ class TestEmail(BaseTest): *nones(None, "", output=""), ("\n Abc@example.tld ", "Abc@example.tld"), # readonly - *xfail(scenarios=[ - ("Abc@example.tld", "admin@ynh.local", {"readonly": True, "default": "admin@ynh.local"}), - ], reason="Should not be overwritten"), + ("Abc@example.tld", "admin@ynh.local", {"readonly": True, "current_value": "admin@ynh.local"}), # Next examples are from https://github.com/JoshData/python-email-validator/blob/main/tests/test_syntax.py # valid email values @@ -1107,9 +1089,7 @@ class TestWebPath(BaseTest): ("https://example.com/folder", "/https://example.com/folder") ], reason="Should fail or scheme+domain removed"), # readonly - *xfail(scenarios=[ - ("/overwrite", "/value", {"readonly": True, "default": "/value"}), - ], reason="Should not be overwritten"), + ("/overwrite", "/value", {"readonly": True, "current_value": "/value"}), # FIXME should path have forbidden_chars? ] # fmt: on @@ -1134,9 +1114,7 @@ class TestUrl(BaseTest): *nones(None, "", output=""), ("http://some.org/folder/file.txt", "http://some.org/folder/file.txt"), # readonly - *xfail(scenarios=[ - ("https://overwrite.org", "https://example.org", {"readonly": True, "default": "https://example.org"}), - ], reason="Should not be overwritten"), + ("https://overwrite.org", "https://example.org", {"readonly": True, "current_value": "https://example.org"}), # rest is taken from https://github.com/pydantic/pydantic/blob/main/tests/test_networks.py # valid *unchanged( @@ -1426,9 +1404,7 @@ class TestSelect(BaseTest): ] }, # readonly - *xfail(scenarios=[ - ("one", "two", {"readonly": True, "choices": ["one", "two"], "default": "two"}), - ], reason="Should not be overwritten"), + ("one", "two", {"readonly": True, "choices": ["one", "two"], "current_value": "two"}), ] # fmt: on @@ -1476,9 +1452,7 @@ class TestTags(BaseTest): *all_fails(*([t] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), *all_fails(*([str(t)] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), # readonly - *xfail(scenarios=[ - ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "default": "one,two"}), - ], reason="Should not be overwritten"), + ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "current_value": "one,two"}), ] # fmt: on @@ -1526,9 +1500,7 @@ class TestDomain(BaseTest): ("doesnt_exist.pouet", FAIL, {}), ("fake.com", FAIL, {"choices": ["fake.com"]}), # readonly - *xpass(scenarios=[ - (domains1[0], domains1[0], {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + (domains1[0], YunohostError, {"readonly": True}), # readonly is forbidden ] }, { @@ -1625,9 +1597,7 @@ class TestApp(BaseTest): (installed_non_webapp["id"], installed_non_webapp["id"]), (installed_non_webapp["id"], FAIL, {"filter": "is_webapp"}), # readonly - *xpass(scenarios=[ - (installed_non_webapp["id"], installed_non_webapp["id"], {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + (installed_non_webapp["id"], YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] @@ -1744,9 +1714,7 @@ class TestUser(BaseTest): ("", regular_username, {"default": regular_username}) ], reason="Should throw 'no default allowed'"), # readonly - *xpass(scenarios=[ - (admin_username, admin_username, {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + (admin_username, YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] @@ -1821,9 +1789,7 @@ class TestGroup(BaseTest): ("", "custom_group", {"default": "custom_group"}), ], reason="Should throw 'default must be in (None, 'all_users', 'visitors', 'admins')"), # readonly - *xpass(scenarios=[ - ("admins", "admins", {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + ("admins", YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 2c56eb754..355956574 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -465,20 +465,10 @@ class ConfigPanel: "max_progression", ] forbidden_keywords += format_description["sections"] - forbidden_readonly_types = ["password", "app", "domain", "user", "file"] for _, _, option in self._iterate(): if option["id"] in forbidden_keywords: raise YunohostError("config_forbidden_keyword", keyword=option["id"]) - if ( - option.get("readonly", False) - and option.get("type", "string") in forbidden_readonly_types - ): - raise YunohostError( - "config_forbidden_readonly_type", - type=option["type"], - id=option["id"], - ) return self.config diff --git a/src/utils/form.py b/src/utils/form.py index 0da3f892d..071bbaa21 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -204,7 +204,16 @@ class BaseOption: self.hooks = hooks self.type = question.get("type", "string") self.visible = question.get("visible", True) + self.readonly = question.get("readonly", False) + if self.readonly and self.type in {"password", "app", "domain", "user", "group", "file"}: + # FIXME i18n + raise YunohostError( + "config_forbidden_readonly_type", + type=self.type, + id=self.name, + ) + self.ask = question.get("ask", self.name) if not isinstance(self.ask, dict): self.ask = {"en": self.ask} @@ -328,9 +337,10 @@ class BaseInputOption(BaseOption): if self.readonly: text_for_user_input_in_cli = colorize(text_for_user_input_in_cli, "purple") if self.choices: - return ( - text_for_user_input_in_cli + f" {self.choices[self.current_value]}" - ) + choice = self.current_value + if isinstance(self.choices, dict) and choice is not None: + choice = self.choices[choice] + return f"{text_for_user_input_in_cli} {choice}" return text_for_user_input_in_cli + f" {self.humanize(self.current_value)}" elif self.choices: # Prevent displaying a shitload of choices @@ -348,7 +358,9 @@ class BaseInputOption(BaseOption): m18n.n("other_available_options", n=remaining_choices) ] - choices_to_display = " | ".join(choices_to_display) + choices_to_display = " | ".join( + str(choice) for choice in choices_to_display + ) text_for_user_input_in_cli += f" [{choices_to_display}]" @@ -946,7 +958,7 @@ def prompt_or_validate_form( interactive = Moulinette.interface.type == "cli" and os.isatty(1) if isinstance(option, ButtonOption): - if option.is_enabled(context): + if option.is_visible(context) and option.is_enabled(context): continue else: raise YunohostValidationError( @@ -955,32 +967,49 @@ def prompt_or_validate_form( help=_value_for_locale(option.help), ) - if option.is_visible(context): + # FIXME not sure why we do not append Buttons to returned options + options.append(option) + + if not option.is_visible(context): + if isinstance(option, BaseInputOption): + # FIXME There could be several use case if the question is not displayed: + # - we doesn't want to give a specific value + # - we want to keep the previous value + # - we want the default value + option.value = context[option.name] = None + + continue + + message = option._format_text_for_user_input_in_cli() + + if option.readonly: + if interactive: + Moulinette.display(message) + + if isinstance(option, BaseInputOption): + option.value = context[option.name] = option.current_value + + continue + + if isinstance(option, BaseInputOption): for i in range(5): - # Display question if no value filled or if it's a readonly message - if interactive: - text_for_user_input_in_cli = ( - option._format_text_for_user_input_in_cli() + if interactive and option.value is None: + prefill = "" + + if option.current_value is not None: + prefill = option.humanize(option.current_value, option) + elif option.default is not None: + prefill = option.humanize(option.default, option) + + option.value = Moulinette.prompt( + message=message, + is_password=isinstance(option, PasswordOption), + confirm=False, + prefill=prefill, + is_multiline=(option.type == "text"), + autocomplete=option.choices or [], + help=_value_for_locale(option.help), ) - if option.readonly: - Moulinette.display(text_for_user_input_in_cli) - option.value = option.current_value - break - elif option.value is None: - prefill = "" - if option.current_value is not None: - prefill = option.humanize(option.current_value, option) - elif option.default is not None: - prefill = option.humanize(option.default, option) - option.value = Moulinette.prompt( - message=text_for_user_input_in_cli, - is_password=option.hide_user_input_in_prompt, - confirm=False, - prefill=prefill, - is_multiline=(option.type == "text"), - autocomplete=option.choices or [], - help=_value_for_locale(option.help), - ) # Apply default value class_default = getattr(option, "default_value", None) @@ -1013,16 +1042,9 @@ def prompt_or_validate_form( post_hook = f"post_ask__{option.name}" if post_hook in option.hooks: option.values.update(option.hooks[post_hook](option)) - else: - # FIXME There could be several use case if the question is not displayed: - # - we doesn't want to give a specific value - # - we want to keep the previous value - # - we want the default value - option.value = option.values[option.name] = None - answers.update(option.values) - context.update(option.values) - options.append(option) + answers.update(option.values) + context.update(option.values) return options @@ -1070,7 +1092,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: for raw_question in raw_questions: question = OPTIONS[raw_question.get("type", "string")](raw_question) - if question.choices: + if isinstance(question, BaseInputOption) and question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default out.append(raw_question) From 07636fe21e15ebf0fcb77aabe9a752771d1c6901 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 02:36:18 +0200 Subject: [PATCH 0068/1116] form: rename text_cli_* to _get_prompt_message + message --- src/utils/form.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index 071bbaa21..57d4cabcc 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -193,6 +193,8 @@ def evaluate_simple_js_expression(expr, context={}): # │ ╰─╯╵ ╵ ╶┴╴╰─╯╵╰╯╶─╯ │ # ╰───────────────────────────────────────────────────────╯ +FORBIDDEN_READONLY_TYPES = {"password", "app", "domain", "user", "group"} + class BaseOption: def __init__( @@ -206,7 +208,7 @@ class BaseOption: self.visible = question.get("visible", True) self.readonly = question.get("readonly", False) - if self.readonly and self.type in {"password", "app", "domain", "user", "group", "file"}: + if self.readonly and self.type in FORBIDDEN_READONLY_TYPES: # FIXME i18n raise YunohostError( "config_forbidden_readonly_type", @@ -224,7 +226,7 @@ class BaseOption: return evaluate_simple_js_expression(self.visible, context=context) - def _format_text_for_user_input_in_cli(self) -> str: + def _get_prompt_message(self) -> str: return _value_for_locale(self.ask) @@ -254,7 +256,7 @@ class AlertOption(BaseReadonlyOption): super().__init__(question, hooks) self.style = question.get("style", "info") - def _format_text_for_user_input_in_cli(self) -> str: + def _get_prompt_message(self) -> str: text = _value_for_locale(self.ask) if self.style in ["success", "info", "warning", "danger"]: @@ -331,17 +333,17 @@ class BaseInputOption(BaseOption): value = value.strip() return value - def _format_text_for_user_input_in_cli(self) -> str: - text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() + def _get_prompt_message(self) -> str: + message = super()._get_prompt_message() if self.readonly: - text_for_user_input_in_cli = colorize(text_for_user_input_in_cli, "purple") + message = colorize(message, "purple") if self.choices: choice = self.current_value if isinstance(self.choices, dict) and choice is not None: choice = self.choices[choice] - return f"{text_for_user_input_in_cli} {choice}" - return text_for_user_input_in_cli + f" {self.humanize(self.current_value)}" + return f"{message} {choice}" + return message + f" {self.humanize(self.current_value)}" elif self.choices: # Prevent displaying a shitload of choices # (e.g. 100+ available users when choosing an app admin...) @@ -362,9 +364,9 @@ class BaseInputOption(BaseOption): str(choice) for choice in choices_to_display ) - text_for_user_input_in_cli += f" [{choices_to_display}]" + message += f" [{choices_to_display}]" - return text_for_user_input_in_cli + return message def _value_pre_validator(self): if self.value in [None, ""] and not self.optional: @@ -590,13 +592,13 @@ class BooleanOption(BaseInputOption): def get(self, key, default=None): return getattr(self, key, default) - def _format_text_for_user_input_in_cli(self): - text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() + def _get_prompt_message(self): + message = super()._get_prompt_message() if not self.readonly: - text_for_user_input_in_cli += " [yes | no]" + message += " [yes | no]" - return text_for_user_input_in_cli + return message # ─ TIME ────────────────────────────────────────────────── @@ -980,7 +982,7 @@ def prompt_or_validate_form( continue - message = option._format_text_for_user_input_in_cli() + message = option._get_prompt_message() if option.readonly: if interactive: From f0f89d8f2a4fb9b932b2f32d8cf66754fde3a077 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 13:58:24 +0200 Subject: [PATCH 0069/1116] form: restrict choices to select, tags, domain, app, user + group --- src/tests/test_questions.py | 85 +++---------------------- src/utils/form.py | 121 +++++++++++++++++++++++------------- 2 files changed, 84 insertions(+), 122 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 9e7be5db4..3e7927dce 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -17,9 +17,9 @@ from yunohost import app, domain, user from yunohost.utils.form import ( OPTIONS, ask_questions_and_parse_answers, + BaseChoicesOption, BaseInputOption, BaseReadonlyOption, - DisplayTextOption, PasswordOption, DomainOption, WebPathOption, @@ -490,14 +490,12 @@ class BaseTest: option, value = _fill_or_prompt_one_option(raw_option, None) expected_message = option.ask["en"] + choices = [] - if option.choices: - choices = ( - option.choices - if isinstance(option.choices, list) - else option.choices.keys() - ) - expected_message += f" [{' | '.join(choices)}]" + if isinstance(option, BaseChoicesOption): + choices = option.choices + if choices: + expected_message += f" [{' | '.join(choices)}]" if option.type == "boolean": expected_message += " [yes | no]" @@ -507,7 +505,7 @@ class BaseTest: confirm=False, # FIXME no confirm? prefill=prefill, is_multiline=option.type == "text", - autocomplete=option.choices or [], + autocomplete=choices, help=option.help["en"], ) @@ -1972,75 +1970,6 @@ def test_question_string_input_test_ask_with_example(): assert example_text in prompt.call_args[1]["message"] -def test_question_string_with_choice(): - questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} - answers = {"some_string": "fr"} - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "fr" - - -def test_question_string_with_choice_prompt(): - questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} - answers = {"some_string": "fr"} - with patch.object(Moulinette, "prompt", return_value="fr"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "fr" - - -def test_question_string_with_choice_bad(): - questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} - answers = {"some_string": "bad"} - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_string_with_choice_ask(): - ask_text = "some question" - choices = ["fr", "en", "es", "it", "ru"] - questions = { - "some_string": { - "ask": ask_text, - "choices": choices, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="ru") as prompt, patch.object( - os, "isatty", return_value=True - ): - ask_questions_and_parse_answers(questions, answers) - assert ask_text in prompt.call_args[1]["message"] - - for choice in choices: - assert choice in prompt.call_args[1]["message"] - - -def test_question_string_with_choice_default(): - questions = { - "some_string": { - "type": "string", - "choices": ["fr", "en"], - "default": "en", - } - } - answers = {} - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "en" - - @pytest.mark.skip # we should do something with this example def test_question_password_input_test_ask_with_example(): ask_text = "some question" diff --git a/src/utils/form.py b/src/utils/form.py index 57d4cabcc..02f51b6c4 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -306,8 +306,6 @@ class BaseInputOption(BaseOption): super().__init__(question, hooks) self.default = question.get("default", None) self.optional = question.get("optional", False) - # Don't restrict choices if there's none specified - self.choices = question.get("choices", None) self.pattern = question.get("pattern", self.pattern) self.help = question.get("help") self.redact = question.get("redact", False) @@ -338,33 +336,7 @@ class BaseInputOption(BaseOption): if self.readonly: message = colorize(message, "purple") - if self.choices: - choice = self.current_value - if isinstance(self.choices, dict) and choice is not None: - choice = self.choices[choice] - return f"{message} {choice}" - return message + f" {self.humanize(self.current_value)}" - elif self.choices: - # Prevent displaying a shitload of choices - # (e.g. 100+ available users when choosing an app admin...) - choices = ( - list(self.choices.keys()) - if isinstance(self.choices, dict) - else self.choices - ) - choices_to_display = choices[:20] - remaining_choices = len(choices[20:]) - - if remaining_choices > 0: - choices_to_display += [ - m18n.n("other_available_options", n=remaining_choices) - ] - - choices_to_display = " | ".join( - str(choice) for choice in choices_to_display - ) - - message += f" [{choices_to_display}]" + return f"{message} {self.humanize(self.current_value)}" return message @@ -374,13 +346,6 @@ class BaseInputOption(BaseOption): # we have an answer, do some post checks if self.value not in [None, ""]: - if self.choices and self.value not in self.choices: - raise YunohostValidationError( - "app_argument_choice_invalid", - name=self.name, - value=self.value, - choices=", ".join(str(choice) for choice in self.choices), - ) if self.pattern and not re.match(self.pattern["regexp"], str(self.value)): raise YunohostValidationError( self.pattern["error"], @@ -746,7 +711,72 @@ class FileOption(BaseInputOption): # ─ CHOICES ─────────────────────────────────────────────── -class TagsOption(BaseInputOption): +class BaseChoicesOption(BaseInputOption): + def __init__( + self, + question: Dict[str, Any], + hooks: Dict[str, Callable] = {}, + ): + super().__init__(question, hooks) + # Don't restrict choices if there's none specified + self.choices = question.get("choices", None) + + def _get_prompt_message(self) -> str: + message = super()._get_prompt_message() + + if self.readonly: + message = message + choice = self.current_value + + if isinstance(self.choices, dict) and choice is not None: + choice = self.choices[choice] + + return f"{colorize(message, 'purple')} {choice}" + + if self.choices: + # Prevent displaying a shitload of choices + # (e.g. 100+ available users when choosing an app admin...) + choices = ( + list(self.choices.keys()) + if isinstance(self.choices, dict) + else self.choices + ) + choices_to_display = choices[:20] + remaining_choices = len(choices[20:]) + + if remaining_choices > 0: + choices_to_display += [ + m18n.n("other_available_options", n=remaining_choices) + ] + + choices_to_display = " | ".join( + str(choice) for choice in choices_to_display + ) + + return f"{message} [{choices_to_display}]" + + return message + + def _value_pre_validator(self): + super()._value_pre_validator() + + # we have an answer, do some post checks + if self.value not in [None, ""]: + if self.choices and self.value not in self.choices: + raise YunohostValidationError( + "app_argument_choice_invalid", + name=self.name, + value=self.value, + choices=", ".join(str(choice) for choice in self.choices), + ) + + +class SelectOption(BaseChoicesOption): + argument_type = "select" + default_value = "" + + +class TagsOption(BaseChoicesOption): argument_type = "tags" default_value = "" @@ -799,7 +829,7 @@ class TagsOption(BaseInputOption): # ─ ENTITIES ────────────────────────────────────────────── -class DomainOption(BaseInputOption): +class DomainOption(BaseChoicesOption): argument_type = "domain" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -828,7 +858,7 @@ class DomainOption(BaseInputOption): return value -class AppOption(BaseInputOption): +class AppOption(BaseChoicesOption): argument_type = "app" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -853,7 +883,7 @@ class AppOption(BaseInputOption): self.choices.update({app["id"]: _app_display(app) for app in apps}) -class UserOption(BaseInputOption): +class UserOption(BaseChoicesOption): argument_type = "user" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -884,7 +914,7 @@ class UserOption(BaseInputOption): break -class GroupOption(BaseInputOption): +class GroupOption(BaseChoicesOption): argument_type = "group" def __init__(self, question, hooks: Dict[str, Callable] = {}): @@ -926,7 +956,7 @@ OPTIONS = { "path": WebPathOption, "url": URLOption, "file": FileOption, - "select": StringOption, + "select": SelectOption, "tags": TagsOption, "domain": DomainOption, "app": AppOption, @@ -997,6 +1027,9 @@ def prompt_or_validate_form( for i in range(5): if interactive and option.value is None: prefill = "" + choices = ( + option.choices if isinstance(option, BaseChoicesOption) else [] + ) if option.current_value is not None: prefill = option.humanize(option.current_value, option) @@ -1009,7 +1042,7 @@ def prompt_or_validate_form( confirm=False, prefill=prefill, is_multiline=(option.type == "text"), - autocomplete=option.choices or [], + autocomplete=choices, help=_value_for_locale(option.help), ) @@ -1094,7 +1127,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: for raw_question in raw_questions: question = OPTIONS[raw_question.get("type", "string")](raw_question) - if isinstance(question, BaseInputOption) and question.choices: + if isinstance(question, BaseChoicesOption) and question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default out.append(raw_question) From c439c47d67b5fe7e0dece709212d4d6d3b18549c Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 14:00:04 +0200 Subject: [PATCH 0070/1116] form: restrict filter to AppOption --- src/utils/form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/form.py b/src/utils/form.py index 02f51b6c4..edae7717b 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -309,7 +309,6 @@ class BaseInputOption(BaseOption): self.pattern = question.get("pattern", self.pattern) self.help = question.get("help") self.redact = question.get("redact", False) - self.filter = question.get("filter", None) # .current_value is the currently stored value self.current_value = question.get("current_value") # .value is the "proposed" value which we got from the user @@ -865,6 +864,7 @@ class AppOption(BaseChoicesOption): from yunohost.app import app_list super().__init__(question, hooks) + self.filter = question.get("filter", None) apps = app_list(full=True)["apps"] From fe2761da4ab2289d5b622ab061d323ad603f9f2a Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 14:05:03 +0200 Subject: [PATCH 0071/1116] configpanel: fix choices --- src/utils/configpanel.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 355956574..2914ae11f 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -30,6 +30,8 @@ from moulinette.utils.log import getActionLogger from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.form import ( OPTIONS, + BaseChoicesOption, + BaseInputOption, BaseOption, FileOption, ask_questions_and_parse_answers, @@ -148,9 +150,11 @@ class ConfigPanel: option["ask"] = ask question_class = OPTIONS[option.get("type", "string")] # FIXME : maybe other properties should be taken from the question, not just choices ?. - option["choices"] = question_class(option).choices - option["default"] = question_class(option).default - option["pattern"] = question_class(option).pattern + if issubclass(question_class, BaseChoicesOption): + option["choices"] = question_class(option).choices + if issubclass(question_class, BaseInputOption): + option["default"] = question_class(option).default + option["pattern"] = question_class(option).pattern else: result[key] = {"ask": ask} if "current_value" in option: From 1c7d427be0fea47d64a3abf67cf7a86b631c4d9a Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 14:28:00 +0200 Subject: [PATCH 0072/1116] form: remove hooks from Option's attrs --- src/utils/form.py | 68 ++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index edae7717b..b455fe812 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -200,10 +200,8 @@ class BaseOption: def __init__( self, question: Dict[str, Any], - hooks: Dict[str, Callable] = {}, ): self.name = question["name"] - self.hooks = hooks self.type = question.get("type", "string") self.visible = question.get("visible", True) @@ -236,8 +234,8 @@ class BaseOption: class BaseReadonlyOption(BaseOption): - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.readonly = True @@ -252,8 +250,8 @@ class MarkdownOption(BaseReadonlyOption): class AlertOption(BaseReadonlyOption): argument_type = "alert" - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.style = question.get("style", "info") def _get_prompt_message(self) -> str: @@ -276,8 +274,8 @@ class ButtonOption(BaseReadonlyOption): argument_type = "button" enabled = True - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.help = question.get("help") self.style = question.get("style", "success") self.enabled = question.get("enabled", True) @@ -298,12 +296,8 @@ class BaseInputOption(BaseOption): hide_user_input_in_prompt = False pattern: Optional[Dict] = None - def __init__( - self, - question: Dict[str, Any], - hooks: Dict[str, Callable] = {}, - ): - super().__init__(question, hooks) + def __init__(self, question: Dict[str, Any]): + super().__init__(question) self.default = question.get("default", None) self.optional = question.get("optional", False) self.pattern = question.get("pattern", self.pattern) @@ -390,8 +384,8 @@ class PasswordOption(BaseInputOption): default_value = "" forbidden_chars = "{}" - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.redact = True if self.default is not None: raise YunohostValidationError( @@ -427,8 +421,8 @@ class NumberOption(BaseInputOption): argument_type = "number" default_value = None - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.min = question.get("min", None) self.max = question.get("max", None) self.step = question.get("step", None) @@ -483,8 +477,8 @@ class BooleanOption(BaseInputOption): yes_answers = ["1", "yes", "y", "true", "t", "on"] no_answers = ["0", "no", "n", "false", "f", "off"] - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.yes = question.get("yes", 1) self.no = question.get("no", 0) if self.default is None: @@ -648,8 +642,8 @@ class FileOption(BaseInputOption): argument_type = "file" upload_dirs: List[str] = [] - def __init__(self, question, hooks: Dict[str, Callable] = {}): - super().__init__(question, hooks) + def __init__(self, question): + super().__init__(question) self.accept = question.get("accept", "") @classmethod @@ -714,9 +708,8 @@ class BaseChoicesOption(BaseInputOption): def __init__( self, question: Dict[str, Any], - hooks: Dict[str, Callable] = {}, ): - super().__init__(question, hooks) + super().__init__(question) # Don't restrict choices if there's none specified self.choices = question.get("choices", None) @@ -831,10 +824,10 @@ class TagsOption(BaseChoicesOption): class DomainOption(BaseChoicesOption): argument_type = "domain" - def __init__(self, question, hooks: Dict[str, Callable] = {}): + def __init__(self, question): from yunohost.domain import domain_list, _get_maindomain - super().__init__(question, hooks) + super().__init__(question) if self.default is None: self.default = _get_maindomain() @@ -860,10 +853,10 @@ class DomainOption(BaseChoicesOption): class AppOption(BaseChoicesOption): argument_type = "app" - def __init__(self, question, hooks: Dict[str, Callable] = {}): + def __init__(self, question): from yunohost.app import app_list - super().__init__(question, hooks) + super().__init__(question) self.filter = question.get("filter", None) apps = app_list(full=True)["apps"] @@ -886,11 +879,11 @@ class AppOption(BaseChoicesOption): class UserOption(BaseChoicesOption): argument_type = "user" - def __init__(self, question, hooks: Dict[str, Callable] = {}): + def __init__(self, question): from yunohost.user import user_list, user_info from yunohost.domain import _get_maindomain - super().__init__(question, hooks) + super().__init__(question) self.choices = { username: f"{infos['fullname']} ({infos['mail']})" @@ -917,7 +910,7 @@ class UserOption(BaseChoicesOption): class GroupOption(BaseChoicesOption): argument_type = "group" - def __init__(self, question, hooks: Dict[str, Callable] = {}): + def __init__(self, question): from yunohost.user import user_group_list super().__init__(question) @@ -972,11 +965,14 @@ OPTIONS = { # ╰───────────────────────────────────────────────────────╯ +Hooks = dict[str, Callable[[BaseInputOption], Any]] + + def prompt_or_validate_form( raw_options: dict[str, Any], prefilled_answers: dict[str, Any] = {}, context: Context = {}, - hooks: dict[str, Callable[[], None]] = {}, + hooks: Hooks = {}, ) -> list[BaseOption]: options = [] answers = {**prefilled_answers} @@ -985,7 +981,7 @@ def prompt_or_validate_form( raw_option["name"] = name raw_option["value"] = answers.get(name) question_class = OPTIONS[raw_option.get("type", "string")] - option = question_class(raw_option, hooks=hooks) + option = question_class(raw_option) interactive = Moulinette.interface.type == "cli" and os.isatty(1) @@ -1075,8 +1071,8 @@ def prompt_or_validate_form( # Search for post actions in hooks post_hook = f"post_ask__{option.name}" - if post_hook in option.hooks: - option.values.update(option.hooks[post_hook](option)) + if post_hook in hooks: + option.values.update(hooks[post_hook](option)) answers.update(option.values) context.update(option.values) @@ -1088,7 +1084,7 @@ def ask_questions_and_parse_answers( raw_options: dict[str, Any], prefilled_answers: Union[str, Mapping[str, Any]] = {}, current_values: Mapping[str, Any] = {}, - hooks: Dict[str, Callable[[], None]] = {}, + hooks: Hooks = {}, ) -> list[BaseOption]: """Parse arguments store in either manifest.json or actions.json or from a config panel against the user answers when they are present. From e87f8ef93a417b4287c6bed3a88210b0be196569 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 15:10:23 +0200 Subject: [PATCH 0073/1116] form: use Enum for Option's type --- src/utils/configpanel.py | 25 ++++--- src/utils/form.py | 154 ++++++++++++++++++++++++++------------- 2 files changed, 118 insertions(+), 61 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 2914ae11f..f5d802356 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -34,6 +34,7 @@ from yunohost.utils.form import ( BaseInputOption, BaseOption, FileOption, + OptionType, ask_questions_and_parse_answers, evaluate_simple_js_expression, ) @@ -148,7 +149,7 @@ class ConfigPanel: if mode == "full": option["ask"] = ask - question_class = OPTIONS[option.get("type", "string")] + question_class = OPTIONS[option.get("type", OptionType.string)] # FIXME : maybe other properties should be taken from the question, not just choices ?. if issubclass(question_class, BaseChoicesOption): option["choices"] = question_class(option).choices @@ -158,7 +159,7 @@ class ConfigPanel: else: result[key] = {"ask": ask} if "current_value" in option: - question_class = OPTIONS[option.get("type", "string")] + question_class = OPTIONS[option.get("type", OptionType.string)] result[key]["value"] = question_class.humanize( option["current_value"], option ) @@ -243,7 +244,7 @@ class ConfigPanel: self.filter_key = "" self._get_config_panel() for panel, section, option in self._iterate(): - if option["type"] == "button": + if option["type"] == OptionType.button: key = f"{panel['id']}.{section['id']}.{option['id']}" actions[key] = _value_for_locale(option["ask"]) @@ -425,7 +426,7 @@ class ConfigPanel: subnode["name"] = key # legacy subnode.setdefault("optional", raw_infos.get("optional", True)) # If this section contains at least one button, it becomes an "action" section - if subnode.get("type") == "button": + if subnode.get("type") == OptionType.button: out["is_action_section"] = True out.setdefault(sublevel, []).append(subnode) # Key/value are a property @@ -500,13 +501,13 @@ class ConfigPanel: # Hydrating config panel with current value for _, section, option in self._iterate(): if option["id"] not in self.values: - allowed_empty_types = [ - "alert", - "display_text", - "markdown", - "file", - "button", - ] + allowed_empty_types = { + OptionType.alert, + OptionType.display_text, + OptionType.markdown, + OptionType.file, + OptionType.button, + } if section["is_action_section"] and option.get("default") is not None: self.values[option["id"]] = option["default"] @@ -587,7 +588,7 @@ class ConfigPanel: section["options"] = [ option for option in section["options"] - if option.get("type", "string") != "button" + if option.get("type", OptionType.string) != OptionType.button or option["id"] == action ] diff --git a/src/utils/form.py b/src/utils/form.py index b455fe812..62750657b 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -23,7 +23,8 @@ import re import shutil import tempfile import urllib.parse -from typing import Any, Callable, Dict, List, Mapping, Optional, Union +from enum import Enum +from typing import Any, Callable, Dict, List, Literal, Mapping, Optional, Union from moulinette import Moulinette, m18n from moulinette.interfaces.cli import colorize @@ -193,7 +194,50 @@ def evaluate_simple_js_expression(expr, context={}): # │ ╰─╯╵ ╵ ╶┴╴╰─╯╵╰╯╶─╯ │ # ╰───────────────────────────────────────────────────────╯ -FORBIDDEN_READONLY_TYPES = {"password", "app", "domain", "user", "group"} + +class OptionType(str, Enum): + # display + display_text = "display_text" + markdown = "markdown" + alert = "alert" + # action + button = "button" + # text + string = "string" + text = "text" + password = "password" + color = "color" + # numeric + number = "number" + range = "range" + # boolean + boolean = "boolean" + # time + date = "date" + time = "time" + # location + email = "email" + path = "path" + url = "url" + # file + file = "file" + # choice + select = "select" + tags = "tags" + # entity + domain = "domain" + app = "app" + user = "user" + group = "group" + + +FORBIDDEN_READONLY_TYPES = { + OptionType.password, + OptionType.app, + OptionType.domain, + OptionType.user, + OptionType.group, +} class BaseOption: @@ -202,7 +246,7 @@ class BaseOption: question: Dict[str, Any], ): self.name = question["name"] - self.type = question.get("type", "string") + self.type = question.get("type", OptionType.string) self.visible = question.get("visible", True) self.readonly = question.get("readonly", False) @@ -240,15 +284,15 @@ class BaseReadonlyOption(BaseOption): class DisplayTextOption(BaseReadonlyOption): - argument_type = "display_text" + type: Literal[OptionType.display_text] = OptionType.display_text class MarkdownOption(BaseReadonlyOption): - argument_type = "markdown" + type: Literal[OptionType.markdown] = OptionType.markdown class AlertOption(BaseReadonlyOption): - argument_type = "alert" + type: Literal[OptionType.alert] = OptionType.alert def __init__(self, question): super().__init__(question) @@ -271,7 +315,7 @@ class AlertOption(BaseReadonlyOption): class ButtonOption(BaseReadonlyOption): - argument_type = "button" + type: Literal[OptionType.button] = OptionType.button enabled = True def __init__(self, question): @@ -373,14 +417,21 @@ class BaseInputOption(BaseOption): # ─ STRINGS ─────────────────────────────────────────────── -class StringOption(BaseInputOption): - argument_type = "string" +class BaseStringOption(BaseInputOption): default_value = "" +class StringOption(BaseStringOption): + type: Literal[OptionType.string] = OptionType.string + + +class TextOption(BaseStringOption): + type: Literal[OptionType.text] = OptionType.text + + class PasswordOption(BaseInputOption): + type: Literal[OptionType.password] = OptionType.password hide_user_input_in_prompt = True - argument_type = "password" default_value = "" forbidden_chars = "{}" @@ -407,7 +458,8 @@ class PasswordOption(BaseInputOption): assert_password_is_strong_enough("user", self.value) -class ColorOption(StringOption): +class ColorOption(BaseStringOption): + type: Literal[OptionType.color] = OptionType.color pattern = { "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", "error": "config_validate_color", # i18n: config_validate_color @@ -418,7 +470,7 @@ class ColorOption(StringOption): class NumberOption(BaseInputOption): - argument_type = "number" + type: Literal[OptionType.number, OptionType.range] = OptionType.number default_value = None def __init__(self, question): @@ -472,7 +524,7 @@ class NumberOption(BaseInputOption): class BooleanOption(BaseInputOption): - argument_type = "boolean" + type: Literal[OptionType.boolean] = OptionType.boolean default_value = 0 yes_answers = ["1", "yes", "y", "true", "t", "on"] no_answers = ["0", "no", "n", "false", "f", "off"] @@ -562,7 +614,8 @@ class BooleanOption(BaseInputOption): # ─ TIME ────────────────────────────────────────────────── -class DateOption(StringOption): +class DateOption(BaseStringOption): + type: Literal[OptionType.date] = OptionType.date pattern = { "regexp": r"^\d{4}-\d\d-\d\d$", "error": "config_validate_date", # i18n: config_validate_date @@ -580,7 +633,8 @@ class DateOption(StringOption): raise YunohostValidationError("config_validate_date") -class TimeOption(StringOption): +class TimeOption(BaseStringOption): + type: Literal[OptionType.time] = OptionType.time pattern = { "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", "error": "config_validate_time", # i18n: config_validate_time @@ -590,7 +644,8 @@ class TimeOption(StringOption): # ─ LOCATIONS ───────────────────────────────────────────── -class EmailOption(StringOption): +class EmailOption(BaseStringOption): + type: Literal[OptionType.email] = OptionType.email pattern = { "regexp": r"^.+@.+", "error": "config_validate_email", # i18n: config_validate_email @@ -598,7 +653,7 @@ class EmailOption(StringOption): class WebPathOption(BaseInputOption): - argument_type = "path" + type: Literal[OptionType.path] = OptionType.path default_value = "" @staticmethod @@ -628,7 +683,8 @@ class WebPathOption(BaseInputOption): return "/" + value.strip().strip(" /") -class URLOption(StringOption): +class URLOption(BaseStringOption): + type: Literal[OptionType.url] = OptionType.url pattern = { "regexp": r"^https?://.*$", "error": "config_validate_url", # i18n: config_validate_url @@ -639,7 +695,7 @@ class URLOption(StringOption): class FileOption(BaseInputOption): - argument_type = "file" + type: Literal[OptionType.file] = OptionType.file upload_dirs: List[str] = [] def __init__(self, question): @@ -764,12 +820,12 @@ class BaseChoicesOption(BaseInputOption): class SelectOption(BaseChoicesOption): - argument_type = "select" + type: Literal[OptionType.select] = OptionType.select default_value = "" class TagsOption(BaseChoicesOption): - argument_type = "tags" + type: Literal[OptionType.tags] = OptionType.tags default_value = "" @staticmethod @@ -822,7 +878,7 @@ class TagsOption(BaseChoicesOption): class DomainOption(BaseChoicesOption): - argument_type = "domain" + type: Literal[OptionType.domain] = OptionType.domain def __init__(self, question): from yunohost.domain import domain_list, _get_maindomain @@ -851,7 +907,7 @@ class DomainOption(BaseChoicesOption): class AppOption(BaseChoicesOption): - argument_type = "app" + type: Literal[OptionType.app] = OptionType.app def __init__(self, question): from yunohost.app import app_list @@ -877,7 +933,7 @@ class AppOption(BaseChoicesOption): class UserOption(BaseChoicesOption): - argument_type = "user" + type: Literal[OptionType.user] = OptionType.user def __init__(self, question): from yunohost.user import user_list, user_info @@ -908,7 +964,7 @@ class UserOption(BaseChoicesOption): class GroupOption(BaseChoicesOption): - argument_type = "group" + type: Literal[OptionType.group] = OptionType.group def __init__(self, question): from yunohost.user import user_group_list @@ -932,29 +988,29 @@ class GroupOption(BaseChoicesOption): OPTIONS = { - "display_text": DisplayTextOption, - "markdown": MarkdownOption, - "alert": AlertOption, - "button": ButtonOption, - "string": StringOption, - "text": StringOption, - "password": PasswordOption, - "color": ColorOption, - "number": NumberOption, - "range": NumberOption, - "boolean": BooleanOption, - "date": DateOption, - "time": TimeOption, - "email": EmailOption, - "path": WebPathOption, - "url": URLOption, - "file": FileOption, - "select": SelectOption, - "tags": TagsOption, - "domain": DomainOption, - "app": AppOption, - "user": UserOption, - "group": GroupOption, + OptionType.display_text: DisplayTextOption, + OptionType.markdown: MarkdownOption, + OptionType.alert: AlertOption, + OptionType.button: ButtonOption, + OptionType.string: StringOption, + OptionType.text: StringOption, + OptionType.password: PasswordOption, + OptionType.color: ColorOption, + OptionType.number: NumberOption, + OptionType.range: NumberOption, + OptionType.boolean: BooleanOption, + OptionType.date: DateOption, + OptionType.time: TimeOption, + OptionType.email: EmailOption, + OptionType.path: WebPathOption, + OptionType.url: URLOption, + OptionType.file: FileOption, + OptionType.select: SelectOption, + OptionType.tags: TagsOption, + OptionType.domain: DomainOption, + OptionType.app: AppOption, + OptionType.user: UserOption, + OptionType.group: GroupOption, } @@ -1122,7 +1178,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: out = [] for raw_question in raw_questions: - question = OPTIONS[raw_question.get("type", "string")](raw_question) + question = OPTIONS[raw_question.get("type", OptionType.string)](raw_question) if isinstance(question, BaseChoicesOption) and question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default From c1f0ac04c7597467b8f179fdaaff30d36f3ae0ce Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 15:54:56 +0200 Subject: [PATCH 0074/1116] rename Option.name to Option.id --- src/app.py | 20 +++++++------- src/tests/test_questions.py | 10 +++---- src/utils/configpanel.py | 6 ++--- src/utils/form.py | 54 ++++++++++++++++++------------------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/app.py b/src/app.py index 97227ed0c..c64a5d860 100644 --- a/src/app.py +++ b/src/app.py @@ -1099,7 +1099,7 @@ def app_install( raw_questions = manifest["install"] questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) args = { - question.name: question.value + question.id: question.value for question in questions if question.value is not None } @@ -1147,7 +1147,7 @@ def app_install( if question.type == "password": continue - app_settings[question.name] = question.value + app_settings[question.id] = question.value _set_app_settings(app_instance_name, app_settings) @@ -1202,16 +1202,16 @@ def app_install( # Reinject user-provider passwords which are not in the app settings # (cf a few line before) if question.type == "password": - env_dict[question.name] = question.value + env_dict[question.id] = question.value # We want to hav the env_dict in the log ... but not password values env_dict_for_logging = env_dict.copy() for question in questions: # Or should it be more generally question.redact ? if question.type == "password": - del env_dict_for_logging[f"YNH_APP_ARG_{question.name.upper()}"] - if question.name in env_dict_for_logging: - del env_dict_for_logging[question.name] + del env_dict_for_logging[f"YNH_APP_ARG_{question.id.upper()}"] + if question.id in env_dict_for_logging: + del env_dict_for_logging[question.id] operation_logger.extra.update({"env": env_dict_for_logging}) @@ -2358,17 +2358,17 @@ def _set_default_ask_questions(questions, script_name="install"): ), # i18n: app_manifest_install_ask_init_admin_permission ] - for question_name, question in questions.items(): - question["name"] = question_name + for question_id, question in questions.items(): + question["id"] = question_id # If this question corresponds to a question with default ask message... if any( - (question.get("type"), question["name"]) == question_with_default + (question.get("type"), question["id"]) == question_with_default for question_with_default in questions_with_default ): # The key is for example "app_manifest_install_ask_domain" question["ask"] = m18n.n( - f"app_manifest_{script_name}_ask_{question['name']}" + f"app_manifest_{script_name}_ask_{question['id']}" ) # Also it in fact doesn't make sense for any of those questions to have an example value nor a default value... diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 3e7927dce..7ef678d19 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -33,7 +33,7 @@ from yunohost.utils.error import YunohostError, YunohostValidationError """ Argument default format: { - "the_name": { + "the_id": { "type": "one_of_the_available_type", // "sting" is not specified "ask": { "en": "the question in english", @@ -50,7 +50,7 @@ Argument default format: } User answers: -{"the_name": "value", ...} +{"the_id": "value", ...} """ @@ -443,7 +443,7 @@ class BaseTest: assert isinstance(option, OPTIONS[raw_option["type"]]) assert option.type == raw_option["type"] - assert option.name == id_ + assert option.id == id_ assert option.ask == {"en": id_} assert option.readonly is (True if is_special_readonly_option else False) assert option.visible is True @@ -1913,7 +1913,7 @@ def test_options_query_string(): ) def _assert_correct_values(options, raw_options): - form = {option.name: option.value for option in options} + form = {option.id: option.value for option in options} for k, v in results.items(): if k == "file_id": @@ -1945,7 +1945,7 @@ def test_question_string_default_type(): out = ask_questions_and_parse_answers(questions, answers)[0] - assert out.name == "some_string" + assert out.id == "some_string" assert out.type == "string" assert out.value == "some_value" diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index f5d802356..42a030cbc 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -521,7 +521,7 @@ class ConfigPanel: f"Config panel question '{option['id']}' should be initialized with a value during install or upgrade.", raw_msg=True, ) - value = self.values[option["name"]] + value = self.values[option["id"]] # Allow to use value instead of current_value in app config script. # e.g. apps may write `echo 'value: "foobar"'` in the config file (which is more intuitive that `echo 'current_value: "foobar"'` @@ -600,14 +600,14 @@ class ConfigPanel: prefilled_answers.update(self.new_values) questions = ask_questions_and_parse_answers( - {question["name"]: question for question in section["options"]}, + {question["id"]: question for question in section["options"]}, prefilled_answers=prefilled_answers, current_values=self.values, hooks=self.hooks, ) self.new_values.update( { - question.name: question.value + question.id: question.value for question in questions if question.value is not None } diff --git a/src/utils/form.py b/src/utils/form.py index 62750657b..57cb1cd5b 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -245,7 +245,7 @@ class BaseOption: self, question: Dict[str, Any], ): - self.name = question["name"] + self.id = question["id"] self.type = question.get("type", OptionType.string) self.visible = question.get("visible", True) @@ -255,10 +255,10 @@ class BaseOption: raise YunohostError( "config_forbidden_readonly_type", type=self.type, - id=self.name, + id=self.id, ) - self.ask = question.get("ask", self.name) + self.ask = question.get("ask", self.id) if not isinstance(self.ask, dict): self.ask = {"en": self.ask} @@ -379,14 +379,14 @@ class BaseInputOption(BaseOption): def _value_pre_validator(self): if self.value in [None, ""] and not self.optional: - raise YunohostValidationError("app_argument_required", name=self.name) + raise YunohostValidationError("app_argument_required", name=self.id) # we have an answer, do some post checks if self.value not in [None, ""]: if self.pattern and not re.match(self.pattern["regexp"], str(self.value)): raise YunohostValidationError( self.pattern["error"], - name=self.name, + name=self.id, value=self.value, ) @@ -440,7 +440,7 @@ class PasswordOption(BaseInputOption): self.redact = True if self.default is not None: raise YunohostValidationError( - "app_argument_password_no_default", name=self.name + "app_argument_password_no_default", name=self.id ) def _value_pre_validator(self): @@ -496,7 +496,7 @@ class NumberOption(BaseInputOption): option = option.__dict__ if isinstance(option, BaseOption) else option raise YunohostValidationError( "app_argument_invalid", - name=option.get("name"), + name=option.get("id"), error=m18n.n("invalid_number"), ) @@ -508,14 +508,14 @@ class NumberOption(BaseInputOption): if self.min is not None and int(self.value) < self.min: raise YunohostValidationError( "app_argument_invalid", - name=self.name, + name=self.id, error=m18n.n("invalid_number_min", min=self.min), ) if self.max is not None and int(self.value) > self.max: raise YunohostValidationError( "app_argument_invalid", - name=self.name, + name=self.id, error=m18n.n("invalid_number_max", max=self.max), ) @@ -554,7 +554,7 @@ class BooleanOption(BaseInputOption): raise YunohostValidationError( "app_argument_choice_invalid", - name=option.get("name"), + name=option.get("id"), value=value, choices="yes/no", ) @@ -594,7 +594,7 @@ class BooleanOption(BaseInputOption): raise YunohostValidationError( "app_argument_choice_invalid", - name=option.get("name"), + name=option.get("id"), value=strvalue, choices="yes/no", ) @@ -663,7 +663,7 @@ class WebPathOption(BaseInputOption): if not isinstance(value, str): raise YunohostValidationError( "app_argument_invalid", - name=option.get("name"), + name=option.get("id"), error="Argument for path should be a string.", ) @@ -676,7 +676,7 @@ class WebPathOption(BaseInputOption): elif option.get("optional") is False: raise YunohostValidationError( "app_argument_invalid", - name=option.get("name"), + name=option.get("id"), error="Option is mandatory", ) @@ -725,7 +725,7 @@ class FileOption(BaseInputOption): ): raise YunohostValidationError( "app_argument_invalid", - name=self.name, + name=self.id, error=m18n.n("file_does_not_exist", path=str(self.value)), ) @@ -740,7 +740,7 @@ class FileOption(BaseInputOption): FileOption.upload_dirs += [upload_dir] - logger.debug(f"Saving file {self.name} for file question into {file_path}") + logger.debug(f"Saving file {self.id} for file question into {file_path}") def is_file_path(s): return isinstance(s, str) and s.startswith("/") and os.path.exists(s) @@ -813,7 +813,7 @@ class BaseChoicesOption(BaseInputOption): if self.choices and self.value not in self.choices: raise YunohostValidationError( "app_argument_choice_invalid", - name=self.name, + name=self.id, value=self.value, choices=", ".join(str(choice) for choice in self.choices), ) @@ -853,13 +853,13 @@ class TagsOption(BaseChoicesOption): if self.choices: raise YunohostValidationError( "app_argument_choice_invalid", - name=self.name, + name=self.id, value=self.value, choices=", ".join(str(choice) for choice in self.choices), ) raise YunohostValidationError( "app_argument_invalid", - name=self.name, + name=self.id, error=f"'{str(self.value)}' is not a list", ) @@ -949,7 +949,7 @@ class UserOption(BaseChoicesOption): if not self.choices: raise YunohostValidationError( "app_argument_invalid", - name=self.name, + name=self.id, error="You should create a YunoHost user first.", ) @@ -1033,9 +1033,9 @@ def prompt_or_validate_form( options = [] answers = {**prefilled_answers} - for name, raw_option in raw_options.items(): - raw_option["name"] = name - raw_option["value"] = answers.get(name) + for id_, raw_option in raw_options.items(): + raw_option["id"] = id_ + raw_option["value"] = answers.get(id_) question_class = OPTIONS[raw_option.get("type", "string")] option = question_class(raw_option) @@ -1047,7 +1047,7 @@ def prompt_or_validate_form( else: raise YunohostValidationError( "config_action_disabled", - action=option.name, + action=option.id, help=_value_for_locale(option.help), ) @@ -1060,7 +1060,7 @@ def prompt_or_validate_form( # - we doesn't want to give a specific value # - we want to keep the previous value # - we want the default value - option.value = context[option.name] = None + option.value = context[option.id] = None continue @@ -1071,7 +1071,7 @@ def prompt_or_validate_form( Moulinette.display(message) if isinstance(option, BaseInputOption): - option.value = context[option.name] = option.current_value + option.value = context[option.id] = option.current_value continue @@ -1123,10 +1123,10 @@ def prompt_or_validate_form( break - option.value = option.values[option.name] = option._value_post_validator() + option.value = option.values[option.id] = option._value_post_validator() # Search for post actions in hooks - post_hook = f"post_ask__{option.name}" + post_hook = f"post_ask__{option.id}" if post_hook in hooks: option.values.update(hooks[post_hook](option)) From 4df7e4681dcdca089a269bb9bb63ce6355b19896 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 30 Apr 2023 17:15:40 +0200 Subject: [PATCH 0075/1116] form: force option type to 'select' if there's 'choices' + add test --- src/tests/test_questions.py | 15 +++++++++++++++ src/utils/form.py | 23 ++++++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 7ef678d19..7737c4546 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -1950,6 +1950,21 @@ def test_question_string_default_type(): assert out.value == "some_value" +def test_option_default_type_with_choices_is_select(): + questions = { + "some_choices": {"choices": ["a", "b"]}, + # LEGACY (`choices` in option `string` used to be valid) + # make sure this result as a `select` option + "some_legacy": {"type": "string", "choices": ["a", "b"]} + } + answers = {"some_choices": "a", "some_legacy": "a"} + + options = ask_questions_and_parse_answers(questions, answers) + for option in options: + assert option.type == "select" + assert option.value == "a" + + @pytest.mark.skip # we should do something with this example def test_question_string_input_test_ask_with_example(): ask_text = "some question" diff --git a/src/utils/form.py b/src/utils/form.py index 57cb1cd5b..1ca03373e 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1014,6 +1014,22 @@ OPTIONS = { } +def hydrate_option_type(raw_option: dict[str, Any]) -> dict[str, Any]: + type_ = raw_option.get( + "type", OptionType.select if "choices" in raw_option else OptionType.string + ) + # LEGACY (`choices` in option `string` used to be valid) + if "choices" in raw_option and type_ == OptionType.string: + logger.warning( + f"Packagers: option {raw_option['id']} has 'choices' but has type 'string', use 'select' instead to remove this warning." + ) + type_ = OptionType.select + + raw_option["type"] = type_ + + return raw_option + + # ╭───────────────────────────────────────────────────────╮ # │ ╷ ╷╶┬╴╶┬╴╷ ╭─╴ │ # │ │ │ │ │ │ ╰─╮ │ @@ -1036,8 +1052,8 @@ def prompt_or_validate_form( for id_, raw_option in raw_options.items(): raw_option["id"] = id_ raw_option["value"] = answers.get(id_) - question_class = OPTIONS[raw_option.get("type", "string")] - option = question_class(raw_option) + raw_option = hydrate_option_type(raw_option) + option = OPTIONS[raw_option["type"]](raw_option) interactive = Moulinette.interface.type == "cli" and os.isatty(1) @@ -1178,7 +1194,8 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: out = [] for raw_question in raw_questions: - question = OPTIONS[raw_question.get("type", OptionType.string)](raw_question) + raw_question = hydrate_option_type(raw_question) + question = OPTIONS[raw_question["type"]](raw_question) if isinstance(question, BaseChoicesOption) and question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default From 097cba4b56130cb048b148708ee1e79809fe9fd2 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 18 May 2023 14:47:09 +0200 Subject: [PATCH 0076/1116] tests:options: fix missing data patching --- src/tests/test_questions.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 190eb0cba..e23be9925 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -1544,6 +1544,10 @@ class TestDomain(BaseTest): ] # fmt: on + def test_options_prompted_with_ask_help(self, prefill_data=None): + with patch_domains(domains=[main_domain], main_domain=main_domain): + super().test_options_prompted_with_ask_help(prefill_data=prefill_data) + def test_scenarios(self, intake, expected_output, raw_option, data): with patch_domains(**data): super().test_scenarios(intake, expected_output, raw_option, data) @@ -1751,6 +1755,15 @@ class TestUser(BaseTest): ] # fmt: on + @pytest.mark.usefixtures("patch_no_tty") + def test_basic_attrs(self): + with patch_users( + users={admin_username: admin_user}, + admin_username=admin_username, + main_domain=main_domain, + ): + self._test_basic_attrs() + def test_options_prompted_with_ask_help(self, prefill_data=None): with patch_users( users={admin_username: admin_user, regular_username: regular_user}, From 3bb32dc1e4a166e7c80520338c6c1fc484046924 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 3 May 2023 19:59:28 +0000 Subject: [PATCH 0077/1116] Init app_shell --- share/actionsmap.yml | 6 ++++++ src/app.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 58787790c..e1de66bc8 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -954,6 +954,12 @@ app: help: Delete the key action: store_true + ### app_shell() + shell: + action_help: Open an interactive shell with the app environment already loaded + arguments: + app: + help: App ID ### app_register_url() register-url: diff --git a/src/app.py b/src/app.py index 2eb201a81..0db33a373 100644 --- a/src/app.py +++ b/src/app.py @@ -1645,6 +1645,26 @@ def app_setting(app, key, value=None, delete=False): _set_app_settings(app, app_settings) +def app_shell(app): + """ + Open an interactive shell with the app environment already loaded + + Keyword argument: + app -- App ID + + """ + app_settings = _get_app_settings(app) or {} + + #TODO init a env_dict + #TODO load the app's environment, parsed from: + #TODO - its settings (phpversion, ...) + #TODO - its service configuration (PATH, NodeJS production mode...) + #TODO this one could be performed in Bash, directly after initiating the subprocess: + #TODO - "Environment" clause: `systemctl show $app.service -p "Environment" --value` + #TODO - Source "EnvironmentFile" clauses + #TODO + #TODO find out how to open an interactive Bash shell from Python + def app_register_url(app, domain, path): """ Book/register a web path for a given app From d27e9a9eea9907f0482e2bfee6fe13bbdda02654 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 9 May 2023 21:29:52 +0000 Subject: [PATCH 0078/1116] Add ynh_load_app_environment helper --- helpers/apps | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/app.py | 11 ++-------- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/helpers/apps b/helpers/apps index 85b74de15..c5fe6cdad 100644 --- a/helpers/apps +++ b/helpers/apps @@ -111,3 +111,61 @@ ynh_remove_apps() { done fi } + +# Load an app environment in the current Bash shell +# +# usage: ynh_install_apps --app="app" +# | arg: -a, --app= - the app ID +# +# Requires YunoHost version 11.0.* or higher. +ynh_load_app_environment() { + # Declare an array to define the options of this helper. + local legacy_args=a + local -A args_array=([a]=app=) + local app + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Retrieve the list of installed apps + local installed_apps_list=($(yunohost app list --output-as json --quiet | jq -r .apps[].id)) + + # Force Bash to be used to run this helper + if [ $0 != "bash" ] + then + ynh_print_err --message="Please use Bash as shell" + exit 1 + fi + + # Make sure the app is installed + if [[ " ${installed_apps_list[*]} " != *" ${app} "* ]] + then + ynh_print_err --message="$app is not in the apps list" + exit 1 + fi + + # Make sure the app has an install_dir setting + install_dir="$(yunohost app setting $app install_dir)" + if [ -z "$install_dir" ] + then + ynh_print_err --message="$app has no install_dir setting (does it use packaging format >=2?)" + exit 1 + fi + + # Load the Environment variables from the app's service + env_var=`systemctl show $app.service -p "Environment" --value` + [ -n "$env_var" ] && export $env_var; + export HOME=$install_dir; + + # Source the EnvironmentFiles from the app's service + env_files=(`systemctl show $app.service -p "EnvironmentFiles" --value`) + if [ ${#env_files[*]} -gt 0 ] + then + for file in ${env_files[*]} + do + [[ $file = /* ]] && source $file + done + fi + + # Open the app shell + su -s /bin/bash $app +} diff --git a/src/app.py b/src/app.py index 0db33a373..2b602f351 100644 --- a/src/app.py +++ b/src/app.py @@ -1655,15 +1655,8 @@ def app_shell(app): """ app_settings = _get_app_settings(app) or {} - #TODO init a env_dict - #TODO load the app's environment, parsed from: - #TODO - its settings (phpversion, ...) - #TODO - its service configuration (PATH, NodeJS production mode...) - #TODO this one could be performed in Bash, directly after initiating the subprocess: - #TODO - "Environment" clause: `systemctl show $app.service -p "Environment" --value` - #TODO - Source "EnvironmentFile" clauses - #TODO - #TODO find out how to open an interactive Bash shell from Python + #TODO Find out how to open an interactive Bash shell from Python + #TODO run `ynh_load_app_environment --app=$app` helper in there def app_register_url(app, domain, path): """ From 68a4f2b4bc6f36caca5203f6bd80d4400c5ae571 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Thu, 18 May 2023 16:10:21 +0000 Subject: [PATCH 0079/1116] Improve ynh_load_environment helper --- helpers/apps | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/helpers/apps b/helpers/apps index c5fe6cdad..bb60fea59 100644 --- a/helpers/apps +++ b/helpers/apps @@ -126,9 +126,6 @@ ynh_load_app_environment() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - # Retrieve the list of installed apps - local installed_apps_list=($(yunohost app list --output-as json --quiet | jq -r .apps[].id)) - # Force Bash to be used to run this helper if [ $0 != "bash" ] then @@ -137,14 +134,21 @@ ynh_load_app_environment() { fi # Make sure the app is installed + local installed_apps_list=($(yunohost app list --output-as json --quiet | jq -r .apps[].id)) if [[ " ${installed_apps_list[*]} " != *" ${app} "* ]] then ynh_print_err --message="$app is not in the apps list" exit 1 fi + # Make sure the app is installed + if ! id -u "$app" &>/dev/null; then + ynh_print_err --message="There is no \"$app\" system user" + exit 1 + fi + # Make sure the app has an install_dir setting - install_dir="$(yunohost app setting $app install_dir)" + local install_dir="$(yunohost app setting $app install_dir)" if [ -z "$install_dir" ] then ynh_print_err --message="$app has no install_dir setting (does it use packaging format >=2?)" @@ -152,18 +156,21 @@ ynh_load_app_environment() { fi # Load the Environment variables from the app's service - env_var=`systemctl show $app.service -p "Environment" --value` + local env_var=`systemctl show $app.service -p "Environment" --value` [ -n "$env_var" ] && export $env_var; export HOME=$install_dir; # Source the EnvironmentFiles from the app's service - env_files=(`systemctl show $app.service -p "EnvironmentFiles" --value`) + local env_files=(`systemctl show $app.service -p "EnvironmentFiles" --value`) if [ ${#env_files[*]} -gt 0 ] then + # set -/+a enables and disables new variables being automatically exported. Needed when using `source`. + set -a for file in ${env_files[*]} do [[ $file = /* ]] && source $file done + set +a fi # Open the app shell From 425670bcfb380135d3df96007eb43b4cf624bfb6 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Thu, 18 May 2023 16:14:30 +0000 Subject: [PATCH 0080/1116] Remove useless var declaration in app_shell function --- src/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app.py b/src/app.py index 2b602f351..a9bfad1a9 100644 --- a/src/app.py +++ b/src/app.py @@ -1653,7 +1653,6 @@ def app_shell(app): app -- App ID """ - app_settings = _get_app_settings(app) or {} #TODO Find out how to open an interactive Bash shell from Python #TODO run `ynh_load_app_environment --app=$app` helper in there From 072dabaf7099082f9280c87a9345065725f468c9 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Thu, 18 May 2023 16:45:17 +0000 Subject: [PATCH 0081/1116] Fix Bash detection for ynh_load_app_environment --- helpers/apps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/apps b/helpers/apps index bb60fea59..b9cc03b58 100644 --- a/helpers/apps +++ b/helpers/apps @@ -127,7 +127,7 @@ ynh_load_app_environment() { ynh_handle_getopts_args "$@" # Force Bash to be used to run this helper - if [ $0 != "bash" ] + if [[ ! $0 =~ \/?bash$ ]] then ynh_print_err --message="Please use Bash as shell" exit 1 From 2b65913b8966d17318d6e2403575b170fee4ed09 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Thu, 18 May 2023 19:35:56 +0000 Subject: [PATCH 0082/1116] Launch app shell --- src/app.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app.py b/src/app.py index a9bfad1a9..6b523d574 100644 --- a/src/app.py +++ b/src/app.py @@ -1653,9 +1653,7 @@ def app_shell(app): app -- App ID """ - - #TODO Find out how to open an interactive Bash shell from Python - #TODO run `ynh_load_app_environment --app=$app` helper in there + subprocess.run(['/bin/bash', '-c', 'source /usr/share/yunohost/helpers && ynh_load_app_environment '+app]) def app_register_url(app, domain, path): """ From e8dd243218556a4dea5c7aa3b3cba446ccf6e278 Mon Sep 17 00:00:00 2001 From: Yann Autissier Date: Fri, 19 May 2023 20:39:29 +0000 Subject: [PATCH 0083/1116] update Content-Security-Policy header for chromium Chromium fails to load a jitsi video conference, refusing to create a worker because it violates the Content Security Policy directive: "script-src https: data: 'unsafe-inline' 'unsafe-eval'". --- conf/nginx/security.conf.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/nginx/security.conf.inc b/conf/nginx/security.conf.inc index fe853155b..44d7f86b4 100644 --- a/conf/nginx/security.conf.inc +++ b/conf/nginx/security.conf.inc @@ -26,7 +26,7 @@ ssl_dhparam /usr/share/yunohost/ffdhe2048.pem; # https://wiki.mozilla.org/Security/Guidelines/Web_Security # https://observatory.mozilla.org/ {% if experimental == "True" %} -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'"; +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'; worker-src 'self' blob:;"; {% else %} more_set_headers "Content-Security-Policy : upgrade-insecure-requests"; {% endif %} From df523cdbf0c8b9eaaddf910a4b72b00cbe2f7f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= Date: Fri, 19 May 2023 11:49:35 +0000 Subject: [PATCH 0084/1116] Translated using Weblate (French) Currently translated at 100.0% (764 of 764 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 1ba11b723..91d52dc86 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -758,9 +758,9 @@ "app_change_url_script_failed": "Une erreur s'est produite dans le script de modification de l'url", "app_failed_to_upgrade_but_continue": "La mise à jour de l'application {failed_app} a échoué, mais YunoHost va continuer avec les mises à jour suivantes comme demandé. Lancez 'yunohost log show {operation_logger_name}' pour voir le journal des échecs", "app_not_upgraded_broken_system_continue": "L'application '{failed_app}' n'a pas réussi à se mettre à jour et a mis le système dans un état alternatif car quelque chose est au moins momentanément \"cassé\" (le paramètre --continue-on-failure est donc ignoré). La conséquence est que les mises à jour des applications suivantes ont été annulées : {apps}", - "app_not_upgraded_broken_system": "L'application '{failed_app}' n'a pas réussi à se mettre à jour et a mis le système dans un état alternatif car quelque chose est au moins momentanément \"cassé\". En conséquence, les mises à jour des applications suivantes ont été annulées : {apps}", + "app_not_upgraded_broken_system": "L'application '{failed_app}' n'a pas réussi à se mettre à jour et a mis le système dans un état de panne. Par conséquent, les mises à niveau des applications suivantes ont été annulées : {apps}", "apps_failed_to_upgrade": "Ces applications n'ont pas pu être mises à jour : {apps}", - "apps_failed_to_upgrade_line": "\n * {app_id} (pour voir le journal/log correspondant, faites un 'yunohost log show {operation_logger_name}')", + "apps_failed_to_upgrade_line": "\n * {app_id} (pour voir le journal correspondant, faites un 'yunohost log show {operation_logger_name}')", "app_failed_to_download_asset": "Échec du téléchargement de la ressource '{source_id}' ({url}) pour {app} : {out}", "app_corrupt_source": "YunoHost a pu télécharger la ressource '{source_id}' ({url}) pour {app}, malheureusement celle-ci ne correspond pas à la somme de contrôle attendue. Cela peut signifier qu'une défaillance temporaire du réseau s'est produite sur votre serveur, OU que la ressource a été modifiée par le mainteneur de l'application en amont (ou un acteur malveillant ?) et que les responsables du paquet de cette application pour YunoHost doivent investiguer et mettre à jour le manifeste de l'application pour refléter ce changement.\n Somme de contrôle sha256 attendue : {expected_sha256}\n Somme de contrôle sha256 téléchargée : {computed_sha256}\n Taille du fichier téléchargé : {size}" -} \ No newline at end of file +} From a508684740e30f0f42b54cb21cc7a72b58293243 Mon Sep 17 00:00:00 2001 From: Ilya Date: Fri, 19 May 2023 07:17:39 +0000 Subject: [PATCH 0085/1116] Translated using Weblate (Russian) Currently translated at 40.0% (306 of 764 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ru/ --- locales/ru.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/locales/ru.json b/locales/ru.json index 2c4e703da..a9c9da3f1 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -325,5 +325,8 @@ "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 + "global_settings_setting_smtp_allow_ipv6_help": "Разрешить использование IPv6 для получения и отправки почты", + "admins": "Администраторы", + "all_users": "Все пользователи YunoHost", + "app_action_failed": "Не удалось выполнить действие {action} для приложения {app}" +} From db9aa8e6c7f022687e9eabeefd2e109c8cf2f1e6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 20 May 2023 18:58:11 +0200 Subject: [PATCH 0086/1116] Update changelog for 11.1.20 --- debian/changelog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/debian/changelog b/debian/changelog index 23192c957..587202566 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +yunohost (11.1.20) stable; urgency=low + + - appsv2: fix funky current_version not being defined when hydrating pre-upgrade notifications (8fa823b4) + - helpers: using YNH_APP_ID instead of YNH_APP_INSTANCE_NAME during ynh_setup_source download, for more consistency and because tests was actually failing since a while because of this (e59a4f84) + - helpers: improve error message for corrupt source in ynh_setup_source, it's more relevant to cite the source url rather than the downloaded output path (d698c4c3) + - nginx: Update "worker" Content-Security-Policy header when in experimental security mode ([#1664](https://github.com/yunohost/yunohost/pull/1664)) + - i18n: Translations updated for French, Indonesian, Russian, Slovak + + Thanks to all contributors <3 ! (axolotle, Éric Gaspar, Ilya, Jose Riha, Neko Nekowazarashi, Yann Autissier) + + -- Alexandre Aubin Sat, 20 May 2023 18:57:26 +0200 + yunohost (11.1.19) stable; urgency=low - helpers: Upgrade n to version 9.1.0 ([#1646](https://github.com/yunohost/yunohost/pull/1646)) From 85b08e44c9ee03151cae1c35ef20902ffdb7ddd4 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Mon, 22 May 2023 15:29:21 +0200 Subject: [PATCH 0087/1116] ci: preinstall more package --- .gitlab/ci/doc.gitlab-ci.yml | 1 - .gitlab/ci/lint.gitlab-ci.yml | 1 - .gitlab/ci/test.gitlab-ci.yml | 2 +- .gitlab/ci/translation.gitlab-ci.yml | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 4f6ea6ba1..183d153a4 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -7,7 +7,6 @@ generate-helpers-doc: image: "before-install" needs: [] before_script: - - apt-get update -y && apt-get install git hub -y - git config --global user.email "yunohost@yunohost.org" - git config --global user.name "$GITHUB_USER" script: diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index 65b74ddca..1eeb71eab 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -30,7 +30,6 @@ black: image: "before-install" needs: [] before_script: - - apt-get update -y && apt-get install git hub -y - git config --global user.email "yunohost@yunohost.org" - git config --global user.name "$GITHUB_USER" - hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index a49fc13b7..2c6e1717d 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -1,6 +1,6 @@ .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 ${CI_PROJECT_DIR}/*.deb php8.2-cli mariadb-client mariadb-server + - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb .test-stage: stage: test diff --git a/.gitlab/ci/translation.gitlab-ci.yml b/.gitlab/ci/translation.gitlab-ci.yml index 83db2b5a4..387860e40 100644 --- a/.gitlab/ci/translation.gitlab-ci.yml +++ b/.gitlab/ci/translation.gitlab-ci.yml @@ -16,7 +16,6 @@ autofix-translated-strings: image: "before-install" needs: [] before_script: - - apt-get update -y && apt-get install git hub -y - git config --global user.email "yunohost@yunohost.org" - git config --global user.name "$GITHUB_USER" - hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo From f046c291e52ee536d5c8830d1bf8226f3151746e Mon Sep 17 00:00:00 2001 From: Kay0u Date: Mon, 22 May 2023 19:32:53 +0200 Subject: [PATCH 0088/1116] add missing args in tests --- src/tests/test_apps.py | 2 +- src/tests/test_backuprestore.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index 747eb5dcd..5db180b7e 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -330,7 +330,7 @@ def test_app_from_catalog(): app_install( "my_webapp", - args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0", + args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0&phpversion=none", ) app_map_ = app_map(raw=True) assert main_domain in app_map_ diff --git a/src/tests/test_backuprestore.py b/src/tests/test_backuprestore.py index 413d44470..bca1b29a5 100644 --- a/src/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -55,7 +55,7 @@ def setup_function(function): if "with_legacy_app_installed" in markers: assert not app_is_installed("legacy_app") - install_app("legacy_app_ynh", "/yolo") + install_app("legacy_app_ynh", "/yolo", "&is_public=true") assert app_is_installed("legacy_app") if "with_backup_recommended_app_installed" in markers: From 23eaf609da112fda99e76d544d25a7634b685188 Mon Sep 17 00:00:00 2001 From: ElderTek Date: Thu, 25 May 2023 00:00:07 +0400 Subject: [PATCH 0089/1116] remove deprecated --- share/actionsmap.yml | 11 ----------- src/domain.py | 4 ---- 2 files changed, 15 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 58787790c..107853c33 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -537,17 +537,6 @@ domain: full: --force help: Do not ask confirmation to remove apps action: store_true - - - ### domain_dns_conf() - dns-conf: - deprecated: true - action_help: Generate sample DNS configuration for a domain - arguments: - domain: - help: Target domain - extra: - pattern: *pattern_domain ### domain_maindomain() main-domain: diff --git a/src/domain.py b/src/domain.py index 4f96d08c4..a2d570b4b 100644 --- a/src/domain.py +++ b/src/domain.py @@ -718,10 +718,6 @@ def domain_cert_renew(domain_list, force=False, no_checks=False, email=False): return certificate_renew(domain_list, force, no_checks, email) -def domain_dns_conf(domain): - return domain_dns_suggest(domain) - - def domain_dns_suggest(domain): from yunohost.dns import domain_dns_suggest From 21c7c41812535da1597b492239790118da2d8ce9 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 24 May 2023 23:08:53 +0200 Subject: [PATCH 0090/1116] Extend ynh_load_app_environment usage examples Co-authored-by: Florent --- helpers/apps | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helpers/apps b/helpers/apps index b9cc03b58..d807a4d87 100644 --- a/helpers/apps +++ b/helpers/apps @@ -117,6 +117,10 @@ ynh_remove_apps() { # usage: ynh_install_apps --app="app" # | arg: -a, --app= - the app ID # +# examples: +# ynh_load_app_environment --app="APP" <<< 'echo "$USER"' +# ynh_load_app_environment --app="APP" < /tmp/some_script.bash +# # Requires YunoHost version 11.0.* or higher. ynh_load_app_environment() { # Declare an array to define the options of this helper. From cc167cd92c60b70c75c89da7e18d35b767aafa1e Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 24 May 2023 21:11:32 +0000 Subject: [PATCH 0091/1116] Rename ynh_load_app_environment into ynh_spawn_app_shell Co-authored-by: Florent --- helpers/apps | 8 ++++---- src/app.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/helpers/apps b/helpers/apps index d807a4d87..fb5ac25b0 100644 --- a/helpers/apps +++ b/helpers/apps @@ -118,11 +118,11 @@ ynh_remove_apps() { # | arg: -a, --app= - the app ID # # examples: -# ynh_load_app_environment --app="APP" <<< 'echo "$USER"' -# ynh_load_app_environment --app="APP" < /tmp/some_script.bash -# +# ynh_spawn_app_shell --app="APP" <<< 'echo "$USER"' +# ynh_spawn_app_shell --app="APP" < /tmp/some_script.bash +# # Requires YunoHost version 11.0.* or higher. -ynh_load_app_environment() { +ynh_spawn_app_shell() { # Declare an array to define the options of this helper. local legacy_args=a local -A args_array=([a]=app=) diff --git a/src/app.py b/src/app.py index 6b523d574..04340b1ba 100644 --- a/src/app.py +++ b/src/app.py @@ -1653,7 +1653,7 @@ def app_shell(app): app -- App ID """ - subprocess.run(['/bin/bash', '-c', 'source /usr/share/yunohost/helpers && ynh_load_app_environment '+app]) + subprocess.run(['/bin/bash', '-c', 'source /usr/share/yunohost/helpers && ynh_spawn_app_shell '+app]) def app_register_url(app, domain, path): """ From 4b4ce9aef63ba4408fdc87d0e13a6a3b1a3d9220 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 24 May 2023 23:13:52 +0200 Subject: [PATCH 0092/1116] Default to WorkingDirectory then install_dir for ynh_spawn_app_shell Co-authored-by: Tagada <36127788+Tagadda@users.noreply.github.com> --- helpers/apps | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/helpers/apps b/helpers/apps index fb5ac25b0..feda02f5e 100644 --- a/helpers/apps +++ b/helpers/apps @@ -178,5 +178,12 @@ ynh_spawn_app_shell() { fi # Open the app shell + local env_dir = $(systemctl show $app.service -p "WorkingDirectory" --value) + if [[ $env_dir = "" ]]; + then + env_dir = $install_dir + fi + + cd $env_dir su -s /bin/bash $app } From ed1b5e567bc18f27031676cf62e98ec83d9a6d8e Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 24 May 2023 21:55:20 +0000 Subject: [PATCH 0093/1116] Force php to its intended version in ynh_spawn_app_shell --- helpers/apps | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/helpers/apps b/helpers/apps index feda02f5e..23889ef43 100644 --- a/helpers/apps +++ b/helpers/apps @@ -164,6 +164,14 @@ ynh_spawn_app_shell() { [ -n "$env_var" ] && export $env_var; export HOME=$install_dir; + # Force `php` to its intended version + local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) + if [ -n "$phpversion" ] + then + eval "php() { php${phpversion} \"\$@\"; }" + export -f php + fi + # Source the EnvironmentFiles from the app's service local env_files=(`systemctl show $app.service -p "EnvironmentFiles" --value`) if [ ${#env_files[*]} -gt 0 ] From a47e491869673574ac8233a179bd75622c29d5ee Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 24 May 2023 22:08:51 +0000 Subject: [PATCH 0094/1116] Cleanup ynh_spawn_app_shell --- helpers/apps | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/helpers/apps b/helpers/apps index 23889ef43..1f3fb5430 100644 --- a/helpers/apps +++ b/helpers/apps @@ -152,7 +152,7 @@ ynh_spawn_app_shell() { fi # Make sure the app has an install_dir setting - local install_dir="$(yunohost app setting $app install_dir)" + local install_dir=$(ynh_app_setting_get --app=$app --key=install_dir) if [ -z "$install_dir" ] then ynh_print_err --message="$app has no install_dir setting (does it use packaging format >=2?)" @@ -185,13 +185,11 @@ ynh_spawn_app_shell() { set +a fi - # Open the app shell + # cd into the WorkingDirectory set in the service, or default to the install_dir local env_dir = $(systemctl show $app.service -p "WorkingDirectory" --value) - if [[ $env_dir = "" ]]; - then - env_dir = $install_dir - fi - + [ -z $env_dir ] && env_dir=$install_dir; cd $env_dir + + # Spawn the app shell su -s /bin/bash $app } From 5fa58f19ce264f52e9d3a6d18f8cbd7ce0b2e358 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 24 May 2023 22:19:10 +0000 Subject: [PATCH 0095/1116] Offer apps to set service name for ynh_spawn_app_shell --- helpers/apps | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/helpers/apps b/helpers/apps index 1f3fb5430..aafcfa7e2 100644 --- a/helpers/apps +++ b/helpers/apps @@ -159,8 +159,12 @@ ynh_spawn_app_shell() { exit 1 fi + # Load the app's service name, or default to $app + local service=$(ynh_app_setting_get --app=$app --key=service) + [ -z "$service" ] && service=$app; + # Load the Environment variables from the app's service - local env_var=`systemctl show $app.service -p "Environment" --value` + local env_var=`systemctl show $service.service -p "Environment" --value` [ -n "$env_var" ] && export $env_var; export HOME=$install_dir; @@ -173,7 +177,7 @@ ynh_spawn_app_shell() { fi # Source the EnvironmentFiles from the app's service - local env_files=(`systemctl show $app.service -p "EnvironmentFiles" --value`) + local env_files=(`systemctl show $service.service -p "EnvironmentFiles" --value`) if [ ${#env_files[*]} -gt 0 ] then # set -/+a enables and disables new variables being automatically exported. Needed when using `source`. @@ -186,7 +190,7 @@ ynh_spawn_app_shell() { fi # cd into the WorkingDirectory set in the service, or default to the install_dir - local env_dir = $(systemctl show $app.service -p "WorkingDirectory" --value) + local env_dir = $(systemctl show $service.service -p "WorkingDirectory" --value) [ -z $env_dir ] && env_dir=$install_dir; cd $env_dir From cacd43e147e444ede67c3c1754d45fadd56ade54 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 24 May 2023 22:21:35 +0000 Subject: [PATCH 0096/1116] Fix error in ynh_spawn_app_shell --- helpers/apps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/apps b/helpers/apps index aafcfa7e2..198aa15d9 100644 --- a/helpers/apps +++ b/helpers/apps @@ -190,7 +190,7 @@ ynh_spawn_app_shell() { fi # cd into the WorkingDirectory set in the service, or default to the install_dir - local env_dir = $(systemctl show $service.service -p "WorkingDirectory" --value) + local env_dir=$(systemctl show $service.service -p "WorkingDirectory" --value) [ -z $env_dir ] && env_dir=$install_dir; cd $env_dir From bb9db08e2902c8734ae547a43f02fec0445783ce Mon Sep 17 00:00:00 2001 From: tituspijean Date: Wed, 24 May 2023 22:32:51 +0000 Subject: [PATCH 0097/1116] Improve ynh_spawn_app_shell documentation --- helpers/apps | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/helpers/apps b/helpers/apps index 198aa15d9..9c46346fe 100644 --- a/helpers/apps +++ b/helpers/apps @@ -112,16 +112,19 @@ ynh_remove_apps() { fi } -# Load an app environment in the current Bash shell +# Spawn a Bash shell with the app environment loaded # -# usage: ynh_install_apps --app="app" +# usage: ynh_spawn_app_shell --app="app" # | arg: -a, --app= - the app ID # # examples: # ynh_spawn_app_shell --app="APP" <<< 'echo "$USER"' # ynh_spawn_app_shell --app="APP" < /tmp/some_script.bash # -# Requires YunoHost version 11.0.* or higher. +# Requires YunoHost version 11.0.* or higher, and that the app relies on packaging v2 or higher. +# The spawned shell will have environment variables loaded and environment files sourced +# from the app's service configuration file (defaults to $app.service, overridable by the packager with `service` setting). +# If the app relies on a specific PHP version, then `php` will be aliased that version. ynh_spawn_app_shell() { # Declare an array to define the options of this helper. local legacy_args=a From 1300585eda965691a078db909a289b9dfef26828 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Thu, 25 May 2023 09:48:55 +0200 Subject: [PATCH 0098/1116] Improve ynh_spawn_app_shell comments Co-authored-by: Florent --- helpers/apps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/apps b/helpers/apps index 9c46346fe..b81e8be99 100644 --- a/helpers/apps +++ b/helpers/apps @@ -148,7 +148,7 @@ ynh_spawn_app_shell() { exit 1 fi - # Make sure the app is installed + # Make sure the app has its own user if ! id -u "$app" &>/dev/null; then ynh_print_err --message="There is no \"$app\" system user" exit 1 From 1552944fdd64bd57c4c2f75a53b563f5db0ca7f1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 May 2023 20:41:40 +0200 Subject: [PATCH 0099/1116] apps: fix auto-catalog update cron job which was broken because --apps doesnt exist anymore --- hooks/conf_regen/01-yunohost | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index d0e6fb783..1bef26a8b 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -97,7 +97,7 @@ EOF # Cron job that upgrade the app list everyday cat >$pending_dir/etc/cron.daily/yunohost-fetch-apps-catalog < /dev/null) & +sleep \$((RANDOM%3600)); yunohost tools update apps > /dev/null EOF # Cron job that renew lets encrypt certificates if there's any that needs renewal From daf51e94bdb3c77787e1169549d4ef6ec8da1af6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 May 2023 21:06:01 +0200 Subject: [PATCH 0100/1116] regeconf: fix security issue where apps' system conf would be owned by the app, which can enable priviledge escalation --- helpers/utils | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/helpers/utils b/helpers/utils index 489c5c261..52d7c734f 100644 --- a/helpers/utils +++ b/helpers/utils @@ -1071,8 +1071,10 @@ _ynh_apply_default_permissions() { fi fi - # Crons should be owned by root otherwise they probably don't run - if echo "$target" | grep -q '^/etc/cron' + # Crons should be owned by root + # Also we don't want systemd conf, nginx conf or others stuff to be owned by the app, + # otherwise they could self-edit their own systemd conf and escalate privilege + if echo "$target" | grep -q '^/etc/cron\|/etc/php\|/etc/nginx/conf.d\|/etc/fail2ban\|/etc/systemd/system' then chmod 400 $target chown root:root $target From e649c092a3e4b5cb110a5b3f33dbfe9f4ca3f9d3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 May 2023 21:44:39 +0200 Subject: [PATCH 0101/1116] regenconf: force systemd, nginx, php and fail2ban conf to be owned by root --- hooks/conf_regen/01-yunohost | 9 +++++++++ hooks/conf_regen/15-nginx | 6 ++++++ hooks/conf_regen/52-fail2ban | 6 ++++++ 3 files changed, 21 insertions(+) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 1bef26a8b..0d6876cf4 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -181,6 +181,15 @@ do_post_regen() { # 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 chmod 755 /etc/yunohost + chown root:root /etc/systemd/system/*.service + chmod 644 /etc/systemd/system/*.service + + if ls -l /etc/php/*/fpm/pool.d/*.conf + then + chown root:root /etc/php/*/fpm/pool.d/*.conf + chmod 644 /etc/php/*/fpm/pool.d/*.conf + fi + # Certs # We do this with find because there could be a lot of them... chown -R root:ssl-cert /etc/yunohost/certs diff --git a/hooks/conf_regen/15-nginx b/hooks/conf_regen/15-nginx index 28d9e90fb..9eabcd8b7 100755 --- a/hooks/conf_regen/15-nginx +++ b/hooks/conf_regen/15-nginx @@ -144,6 +144,12 @@ do_pre_regen() { do_post_regen() { regen_conf_files=$1 + if ls -l /etc/nginx/conf.d/*.d/*.conf + then + chown root:root /etc/nginx/conf.d/*.d/*.conf + chmod 644 /etc/nginx/conf.d/*.d/*.conf + fi + [ -z "$regen_conf_files" ] && exit 0 # create NGINX conf directories for domains diff --git a/hooks/conf_regen/52-fail2ban b/hooks/conf_regen/52-fail2ban index d463892c7..db3cf0da7 100755 --- a/hooks/conf_regen/52-fail2ban +++ b/hooks/conf_regen/52-fail2ban @@ -24,6 +24,12 @@ do_pre_regen() { do_post_regen() { regen_conf_files=$1 + if ls -l /etc/fail2ban/jail.d/*.conf + then + chown root:root /etc/fail2ban/jail.d/*.conf + chmod 644 /etc/fail2ban/jail.d/*.conf + fi + [[ -z "$regen_conf_files" ]] \ || systemctl reload fail2ban } From db7ab2a98b276c23dbc2cf67c6e92e116536f36f Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 30 May 2023 11:18:54 +0000 Subject: [PATCH 0102/1116] Homogeneize command subtitutions in ynh_spawn_app_shell --- helpers/apps | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/apps b/helpers/apps index b81e8be99..4b64ecdbb 100644 --- a/helpers/apps +++ b/helpers/apps @@ -167,7 +167,7 @@ ynh_spawn_app_shell() { [ -z "$service" ] && service=$app; # Load the Environment variables from the app's service - local env_var=`systemctl show $service.service -p "Environment" --value` + local env_var=$(systemctl show $service.service -p "Environment" --value) [ -n "$env_var" ] && export $env_var; export HOME=$install_dir; @@ -180,7 +180,7 @@ ynh_spawn_app_shell() { fi # Source the EnvironmentFiles from the app's service - local env_files=(`systemctl show $service.service -p "EnvironmentFiles" --value`) + local env_files=($(systemctl show $service.service -p "EnvironmentFiles" --value)) if [ ${#env_files[*]} -gt 0 ] then # set -/+a enables and disables new variables being automatically exported. Needed when using `source`. From f3faac87f83dd9deebed02b7700ed3f23308f7c7 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 30 May 2023 11:27:33 +0000 Subject: [PATCH 0103/1116] Improve comments of ynh_spawn_app_shell --- helpers/apps | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helpers/apps b/helpers/apps index 4b64ecdbb..4b253ff90 100644 --- a/helpers/apps +++ b/helpers/apps @@ -166,12 +166,15 @@ ynh_spawn_app_shell() { local service=$(ynh_app_setting_get --app=$app --key=service) [ -z "$service" ] && service=$app; + # Export HOME variable + export HOME=$install_dir; + # Load the Environment variables from the app's service local env_var=$(systemctl show $service.service -p "Environment" --value) [ -n "$env_var" ] && export $env_var; - export HOME=$install_dir; # Force `php` to its intended version + # We use `eval`+`export` since `alias` is not propagated to subshells, even with `export` local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) if [ -n "$phpversion" ] then From fee5375dc47e3890930e82db63d5c98aea2b9a39 Mon Sep 17 00:00:00 2001 From: Tagada <36127788+Tagadda@users.noreply.github.com> Date: Sun, 4 Jun 2023 23:50:23 +0200 Subject: [PATCH 0104/1116] more verbose logs for user_group _update fix YunoHost/issues#2193 --- locales/en.json | 4 ++++ src/user.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/locales/en.json b/locales/en.json index 4dcb00ee6..bfc564afd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -467,13 +467,17 @@ "group_creation_failed": "Could not create the group '{group}': {error}", "group_deleted": "Group '{group}' deleted", "group_deletion_failed": "Could not delete the group '{group}': {error}", + "group_mailalias_add": "The email alias '{mail}' will be added to the group '{group}'", + "group_mailalias_remove": "The email alias '{mail}' will be removed from the group '{group}'", "group_no_change": "Nothing to change for group '{group}'", "group_unknown": "The group '{group}' is unknown", "group_update_aliases": "Updating aliases for group '{group}'", "group_update_failed": "Could not update the group '{group}': {error}", "group_updated": "Group '{group}' updated", + "group_user_add": "The user '{user}' will be added to the 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}", + "group_user_remove": "The user '{user}' will be removed from the group '{group}'", "hook_exec_failed": "Could not run script: {path}", "hook_exec_not_terminated": "Script did not finish properly: {path}", "hook_json_return_error": "Could not read return from hook {path}. Error: {msg}. Raw content: {raw_content}", diff --git a/src/user.py b/src/user.py index f17a60942..3f453f69e 100644 --- a/src/user.py +++ b/src/user.py @@ -1189,6 +1189,7 @@ def user_group_update( ) else: operation_logger.related_to.append(("user", user)) + logger.info(m18n.n("group_user_add", group=groupname, user=user)) new_group_members += users_to_add @@ -1202,6 +1203,7 @@ def user_group_update( ) else: operation_logger.related_to.append(("user", user)) + logger.info(m18n.n("group_user_remove", group=groupname, user=user)) # Remove users_to_remove from new_group_members # Kinda like a new_group_members -= users_to_remove @@ -1237,6 +1239,7 @@ def user_group_update( "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] ) new_group_mail.append(mail) + logger.info(m18n.n("group_mailalias_add", group=groupname, mail=mail)) if remove_mailalias: from yunohost.domain import _get_maindomain @@ -1256,6 +1259,7 @@ def user_group_update( ) if mail in new_group_mail: new_group_mail.remove(mail) + logger.info(m18n.n("group_mailalias_remove", group=groupname, mail=mail)) else: raise YunohostValidationError("mail_alias_remove_failed", mail=mail) From 78cd79ec480c5e4643b792ed9fcb8dd36fb882cd Mon Sep 17 00:00:00 2001 From: Kayou Date: Mon, 5 Jun 2023 10:11:50 +0200 Subject: [PATCH 0105/1116] Update debian/changelog --- debian/changelog | 1 - 1 file changed, 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 69418598c..bc06c7e42 100644 --- a/debian/changelog +++ b/debian/changelog @@ -15,7 +15,6 @@ yunohost (11.1.20) stable; urgency=low Thanks to all contributors <3 ! (axolotle, Éric Gaspar, Ilya, Jose Riha, Neko Nekowazarashi, Yann Autissier) -- Alexandre Aubin Sat, 20 May 2023 18:57:26 +0200 ->>>>>>> origin/dev yunohost (11.1.19) stable; urgency=low From d42c99835a67ad614c0b6ff5595e42c36e9067fd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 9 Jun 2023 22:30:32 +0200 Subject: [PATCH 0106/1116] nginx: use /var/www/.well-known folder for ynh diagnosis and acme challenge, because /tmp/ could be manipulated by user to serve maliciously crafted files --- conf/nginx/plain/acme-challenge.conf.inc | 2 +- conf/nginx/server.tpl.conf | 2 +- src/certificate.py | 4 ++-- src/diagnosers/21-web.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/conf/nginx/plain/acme-challenge.conf.inc b/conf/nginx/plain/acme-challenge.conf.inc index 35c4b80c2..859aa6817 100644 --- a/conf/nginx/plain/acme-challenge.conf.inc +++ b/conf/nginx/plain/acme-challenge.conf.inc @@ -1,6 +1,6 @@ location ^~ '/.well-known/acme-challenge/' { default_type "text/plain"; - alias /tmp/acme-challenge-public/; + alias /var/www/.well-known/acme-challenge-public/; gzip off; } diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index d3ff77714..16b5c46c2 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -13,7 +13,7 @@ server { include /etc/nginx/conf.d/acme-challenge.conf.inc; location ^~ '/.well-known/ynh-diagnosis/' { - alias /tmp/.well-known/ynh-diagnosis/; + alias /var/www/.well-known/ynh-diagnosis/; } {% if mail_enabled == "True" %} diff --git a/src/certificate.py b/src/certificate.py index 52e0d8c1b..76d3f32b7 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -41,8 +41,8 @@ from yunohost.log import OperationLogger logger = getActionLogger("yunohost.certmanager") CERT_FOLDER = "/etc/yunohost/certs/" -TMP_FOLDER = "/tmp/acme-challenge-private/" -WEBROOT_FOLDER = "/tmp/acme-challenge-public/" +TMP_FOLDER = "/var/www/.well-known/acme-challenge-private/" +WEBROOT_FOLDER = "/var/www/.well-known/acme-challenge-public/" SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem" ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem" diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index 2050cd658..ce6de4b17 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -60,9 +60,9 @@ class MyDiagnoser(Diagnoser): domains_to_check.append(domain) self.nonce = "".join(random.choice("0123456789abcedf") for i in range(16)) - 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) + rm("/var/www/.well-known/ynh-diagnosis/", recursive=True, force=True) + mkdir("/var/www/.well-known/ynh-diagnosis/", parents=True) + os.system("touch /var/www/.well-known/ynh-diagnosis/%s" % self.nonce) if not domains_to_check: return From 1087c800a6ea8ec428997442a52a88bd148ccc1c Mon Sep 17 00:00:00 2001 From: Kuba Bazan Date: Fri, 9 Jun 2023 17:50:42 +0000 Subject: [PATCH 0107/1116] Translated using Weblate (Polish) Currently translated at 25.7% (197 of 764 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index c58f7223e..0b3dc5e73 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -80,7 +80,7 @@ "app_already_installed_cant_change_url": "Ta aplikacja jest już zainstalowana. URL nie może zostać zmieniony przy użyciu tej funkcji. Sprawdź czy można zmienić w `app changeurl`", "app_id_invalid": "Nieprawidłowy identyfikator aplikacji(ID)", "app_change_url_require_full_domain": "Nie można przenieść aplikacji {app} na nowy adres URL, ponieważ wymaga ona pełnej domeny (tj. ze ścieżką = /)", - "app_install_files_invalid": "Tych plików nie można zainstalować", + "app_install_files_invalid": "Te pliki nie mogą zostać zainstalowane.", "app_make_default_location_already_used": "Nie można ustawić '{app}' jako domyślnej aplikacji w domenie '{domain}' ponieważ jest już używana przez '{other_app}'", "app_change_url_identical_domains": "Stara i nowa domena/ścieżka_url są identyczne („{domain}{path}”), nic nie trzeba robić.", "app_config_unable_to_read": "Nie udało się odczytać wartości panelu konfiguracji.", @@ -136,7 +136,7 @@ "backup_archive_corrupted": "Wygląda na to, że archiwum kopii zapasowej '{archive}' jest uszkodzone: {error}", "backup_cleaning_failed": "Nie udało się wyczyścić folderu tymczasowej kopii zapasowej", "backup_create_size_estimation": "Archiwum będzie zawierać około {size} danych.", - "app_location_unavailable": "Ten adres URL jest niedostępny lub powoduje konflikt z już zainstalowanymi aplikacja(mi):\n{apps}", + "app_location_unavailable": "Ten adres URL jest niedostępny lub koliduje z już zainstalowanymi aplikacjami:\n{apps}", "app_restore_failed": "Nie można przywrócić {app}: {error}", "app_restore_script_failed": "Wystąpił błąd w skrypcie przywracania aplikacji", "app_full_domain_unavailable": "Przepraszamy, ta aplikacja musi być zainstalowana we własnej domenie, ale inna aplikacja jest już zainstalowana w tej domenie „{domain}”. Zamiast tego możesz użyć subdomeny dedykowanej tej aplikacji.", @@ -179,5 +179,40 @@ "certmanager_cert_install_success_selfsigned": "Pomyślna instalacja certyfikatu self-signed dla domeny '{domain}'", "certmanager_cert_renew_failed": "Nieudane odnowienie certyfikatu Let's Encrypt dla {domains}", "apps_failed_to_upgrade": "Nieudana aktualizacja aplikacji: {apps}", - "backup_output_directory_required": "Musisz wybrać katalog dla kopii zapasowej" -} \ No newline at end of file + "backup_output_directory_required": "Musisz wybrać katalog dla kopii zapasowej", + "app_failed_to_download_asset": "Nie udało się pobrać zasobu '{source_id}' ({url}) dla {app}: {out}", + "backup_with_no_backup_script_for_app": "Aplikacja '{app}' nie posiada skryptu kopii zapasowej. Ignorowanie.", + "backup_with_no_restore_script_for_app": "Aplikacja {app} nie posiada skryptu przywracania, co oznacza, że nie będzie można automatycznie przywrócić kopii zapasowej tej aplikacji.", + "certmanager_acme_not_configured_for_domain": "Wyzwanie ACME nie może zostać uruchomione dla domeny {domain}, ponieważ jej konfiguracja nginx nie zawiera odpowiedniego fragmentu kodu... Upewnij się, że konfiguracja nginx jest aktualna, używając polecenia yunohost tools regen-conf nginx --dry-run --with-diff.", + "certmanager_domain_dns_ip_differs_from_public_ip": "Rekordy DNS dla domeny '{domain}' różnią się od adresu IP tego serwera. Sprawdź kategorię 'Rekordy DNS' (podstawowe) w diagnozie, aby uzyskać więcej informacji. Jeśli niedawno dokonałeś zmiany rekordu A, poczekaj, aż zostanie on zaktualizowany (można skorzystać z narzędzi online do sprawdzania propagacji DNS). (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", + "confirm_app_install_danger": "UWAGA! Ta aplikacja jest wciąż w fazie eksperymentalnej (jeśli nie działa jawnie)! Prawdopodobnie NIE powinieneś jej instalować, chyba że wiesz, co robisz. NIE ZOSTANIE udzielone wsparcie, jeśli ta aplikacja nie będzie działać poprawnie lub spowoduje uszkodzenie systemu... Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}", + "confirm_app_install_thirdparty": "UWAGA! Ta aplikacja nie jest częścią katalogu aplikacji YunoHost. Instalowanie aplikacji innych firm może naruszyć integralność i bezpieczeństwo systemu. Prawdopodobnie NIE powinieneś jej instalować, chyba że wiesz, co robisz. NIE ZOSTANIE udzielone wsparcie, jeśli ta aplikacja nie będzie działać poprawnie lub spowoduje uszkodzenie systemu... Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'", + "config_apply_failed": "Nie udało się zastosować nowej konfiguracji: {error}", + "config_cant_set_value_on_section": "Nie można ustawić pojedynczej wartości dla całej sekcji konfiguracji.", + "config_no_panel": "Nie znaleziono panelu konfiguracji.", + "config_unknown_filter_key": "Klucz filtru '{filter_key}' jest niepoprawny.", + "config_validate_email": "Proszę podać poprawny adres e-mail", + "backup_hook_unknown": "Nieznany jest hook kopii zapasowej '{hook}'.", + "backup_no_uncompress_archive_dir": "Nie istnieje taki katalog nieskompresowanego archiwum.", + "backup_output_symlink_dir_broken": "Twój katalog archiwum '{path}' to uszkodzony dowiązanie symboliczne. Być może zapomniałeś o ponownym zamontowaniu lub podłączeniu nośnika przechowującego, do którego on wskazuje.", + "backup_system_part_failed": "Nie można wykonać kopii zapasowej części systemu '{part}'", + "config_validate_color": "Powinien być poprawnym szesnastkowym kodem koloru RGB.", + "config_validate_date": "Data powinna być poprawna w formacie RRRR-MM-DD", + "config_validate_time": "Podaj poprawny czas w formacie GG:MM", + "certmanager_domain_not_diagnosed_yet": "Nie ma jeszcze wyników diagnozy dla domeny {domain}. Proszę ponownie uruchomić diagnozę dla kategorii 'Rekordy DNS' i 'Strona internetowa' w sekcji diagnozy, aby sprawdzić, czy domena jest gotowa do użycia Let's Encrypt. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", + "certmanager_cannot_read_cert": "Wystąpił problem podczas próby otwarcia bieżącego certyfikatu dla domeny {domain} (plik: {file}), przyczyna: {reason}", + "certmanager_no_cert_file": "Nie można odczytać pliku certyfikatu dla domeny {domain} (plik: {file}).", + "certmanager_self_ca_conf_file_not_found": "Nie można znaleźć pliku konfiguracyjnego dla autorytetu samopodpisującego (plik: {file})", + "backup_running_hooks": "Uruchamianie hooków kopii zapasowej...", + "backup_permission": "Uprawnienia kopii zapasowej dla aplikacji {app}", + "certmanager_domain_cert_not_selfsigned": "Certyfikat dla domeny {domain} nie jest samopodpisany. Czy na pewno chcesz go zastąpić? (Użyj opcji '--force', aby to zrobić.)", + "config_action_disabled": "Nie można uruchomić akcji '{action}', ponieważ jest ona wyłączona. Upewnij się, że spełnione są jej ograniczenia. Pomoc: {help}", + "config_action_failed": "Nie udało się uruchomić akcji '{action}': {error}", + "config_forbidden_readonly_type": "Typ '{type}' nie może być ustawiony jako tylko do odczytu. Użyj innego typu, aby wyświetlić tę wartość (odpowiednie ID argumentu: '{id}')", + "config_forbidden_keyword": "Słowo kluczowe '{keyword}' jest zastrzeżone. Nie można tworzyć ani używać panelu konfiguracji z pytaniem o tym identyfikatorze.", + "backup_output_directory_forbidden": "Wybierz inną ścieżkę docelową. Kopie zapasowe nie mogą być tworzone w podfolderach /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ani /home/yunohost.backup/archives", + "confirm_app_insufficient_ram": "UWAGA! Ta aplikacja wymaga {required} pamięci RAM do zainstalowania/aktualizacji, a obecnie dostępne jest tylko {current}. Nawet jeśli aplikacja mogłaby działać, proces instalacji/aktualizacji wymaga dużej ilości pamięci RAM, więc serwer może się zawiesić i niepowodzenie może być katastrofalne. Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'", + "app_not_upgraded_broken_system": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu. W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}", + "app_not_upgraded_broken_system_continue": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu (parametr --continue-on-failure jest ignorowany). W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}", + "certmanager_domain_http_not_working": "Domena {domain} wydaje się niedostępna przez HTTP. Sprawdź kategorię 'Strona internetowa' diagnostyki, aby uzyskać więcej informacji. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)" +} From 8caff6a9dcd2762ee67e3640ad8669917cc803aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Sat, 10 Jun 2023 22:27:39 +0200 Subject: [PATCH 0108/1116] Allow passing a list in the manifest.toml for the apt resource packages --- src/utils/resources.py | 43 +++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 9891fe9c6..18f1aa7eb 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1011,16 +1011,16 @@ class AptDependenciesAppResource(AppResource): ##### Example ```toml [resources.apt] - packages = "nyancat, lolcat, sl" + 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" + extras.yarn.packages = ["yarn"] ``` ##### Properties - - `packages`: Comma-separated list of packages to be installed via `apt` + - `packages`: List of packages to be installed via `apt` - `packages_from_raw_bash`: A multi-line bash snippet (using triple quotes as open/close) which should echo additional packages to be installed. Meant to be used for packages to be conditionally installed depending on architecture, debian version, install questions, or other logic. - `extras`: A dict of (repo, key, packages) corresponding to "extra" repositories to fetch dependencies from @@ -1047,17 +1047,11 @@ class AptDependenciesAppResource(AppResource): 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", - raw_msg=True, - ) - super().__init__(properties, *args, **kwargs) + if isinstance(self.packages, str): + self.packages = [value.strip() for value in self.packages.split(",")] + if self.packages_from_raw_bash: out, err = self.check_output_bash_snippet(self.packages_from_raw_bash) if err: @@ -1065,14 +1059,29 @@ class AptDependenciesAppResource(AppResource): "Error while running apt resource packages_from_raw_bash snippet:" ) logger.error(err) - self.packages += ", " + out.replace("\n", ", ") + self.packages += out.split("\n") + + for key, values in self.extras.items(): + if isinstance(values.get("packages"), str): + values["packages"] = [value.strip() for value in values["packages"].split(",")] + + if not isinstance(values.get("repo"), str) \ + or not isinstance(values.get("key"), str) \ + or not isinstance(values.get("packages"), list): + raise YunohostError( + "In apt resource in the manifest: 'extras' repo should have the keys 'repo', 'key' defined as strings and 'packages' defined as list", + raw_msg=True, + ) def provision_or_update(self, context: Dict = {}): - script = [f"ynh_install_app_dependencies {self.packages}"] + script = " ".join(["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']}'" - ] + script += " ".join([ + "ynh_install_extra_app_dependencies", + f"--repo='{values['repo']}'", + f"--key='{values['key']}'", + f"--package='{' '.join(values['packages'])}'" + ]) # FIXME : we're feeding the raw value of values['packages'] to the helper .. if we want to be consistent, may they should be comma-separated, though in the majority of cases, only a single package is installed from an extra repo.. self._run_script("provision_or_update", "\n".join(script)) From 69339f8d0eda216ca159ad385e802e725c1b1fe9 Mon Sep 17 00:00:00 2001 From: Tymofii-Lytvynenko Date: Sat, 10 Jun 2023 23:04:51 +0000 Subject: [PATCH 0109/1116] Translated using Weblate (Ukrainian) Currently translated at 100.0% (768 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/uk/ --- locales/uk.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/locales/uk.json b/locales/uk.json index fca0ea360..07cbfe6da 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -234,7 +234,7 @@ "group_already_exist_on_system": "Група {group} вже існує в групах системи", "group_already_exist": "Група {group} вже існує", "good_practices_about_user_password": "Зараз ви збираєтеся поставити новий пароль користувача. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", - "good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адміністрування. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольного гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", + "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-ретрансляції", @@ -760,5 +760,11 @@ "app_not_enough_ram": "Для встановлення/оновлення цього застосунку потрібно {required} оперативної пам'яті, але наразі доступно лише {current}.", "app_resource_failed": "Не вдалося надати, позбавити або оновити ресурси для {app}: {error}", "apps_failed_to_upgrade": "Ці застосунки не вдалося оновити:{apps}", - "apps_failed_to_upgrade_line": "\n * {app_id} (щоб побачити відповідний журнал, виконайте 'yunohost log show {operation_logger_name}')" -} \ No newline at end of file + "apps_failed_to_upgrade_line": "\n * {app_id} (щоб побачити відповідний журнал, виконайте 'yunohost log show {operation_logger_name}')", + "group_mailalias_add": "Псевдонім електронної пошти '{mail}' буде додано до групи '{group}'", + "group_mailalias_remove": "Псевдонім електронної пошти '{mail}' буде вилучено з групи '{group}'", + "group_user_add": "Користувача '{user}' буде додано до групи '{group}'", + "group_user_remove": "Користувача '{user}' буде вилучено з групи '{group}'", + "app_corrupt_source": "YunoHost зміг завантажити ресурс '{source_id}' ({url}) для {app}, але він не відповідає очікуваній контрольній сумі. Це може означати, що на вашому сервері стався тимчасовий збій мережі, АБО ресурс був якимось чином змінений висхідним супровідником (або зловмисником?), і пакувальникам YunoHost потрібно дослідити і оновити маніфест застосунку, щоб відобразити цю зміну.\n Очікувана контрольна сума sha256: {expected_sha256}\n Обчислена контрольна сума sha256: {computed_sha256}\n Розмір завантаженого файлу: {size}", + "app_failed_to_download_asset": "Не вдалося завантажити ресурс '{source_id}' ({url}) для {app}: {out}" +} From bc42fd7ab23dfb99e314b69acb3999bdaaed0a68 Mon Sep 17 00:00:00 2001 From: ppr Date: Sun, 11 Jun 2023 08:07:42 +0000 Subject: [PATCH 0110/1116] Translated using Weblate (French) Currently translated at 100.0% (768 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 91d52dc86..f98470c99 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -762,5 +762,9 @@ "apps_failed_to_upgrade": "Ces applications n'ont pas pu être mises à jour : {apps}", "apps_failed_to_upgrade_line": "\n * {app_id} (pour voir le journal correspondant, faites un 'yunohost log show {operation_logger_name}')", "app_failed_to_download_asset": "Échec du téléchargement de la ressource '{source_id}' ({url}) pour {app} : {out}", - "app_corrupt_source": "YunoHost a pu télécharger la ressource '{source_id}' ({url}) pour {app}, malheureusement celle-ci ne correspond pas à la somme de contrôle attendue. Cela peut signifier qu'une défaillance temporaire du réseau s'est produite sur votre serveur, OU que la ressource a été modifiée par le mainteneur de l'application en amont (ou un acteur malveillant ?) et que les responsables du paquet de cette application pour YunoHost doivent investiguer et mettre à jour le manifeste de l'application pour refléter ce changement.\n Somme de contrôle sha256 attendue : {expected_sha256}\n Somme de contrôle sha256 téléchargée : {computed_sha256}\n Taille du fichier téléchargé : {size}" + "app_corrupt_source": "YunoHost a pu télécharger la ressource '{source_id}' ({url}) pour {app}, malheureusement celle-ci ne correspond pas à la somme de contrôle attendue. Cela peut signifier qu'une défaillance temporaire du réseau s'est produite sur votre serveur, OU que la ressource a été modifiée par le mainteneur de l'application en amont (ou un acteur malveillant ?) et que les responsables du paquet de cette application pour YunoHost doivent investiguer et mettre à jour le manifeste de l'application pour refléter ce changement.\n Somme de contrôle sha256 attendue : {expected_sha256}\n Somme de contrôle sha256 téléchargée : {computed_sha256}\n Taille du fichier téléchargé : {size}", + "group_mailalias_add": "L'alias de courrier électronique '{mail}' sera ajouté au groupe '{group}'", + "group_user_add": "L'utilisateur '{user}' sera ajouté au groupe '{group}'", + "group_user_remove": "L'utilisateur '{user}' sera retiré du groupe '{group}'", + "group_mailalias_remove": "L'alias de courrier électronique '{mail}' sera supprimé du groupe '{group}'" } From fcf263242eb739a4d783592ff9a8db540eb736b9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 11 Jun 2023 19:35:49 +0200 Subject: [PATCH 0111/1116] Update changelog for 11.1.21 --- debian/changelog | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/debian/changelog b/debian/changelog index 587202566..e6d4d542a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,17 @@ +yunohost (11.1.21) stable; urgency=low + + - users: more verbose logs for user_group_update operations ([#1668](https://github.com/yunohost/yunohost/pull/1668)) + - apps: fix auto-catalog update cron job which was broken because --apps doesnt exist anymore (1552944f) + - apps: Add a 'yunohost app shell' command to open a shell into an app environment ([#1656](https://github.com/yunohost/yunohost/pull/1656)) + - security/regenconf: fix security issue where apps' system conf would be owned by the app, which can enable priviledge escalation (daf51e94) + - security/regenconf: force systemd, nginx, php and fail2ban conf to be owned by root (e649c092) + - security/nginx: use /var/www/.well-known folder for ynh diagnosis and acme challenge, because /tmp/ could be manipulated by user to serve maliciously crafted files (d42c9983) + - i18n: Translations updated for French, Polish, Ukrainian + + Thanks to all contributors <3 ! (Kay0u, Kuba Bazan, ppr, sudo, Tagada, tituspijean, Tymofii-Lytvynenko) + + -- Alexandre Aubin Sun, 11 Jun 2023 19:20:27 +0200 + yunohost (11.1.20) stable; urgency=low - appsv2: fix funky current_version not being defined when hydrating pre-upgrade notifications (8fa823b4) From e6f134bc913e3097241919334902772175b11d95 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 12 Jun 2023 00:02:43 +0200 Subject: [PATCH 0112/1116] Fix stupid issue with code that changes /dev/null perms... --- hooks/conf_regen/01-yunohost | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 0d6876cf4..1b15814f2 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -181,8 +181,11 @@ do_post_regen() { # 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 chmod 755 /etc/yunohost - chown root:root /etc/systemd/system/*.service - chmod 644 /etc/systemd/system/*.service + # Stupid fix for a previous commit that changed /dev/null perms because some files in /etc/systemd/system are symlinks >_> + chown 666 /dev/null + + find /etc/systemd/system/*.service -type f | xargs -r0 chown root:root + find /etc/systemd/system/*.service -type f | xargs -r0 chmod 0644 if ls -l /etc/php/*/fpm/pool.d/*.conf then From 1222c47620244e80983d730e8c888de2b7eacaae Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 12 Jun 2023 00:03:10 +0200 Subject: [PATCH 0113/1116] Update changelog for 11.1.21.1 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index e6d4d542a..d12520d3c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (11.1.21.1) stable; urgency=low + + - Fix stupid issue with code that changes /dev/null perms... (e6f134bc) + + -- Alexandre Aubin Mon, 12 Jun 2023 00:02:47 +0200 + yunohost (11.1.21) stable; urgency=low - users: more verbose logs for user_group_update operations ([#1668](https://github.com/yunohost/yunohost/pull/1668)) From 313a16476a947924ebbe9a61b232fdc2681818ca Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 12 Jun 2023 00:25:38 +0200 Subject: [PATCH 0114/1116] Aleks loves xargs syntax >_> --- hooks/conf_regen/01-yunohost | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 1b15814f2..198eab3e7 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -184,8 +184,8 @@ do_post_regen() { # Stupid fix for a previous commit that changed /dev/null perms because some files in /etc/systemd/system are symlinks >_> chown 666 /dev/null - find /etc/systemd/system/*.service -type f | xargs -r0 chown root:root - find /etc/systemd/system/*.service -type f | xargs -r0 chmod 0644 + find /etc/systemd/system/*.service -type f | xargs -r chown root:root + find /etc/systemd/system/*.service -type f | xargs -r chmod 0644 if ls -l /etc/php/*/fpm/pool.d/*.conf then From e1569f962bce6405b913f5713a49a65ad258a34a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 12 Jun 2023 00:26:43 +0200 Subject: [PATCH 0115/1116] Update changelog for 11.1.21.2 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index d12520d3c..ed797d30a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (11.1.21.2) stable; urgency=low + + - Aleks loves xargs syntax >_> (313a1647) + + -- Alexandre Aubin Mon, 12 Jun 2023 00:25:44 +0200 + yunohost (11.1.21.1) stable; urgency=low - Fix stupid issue with code that changes /dev/null perms... (e6f134bc) From 2f982e26a92056d4486140e574a6fa0ddc1be05a Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Mon, 12 Jun 2023 00:30:59 +0000 Subject: [PATCH 0116/1116] [CI] Format code with Black --- src/app.py | 9 ++++++++- src/user.py | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app.py b/src/app.py index 04340b1ba..3b749725d 100644 --- a/src/app.py +++ b/src/app.py @@ -1653,7 +1653,14 @@ def app_shell(app): app -- App ID """ - subprocess.run(['/bin/bash', '-c', 'source /usr/share/yunohost/helpers && ynh_spawn_app_shell '+app]) + subprocess.run( + [ + "/bin/bash", + "-c", + "source /usr/share/yunohost/helpers && ynh_spawn_app_shell " + app, + ] + ) + def app_register_url(app, domain, path): """ diff --git a/src/user.py b/src/user.py index 3f453f69e..00876854e 100644 --- a/src/user.py +++ b/src/user.py @@ -1259,7 +1259,9 @@ def user_group_update( ) if mail in new_group_mail: new_group_mail.remove(mail) - logger.info(m18n.n("group_mailalias_remove", group=groupname, mail=mail)) + logger.info( + m18n.n("group_mailalias_remove", group=groupname, mail=mail) + ) else: raise YunohostValidationError("mail_alias_remove_failed", mail=mail) From 84984ad89a0839251250146c5298188ef761eace Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 12 Jun 2023 17:26:24 +0200 Subject: [PATCH 0117/1116] Fix again /var/www/.well-known/ynh-diagnosis/ perms which are too broad and could be exploited to serve malicious files x_x --- hooks/conf_regen/01-yunohost | 2 ++ src/diagnosers/21-web.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 198eab3e7..ed09edb79 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -178,6 +178,8 @@ do_post_regen() { chown root:admins /home/yunohost.backup/archives chown root:root /var/cache/yunohost + [ ! -e /var/www/.well-known/ynh-diagnosis/ ] || chmod 775 /var/www/.well-known/ynh-diagnosis/ + # 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 chmod 755 /etc/yunohost diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index ce6de4b17..cc6edd7dc 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -61,7 +61,7 @@ class MyDiagnoser(Diagnoser): self.nonce = "".join(random.choice("0123456789abcedf") for i in range(16)) rm("/var/www/.well-known/ynh-diagnosis/", recursive=True, force=True) - mkdir("/var/www/.well-known/ynh-diagnosis/", parents=True) + mkdir("/var/www/.well-known/ynh-diagnosis/", parents=True, mode=0o0775) os.system("touch /var/www/.well-known/ynh-diagnosis/%s" % self.nonce) if not domains_to_check: From 6278c6858674a0aa5edf5f170b388f34f5a5d6eb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 12 Jun 2023 17:42:10 +0200 Subject: [PATCH 0118/1116] Update changelog for 11.1.21.3 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index ed797d30a..b37025a4e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (11.1.21.3) stable; urgency=low + + - Fix again /var/www/.well-known/ynh-diagnosis/ perms which are too broad and could be exploited to serve malicious files x_x (84984ad8) + + -- Alexandre Aubin Mon, 12 Jun 2023 17:41:26 +0200 + yunohost (11.1.21.2) stable; urgency=low - Aleks loves xargs syntax >_> (313a1647) From 8242cab735d12efe622600ce2c7cd64c1a6c380d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 13 Jun 2023 12:28:50 +0200 Subject: [PATCH 0119/1116] Get rid of previous tmp hack about /dev/null for people that went through the very first 11.1.21, because it's causing issue in unpriviledged LXC or similar context --- hooks/conf_regen/01-yunohost | 3 --- 1 file changed, 3 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index ed09edb79..1d7a449e4 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -183,9 +183,6 @@ do_post_regen() { # 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 chmod 755 /etc/yunohost - # Stupid fix for a previous commit that changed /dev/null perms because some files in /etc/systemd/system are symlinks >_> - chown 666 /dev/null - find /etc/systemd/system/*.service -type f | xargs -r chown root:root find /etc/systemd/system/*.service -type f | xargs -r chmod 0644 From 48ee78afa23b7de78ad3ac2224b329d567ef98cf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 13 Jun 2023 14:48:30 +0200 Subject: [PATCH 0120/1116] fix tests: my_webapp is using manifest v2 now --- src/tests/test_apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index 5db180b7e..1a3f5e97b 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -112,7 +112,7 @@ 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 - if "manifestv2" in app: + if "manifestv2" in app or "my_webapp" in app: yield "/etc/yunohost/apps/%s/manifest.toml" % app else: yield "/etc/yunohost/apps/%s/manifest.json" % app From 8728b2030cea9c83f3c65e77406cc2524ca4dfd8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 13 Jun 2023 14:55:28 +0200 Subject: [PATCH 0121/1116] Remove migrations/0027_migrate_to_bookworm for now because it's triggering errors on the CI, at least half of it should be reworked, and it should be in a separated PR to target dev(=bullseye) --- src/migrations/0027_migrate_to_bookworm.py | 546 --------------------- 1 file changed, 546 deletions(-) delete mode 100644 src/migrations/0027_migrate_to_bookworm.py diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py deleted file mode 100644 index 85e2235af..000000000 --- a/src/migrations/0027_migrate_to_bookworm.py +++ /dev/null @@ -1,546 +0,0 @@ -import glob -import os - -from moulinette import m18n -from yunohost.utils.error import YunohostError -from moulinette.utils.log import getActionLogger -from moulinette.utils.process import check_output, call_async_output -from moulinette.utils.filesystem import read_file, rm, write_to_file - -from yunohost.tools import ( - Migration, - tools_update, - tools_upgrade, - _apt_log_line_is_relevant, -) -from yunohost.app import unstable_apps -from yunohost.regenconf import manually_modified_files, _force_clear_hashes -from yunohost.utils.system import ( - free_space_in_directory, - get_ynh_package_version, - _list_upgradable_apt_packages, -) -from yunohost.service import _get_services, _save_services - -logger = getActionLogger("yunohost.migration") - -N_CURRENT_DEBIAN = 10 -N_CURRENT_YUNOHOST = 4 - -VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bookworm_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): - "Upgrade the system to Debian Bookworm and Yunohost 11.x" - - mode = "manual" - - def run(self): - self.check_assertions() - - logger.info(m18n.n("migration_0021_start")) - - # - # Add new apt .deb signing key - # - - new_apt_key = "https://forge.yunohost.org/yunohost_bookworm.asc" - check_output(f"wget -O- {new_apt_key} -q | apt-key add -qq -") - - # - # Patch sources.list - # - logger.info(m18n.n("migration_0021_patching_sources_list")) - self.patch_apt_sources_list() - - # Stupid OVH has some repo configured which dont work with bookworm 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 - # hence triggering some dependency conflict (or foobar-ynh-deps uninstall) - # Adding it there shouldnt be a big deal - Yunohost 11.x does add it - # through its regen conf anyway. - if not os.path.exists("/etc/apt/sources.list.d/extra_php_version.list"): - open("/etc/apt/sources.list.d/extra_php_version.list", "w").write( - "deb https://packages.sury.org/php/ bookworm main" - ) - - # 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 Bookworm - # 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 - # - - tools_update(target="system") - - # Tell libc6 it's okay to restart system stuff during the upgrade - os.system( - "echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections" - ) - - # Do not restart nginx during the upgrade of nginx-common and nginx-extras ... - # c.f. https://manpages.debian.org/bookworm/init-system-helpers/deb-systemd-invoke.1p.en.html - # and zcat /usr/share/doc/init-system-helpers/README.policy-rc.d.gz - # and the code inside /usr/bin/deb-systemd-invoke to see how it calls /usr/sbin/policy-rc.d ... - # and also invoke-rc.d ... - write_to_file( - "/usr/sbin/policy-rc.d", - '#!/bin/bash\n[[ "$1" =~ "nginx" ]] && [[ "$2" == "restart" ]] && exit 101 || exit 0', - ) - os.system("chmod +x /usr/sbin/policy-rc.d") - - # Don't send an email to root about the postgresql migration. It should be handled automatically after. - os.system( - "echo 'postgresql-common postgresql-common/obsolete-major seen true' | debconf-set-selections" - ) - - # - # Patch yunohost conflicts - # - logger.info(m18n.n("migration_0021_patch_yunohost_conflicts")) - - self.patch_yunohost_conflicts() - - # - # Specific tweaking to get rid of custom my.cnf and use debian's default one - # (my.cnf is actually a symlink to mariadb.cnf) - # - - _force_clear_hashes(["/etc/mysql/my.cnf"]) - rm("/etc/mysql/mariadb.cnf", force=True) - rm("/etc/mysql/my.cnf", force=True) - ret = self.apt_install( - "mariadb-common --reinstall -o Dpkg::Options::='--force-confmiss'" - ) - if ret != 0: - raise YunohostError("Failed to reinstall mariadb-common ?", raw_msg=True) - - # - # /usr/share/yunohost/yunohost-config/ssl/yunoCA -> /usr/share/yunohost/ssl - # - if os.path.exists("/usr/share/yunohost/yunohost-config/ssl/yunoCA"): - os.system( - "mv /usr/share/yunohost/yunohost-config/ssl/yunoCA /usr/share/yunohost/ssl" - ) - rm("/usr/share/yunohost/yunohost-config", recursive=True, force=True) - - # - # /home/yunohost.conf -> /var/cache/yunohost/regenconf - # - if os.path.exists("/home/yunohost.conf"): - os.system("mv /home/yunohost.conf /var/cache/yunohost/regenconf") - rm("/home/yunohost.conf", recursive=True, force=True) - - # Remove legacy postgresql service record added by helpers, - # will now be dynamically handled by the core in bookworm - services = _get_services() - if "postgresql" in services: - 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", - ) - - # - # Main upgrade - # - logger.info(m18n.n("migration_0021_main_upgrade")) - - apps_packages = self.get_apps_equivs_packages() - self.hold(apps_packages) - tools_upgrade(target="system", allow_yunohost_upgrade=False) - - if self.debian_major_version() == N_CURRENT_DEBIAN: - raise YunohostError("migration_0021_still_on_bullseye_after_main_upgrade") - - # Force explicit install of php8.2fpm and other old 'default' dependencies - # that are now only in Recommends - # - # Also, we need to install php8.2 equivalents of other php7.4 dependencies. - # For example, Nextcloud may depend on php7.3-zip, and after the php pool migration - # to autoupgrade Nextcloud to 8.2, it will need the php8.2-zip to work. - # The following list is based on an ad-hoc analysis of php deps found in the - # app ecosystem, with a known equivalent on php8.2. - # - # This is kinda a dirty hack as it doesnt properly update the *-ynh-deps virtual packages - # with the proper list of dependencies, and the dependencies install this way - # will get flagged as 'manually installed'. - # - # We'll probably want to do something during the Bookworm->Bookworm migration to re-flag - # these as 'auto' so they get autoremoved if not needed anymore. - # Also hopefully by then we'll have manifestv2 (maybe) and will be able to use - # the apt resource mecanism to regenerate the *-ynh-deps virtual packages ;) - - php74packages_suffixes = [ - "apcu", - "bcmath", - "bz2", - "dom", - "gmp", - "igbinary", - "imagick", - "imap", - "mbstring", - "memcached", - "mysqli", - "mysqlnd", - "pgsql", - "redis", - "simplexml", - "soap", - "sqlite3", - "ssh2", - "tidy", - "xml", - "xmlrpc", - "xsl", - "zip", - ] - - cmd = ( - "apt show '*-ynh-deps' 2>/dev/null" - " | grep Depends" - f" | grep -o -E \"php7.4-({'|'.join(php74packages_suffixes)})\"" - " | sort | uniq" - " | sed 's/php7.4/php8.2/g'" - " || true" - ) - - basephp82packages_to_install = [ - "php8.2-fpm", - "php8.2-common", - "php8.2-ldap", - "php8.2-intl", - "php8.2-mysql", - "php8.2-gd", - "php8.2-curl", - "php-php-gettext", - ] - - php74packages_to_install = basephp82packages_to_install + [ - f.strip() for f in check_output(cmd).split("\n") if f.strip() - ] - - ret = self.apt_install( - f"{' '.join(php74packages_to_install)} " - "$(dpkg --list | grep ynh-deps | awk '{print $2}') " - "-o Dpkg::Options::='--force-confmiss'" - ) - if ret != 0: - raise YunohostError( - "Failed to force the install of php dependencies ?", raw_msg=True - ) - - # Clean the mess - logger.info(m18n.n("migration_0021_cleaning_up")) - 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 - # - logger.info(m18n.n("migration_0021_yunohost_upgrade")) - - self.unhold(apps_packages) - - cmd = "LC_ALL=C" - cmd += " DEBIAN_FRONTEND=noninteractive" - cmd += " APT_LISTCHANGES_FRONTEND=none" - cmd += " apt dist-upgrade " - cmd += " --quiet -o=Dpkg::Use-Pty=0 --fix-broken --dry-run" - cmd += " | grep -q 'ynh-deps'" - - logger.info("Simulating upgrade...") - if os.system(cmd) == 0: - raise YunohostError( - "The upgrade cannot be completed, because some app dependencies would need to be removed?", - raw_msg=True, - ) - - postupgradecmds = f"apt-mark auto {' '.join(basephp74packages_to_install)}\n" - postupgradecmds += "rm -f /usr/sbin/policy-rc.d\n" - postupgradecmds += "echo 'Restarting nginx...' >&2\n" - postupgradecmds += "systemctl restart nginx\n" - - tools_upgrade(target="system", postupgradecmds=postupgradecmds) - - def debian_major_version(self): - # The python module "platform" and lsb_release are not reliable because - # on some setup, they may still return Release=9 even after upgrading to - # bullseye ... (Apparently this is related to OVH overriding some stuff - # with /etc/lsb-release for instance -_-) - # Instead, we rely on /etc/os-release which should be the raw info from - # the distribution... - return int( - check_output( - "grep VERSION_ID /etc/os-release | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2" - ) - ) - - def yunohost_major_version(self): - return int(get_ynh_package_version("yunohost")["version"].split(".")[0]) - - def check_assertions(self): - # Be on bullseye (10.x) and yunohost 4.x - # NB : we do both check to cover situations where the upgrade crashed - # in the middle and debian version could be > 9.x but yunohost package - # would still be in 3.x... - if ( - not self.debian_major_version() == N_CURRENT_DEBIAN - and not self.yunohost_major_version() == N_CURRENT_YUNOHOST - ): - 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_bullseye2") - - # 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/ ? - # FIXME: Create a way to ignore this check, on some system 70M is enough... - 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 'bookworm' is already in the sources.list ... - # which means maybe a previous upgrade crashed and we're re-running it) - if os.path.exists("/etc/apt/sources.list") and " bookworm " not in read_file( - "/etc/apt/sources.list" - ): - tools_update(target="system") - upgradable_system_packages = list(_list_upgradable_apt_packages()) - upgradable_system_packages = [ - package["name"] for package in upgradable_system_packages - ] - upgradable_system_packages = set(upgradable_system_packages) - # Lime2 have hold packages to avoid ethernet instability - # See https://github.com/YunoHost/arm-images/commit/b4ef8c99554fd1a122a306db7abacc4e2f2942df - lime2_hold_packages = set( - [ - "armbian-firmware", - "armbian-bsp-cli-lime2", - "linux-dtb-current-sunxi", - "linux-image-current-sunxi", - "linux-u-boot-lime2-current", - "linux-image-next-sunxi", - ] - ) - if upgradable_system_packages - lime2_hold_packages: - raise YunohostError("migration_0021_system_not_fully_up_to_date") - - @property - def disclaimer(self): - # Avoid having a super long disclaimer + uncessary check if we ain't - # on bullseye / yunohost 4.x anymore - # NB : we do both check to cover situations where the upgrade crashed - # in the middle and debian version could be >= 10.x but yunohost package - # would still be in 4.x... - if ( - not self.debian_major_version() == N_CURRENT_DEBIAN - and not self.yunohost_major_version() == N_CURRENT_YUNOHOST - ): - return None - - # Get list of problematic apps ? I.e. not official or community+working - problematic_apps = unstable_apps() - problematic_apps = "".join(["\n - " + app for app in problematic_apps]) - - # Manually modified files ? (c.f. yunohost service regen-conf) - modified_files = manually_modified_files() - modified_files = "".join(["\n - " + f for f in modified_files]) - - message = m18n.n("migration_0021_general_warning") - - 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/20590\n\n" - + message - ) - - if problematic_apps: - message += "\n\n" + m18n.n( - "migration_0021_problematic_apps_warning", - problematic_apps=problematic_apps, - ) - - if modified_files: - message += "\n\n" + m18n.n( - "migration_0021_modified_files", manually_modified_files=modified_files - ) - - return message - - def patch_apt_sources_list(self): - sources_list = glob.glob("/etc/apt/sources.list.d/*.list") - if os.path.exists("/etc/apt/sources.list"): - sources_list.append("/etc/apt/sources.list") - - # This : - # - replace single 'bullseye' occurence by 'bulleye' - # - comments lines containing "backports" - for f in sources_list: - command = ( - f"sed -i {f} " - "-e 's@ bullseye @ bookworm @g' " - "-e '/backports/ s@^#*@#@' " - "-e 's@ bullseye-@ bookworm-@g' " - ) - os.system(command) - - def get_apps_equivs_packages(self): - command = ( - "dpkg --get-selections" - " | grep -v deinstall" - " | awk '{print $1}'" - " | { grep 'ynh-deps$' || true; }" - ) - - output = check_output(command) - - return output.split("\n") if output else [] - - def hold(self, packages): - for package in packages: - os.system(f"apt-mark hold {package}") - - def unhold(self, packages): - for package in packages: - os.system(f"apt-mark unhold {package}") - - def apt_install(self, cmd): - def is_relevant(line): - return "Reading database ..." not in line.rstrip() - - callbacks = ( - lambda l: logger.info("+ " + l.rstrip() + "\r") - if _apt_log_line_is_relevant(l) - else logger.debug(l.rstrip() + "\r"), - lambda l: logger.warning(l.rstrip()) - if _apt_log_line_is_relevant(l) - else logger.debug(l.rstrip()), - ) - - cmd = ( - "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt install --quiet -o=Dpkg::Use-Pty=0 --fix-broken --assume-yes " - + cmd - ) - - logger.debug("Running: %s" % cmd) - - return call_async_output(cmd, callbacks, shell=True) - - def patch_yunohost_conflicts(self): - # - # This is a super dirty hack to remove the conflicts from yunohost's debian/control file - # Those conflicts are there to prevent mistakenly upgrading critical packages - # such as dovecot, postfix, nginx, openssl, etc... usually related to mistakenly - # using backports etc. - # - # The hack consists in savagely removing the conflicts directly in /var/lib/dpkg/status - # - - # We only patch the conflict if we're on yunohost 4.x - if self.yunohost_major_version() != N_CURRENT_YUNOHOST: - return - - conflicts = check_output("dpkg-query -s yunohost | grep '^Conflicts:'").strip() - if conflicts: - # We want to keep conflicting with apache/bind9 tho - new_conflicts = "Conflicts: apache2, bind9" - - command = ( - f"sed -i /var/lib/dpkg/status -e 's@{conflicts}@{new_conflicts}@g'" - ) - logger.debug(f"Running: {command}") - os.system(command) From c4c353843c6fc147d1cbaa92e21edc3b09dda702 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 13 Jun 2023 14:56:21 +0200 Subject: [PATCH 0122/1116] Unused vars, unhappy linter gods --- src/firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firewall.py b/src/firewall.py index d6e4b5317..ccd7e6d88 100644 --- a/src/firewall.py +++ b/src/firewall.py @@ -404,7 +404,7 @@ def firewall_upnp(action="status", no_refresh=False): logger.debug("discovering UPnP devices...") try: nb_dev = upnpc.discover() - except Exception as e: + except Exception: logger.warning("Failed to find any UPnP device on the network") nb_dev = -1 enabled = False From 194eb9c6c7d6684a2802311bd6afcece7336d7d6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 13 Jun 2023 20:14:46 +0200 Subject: [PATCH 0123/1116] conf: Update ciphers for nginx, postfix, dovecot --- conf/dovecot/dovecot.conf | 7 +++---- conf/nginx/security.conf.inc | 10 +++++----- conf/postfix/main.cf | 10 +++++----- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index e614c3796..14d407563 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -13,9 +13,8 @@ protocols = imap sieve {% if pop3_enabled == "True" %}pop3{% endif %} mail_plugins = $mail_plugins quota notify push_notification ############################################################################### - -# generated 2020-08-18, Mozilla Guideline v5.6, Dovecot 2.3.4, OpenSSL 1.1.1d, intermediate configuration -# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.4&config=intermediate&openssl=1.1.1d&guideline=5.6 +# generated 2023-06-13, Mozilla Guideline v5.7, Dovecot 2.3.19, OpenSSL 3.0.9, intermediate configuration +# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.19&config=intermediate&openssl=3.0.9&guideline=5.7 ssl = required @@ -32,7 +31,7 @@ ssl_dh = = 1024 bits smtpd_tls_dh1024_param_file = /usr/share/yunohost/ffdhe2048.pem -tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 +tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 {% else %} -# generated 2020-08-18, Mozilla Guideline v5.6, Postfix 3.4.14, OpenSSL 1.1.1d, modern configuration -# https://ssl-config.mozilla.org/#server=postfix&version=3.4.14&config=modern&openssl=1.1.1d&guideline=5.6 +# generated 2023-06-13, Mozilla Guideline v5.7, Postfix 3.7.5, OpenSSL 3.0.9, modern configuration +# https://ssl-config.mozilla.org/#server=postfix&version=3.7.5&config=modern&openssl=3.0.9&guideline=5.7 smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2 smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2 From 81b96ad6d45c16eeae0bbcd4774bec139b63ca08 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 13 Jun 2023 21:30:20 +0200 Subject: [PATCH 0124/1116] tests: tmp tweaks to adapt for removed deprecated features --- src/tests/test_backuprestore.py | 5 ++++- src/tests/test_permission.py | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tests/test_backuprestore.py b/src/tests/test_backuprestore.py index 873deec7d..eb59d4fea 100644 --- a/src/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -439,7 +439,8 @@ def test_backup_using_copy_method(): # App restore # # - +# FIXME : switch to a backup from 11.x +@pytest.mark.skip @pytest.mark.with_wordpress_archive_from_4p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_wordpress_from_Ynh4p2(): @@ -504,6 +505,8 @@ def test_restore_app_not_in_backup(mocker): assert not _is_installed("yoloswag") +# FIXME : switch to a backup from 11.x +@pytest.mark.skip @pytest.mark.with_wordpress_archive_from_4p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_already_installed(mocker): diff --git a/src/tests/test_permission.py b/src/tests/test_permission.py index 8620e9611..dc9121745 100644 --- a/src/tests/test_permission.py +++ b/src/tests/test_permission.py @@ -1131,7 +1131,7 @@ def test_permission_app_propagation_on_ssowat(): def test_permission_legacy_app_propagation_on_ssowat(): app_install( os.path.join(get_test_apps_dir(), "legacy_app_ynh"), - args="domain=%s&domain_2=%s&path=%s&is_public=1" + args="domain=%s&domain_2=%s&path=%s&is_public=0" % (maindomain, other_domains[0], "/legacy"), force=True, ) @@ -1139,12 +1139,12 @@ def test_permission_legacy_app_propagation_on_ssowat(): # App is configured as public by default using the legacy unprotected_uri mechanics # It should automatically be migrated during the install res = user_permission_list(full=True)["permissions"] - assert "visitors" in res["legacy_app.main"]["allowed"] + assert "visitors" not in res["legacy_app.main"]["allowed"] assert "all_users" in res["legacy_app.main"]["allowed"] app_webroot = "https://%s/legacy" % maindomain - assert can_access_webpage(app_webroot, logged_as=None) + assert not can_access_webpage(app_webroot, logged_as=None) assert can_access_webpage(app_webroot, logged_as="alice") # Try to update the permission and check that permissions are still consistent From f6ab380730c7b687dcf8b82985c229d7b0c880ba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 14 Jun 2023 01:34:42 +0200 Subject: [PATCH 0125/1116] helpers/php: Default PHP version in bookworm is now 8.2 --- helpers/php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/php b/helpers/php index 1b28b32f7..7e8d35d6e 100644 --- a/helpers/php +++ b/helpers/php @@ -1,6 +1,6 @@ #!/bin/bash -readonly YNH_DEFAULT_PHP_VERSION=7.4 +readonly YNH_DEFAULT_PHP_VERSION=8.2 # Declare the actual PHP version to use. # A packager willing to use another version of PHP can override the variable into its _common.sh. YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} From 8ac48ee09e7d3d77e3b636e6700c5b9a26dedc93 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 14 Jun 2023 08:04:16 +0200 Subject: [PATCH 0126/1116] Drop deprecated firstname/lastname in user_create/update + also drop old deprecated cert- commands --- share/actionsmap.yml | 76 +----------------------------------- src/domain.py | 3 +- src/tests/test_user-group.py | 8 +--- src/user.py | 45 ++++++--------------- 4 files changed, 17 insertions(+), 115 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index e02f5c1d0..444533a1d 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -70,26 +70,10 @@ user: help: The full name of the user. For example 'Camille Dupont' extra: ask: ask_fullname - required: False + required: True pattern: &pattern_fullname - !!str ^([^\W_]{1,30}[ ,.'-]{0,3})+$ - "pattern_fullname" - -f: - full: --firstname - help: Deprecated. Use --fullname instead. - extra: - required: False - pattern: &pattern_firstname - - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - - "pattern_firstname" - -l: - full: --lastname - help: Deprecated. Use --fullname instead. - extra: - required: False - pattern: &pattern_lastname - - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - - "pattern_lastname" -p: full: --password help: User password @@ -147,16 +131,6 @@ user: 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: full: --mail extra: @@ -551,54 +525,6 @@ domain: extra: pattern: *pattern_domain - ### certificate_status() - cert-status: - deprecated: true - action_help: List status of current certificates (all by default). - arguments: - domain_list: - help: Domains to check - nargs: "*" - --full: - help: Show more details - action: store_true - - ### certificate_install() - cert-install: - deprecated: true - action_help: Install Let's Encrypt certificates for given domains (all by default). - arguments: - domain_list: - help: Domains for which to install the certificates - nargs: "*" - --force: - help: Install even if current certificate is not self-signed - action: store_true - --no-checks: - help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to install. (Not recommended) - action: store_true - --self-signed: - help: Install self-signed certificate instead of Let's Encrypt - action: store_true - - ### certificate_renew() - cert-renew: - deprecated: true - action_help: Renew the Let's Encrypt certificates for given domains (all by default). - arguments: - domain_list: - help: Domains for which to renew the certificates - nargs: "*" - --force: - help: Ignore the validity threshold (30 days) - action: store_true - --email: - help: Send an email to root with logs if some renewing fails - action: store_true - --no-checks: - help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) - action: store_true - ### domain_url_available() url-available: hide_in_help: True diff --git a/src/domain.py b/src/domain.py index a2d570b4b..834730890 100644 --- a/src/domain.py +++ b/src/domain.py @@ -154,11 +154,12 @@ def domain_info(domain): from yunohost.app import app_info from yunohost.dns import _get_registar_settings + from yunohost.certificate import certificate_status _assert_domain_exists(domain) registrar, _ = _get_registar_settings(domain) - certificate = domain_cert_status([domain], full=True)["certificates"][domain] + certificate = certificate_status([domain], full=True)["certificates"][domain] apps = [] for app in _installed_apps(): diff --git a/src/tests/test_user-group.py b/src/tests/test_user-group.py index 57f9ffa3f..f347fc9bc 100644 --- a/src/tests/test_user-group.py +++ b/src/tests/test_user-group.py @@ -263,12 +263,6 @@ def test_del_group_that_does_not_exist(mocker): def test_update_user(): - with message("user_updated"): - user_update("alice", firstname="NewName", lastname="NewLast") - - info = user_info("alice") - assert info["fullname"] == "NewName NewLast" - with message("user_updated"): user_update("alice", fullname="New2Name New2Last") @@ -315,7 +309,7 @@ def test_update_group_remove_user_not_already_in(): def test_update_user_that_doesnt_exist(mocker): with raiseYunohostError(mocker, "user_unknown"): - user_update("doesnt_exist", firstname="NewName", lastname="NewLast") + user_update("doesnt_exist", fullname="Foo Bar") def test_update_group_that_doesnt_exist(mocker): diff --git a/src/user.py b/src/user.py index 00876854e..9627a37cb 100644 --- a/src/user.py +++ b/src/user.py @@ -141,33 +141,20 @@ def user_create( domain, password, fullname=None, - firstname=None, - lastname=None, mailbox_quota="0", admin=False, from_import=False, loginShell=None, ): - 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... + raise YunohostValidationError( + "You should specify the fullname of the user using option -F" + ) + 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 @@ -364,8 +351,6 @@ def user_delete(operation_logger, username, purge=False, from_import=False): def user_update( operation_logger, username, - firstname=None, - lastname=None, mail=None, change_password=None, add_mailforward=None, @@ -377,17 +362,15 @@ def user_update( fullname=None, loginShell=None, ): - if firstname or lastname: - logger.warning( - "Options --firstname / --lastname of 'yunohost user create' are deprecated. We recommend using --fullname instead." - ) - 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... + else: + firstname = None + lastname = None from yunohost.domain import domain_list from yunohost.app import app_ssowatconf @@ -884,8 +867,7 @@ def user_import(operation_logger, csvfile, update=False, delete=False): user_update( new_infos["username"], - firstname=new_infos["firstname"], - lastname=new_infos["lastname"], + fullname=(new_infos["firstname"] + " " + new_infos["lastname"]).strip(), change_password=new_infos["password"], mailbox_quota=new_infos["mailbox-quota"], mail=new_infos["mail"], @@ -930,8 +912,7 @@ def user_import(operation_logger, csvfile, update=False, delete=False): user["password"], user["mailbox-quota"], from_import=True, - firstname=user["firstname"], - lastname=user["lastname"], + fullname=(user["firstname"] + " " + user["lastname"]).strip(), ) update(user) result["created"] += 1 From a673b3ed420723054c0aef6761cdf8c03fa77b6e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 14 Jun 2023 10:28:58 +0200 Subject: [PATCH 0127/1116] Postgresql is now version 15 --- helpers/postgresql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/postgresql b/helpers/postgresql index 796a36214..8ff63df43 100644 --- a/helpers/postgresql +++ b/helpers/postgresql @@ -1,7 +1,7 @@ #!/bin/bash PSQL_ROOT_PWD_FILE=/etc/yunohost/psql -PSQL_VERSION=13 +PSQL_VERSION=15 # Open a connection as a user # From 29338f79bc7e7ad3edc30e3a81ae31bb5651a90b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 14 Jun 2023 15:47:17 +0200 Subject: [PATCH 0128/1116] apps: don't attempt to del password key if it doesn't exist --- src/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 3b749725d..03e12c84e 100644 --- a/src/app.py +++ b/src/app.py @@ -1209,7 +1209,8 @@ def app_install( for question in questions: # Or should it be more generally question.redact ? if question.type == "password": - del env_dict_for_logging[f"YNH_APP_ARG_{question.name.upper()}"] + if f"YNH_APP_ARG_{question.name.upper()}" in env_dict_for_logging: + del env_dict_for_logging[f"YNH_APP_ARG_{question.name.upper()}"] if question.name in env_dict_for_logging: del env_dict_for_logging[question.name] From 19eb48b6e73267685e1417122ac47908c1cf2472 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 14 Jun 2023 15:49:16 +0200 Subject: [PATCH 0129/1116] Update changelog for 11.1.21.4 --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index b37025a4e..2c33e3917 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (11.1.21.4) stable; urgency=low + + - regenconf: Get rid of previous tmp hack about /dev/null for people that went through the very first 11.1.21, because it's causing issue in unpriviledged LXC or similar context (8242cab7) + - apps: don't attempt to del password key if it doesn't exist (29338f79) + + -- Alexandre Aubin Wed, 14 Jun 2023 15:48:33 +0200 + yunohost (11.1.21.3) stable; urgency=low - Fix again /var/www/.well-known/ynh-diagnosis/ perms which are too broad and could be exploited to serve malicious files x_x (84984ad8) From ced6d5c975caa90fa51ff5506dc5a82ad992f0bf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 18 Jun 2023 15:45:44 +0200 Subject: [PATCH 0130/1116] apps: fix version.parse now refusing to parse legacy version numbers --- src/app.py | 65 ++++++++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/src/app.py b/src/app.py index 55d351a84..70c0657df 100644 --- a/src/app.py +++ b/src/app.py @@ -241,8 +241,8 @@ def _app_upgradable(app_infos): # Determine upgradability app_in_catalog = app_infos.get("from_catalog") - installed_version = version.parse(app_infos.get("version", "0~ynh0")) - version_in_catalog = version.parse( + installed_version = _parse_app_version(app_infos.get("version", "0~ynh0")) + version_in_catalog = _parse_app_version( app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0") ) @@ -257,25 +257,7 @@ def _app_upgradable(app_infos): ): return "bad_quality" - # If the app uses the standard version scheme, use it to determine - # upgradability - if "~ynh" in str(installed_version) and "~ynh" in str(version_in_catalog): - if installed_version < version_in_catalog: - return "yes" - else: - return "no" - - # Legacy stuff for app with old / non-standard version numbers... - - # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded - if not app_infos["from_catalog"].get("lastUpdate") or not app_infos[ - "from_catalog" - ].get("git"): - return "url_required" - - settings = app_infos["settings"] - local_update_time = settings.get("update_time", settings.get("install_time", 0)) - if app_infos["from_catalog"]["lastUpdate"] > local_update_time: + if installed_version < version_in_catalog: return "yes" else: return "no" @@ -620,9 +602,11 @@ def app_upgrade( # Manage upgrade type and avoid any upgrade if there is nothing to do upgrade_type = "UNKNOWN" # Get current_version and new version - app_new_version = version.parse(manifest.get("version", "?")) - app_current_version = version.parse(app_dict.get("version", "?")) - if "~ynh" in str(app_current_version) and "~ynh" in str(app_new_version): + app_new_version_raw = manifest.get("version", "?") + app_current_version_raw = app_dict.get("version", "?") + app_new_version = _parse_app_version(app_new_version_raw) + app_current_version = _parse_app_version(app_current_version_raw) + if "~ynh" in str(app_current_version_raw) and "~ynh" in str(app_new_version_raw): if app_current_version >= app_new_version and not force: # In case of upgrade from file or custom repository # No new version available @@ -642,10 +626,10 @@ def app_upgrade( upgrade_type = "UPGRADE_FORCED" else: app_current_version_upstream, app_current_version_pkg = str( - app_current_version + app_current_version_raw ).split("~ynh") app_new_version_upstream, app_new_version_pkg = str( - app_new_version + app_new_version_raw ).split("~ynh") if app_current_version_upstream == app_new_version_upstream: upgrade_type = "UPGRADE_PACKAGE" @@ -675,7 +659,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["PRE_UPGRADE"], - current_version=app_current_version, + current_version=app_current_version_raw, data=settings, ) _display_notifications(notifications, force=force) @@ -732,8 +716,8 @@ def app_upgrade( env_dict_more = { "YNH_APP_UPGRADE_TYPE": upgrade_type, - "YNH_APP_MANIFEST_VERSION": str(app_new_version), - "YNH_APP_CURRENT_VERSION": str(app_current_version), + "YNH_APP_MANIFEST_VERSION": str(app_new_version_raw), + "YNH_APP_CURRENT_VERSION": str(app_current_version_raw), } if manifest["packaging_format"] < 2: @@ -916,7 +900,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["POST_UPGRADE"], - current_version=app_current_version, + current_version=app_current_version_raw, data=settings, ) if Moulinette.interface.type == "cli": @@ -1924,6 +1908,20 @@ def _set_app_settings(app, settings): yaml.safe_dump(settings, f, default_flow_style=False) +def _parse_app_version(v): + + if v == "?": + return (0,0) + + try: + if "~" in v: + return (version.parse(v.split("~")[0]), int(v.split("~")[1].replace("ynh", ""))) + else: + return (version.parse(v), 0) + except Exception as e: + raise YunohostError(f"Failed to parse app version '{v}' : {e}", raw_msg=True) + + def _get_manifest_of_app(path): "Get app manifest stored in json or in toml" @@ -3020,12 +3018,7 @@ def _notification_is_dismissed(name, settings): def _filter_and_hydrate_notifications(notifications, current_version=None, data={}): def is_version_more_recent_than_current_version(name, current_version): current_version = str(current_version) - # Boring code to handle the fact that "0.1 < 9999~ynh1" is False - - if "~" in name: - return version.parse(name) > version.parse(current_version) - else: - return version.parse(name) > version.parse(current_version.split("~")[0]) + return _parse_app_version(name) > _parse_app_version(current_version) return { # Should we render the markdown maybe? idk From 8a865dadddbbe82a565a55524261f21c8510fa1b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 19 Jun 2023 16:04:31 +0200 Subject: [PATCH 0131/1116] apps: add YNH_DEFAULT_PHP_VERSION in app's dict as a boring workaround/fix for apps using YNH_DEFAULT_PHP_VERSION in _common.sh *before* sourcing helpers ... --- src/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.py b/src/app.py index 70c0657df..4d11b47fb 100644 --- a/src/app.py +++ b/src/app.py @@ -2792,6 +2792,7 @@ def _make_environment_for_app_script( app_id, app_instance_nb = _parse_app_instance_name(app) env_dict = { + "YNH_DEFAULT_PHP_VERSION": "8.2", "YNH_APP_ID": app_id, "YNH_APP_INSTANCE_NAME": app, "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), From 460e39a2f0c278a60ef051cc03920e7656e56fa0 Mon Sep 17 00:00:00 2001 From: Nicolas Palix Date: Tue, 20 Jun 2023 15:20:51 +0200 Subject: [PATCH 0132/1116] Support multiple TXT entries for TLD The dig of TXT for @ can returns multiple entries. In that case, the DNS diagnosis fails. The modification preserves the handling of DMARC and the likes which use a single entry and a specfic domain name. For single entry list, the behavior is preserved. If mutliple TXT entries are defined for @, only the v=spf1 one is returned. Signed-off-by: Nicolas Palix --- src/diagnosers/12-dnsrecords.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py index 2d46f979c..be9bf5418 100644 --- a/src/diagnosers/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -182,6 +182,10 @@ class MyDiagnoser(Diagnoser): if success != "ok": return None else: + if type_ == "TXT" and isinstance(answers,list): + for part in answers: + if part.startswith('"v=spf1'): + return part return answers[0] if len(answers) == 1 else answers def current_record_match_expected(self, r): From f9850a2264f4392fc1b7d9af45d22584948c8ada Mon Sep 17 00:00:00 2001 From: Yann Autissier Date: Tue, 20 Jun 2023 17:49:04 +0200 Subject: [PATCH 0133/1116] keep fail2ban rules on firewall reload (#1661) * keep fail2ban rules on firewall reload reloading firewall flushes all iptables rules to create new ones, dropping fail2ban rules in the same time. * restart fail2ban instead of reload Reloading fail2ban does not create f2b-* iptables rules. --- src/firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firewall.py b/src/firewall.py index 310d263c6..392678fe1 100644 --- a/src/firewall.py +++ b/src/firewall.py @@ -331,7 +331,7 @@ def firewall_reload(skip_upnp=False): # Refresh port forwarding with UPnP firewall_upnp(no_refresh=False) - _run_service_command("reload", "fail2ban") + _run_service_command("restart", "fail2ban") if errors: logger.warning(m18n.n("firewall_rules_cmd_failed")) From fb4693be3959d6306eb4a23c62b13992b2c547d2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 20 Jun 2023 17:59:09 +0200 Subject: [PATCH 0134/1116] apps: be able to customize the user-part and domain-part of email when using allow_email on system user --- src/app.py | 4 +++- src/utils/resources.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app.py b/src/app.py index 88d79e750..a7d25cfa6 100644 --- a/src/app.py +++ b/src/app.py @@ -3173,7 +3173,9 @@ def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): hashed_password = _hash_user_password(settings["mail_pwd"]) dovecot_passwd.append(f"{app}:{hashed_password}::::::allow_nets=127.0.0.1/24") if postfix: - postfix_map.append(f"{app}@{settings['domain']} {app}") + mail_user = settings.get("mail_user", app) + mail_domain = settings.get("mail_domain", settings["domain"]) + postfix_map.append(f"{mail_user}@{mail_domain} {app}") if dovecot: app_senders_passwd = "/etc/dovecot/app-senders-passwd" diff --git a/src/utils/resources.py b/src/utils/resources.py index 4e1907ab4..925ce6ee8 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -451,7 +451,7 @@ class SystemuserAppResource(AppResource): ##### Properties: - `allow_ssh`: (default: False) Adds the user to the ssh.app group, allowing SSH connection via this user - `allow_sftp`: (default: False) Adds the user to the sftp.app group, allowing SFTP connection via this user - - `allow_email`: (default: False) Enable authentication on the mail stack for the system user and send mail using `__APP__@__DOMAIN__`. A `mail_pwd` setting is automatically defined (similar to `db_pwd` for databases). You can then configure the app to use `__APP__` and `__MAIL_PWD__` as SMTP credentials (with host 127.0.0.1) + - `allow_email`: (default: False) Enable authentication on the mail stack for the system user and send mail using `__APP__@__DOMAIN__`. A `mail_pwd` setting is automatically defined (similar to `db_pwd` for databases). You can then configure the app to use `__APP__` and `__MAIL_PWD__` as SMTP credentials (with host 127.0.0.1). You can also tweak the user-part of the domain-part of the email used by manually defining a custom setting `mail_user` or `mail_domain` - `home`: (default: `/var/www/__APP__`) Defines the home property for this user. NB: unfortunately you can't simply use `__INSTALL_DIR__` or `__DATA_DIR__` for now ##### Provision/Update: From f47d4961830b8a440cb82396549eeb8b1adc19e1 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 20 Jun 2023 16:35:42 +0000 Subject: [PATCH 0135/1116] Ensure that app_shell() does not lock the CLI --- share/actionsmap.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index e1de66bc8..0a12b94a1 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -957,6 +957,8 @@ app: ### app_shell() shell: action_help: Open an interactive shell with the app environment already loaded + # Here we set a GET only not to lock the command line. There is no actual API endpoint for app_shell() + api: GET /apps//shell arguments: app: help: App ID From b2aaefe0e6a20f92ad0822b6de8032f1e4122b6d Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 20 Jun 2023 16:44:22 +0000 Subject: [PATCH 0136/1116] Add phpflags setting for app_shell() --- helpers/apps | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helpers/apps b/helpers/apps index 4b253ff90..7a93298c0 100644 --- a/helpers/apps +++ b/helpers/apps @@ -124,7 +124,7 @@ ynh_remove_apps() { # Requires YunoHost version 11.0.* or higher, and that the app relies on packaging v2 or higher. # The spawned shell will have environment variables loaded and environment files sourced # from the app's service configuration file (defaults to $app.service, overridable by the packager with `service` setting). -# If the app relies on a specific PHP version, then `php` will be aliased that version. +# If the app relies on a specific PHP version, then `php` will be aliased that version. The PHP command will also be appended with the `phpflags` settings. ynh_spawn_app_shell() { # Declare an array to define the options of this helper. local legacy_args=a @@ -176,9 +176,10 @@ ynh_spawn_app_shell() { # Force `php` to its intended version # We use `eval`+`export` since `alias` is not propagated to subshells, even with `export` local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) + local phpflags=$(ynh_app_setting_get --app=$app --key=phpflags) if [ -n "$phpversion" ] then - eval "php() { php${phpversion} \"\$@\"; }" + eval "php() { php${phpversion} ${phpflags} \"\$@\"; }" export -f php fi From e87ee09b3ee9c09bf9b1f1c37ade06a503d79888 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 23 Jun 2023 02:30:38 +0200 Subject: [PATCH 0137/1116] postinstall: crash early if the username already exists on the system --- src/tools.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tools.py b/src/tools.py index 740f92c9d..488ed516b 100644 --- a/src/tools.py +++ b/src/tools.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # +import pwd import re import os import subprocess @@ -174,6 +175,12 @@ def tools_postinstall( raw_msg=True, ) + # Crash early if the username is already a system user, which is + # a common confusion. We don't want to crash later and end up in an half-configured state. + all_existing_usernames = {x.pw_name for x in pwd.getpwall()} + if username in all_existing_usernames: + raise YunohostValidationError("system_username_exists") + if username in ADMIN_ALIASES: raise YunohostValidationError( f"Unfortunately, {username} cannot be used as a username", raw_msg=True From 510e82fa22b8f0b52528dc4bd32b747d4543a5b3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 23 Jun 2023 19:08:53 +0200 Subject: [PATCH 0138/1116] quality: fix mypy complaining about types for the 'extras' key in apt resource --- src/utils/resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 18f1aa7eb..ff4e9877f 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -22,7 +22,7 @@ import shutil import random import tempfile import subprocess -from typing import Dict, Any, List +from typing import Dict, Any, List, Union from moulinette import m18n from moulinette.utils.process import check_output @@ -1044,7 +1044,7 @@ class AptDependenciesAppResource(AppResource): packages: List = [] packages_from_raw_bash: str = "" - extras: Dict[str, Dict[str, str]] = {} + extras: Dict[str, Dict[str, Union[str, List]]] = {} def __init__(self, properties: Dict[str, Any], *args, **kwargs): super().__init__(properties, *args, **kwargs) From f571aff93c0c2038c55f672294230d258a8606a2 Mon Sep 17 00:00:00 2001 From: orhtej2 <2871798+orhtej2@users.noreply.github.com> Date: Mon, 3 Jul 2023 22:24:47 +0200 Subject: [PATCH 0139/1116] Allow installation from gitea [Gitea](https://about.gitea.com/) has branch URL in form `https://domain.tld/gitea/path//_ynh/src/branch/`. --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 03e12c84e..b7ff03079 100644 --- a/src/app.py +++ b/src/app.py @@ -84,7 +84,7 @@ re_app_instance_name = re.compile( ) APP_REPO_URL = re.compile( - r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./~]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?tree/[a-zA-Z0-9-_.]+)?(\.git)?/?$" + r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./~]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?(tree|src/branch)/[a-zA-Z0-9-_.]+)?(\.git)?/?$" ) APP_FILES_TO_COPY = [ From 5c4493ce960f5bb127353074e0f674b970aa6dfa Mon Sep 17 00:00:00 2001 From: orhtej2 <2871798+orhtej2@users.noreply.github.com> Date: Mon, 3 Jul 2023 22:36:34 +0200 Subject: [PATCH 0140/1116] Further update allowed URLs. --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index b7ff03079..a90e273a2 100644 --- a/src/app.py +++ b/src/app.py @@ -84,7 +84,7 @@ re_app_instance_name = re.compile( ) APP_REPO_URL = re.compile( - r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./~]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?(tree|src/branch)/[a-zA-Z0-9-_.]+)?(\.git)?/?$" + r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./~]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?(tree|src/(branch|tag|commit))/[a-zA-Z0-9-_.]+)?(\.git)?/?$" ) APP_FILES_TO_COPY = [ From 6f48cbc4a7f8fa0d7675c4bccbfa51c0f232cb60 Mon Sep 17 00:00:00 2001 From: orhtej2 <2871798+orhtej2@users.noreply.github.com> Date: Mon, 3 Jul 2023 22:40:14 +0200 Subject: [PATCH 0141/1116] Added tests for Gitea URLs. --- src/tests/test_appurl.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/tests/test_appurl.py b/src/tests/test_appurl.py index 351bb4e83..996a5a2c3 100644 --- a/src/tests/test_appurl.py +++ b/src/tests/test_appurl.py @@ -69,8 +69,19 @@ def test_repo_url_definition(): assert _is_app_repo_url("git@github.com:YunoHost-Apps/foobar_ynh.git") assert _is_app_repo_url("https://git.super.host/~max/foobar_ynh") + ### Gitea + assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh") + assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh/src/branch/branch_name") + assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh/src/tag/tag_name") + assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh/src/commit/abcd1234") + + ### Invalid patterns + + # no schema assert not _is_app_repo_url("github.com/YunoHost-Apps/foobar_ynh") + # http assert not _is_app_repo_url("http://github.com/YunoHost-Apps/foobar_ynh") + # does not end in `_ynh` assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_wat") assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh_wat") assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar/tree/testing") From 7d2ecc358ea52620cb6160af9c6b3f07f7d1610c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Jul 2023 03:02:20 +0200 Subject: [PATCH 0142/1116] quality: ignore complain from mypy --- src/utils/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index ff4e9877f..11e4f6162 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1063,7 +1063,7 @@ class AptDependenciesAppResource(AppResource): for key, values in self.extras.items(): if isinstance(values.get("packages"), str): - values["packages"] = [value.strip() for value in values["packages"].split(",")] + values["packages"] = [value.strip() for value in values["packages"].split(",")] # type: ignore if not isinstance(values.get("repo"), str) \ or not isinstance(values.get("key"), str) \ From dc0fa8c4ac79ce5b5bb7957f936e838eb53d9dc6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Jul 2023 03:27:33 +0200 Subject: [PATCH 0143/1116] app resources: fix apt resource broken by previous commits ... --- src/utils/resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 11e4f6162..265721ded 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1076,7 +1076,7 @@ class AptDependenciesAppResource(AppResource): def provision_or_update(self, context: Dict = {}): script = " ".join(["ynh_install_app_dependencies", *self.packages]) for repo, values in self.extras.items(): - script += " ".join([ + script += "\n" + " ".join([ "ynh_install_extra_app_dependencies", f"--repo='{values['repo']}'", f"--key='{values['key']}'", @@ -1084,7 +1084,7 @@ class AptDependenciesAppResource(AppResource): ]) # 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)) + self._run_script("provision_or_update", script) def deprovision(self, context: Dict = {}): self._run_script("deprovision", "ynh_remove_app_dependencies") From 36a17dfdbd611c0072b4dad71b4c4f07506713b5 Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 4 Jul 2023 14:15:50 +0200 Subject: [PATCH 0144/1116] change string into fstring in resources.py --- src/utils/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 265721ded..7f6f263de 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1175,7 +1175,7 @@ class PortsResource(AppResource): 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" + legacy_setting_name = f"{name}_port" port_value = self.get_setting(legacy_setting_name) if port_value: self.set_setting(setting_name, port_value) From 3957b10e92672ebd4e22d9d24d82f301e7eeec66 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Jul 2023 15:00:02 +0200 Subject: [PATCH 0145/1116] nginx: replace $http_host by $host, cf https://github.com/yandex/gixy/blob/master/docs/en/plugins/hostspoofing.md / Credit to A.Wolski --- conf/nginx/redirect_to_admin.conf | 2 +- conf/nginx/server.tpl.conf | 2 +- conf/nginx/yunohost_api.conf.inc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/nginx/redirect_to_admin.conf b/conf/nginx/redirect_to_admin.conf index 22748daa3..1d7933c6a 100644 --- a/conf/nginx/redirect_to_admin.conf +++ b/conf/nginx/redirect_to_admin.conf @@ -1,3 +1,3 @@ location / { - return 302 https://$http_host/yunohost/admin; + return 302 https://$host/yunohost/admin; } diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index 16b5c46c2..ccba8a082 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -25,7 +25,7 @@ server { {# 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" %} location / { - return 301 https://$http_host$request_uri; + return 301 https://$host$request_uri; } {# The app config snippets are not included in the HTTP conf unless HTTPS redirect is disabled, because app's location may blocks will conflict or bypass/ignore the HTTPS redirection. #} {% else %} diff --git a/conf/nginx/yunohost_api.conf.inc b/conf/nginx/yunohost_api.conf.inc index c9ae34f82..f434dbe96 100644 --- a/conf/nginx/yunohost_api.conf.inc +++ b/conf/nginx/yunohost_api.conf.inc @@ -4,7 +4,7 @@ location /yunohost/api/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_set_header Host $http_host; + proxy_set_header Host $host; {% if webadmin_allowlist_enabled == "True" %} {% for ip in webadmin_allowlist.split(',') %} From fd7136446ec42ee99a3ee30dfbd9e8cb78967dfe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Jul 2023 18:01:49 +0200 Subject: [PATCH 0146/1116] Simplify ynh_add_fpm_config helper --- helpers/php | 80 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/helpers/php b/helpers/php index 417dbbc61..c9e5b1cb8 100644 --- a/helpers/php +++ b/helpers/php @@ -7,33 +7,44 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} # Create a dedicated PHP-FPM config # -# usage 1: ynh_add_fpm_config [--phpversion=7.X] [--use_template] [--package=packages] [--dedicated_service] -# | arg: -v, --phpversion= - Version of PHP to use. -# | arg: -t, --use_template - Use this helper in template mode. -# | arg: -p, --package= - Additionnal PHP packages to install -# | arg: -d, --dedicated_service - Use a dedicated PHP-FPM service instead of the common one. +# usage: ynh_add_fpm_config # -# ----------------------------------------------------------------------------- +# Case 1 (recommended) : your provided a snippet conf/extra_php-fpm.conf # -# usage 2: ynh_add_fpm_config [--phpversion=7.X] --usage=usage --footprint=footprint [--package=packages] [--dedicated_service] -# | arg: -v, --phpversion= - Version of PHP to use. -# | arg: -f, --footprint= - Memory footprint of the service (low/medium/high). +# The actual PHP configuration will be automatically generated, +# and your extra_php-fpm.conf will be appended (typically contains PHP upload limits) +# +# The resulting configuration will be deployed to the appropriate place, /etc/php/$phpversion/fpm/pool.d/$app.conf +# +# Performance-related options in the PHP conf, such as : +# pm.max_children, pm.start_servers, pm.min_spare_servers pm.max_spare_servers +# are computed from two parameters called "usage" and "footprint" which can be set to low/medium/high. (cf details below) +# +# If you wish to tweak those, please initialize the settings `fpm_usage` and `fpm_footprint` +# *prior* to calling this helper. Otherwise, "low" will be used as a default for both values. +# +# Otherwise, if you want the user to have control over these, we encourage to create a config panel +# (which should ultimately be standardized by the core ...) +# +# Case 2 (deprecate) : you provided an entire conf/php-fpm.conf +# +# The configuration will be hydrated, replacing __FOOBAR__ placeholders with $foobar values, etc. +# +# The resulting configuration will be deployed to the appropriate place, /etc/php/$phpversion/fpm/pool.d/$app.conf +# +# ---------------------- +# +# fpm_footprint: Memory footprint of the service (low/medium/high). # low - Less than 20 MB of RAM by pool. # medium - Between 20 MB and 40 MB of RAM by pool. # high - More than 40 MB of RAM by pool. -# Or specify exactly the footprint, the load of the service as MB by pool instead of having a standard value. -# To have this value, use the following command and stress the service. -# watch -n0.5 ps -o user,cmd,%cpu,rss -u APP +# N - Or you can specify a quantitative footprint as MB by pool (use watch -n0.5 ps -o user,cmd,%cpu,rss -u APP) # -# | arg: -u, --usage= - Expected usage of the service (low/medium/high). +# fpm_usage: Expected usage of the service (low/medium/high). # low - Personal usage, behind the SSO. # medium - Low usage, few people or/and publicly accessible. # high - High usage, frequently visited website. # -# | arg: -p, --package= - Additionnal PHP packages to install for a specific version of PHP -# | arg: -d, --dedicated_service - Use a dedicated PHP-FPM service instead of the common one. -# -# # The footprint of the service will be used to defined the maximum footprint we can allow, which is half the maximum RAM. # So it will be used to defined 'pm.max_children' # A lower value for the footprint will allow more children for 'pm.max_children'. And so for @@ -59,10 +70,9 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} ynh_add_fpm_config() { local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. - local legacy_args=vtufpd - local -A args_array=([v]=phpversion= [t]=use_template [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service) + local legacy_args=vufpd + local -A args_array=([v]=phpversion= [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service) local phpversion - local use_template local usage local footprint local package @@ -72,11 +82,28 @@ ynh_add_fpm_config() { package=${package:-} # The default behaviour is to use the template. - use_template="${use_template:-1}" + local autogenconf=false usage="${usage:-}" footprint="${footprint:-}" - if [ -n "$usage" ] || [ -n "$footprint" ]; then - use_template=0 + if [ -n "$usage" ] || [ -n "$footprint" ] || [[ -e $YNH_APP_BASEDIR/conf/extra_php-fpm.conf ]]; then + autogenconf=true + + # If no usage provided, default to the value existing in setting ... or to low + local fpm_usage_in_setting=$(ynh_app_setting_get --app=$app --key=fpm_usage) + if [ -z "$usage" ] + then + usage=${fpm_usage_in_setting:-low} + ynh_app_setting_set --app=$app --key=fpm_usage --value=$usage + fi + + # If no footprint provided, default to the value existing in setting ... or to low + local fpm_footprint_in_setting=$(ynh_app_setting_get --app=$app --key=fpm_footprint) + if [ -z "$footprint" ] + then + footprint=${fpm_footprint_in_setting:-low} + ynh_app_setting_set --app=$app --key=fpm_footprint --value=$footprint + fi + fi # Do not use a dedicated service by default dedicated_service=${dedicated_service:-0} @@ -111,6 +138,7 @@ ynh_add_fpm_config() { fi if [ $dedicated_service -eq 1 ]; then + ynh_print_warn --message "Argument --dedicated_service of ynh_add_fpm_config is deprecated and to be removed in the future" local fpm_service="${app}-phpfpm" local fpm_config_dir="/etc/php/$phpversion/dedicated-fpm" else @@ -141,7 +169,7 @@ ynh_add_fpm_config() { fi fi - if [ $use_template -eq 1 ]; then + if [ $autogenconf == "false" ]; then # Usage 1, use the template in conf/php-fpm.conf local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf" # Make sure now that the template indeed exists @@ -149,10 +177,6 @@ ynh_add_fpm_config() { else # Usage 2, generate a PHP-FPM config file with ynh_get_scalable_phpfpm - # Store settings - ynh_app_setting_set --app=$app --key=fpm_footprint --value=$footprint - ynh_app_setting_set --app=$app --key=fpm_usage --value=$usage - # Define the values to use for the configuration of PHP. ynh_get_scalable_phpfpm --usage=$usage --footprint=$footprint From cab7667dcca4b44663c7ec6d5b939a00ba3bda4e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Jul 2023 19:48:55 +0200 Subject: [PATCH 0147/1116] misc: more boring irrelevant postgresql warnings to filter out --- src/hook.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hook.py b/src/hook.py index 4b07d1c17..6c9c84a00 100644 --- a/src/hook.py +++ b/src/hook.py @@ -359,6 +359,7 @@ def hook_exec( r"Removing obsolete dictionary files", r"Creating new PostgreSQL cluster", r"/usr/lib/postgresql/13/bin/initdb", + r"/usr/lib/postgresql/15/bin/initdb", r"The files belonging to this database system will be owned by user", r"This user must also own the server process.", r"The database cluster will be initialized with locale", @@ -366,6 +367,7 @@ def hook_exec( r"The default text search configuration will be set to", r"Data page checksums are disabled.", r"fixing permissions on existing directory /var/lib/postgresql/13/main ... ok", + r"fixing permissions on existing directory /var/lib/postgresql/15/main ... ok", r"creating subdirectories \.\.\. ok", r"selecting dynamic .* \.\.\. ", r"selecting default .* \.\.\. ", From 7924bb2b28436e6be7949b559c9eaa22981b3de4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Jul 2023 23:29:36 +0200 Subject: [PATCH 0148/1116] tests: fix my_webapp test that has been failing for a while --- src/tests/test_apps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index 1a3f5e97b..e6e1342ba 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -330,7 +330,7 @@ def test_app_from_catalog(): app_install( "my_webapp", - args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0&phpversion=none", + args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&init_main_permission=visitors&with_mysql=0&phpversion=none", ) app_map_ = app_map(raw=True) assert main_domain in app_map_ @@ -339,7 +339,7 @@ def test_app_from_catalog(): assert app_map_[main_domain]["/site"]["id"] == "my_webapp" assert app_is_installed(main_domain, "my_webapp") - assert app_is_exposed_on_http(main_domain, "/site", "Custom Web App") + assert app_is_exposed_on_http(main_domain, "/site", "you have just installed My Webapp") # Try upgrade, should do nothing app_upgrade("my_webapp") From 4152cb0dd1d76107cf1322e34db2ecbe6abc3923 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 17:34:21 +0200 Subject: [PATCH 0149/1116] apps: fix a bug where YunoHost would complain that 'it needs X RAM but only Y left' with Y > X because some apps have a higher runtime RAM requirement than build time ... --- src/app.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index a90e273a2..cce0aa51c 100644 --- a/src/app.py +++ b/src/app.py @@ -2782,10 +2782,18 @@ def _check_manifest_requirements( ram_requirement["runtime"] ) + # Some apps have a higher runtime value than build ... + if ram_requirement["build"] != "?" and ram_requirement["runtime"] != "?": + max_build_runtime = (ram_requirement["build"] + if human_to_binary(ram_requirement["build"]) > human_to_binary(ram_requirement["runtime"]) + else ram_requirement["runtime"]) + else: + max_build_runtime = ram_requirement["build"] + yield ( "ram", can_build and can_run, - {"current": binary_to_human(ram), "required": ram_requirement["build"]}, + {"current": binary_to_human(ram), "required": max_build_runtime}, "app_not_enough_ram", # i18n: app_not_enough_ram ) From b98ac21a0663b5e1078d7505deb51d114b32e5c5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 18 Jun 2023 15:45:44 +0200 Subject: [PATCH 0150/1116] apps: fix version.parse now refusing to parse legacy version numbers --- src/app.py | 65 ++++++++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/src/app.py b/src/app.py index cce0aa51c..64bb8c530 100644 --- a/src/app.py +++ b/src/app.py @@ -241,8 +241,8 @@ def _app_upgradable(app_infos): # Determine upgradability app_in_catalog = app_infos.get("from_catalog") - installed_version = version.parse(app_infos.get("version", "0~ynh0")) - version_in_catalog = version.parse( + installed_version = _parse_app_version(app_infos.get("version", "0~ynh0")) + version_in_catalog = _parse_app_version( app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0") ) @@ -257,25 +257,7 @@ def _app_upgradable(app_infos): ): return "bad_quality" - # If the app uses the standard version scheme, use it to determine - # upgradability - if "~ynh" in str(installed_version) and "~ynh" in str(version_in_catalog): - if installed_version < version_in_catalog: - return "yes" - else: - return "no" - - # Legacy stuff for app with old / non-standard version numbers... - - # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded - if not app_infos["from_catalog"].get("lastUpdate") or not app_infos[ - "from_catalog" - ].get("git"): - return "url_required" - - settings = app_infos["settings"] - local_update_time = settings.get("update_time", settings.get("install_time", 0)) - if app_infos["from_catalog"]["lastUpdate"] > local_update_time: + if installed_version < version_in_catalog: return "yes" else: return "no" @@ -620,9 +602,11 @@ def app_upgrade( # Manage upgrade type and avoid any upgrade if there is nothing to do upgrade_type = "UNKNOWN" # Get current_version and new version - app_new_version = version.parse(manifest.get("version", "?")) - app_current_version = version.parse(app_dict.get("version", "?")) - if "~ynh" in str(app_current_version) and "~ynh" in str(app_new_version): + app_new_version_raw = manifest.get("version", "?") + app_current_version_raw = app_dict.get("version", "?") + app_new_version = _parse_app_version(app_new_version_raw) + app_current_version = _parse_app_version(app_current_version_raw) + if "~ynh" in str(app_current_version_raw) and "~ynh" in str(app_new_version_raw): if app_current_version >= app_new_version and not force: # In case of upgrade from file or custom repository # No new version available @@ -642,10 +626,10 @@ def app_upgrade( upgrade_type = "UPGRADE_FORCED" else: app_current_version_upstream, app_current_version_pkg = str( - app_current_version + app_current_version_raw ).split("~ynh") app_new_version_upstream, app_new_version_pkg = str( - app_new_version + app_new_version_raw ).split("~ynh") if app_current_version_upstream == app_new_version_upstream: upgrade_type = "UPGRADE_PACKAGE" @@ -675,7 +659,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["PRE_UPGRADE"], - current_version=app_current_version, + current_version=app_current_version_raw, data=settings, ) _display_notifications(notifications, force=force) @@ -732,8 +716,8 @@ def app_upgrade( env_dict_more = { "YNH_APP_UPGRADE_TYPE": upgrade_type, - "YNH_APP_MANIFEST_VERSION": str(app_new_version), - "YNH_APP_CURRENT_VERSION": str(app_current_version), + "YNH_APP_MANIFEST_VERSION": str(app_new_version_raw), + "YNH_APP_CURRENT_VERSION": str(app_current_version_raw), } if manifest["packaging_format"] < 2: @@ -916,7 +900,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["POST_UPGRADE"], - current_version=app_current_version, + current_version=app_current_version_raw, data=settings, ) if Moulinette.interface.type == "cli": @@ -2054,6 +2038,20 @@ def _set_app_settings(app, settings): yaml.safe_dump(settings, f, default_flow_style=False) +def _parse_app_version(v): + + if v == "?": + return (0,0) + + try: + if "~" in v: + return (version.parse(v.split("~")[0]), int(v.split("~")[1].replace("ynh", ""))) + else: + return (version.parse(v), 0) + except Exception as e: + raise YunohostError(f"Failed to parse app version '{v}' : {e}", raw_msg=True) + + def _get_manifest_of_app(path): "Get app manifest stored in json or in toml" @@ -3158,12 +3156,7 @@ def _notification_is_dismissed(name, settings): def _filter_and_hydrate_notifications(notifications, current_version=None, data={}): def is_version_more_recent_than_current_version(name, current_version): current_version = str(current_version) - # Boring code to handle the fact that "0.1 < 9999~ynh1" is False - - if "~" in name: - return version.parse(name) > version.parse(current_version) - else: - return version.parse(name) > version.parse(current_version.split("~")[0]) + return _parse_app_version(name) > _parse_app_version(current_version) return { # Should we render the markdown maybe? idk From 798a5469eb772982e6d1874a19d24ec543c417cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Sun, 18 Jun 2023 05:06:05 +0000 Subject: [PATCH 0151/1116] Translated using Weblate (Galician) Currently translated at 100.0% (768 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/locales/gl.json b/locales/gl.json index b8b6e5cd0..3aaacd9c9 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -762,5 +762,9 @@ "log_resource_snippet": "Aprovisionamento/desaprovisionamento/actualización dun recurso", "app_resource_failed": "Fallou o aprovisionamento, desaprovisionamento ou actualización de recursos para {app}: {error}", "app_failed_to_download_asset": "Fallou a descarga do recurso '{source_id}' ({url}) para {app}: {out}", - "app_corrupt_source": "YunoHost foi quen de descargar o recurso '{source_id}' ({url}) para {app}, pero a suma de comprobación para o recurso non concorda. Pode significar que houbo un fallo temporal na conexión do servidor á rede, OU que o recurso sufreu, dalgún xeito, cambios desde que os desenvolvedores orixinais (ou unha terceira parte maliciosa?), o equipo de YunoHost ten que investigar e actualizar o manifesto da app para mostrar este cambio.\n Suma sha256 agardada: {expected_sha256} \n Suma sha256 do descargado: {computed_sha256}\n Tamaño do ficheiro: {size}" -} \ No newline at end of file + "app_corrupt_source": "YunoHost foi quen de descargar o recurso '{source_id}' ({url}) para {app}, pero a suma de comprobación para o recurso non concorda. Pode significar que houbo un fallo temporal na conexión do servidor á rede, OU que o recurso sufreu, dalgún xeito, cambios desde que os desenvolvedores orixinais (ou unha terceira parte maliciosa?), o equipo de YunoHost ten que investigar e actualizar o manifesto da app para mostrar este cambio.\n Suma sha256 agardada: {expected_sha256} \n Suma sha256 do descargado: {computed_sha256}\n Tamaño do ficheiro: {size}", + "group_mailalias_add": "Vaise engadir o alias de correo '{mail}' ao grupo '{group}'", + "group_mailalias_remove": "Vaise quitar o alias de email '{mail}' do grupo '{group}'", + "group_user_add": "Vaise engadir a '{user}' ao grupo '{grupo}'", + "group_user_remove": "Vaise quitar a '{user}' do grupo '{grupo}'" +} From e0a1f8ba0b74728c9aa9a440382c5ad72ad9e384 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Sun, 18 Jun 2023 16:08:28 +0000 Subject: [PATCH 0152/1116] Translated using Weblate (Basque) Currently translated at 96.7% (743 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/eu/ --- locales/eu.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/eu.json b/locales/eu.json index 0d424e6ca..bfdf54500 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -669,12 +669,12 @@ "migration_description_0024_rebuild_python_venv": "Konpondu Python aplikazioa Bullseye eguneraketa eta gero", "migration_0024_rebuild_python_venv_disclaimer_base": "Debian Bullseye eguneraketa dela-eta, Python aplikazio batzuk birsortu behar dira Debianekin datorren Pythonen bertsiora egokitzeko (teknikoki 'virtualenv' deritzaiona birsortu behar da). Egin artean, litekeena da Python aplikazio horiek ez funtzionatzea. YunoHost saia daiteke beherago ageri diren aplikazioen virtualenv edo ingurune birtualak birsortzen. Beste aplikazio batzuen kasuan, edo birsortze saiakerak kale egingo balu, aplikazio horien eguneraketa behartu beharko duzu.", "migration_0021_not_buster2": "Zerbitzariak darabilen Debian bertsioa ez da Buster! Dagoeneko Buster -> Bullseye migrazioa exekutatu baduzu, errore honek migrazioa erabat arrakastatsua izan ez zela esan nahi du (bestela YunoHostek amaitutzat markatuko luke). Komenigarria izango litzateke, laguntza taldearekin batera, zer gertatu zen aztertzea. Horretarako `migrazioaren erregistro **osoa** beharko duzue, Tresnak > Erregistroak atalean eskuragarri dagoena.", - "admins": "Administratzaileak", + "admins": "Administratzaileek", "app_action_failed": "{app} aplikaziorako {action} eragiketak huts egin du", "config_action_disabled": "Ezin izan da '{action}' eragiketa exekutatu ezgaituta dagoelako, egiaztatu bere mugak betetzen dituzula. Laguntza: {help}", - "all_users": "YunoHosten erabiltzaile guztiak", + "all_users": "YunoHosten erabiltzaile guztiek", "app_manifest_install_ask_init_admin_permission": "Nork izan beharko luke aplikazio honetako administrazio aukeretara sarbidea? (Aldatzea dago)", - "app_manifest_install_ask_init_main_permission": "Nor izan beharko luke aplikazio honetara sarbidea? (Aldatzea dago)", + "app_manifest_install_ask_init_main_permission": "Nork izan beharko luke aplikazio honetara sarbidea? (Aldatzea dago)", "ask_admin_fullname": "Administratzailearen izen osoa", "ask_admin_username": "Administratzailearen erabiltzaile-izena", "ask_fullname": "Izen osoa", @@ -689,7 +689,7 @@ "log_settings_reset": "Berrezarri ezarpenak", "log_settings_reset_all": "Berrezarri ezarpen guztiak", "root_password_changed": "root pasahitza aldatu da", - "visitors": "Bisitariak", + "visitors": "Bisitariek", "global_settings_setting_security_experimental_enabled": "Segurtasun ezaugarri esperimentalak", "registrar_infos": "Erregistro-enpresaren informazioa", "global_settings_setting_pop3_enabled": "Gaitu POP3", @@ -763,4 +763,4 @@ "app_failed_to_download_asset": "{app} aplikaziorako '{source_id}' ({url}) baliabidea deskargatzeak huts egin du: {out}", "apps_failed_to_upgrade": "Aplikazio hauen bertsio-berritzeak huts egin du: {apps}", "apps_failed_to_upgrade_line": "\n * {app_id} (dagokion erregistroa ikusteko, exekutatu 'yunohost log show {operation_logger_name}')" -} \ No newline at end of file +} From 9c3895300fbfabb2d39958b8cc384bfc59ce7217 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Sat, 1 Jul 2023 14:35:44 +0000 Subject: [PATCH 0153/1116] Translated using Weblate (Basque) Currently translated at 97.2% (747 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/eu/ --- locales/eu.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/eu.json b/locales/eu.json index bfdf54500..0267b3366 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -762,5 +762,9 @@ "app_not_upgraded_broken_system_continue": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du eta sistema hondatu du (beraz, --continue-on-failure aukerari muzin egin zaio) eta ondorengo aplikazioen bertsio-berritzeak ezeztatu dira: {apps}", "app_failed_to_download_asset": "{app} aplikaziorako '{source_id}' ({url}) baliabidea deskargatzeak huts egin du: {out}", "apps_failed_to_upgrade": "Aplikazio hauen bertsio-berritzeak huts egin du: {apps}", - "apps_failed_to_upgrade_line": "\n * {app_id} (dagokion erregistroa ikusteko, exekutatu 'yunohost log show {operation_logger_name}')" + "apps_failed_to_upgrade_line": "\n * {app_id} (dagokion erregistroa ikusteko, exekutatu 'yunohost log show {operation_logger_name}')", + "group_mailalias_add": "'{mail}' ePosta aliasa jarri zaio '{group}' taldeari", + "group_mailalias_remove": "'{mail}' ePosta aliasa kendu zaio '{group}' taldeari", + "group_user_remove": "'{user}' erabiltzailea '{group}' taldetik kenduko da", + "group_user_add": "'{user}' erabiltzailea '{group}' taldera gehituko da" } From 48c81a4175341d9df016e900286dbb0515d8e783 Mon Sep 17 00:00:00 2001 From: Grzegorz Cichocki Date: Sun, 2 Jul 2023 22:32:15 +0000 Subject: [PATCH 0154/1116] Translated using Weblate (Polish) Currently translated at 33.4% (257 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 64 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index 0b3dc5e73..52f2de3ca 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -29,7 +29,7 @@ "system_upgraded": "Zaktualizowano system", "diagnosis_description_regenconf": "Konfiguracja systemu", "diagnosis_description_apps": "Aplikacje", - "diagnosis_description_basesystem": "Podstawowy system", + "diagnosis_description_basesystem": "Baza systemu", "unlimit": "Brak limitu", "global_settings_setting_pop3_enabled": "Włącz POP3", "domain_created": "Utworzono domenę", @@ -214,5 +214,65 @@ "confirm_app_insufficient_ram": "UWAGA! Ta aplikacja wymaga {required} pamięci RAM do zainstalowania/aktualizacji, a obecnie dostępne jest tylko {current}. Nawet jeśli aplikacja mogłaby działać, proces instalacji/aktualizacji wymaga dużej ilości pamięci RAM, więc serwer może się zawiesić i niepowodzenie może być katastrofalne. Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'", "app_not_upgraded_broken_system": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu. W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}", "app_not_upgraded_broken_system_continue": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu (parametr --continue-on-failure jest ignorowany). W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}", - "certmanager_domain_http_not_working": "Domena {domain} wydaje się niedostępna przez HTTP. Sprawdź kategorię 'Strona internetowa' diagnostyki, aby uzyskać więcej informacji. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)" + "certmanager_domain_http_not_working": "Domena {domain} wydaje się niedostępna przez HTTP. Sprawdź kategorię 'Strona internetowa' diagnostyki, aby uzyskać więcej informacji. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", + "migration_0021_system_not_fully_up_to_date": "Twój system nie jest w pełni zaktualizowany! Proszę, wykonaj zwykłą aktualizację oprogramowania zanim rozpoczniesz migrację na system Bullseye.", + "global_settings_setting_smtp_relay_port": "Port przekaźnika SMTP", + "domain_config_cert_renew": "Odnów certyfikat Let's Encrypt", + "root_password_changed": "Hasło root zostało zmienione", + "diagnosis_services_running": "Usługa {service} działa!", + "global_settings_setting_admin_strength": "Wymogi dotyczące siły hasła administratora", + "global_settings_setting_admin_strength_help": "Wymagania te są egzekwowane tylko podczas inicjalizacji lub zmiany hasła", + "global_settings_setting_pop3_enabled_help": "Włącz protokołu POP3 dla serwera poczty", + "global_settings_setting_postfix_compatibility": "Kompatybilność Postfix", + "global_settings_setting_smtp_relay_user": "Nazwa użytkownika przekaźnika SMTP", + "global_settings_setting_ssh_password_authentication_help": "Zezwól na logowanie hasłem przez SSH", + "diagnosis_apps_allgood": "Wszystkie zainstalowane aplikacje są zgodne z podstawowymi zasadami pakowania", + "diagnosis_basesystem_hardware": "Architektura sprzętowa serwera to {virt} {arch}", + "diagnosis_ip_connected_ipv4": "Serwer jest połączony z Internet z użyciem IPv4!", + "diagnosis_ip_no_ipv6": "Serwer nie ma działającego połączenia z użyciem IPv6.", + "diagnosis_http_hairpinning_issue": "Wygląda na to, że sieć lokalna nie ma \"hairpinning\".", + "backup_unable_to_organize_files": "Nie można użyć szybkiej metody porządkowania plików w archiwum", + "log_letsencrypt_cert_renew": "Odnów '{}' certyfikat Let's Encrypt", + "global_settings_setting_passwordless_sudo": "Umożliw administratorom korzystania z 'sudo' bez konieczności ponownego wpisywania hasła", + "global_settings_setting_smtp_relay_enabled": "Włącz przekaźnik SMTP", + "global_settings_setting_smtp_relay_host": "Host przekaźnika SMTP", + "global_settings_setting_user_strength": "Wymagania dotyczące siły hasła użytkownika", + "domain_config_mail_in": "Odbieranie maili", + "global_settings_setting_webadmin_allowlist_enabled_help": "Zezwól tylko kilku adresom IP na dostęp do panelu webadmin.", + "diagnosis_basesystem_kernel": "Serwer działa pod kontrolą jądra Linuksa {kernel_version}", + "diagnosis_dns_good_conf": "Rekordy DNS zostały poprawnie skonfigurowane dla domeny {domain} (category {category})", + "diagnosis_ram_ok": "System nadal ma {available} ({available_percent}%) wolnej pamięci RAM z całej puli {total}.", + "diagnosis_http_ok": "Domena {domain} jest dostępna przez HTTP z poziomu sieci zewnętrznej.", + "diagnosis_swap_tip": "Pamiętaj, że wykorzystywanie partycji swap na karcie pamięci SD lub na dysku SSD może znacznie skrócić czas działania tego urządzenia.", + "diagnosis_basesystem_host": "Serwer działa pod kontrolą systemu Debian {debian_version}", + "diagnosis_basesystem_ynh_main_version": "Serwer działa pod kontrolą oprogramowania YunoHost {main_version} ({repo})", + "diagnosis_diskusage_verylow": "Przestrzeń {mountpoint} (na dysku {device}) ma tylko {free} ({free_percent}%) wolnego miejsca z całej puli {total}! Rozważ pozbycie się niepotrzebnych plików!", + "global_settings_setting_root_password": "Nowe hasło root", + "global_settings_setting_root_password_confirm": "Powtórz nowe hasło root", + "global_settings_setting_security_experimental_enabled": "Eksperymentalne funkcje bezpieczeństwa", + "global_settings_setting_smtp_relay_password": "Hasło przekaźnika SMTP", + "global_settings_setting_user_strength_help": "Wymagania te są egzekwowane tylko podczas inicjalizacji lub zmiany hasła", + "global_settings_setting_webadmin_allowlist_enabled": "Włącz listę dozwolonych adresów IP dla panelu webadmin", + "root_password_desynchronized": "Hasło administratora zostało zmienione, ale YunoHost nie mógł wykorzystać tego hasła jako hasło root!", + "service_already_started": "Usługa '{service}' już jest włączona", + "diagnosis_ip_dnsresolution_working": "Rozpoznawanie nazw domen działa!", + "diagnosis_regenconf_manually_modified": "Wygląda na to, że plik konfiguracyjny {file} został zmodyfikowany ręcznie.", + "diagnosis_diskusage_ok": "Przestrzeń {mountpoint} (na dysku {device}) nadal ma {free} ({free_percent}%) wolnego miejsca z całej puli {total}!", + "diagnosis_diskusage_low": "Przestrzeń {mountpoint} (na dysku {device}) ma tylko {free} ({free_percent}%) wolnego miejsca z całej puli {total}! Uważaj na możliwe zapełnienie dysku w bliskiej przyszłości.", + "diagnosis_ip_connected_ipv6": "Serwer nie jest połączony z internetem z użyciem IPv6!", + "global_settings_setting_smtp_relay_enabled_help": "Włączenie przekaźnika SMTP, który ma być używany do wysyłania poczty zamiast tej instancji yunohost może być przydatne, jeśli znajdujesz się w jednej z następujących sytuacji: Twój port 25 jest zablokowany przez dostawcę usług internetowych lub dostawcę VPS, masz adres IP zamieszkania wymieniony w DUHL, nie jesteś w stanie skonfigurować odwrotnego DNS lub ten serwer nie jest bezpośrednio widoczny w Internecie i chcesz użyć innego do wysyłania wiadomości e-mail.", + "global_settings_setting_backup_compress_tar_archives_help": "Podczas tworzenia nowych kopii zapasowych archiwa będą skompresowane (.tar.gz), a nie nieskompresowane jak dotychczas (.tar). Uwaga: włączenie tej opcji oznacza tworzenie mniejszych archiwów kopii zapasowych, ale początkowa procedura tworzenia kopii zapasowej będzie znacznie dłuższa i mocniej obciąży procesor.", + "domain_config_mail_out": "Wysyłanie maili", + "domain_dns_registrar_supported": "YunoHost automatycznie wykrył, że ta domena jest obsługiwana przez rejestratora **{registrar}**. Jeśli chcesz, YunoHost automatycznie skonfiguruje rekordy DNS, ale musisz podać odpowiednie dane uwierzytelniające API. Dokumentację dotyczącą uzyskiwania poświadczeń API można znaleźć na tej stronie: https://yunohost.org/registar_api_{registrar}. (Można również ręcznie skonfigurować rekordy DNS zgodnie z dokumentacją na stronie https://yunohost.org/dns )", + "domain_config_cert_summary_letsencrypt": "Świetnie! Wykorzystujesz właściwy certyfikaty Let's Encrypt!", + "global_settings_setting_portal_theme": "Motyw portalu", + "global_settings_setting_portal_theme_help": "Więcej informacji na temat tworzenia niestandardowych motywów portalu można znaleźć na stronie https://yunohost.org/theming", + "global_settings_setting_dns_exposure": "Wersje IP do uwzględnienia w konfiguracji i diagnostyce DNS", + "domain_config_auth_token": "Token uwierzytelniający", + "global_settings_setting_dns_exposure_help": "Uwaga: Ma to wpływ tylko na zalecaną konfigurację DNS i kontrole diagnostyczne. Nie ma to wpływu na konfigurację systemu.", + "global_settings_setting_security_experimental_enabled_help": "Uruchom eksperymentalne funkcje bezpieczeństwa (nie włączaj, jeśli nie wiesz co robisz!)", + "global_settings_setting_smtp_allow_ipv6_help": "Zezwól na wykorzystywanie IPv7 do odbierania i wysyłania maili", + "global_settings_setting_ssh_password_authentication": "Logowanie hasłem", + "diagnosis_backports_in_sources_list": "Wygląda na to że apt (menedżer pakietów) został skonfigurowany tak, aby wykorzystywać repozytorium backported. Nie zalecamy wykorzystywania repozytorium backported, ponieważ może powodować problemy ze stabilnością i/lub konflikty z konfiguracją. No chyba, że wiesz co robisz.", + "domain_config_xmpp_help": "Uwaga: niektóre funkcje XMPP będą wymagały aktualizacji rekordów DNS i odnowienia certyfikatu Lets Encrypt w celu ich włączenia" } From 76481dae22cebe37bbdb55f7ea10f688790dd14d Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 9 Jul 2023 04:32:52 +0200 Subject: [PATCH 0155/1116] Added translation using Weblate (Japanese) --- locales/ja.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/ja.json diff --git a/locales/ja.json b/locales/ja.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/ja.json @@ -0,0 +1 @@ +{} From 392695535e99eec745ce116beffb74326d91decf Mon Sep 17 00:00:00 2001 From: motcha Date: Sun, 9 Jul 2023 05:49:40 +0000 Subject: [PATCH 0156/1116] Translated using Weblate (Japanese) Currently translated at 0.1% (1 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ja/ --- locales/ja.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index 0967ef424..a76ec9f48 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1 +1,3 @@ -{} +{ + "password_too_simple_1": "パスワードは少なくとも8文字必要です" +} From 3f0a23105edc75fb44f8e0c5155c156f6d7092f5 Mon Sep 17 00:00:00 2001 From: motcha Date: Sun, 9 Jul 2023 15:17:43 +0000 Subject: [PATCH 0157/1116] Translated using Weblate (Japanese) Currently translated at 70.8% (544 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ja/ --- locales/ja.json | 769 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 768 insertions(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index a76ec9f48..90645193b 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1,3 +1,770 @@ { - "password_too_simple_1": "パスワードは少なくとも8文字必要です" + "password_too_simple_1": "パスワードは少なくとも8文字必要です", + "aborting": "中止します。", + "action_invalid": "不正なアクション ’ {action}’", + "additional_urls_already_added": "アクセス許可 '{permission}' に対する追加URLには ‘{url}’ が既に追加されています", + "admin_password": "管理者パスワード", + "app_action_cannot_be_ran_because_required_services_down": "このアクションを実行するには、次の必要なサービスが実行されている必要があります: {services} 。続行するには再起動してみてください (そして何故ダウンしているのか調査してください)。", + "app_action_failed": "‘{name}’ アプリのアクション ’{action}' に失敗しました", + "app_argument_invalid": "引数 '{name}' の有効な値を選択してください: {error}", + "app_argument_password_no_default": "パスワード引数 '{name}' の解析中にエラーが発生しました: セキュリティ上の理由から、パスワード引数にデフォルト値を設定することはできません", + "app_argument_required": "‘{name}’ は必要です。", + "app_change_url_failed": "{app}のURLを変更できませんでした:{error}", + "app_change_url_identical_domains": "古いドメインと新しいドメイン/url_pathは同一であり( '{domain}{path}')、何もしません。", + "app_change_url_script_failed": "URL 変更スクリプト内でエラーが発生しました", + "app_failed_to_upgrade_but_continue": "アプリの{failed_app}アップグレードに失敗しました。要求に応じて次のアップグレードに進みます。「yunohostログショー{operation_logger_name}」を実行して失敗ログを表示します", + "app_full_domain_unavailable": "申し訳ありませんが、このアプリは独自のドメインにインストールする必要がありますが、他のアプリは既にドメイン '{domain}' にインストールされています。代わりに、このアプリ専用のサブドメインを使用できます。", + "app_id_invalid": "不正なアプリID", + "app_install_failed": "インストールできません {app}:{error}", + "app_manifest_install_ask_password": "このアプリの管理パスワードを選択してください", + "app_manifest_install_ask_path": "このアプリをインストールするURLパス(ドメインの後)を選択します", + "app_not_properly_removed": "{app}が正しく削除されていません", + "app_not_upgraded": "アプリ「{failed_app}」のアップグレードに失敗したため、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_start_remove": "‘{app}’ を削除しています…", + "app_start_restore": "‘{app}’ をリストアしています…", + "ask_main_domain": "メインドメイン", + "ask_new_admin_password": "新しい管理者パスワード", + "ask_new_domain": "新しいドメイン", + "ask_new_path": "新しいパス", + "ask_password": "パスワード", + "ask_user_domain": "ユーザーのメールアドレスと XMPP アカウントに使用するドメイン", + "backup_abstract_method": "このバックアップ方法はまだ実装されていません", + "backup_actually_backuping": "収集したファイルからバックアップアーカイブを作成しています...", + "backup_archive_corrupted": "バックアップアーカイブ ’{archive}’ は破損しているようです: {error}", + "backup_archive_name_exists": "この名前のバックアップアーカイブはすでに存在します。", + "backup_archive_name_unknown": "「{name}」という名前の不明なローカルバックアップアーカイブ", + "backup_archive_open_failed": "バックアップアーカイブを開けませんでした", + "backup_archive_system_part_not_available": "このバックアップでは、システム部分 '{part}' を使用できません", + "backup_method_custom_finished": "カスタム バックアップ方法 '{method}' が完了しました", + "certmanager_attempt_to_replace_valid_cert": "ドメイン {domain} の適切で有効な証明書を上書きしようとしています。(—force でバイパスする)", + "certmanager_cannot_read_cert": "ドメイン {domain} (ファイル: {file}) の現在の証明書を開こうとしたときに問題が発生しました。理由: {reason}", + "certmanager_cert_install_failed": "{domains}のLet’s Encrypt 証明書のインストールに失敗しました", + "certmanager_cert_install_failed_selfsigned": "{domains} ドメインの自己署名証明書のインストールに失敗しました", + "certmanager_cert_install_success": "Let’s Encrypt 証明書が ‘{domain}’ にインストールされました", + "certmanager_cert_install_success_selfsigned": "ドメイン「{domain}」に自己署名証明書がインストールされました", + "certmanager_domain_dns_ip_differs_from_public_ip": "ドメイン '{domain}' の DNS レコードは、このサーバーの IP とは異なります。詳細については、診断の「DNSレコード」(基本)カテゴリを確認してください。最近 A レコードを変更した場合は、反映されるまでお待ちください (一部の DNS 伝達チェッカーはオンラインで入手できます)。(何をしているかがわかっている場合は、 '--no-checks'を使用してこれらのチェックをオフにします。", + "certmanager_domain_http_not_working": "ドメイン{domain}はHTTP経由でアクセスできないようです。詳細については、診断の「Web」カテゴリを確認してください。(何をしているかがわかっている場合は、 '--no-checks'を使用してこれらのチェックをオフにします。", + "certmanager_unable_to_parse_self_CA_name": "自己署名機関の名前を解析できませんでした (ファイル: {file})", + "certmanager_domain_not_diagnosed_yet": "ドメイン{domain}の診断結果はまだありません。診断セクションのカテゴリ「DNSレコード」と「Web」の診断を再実行して、ドメインが暗号化の準備ができているかどうかを確認してください。(または、何をしているかがわかっている場合は、「--no-checks」を使用してこれらのチェックをオフにします。", + "confirm_app_insufficient_ram": "危険!このアプリのインストール/アップグレードには{required}RAMが必要ですが、現在利用可能なのは{current}つだけです。このアプリを実行できたとしても、そのインストール/アップグレードプロセスには大量のRAMが必要なため、サーバーがフリーズして惨めに失敗する可能性があります。とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", + "confirm_notifications_read": "警告:続行する前に上記のアプリ通知を確認する必要があります、知っておくべき重要なことがあるかもしれません。[{answers}]", + "custom_app_url_required": "カスタム App をアップグレードするには URL を指定する必要があります{app}", + "danger": "危険:", + "diagnosis_cant_run_because_of_dep": "{dep}に関連する重要な問題がある間、{category}診断を実行できません。", + "diagnosis_description_apps": "アプリケーション", + "diagnosis_description_basesystem": "システム", + "diagnosis_description_dnsrecords": "DNS レコード", + "diagnosis_description_ip": "インターネット接続", + "diagnosis_description_mail": "メールアドレス", + "diagnosis_description_ports": "ポート開放", + "diagnosis_high_number_auth_failures": "最近、疑わしいほど多くの認証失敗が発生しています。fail2banが実行されていて正しく構成されていることを確認するか、https://yunohost.org/security で説明されているようにSSHにカスタムポートを使用することをお勧めします。", + "diagnosis_http_bad_status_code": "サーバーの代わりに別のマシン(おそらくインターネットルーター)が応答したようです。
1.この問題の最も一般的な原因は、ポート80(および443)が サーバーに正しく転送されていないことです。
2.より複雑なセットアップでは、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_http_hairpinning_issue_details": "これはおそらくISPボックス/ルーターが原因です。その結果、ローカルネットワークの外部の人々は期待どおりにサーバーにアクセスできますが、ドメイン名またはグローバルIPを使用する場合、ローカルネットワーク内の人々(おそらくあなたのような人)はアクセスできません。https://yunohost.org/dns_local_network を見ることによって状況を改善できるかもしれません", + "diagnosis_ignored_issues": "(+{nb_ignored}無視された問題)", + "diagnosis_ip_dnsresolution_working": "ドメイン名前解決は機能しています!", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 は通常、システムまたはプロバイダー (使用可能な場合) によって自動的に構成されます。それ以外の場合は、こちらのドキュメントで説明されているように、いくつかのことを手動で構成する必要があります: https://yunohost.org/#/ipv6。", + "diagnosis_ip_not_connected_at_all": "サーバーがインターネットに接続されていないようですね!?", + "diagnosis_ip_weird_resolvconf": "DNS名前解決は機能しているようですが、カスタムされた/etc/resolv.confを使用しているようです。", + "diagnosis_ip_weird_resolvconf_details": "ファイルは/etc/resolv.conf、(dnsmasq)を指す127.0.0.1それ自体への/etc/resolvconf/run/resolv.confシンボリックリンクである必要があります。DNSリゾルバーを手動で設定する場合は、編集/etc/resolv.dnsmasq.confしてください。", + "diagnosis_mail_blacklist_listed_by": "あなたのIPまたはドメイン {item} はブラックリスト {blacklist_name} に登録されています", + "diagnosis_mail_blacklist_ok": "このサーバーが使用するIPとドメインはブラックリストに登録されていないようです", + "diagnosis_mail_ehlo_could_not_diagnose_details": "エラー: {error}", + "diagnosis_mail_fcrdns_ok": "逆引きDNSが正しく構成されています!", + "diagnosis_mail_fcrdns_nok_alternatives_4": "一部のプロバイダーでは、逆引きDNSを構成できません(または機能が壊れている可能性があります…)。そのせいで問題が発生している場合は、次の解決策を検討してください。
- 一部のISPが提供するメールサーバーリレーを使用する ことで代替できますが、ISPが電子メールトラフィックを盗み見る可能性があることを意味します。
- プライバシーに配慮した代替手段は、この種の制限を回避するために*専用のパブリックIP*を持つVPNを使用することです。https://yunohost.org/#/vpn_advantage を見る
-または、別のプロバイダーに切り替えることが可能です", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "一部のプロバイダーは、ネット中立性を気にしないため、送信ポート25のブロックを解除することを許可しません。
-それらのいくつかは 、メールサーバーリレーを使用する 代替手段を提供しますが、リレーが電子メールトラフィックをスパイできることを意味します。
- プライバシーに配慮した代替手段は、*専用のパブリックIP*を持つVPNを使用して、これらの種類の制限を回避することです。https://yunohost.org/#/vpn_advantage を見る
-よりネット中立性に優しいプロバイダーへの切り替えを検討することもできます", + "diagnosis_mail_outgoing_port_25_ok": "SMTP メール サーバーは電子メールを送信できます (送信ポート 25 はブロックされません)。", + "diagnosis_mail_queue_ok": "メールキュー内の保留中のメール{nb_pending}", + "diagnosis_mail_queue_too_big": "メールキュー内の保留中のメールが多すぎます({nb_pending}メール)", + "diagnosis_mail_queue_unavailable": "キュー内の保留中の電子メールの数を調べることはできません", + "diagnosis_mail_queue_unavailable_details": "エラー: {error}", + "diagnosis_no_cache": "カテゴリ '{category}' の診断キャッシュがまだありません", + "diagnosis_ports_forwarding_tip": "この問題を解決するには、ほとんどの場合、https://yunohost.org/isp_box_config で説明されているように、インターネットルーターでポート転送を構成する必要があります", + "diagnosis_ports_needed_by": "このポートの公開は、{category}機能 (サービス {service}) に必要です。", + "diagnosis_ports_ok": "ポート {port} は外部から到達可能です。", + "diagnosis_ports_partially_unreachable": "ポート {port} は、IPv{failed} では外部から到達できません。", + "diagnosis_security_vulnerable_to_meltdown": "Meltdown(重大なセキュリティの脆弱性)に対して脆弱に見えます", + "diagnosis_services_conf_broken": "サービス{service}の構成が壊れています!", + "diagnosis_services_running": "サービス{service}が実行されています!", + "diagnosis_sshd_config_inconsistent": "SSHポートが/ etc / ssh / sshd_configで手動で変更されたようです。YunoHost 4.2以降、手動で構成を編集する必要がないように、新しいグローバル設定「security.ssh.ssh_port」を使用できます。", + "diagnosis_swap_none": "システムにスワップがまったくない。システムのメモリ不足の状況を回避するために、少なくとも {recommended} つのスワップを追加することを検討する必要があります。", + "diagnosis_swap_notsomuch": "システムにはスワップが {total} しかありません。システムのメモリ不足の状況を回避するために、少なくとも {recommended} のスワップを用意することを検討してください。", + "diagnosis_swap_ok": "システムには {total} のスワップがあります!", + "domain_cert_gen_failed": "証明書を生成できませんでした", + "domain_config_acme_eligible": "ACMEの資格", + "domain_config_cert_summary": "証明書の状態", + "domain_config_cert_summary_abouttoexpire": "現在の証明書の有効期限が近づいています。すぐに自動的に更新されるはずです。", + "domain_config_cert_summary_expired": "クリティカル: 現在の証明書が無効です!HTTPSはまったく機能しません!", + "domain_config_cert_validity": "データの入力規則", + "domain_config_xmpp": "インスタント メッセージング (XMPP)", + "domain_dns_conf_is_just_a_recommendation": "このコマンドは、*推奨*構成を表示します。実際にはDNS構成は設定されません。この推奨事項に従って、レジストラーで DNS ゾーンを構成するのはユーザーの責任です。", + "domain_dns_conf_special_use_tld": "このドメインは、.local や .test などの特殊な用途のトップレベル ドメイン (TLD) に基づいているため、実際の DNS レコードを持つことは想定されていません。", + "domain_dns_push_already_up_to_date": "レコードはすでに最新であり、何もする必要はありません。", + "domain_dns_push_failed": "DNS レコードの更新が失敗しました。", + "domain_dyndns_already_subscribed": "すでに DynDNS ドメインにサブスクライブしている", + "dyndns_key_generating": "DNS キーを生成しています...しばらく時間がかかる場合があります。", + "dyndns_key_not_found": "ドメインの DNS キーが見つかりません", + "firewall_reload_failed": "バックアップアーカイブを開けませんでした", + "global_settings_setting_postfix_compatibility_help": "Postfix サーバーの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します", + "global_settings_setting_root_password": "新しい管理者パスワード", + "global_settings_setting_root_password_confirm": "新しい管理者パスワード", + "global_settings_setting_smtp_allow_ipv6": "IPv6 を許可する", + "global_settings_setting_user_strength_help": "これらの要件は、パスワードを初期化または変更する場合にのみ適用されます", + "group_cannot_be_deleted": "グループ{group}を手動で削除することはできません。", + "group_created": "グループ '{group}' が作成されました", + "group_mailalias_add": "メール エイリアス '{mail}' がグループ '{group}' に追加されます。", + "group_mailalias_remove": "メール エイリアス '{mail}' がグループ '{group}' から削除されます。", + "group_no_change": "グループ '{group}' に対して変更はありません", + "group_unknown": "グループ '{group}' は不明です", + "group_user_already_in_group": "ユーザー {user} は既にグループ {group} に所属しています", + "group_user_not_in_group": "ユーザー {user}がグループ {group} にない", + "group_user_remove": "ユーザー '{user}' はグループ '{group}' から削除されます。", + "hook_exec_failed": "スクリプトを実行できませんでした: {path}", + "hook_exec_not_terminated": "スクリプトが正しく終了しませんでした: {path}", + "log_app_install": "‘{}’ アプリをインストールする", + "log_user_permission_update": "アクセス許可 '{}' のアクセスを更新する", + "log_user_update": "ユーザー '{}' の情報を更新する", + "mail_alias_remove_failed": "電子メール エイリアス '{mail}' を削除できませんでした", + "mail_domain_unknown": "ドメイン '{domain}' の電子メール アドレスが無効です。このサーバーによって管理されているドメインを使用してください。", + "mail_forward_remove_failed": "電子メール転送 '{mail}' を削除できませんでした", + "mail_unavailable": "この電子メール アドレスは、管理者グループ用に予約されています", + "migration_0021_start": "Bullseyeへの移行開始", + "migration_0021_yunohost_upgrade": "YunoHostコアのアップグレードを開始しています...", + "migration_description_0026_new_admins_group": "新しい「複数の管理者」システムに移行する", + "migration_ldap_backup_before_migration": "実際の移行の前に、LDAP データベースとアプリ設定のバックアップを作成します。", + "migration_ldap_can_not_backup_before_migration": "移行が失敗する前に、システムのバックアップを完了できませんでした。エラー: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "移行できませんでした...システムをロールバックしようとしています。", + "permission_updated": "アクセス許可 '{permission}' が更新されました", + "restore_confirm_yunohost_installed": "すでにインストールされているシステムを復元しますか?[{answers}]", + "restore_extracting": "アーカイブから必要なファイルを抽出しています...", + "restore_failed": "バックアップを復元する ‘{name}’", + "restore_hook_unavailable": "「{part}」の復元スクリプトは、システムで使用できず、アーカイブでも利用できません", + "restore_not_enough_disk_space": "十分なスペースがありません(スペース:{free_space} B、必要なスペース:{needed_space} B、セキュリティマージン:{margin} B)", + "restore_nothings_done": "何も復元されませんでした", + "restore_removing_tmp_dir_failed": "古い一時ディレクトリを削除できませんでした", + "restore_running_app_script": "アプリ「{app}」を復元しています...", + "restore_running_hooks": "復元フックを実行しています...", + "restore_system_part_failed": "「{part}」システム部分を復元できませんでした", + "root_password_changed": "パスワード確認", + "server_reboot": "サーバーが再起動します", + "server_shutdown_confirm": "サーバーはすぐにシャットダウンしますが、よろしいですか?[{answers}]", + "service_add_failed": "サービス '{service}' を追加できませんでした", + "service_added": "サービス '{service}' が追加されました", + "service_already_started": "サービス '{service}' は既に実行されています", + "service_description_dnsmasq": "ドメイン名解決 (DNS) を処理します。", + "service_description_dovecot": "電子メールクライアントが電子メールにアクセス/フェッチすることを許可します(IMAPおよびPOP3経由)", + "service_description_fail2ban": "インターネットからのブルートフォース攻撃やその他の種類の攻撃から保護します", + "service_description_metronome": "XMPP インスタント メッセージング アカウントを管理する", + "service_description_mysql": "アプリ データの格納 (SQL データベース)", + "service_description_postfix": "電子メールの送受信に使用", + "service_description_postgresql": "アプリ データの格納 (SQL データベース)", + "service_enable_failed": "起動時にサービス '{service}' を自動的に開始できませんでした。\n\n最近のサービスログ:{logs}", + "service_enabled": "サービス '{service}' は、システムの起動時に自動的に開始されるようになりました。", + "service_reloaded": "サービス '{service}' がリロードされました", + "service_not_reloading_because_conf_broken": "構成が壊れているため、サービス「{name}」をリロード/再起動しません:{errors}", + "show_tile_cant_be_enabled_for_regex": "権限 '{permission}' の URL は正規表現であるため、現在 'show_tile' を有効にすることはできません。", + "show_tile_cant_be_enabled_for_url_not_defined": "最初にアクセス許可 '{permission}' の URL を定義する必要があるため、現在 'show_tile' を有効にすることはできません。", + "ssowat_conf_generated": "SSOワット構成の再生成", + "system_upgraded": "システムのアップグレード", + "unlimit": "クォータなし", + "update_apt_cache_failed": "APT (Debian のパッケージマネージャ) のキャッシュを更新できません。問題のある行を特定するのに役立つ可能性のあるsources.list行のダンプを次に示します。\n{sourceslist}", + "update_apt_cache_warning": "APT(Debianのパッケージマネージャー)のキャッシュを更新中に問題が発生しました。問題のある行を特定するのに役立つ可能性のあるsources.list行のダンプを次に示します。\n{sourceslist}", + "admins": "管理者", + "all_users": "YunoHostの全ユーザー", + "already_up_to_date": "何もすることはありません。すべて最新です。", + "app_action_broke_system": "このアクションは、これらの重要なサービスを壊したようです: {services}", + "app_already_installed": "アプリ '{app}' は既にインストール済み", + "app_already_installed_cant_change_url": "このアプリは既にインストールされています。この機能だけではURLを変更することはできません。利用可能な場合は、`app changeurl`を確認してください。", + "app_already_up_to_date": "{app} アプリは既に最新です", + "app_arch_not_supported": "このアプリはアーキテクチャ {required} にのみインストールできますが、サーバーのアーキテクチャは{current} です", + "app_argument_choice_invalid": "引数 '{name}' に有効な値を選択してください: '{value}' は使用可能な選択肢に含まれていません ({choices})", + "app_change_url_no_script": "アプリ「{app_name}」はまだURLの変更をサポートしていません。多分あなたはそれをアップグレードする必要があります。", + "app_change_url_require_full_domain": "{app}は完全なドメイン(つまり、path = /)を必要とするため、この新しいURLに移動できません。", + "app_change_url_success": "{app} URL が{domain}{path}されました", + "app_config_unable_to_apply": "設定パネルの値を適用できませんでした。", + "app_config_unable_to_read": "設定パネルの値の読み取りに失敗しました。", + "app_corrupt_source": "YunoHost はアセット '{source_id}' ({url}) を {app} 用にダウンロードできましたが、アセットのチェックサムが期待されるものと一致しません。これは、あなたのサーバーで一時的なネットワーク障害が発生したか、もしくはアセットがアップストリームメンテナ(または悪意のあるアクター?)によって何らかの形で変更され、YunoHostパッケージャーがアプリマニフェストを調査/更新する必要があることを意味する可能性があります。\n 期待される sha256 チェックサム: {expected_sha256}\n ダウンロードしたsha256チェックサム: {computed_sha256}\n ダウンロードしたファイルサイズ: {size}", + "app_extraction_failed": "インストール ファイルを抽出できませんでした", + "app_failed_to_download_asset": "{app}のアセット「{source_id}」({url})をダウンロードできませんでした:{out}", + "app_install_files_invalid": "これらのファイルはインストールできません", + "app_install_script_failed": "アプリのインストールスクリプト内部でエラーが発生しました", + "app_label_deprecated": "このコマンドは非推奨です。新しいコマンド ’yunohost user permission update’ を使用して、アプリラベルを管理してください。", + "app_location_unavailable": "この URL は利用できないか、既にインストールされているアプリと競合しています。\n{apps}", + "app_make_default_location_already_used": "「{app}」をドメインのデフォルトアプリにすることはできません。「{domain}」は「{other_app}」によってすでに使用されています", + "app_manifest_install_ask_admin": "このアプリの管理者ユーザーを選択する", + "app_manifest_install_ask_domain": "このアプリをインストールするドメインを選択してください", + "app_manifest_install_ask_init_admin_permission": "このアプリの管理機能にアクセスできるのは誰ですか?(これは後で変更できます)", + "app_manifest_install_ask_init_main_permission": "誰がこのアプリにアクセスできる必要がありますか?(これは後で変更できます)", + "app_manifest_install_ask_is_public": "このアプリは匿名の訪問者に公開する必要がありますか?", + "app_not_correctly_installed": "{app}が正しくインストールされていないようです", + "app_not_enough_disk": "このアプリには{required}の空き容量が必要です。", + "app_not_enough_ram": "このアプリのインストール/アップグレードには{required} のRAMが必要ですが、現在利用可能なのは {current} だけです。", + "app_not_installed": "インストールされているアプリのリストに{app}が見つかりませんでした: {all_apps}", + "app_not_upgraded_broken_system": "アプリ「{failed_app}」はアップグレードに失敗し、システムを壊れた状態にしたため、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_not_upgraded_broken_system_continue": "アプリ ’{failed_app}’ はアップグレードに失敗し、システムを壊れた状態にした(そのためcontinue-on-failureは無視されます)ので、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_restore_failed": "{app}を復元できませんでした: {error}", + "app_restore_script_failed": "アプリのリストアスクリプト内でエラーが発生しました", + "app_sources_fetch_failed": "ソースファイルをフェッチできませんでしたが、URLは正しいですか?", + "app_packaging_format_not_supported": "このアプリは、パッケージ形式がYunoHostバージョンでサポートされていないため、インストールできません。おそらく、システムのアップグレードを検討する必要があります。", + "app_remove_after_failed_install": "インストールの失敗後にアプリを削除しています...", + "app_removed": "'{app}' はアンインストール済", + "app_requirements_checking": "{app} の依存関係を確認しています…", + "app_resource_failed": "{app}のリソースのプロビジョニング、プロビジョニング解除、または更新に失敗しました: {error}", + "app_start_backup": "{app}用にバックアップするファイルを収集しています...", + "app_start_install": "‘{app}’ をインストールしています…", + "app_unknown": "未知のアプリ", + "app_unsupported_remote_type": "アプリで使用されている、サポートされないリモートの種類", + "apps_catalog_init_success": "アプリ カタログ システムが初期化されました!", + "apps_catalog_obsolete_cache": "アプリケーションカタログキャッシュが空であるか、古くなっています。", + "apps_catalog_update_success": "アプリケーションカタログを更新しました!", + "apps_catalog_updating": "アプリケーションカタログを更新しています...", + "app_upgrade_app_name": "'{app}' をアップグレードしています…", + "app_upgrade_failed": "アップグレードに失敗しました {app}: {error}", + "app_upgrade_script_failed": "アプリのアップグレードスクリプト内でエラーが発生しました", + "app_upgrade_several_apps": "次のアプリがアップグレードされます: {apps}", + "app_upgrade_some_app_failed": "一部のアプリをアップグレードできませんでした", + "app_upgraded": "'{app}' アップグレード済", + "app_yunohost_version_not_supported": "このアプリは YunoHost >= {required} を必要としますが、現在インストールされているバージョンは{current} です", + "apps_already_up_to_date": "全てのアプリが最新になりました!", + "apps_catalog_failed_to_download": "{apps_catalog} アプリ カタログをダウンロードできません: {error}", + "apps_failed_to_upgrade": "これらのアプリケーションのアップグレードに失敗しました: {apps}", + "apps_failed_to_upgrade_line": "\n * {app_id} (対応するログを表示するには、’yunohost log show {operation_logger_name}’ を実行してください)", + "ask_admin_fullname": "管理者 フルネーム", + "ask_admin_username": "管理者ユーザー名", + "ask_fullname": "フルネーム", + "backup_app_failed": "{app}をバックアップできませんでした", + "backup_applying_method_copy": "すべてのファイルをバックアップにコピーしています...", + "backup_applying_method_custom": "カスタムバックアップメソッド ’{method}’ を呼び出しています...", + "backup_applying_method_tar": "バックアップ TAR アーカイブを作成しています...", + "backup_archive_app_not_found": "バックアップアーカイブに{app}が見つかりませんでした", + "backup_archive_broken_link": "バックアップアーカイブにアクセスできませんでした({path}へのリンクが壊れています)", + "backup_archive_cant_retrieve_info_json": "アーカイブ '{archive}' の情報を読み込めませんでした... info.json ファイルを取得できません (または有効な json ではありません)。", + "backup_archive_writing_error": "圧縮アーカイブ '{archive}' にバックアップするファイル '{source}' (アーカイブ '{dest}' で指定) を追加できませんでした", + "backup_ask_for_copying_if_needed": "一時的に{size}MBを使用してバックアップを実行しますか?(この方法は、より効率的な方法で準備できなかったファイルがあるため、この方法が使用されます。", + "backup_cant_mount_uncompress_archive": "非圧縮アーカイブを書き込み保護としてマウントできませんでした", + "backup_cleaning_failed": "一時バックアップフォルダをクリーンアップできませんでした", + "backup_copying_to_organize_the_archive": "アーカイブを整理するために{size}MBをコピーしています", + "backup_couldnt_bind": "{src}を{dest}にバインドできませんでした。", + "backup_create_size_estimation": "アーカイブには約{size}のデータが含まれます。", + "backup_created": "バックアップを作成しました: {name}'", + "backup_creation_failed": "バックアップ作成できませんでした", + "backup_csv_addition_failed": "バックアップするファイルをCSVファイルに追加できませんでした", + "backup_csv_creation_failed": "復元に必要な CSV ファイルを作成できませんでした", + "backup_custom_backup_error": "カスタムバックアップ方法は「バックアップ」ステップを通過できませんでした", + "backup_custom_mount_error": "カスタムバックアップ方法は「マウント」ステップを通過できませんでした", + "backup_delete_error": "‘{path}’ を削除する", + "backup_deleted": "バックアップは削除されました: {name}", + "backup_nothings_done": "保存するものがありません", + "backup_output_directory_forbidden": "別の出力ディレクトリを選択します。バックアップは、/bin、/boot、/dev、/etc、/lib、/root、/run、/sbin、/sys、/usr、/var、または/home/yunohost.backup/archives のサブフォルダには作成できません", + "backup_output_directory_not_empty": "空の出力ディレクトリを選択する必要があります", + "backup_output_directory_required": "バックアップ用の出力ディレクトリを指定する必要があります", + "backup_hook_unknown": "バックアップ フック '{hook}' が不明です", + "backup_method_copy_finished": "バックアップコピーがファイナライズされました", + "backup_method_tar_finished": "TARバックアップアーカイブが作成されました", + "backup_output_symlink_dir_broken": "アーカイブディレクトリ '{path}' は壊れたシンボリックリンクです。たぶん、あなたはそれが指す記憶媒体を再/マウントまたは差し込むのを忘れました。", + "backup_mount_archive_for_restore": "復元のためにアーカイブを準備しています...", + "backup_no_uncompress_archive_dir": "そのような圧縮されていないアーカイブディレクトリはありません", + "certmanager_warning_subdomain_dns_record": "サブドメイン '{subdomain}' は '{domain}' と同じ IP アドレスに解決されません。一部の機能は、これを修正して証明書を再生成するまで使用できません。", + "config_action_disabled": "アクション '{action}' は無効になっているため実行できませんでした。制約を満たしていることを確認してください。ヘルプ: {help}", + "backup_permission": "{app}のバックアップ権限", + "backup_running_hooks": "バックアップフックを実行しています...", + "backup_system_part_failed": "「{part}」システム部分をバックアップできませんでした", + "backup_unable_to_organize_files": "簡単な方法を使用してアーカイブ内のファイルを整理できませんでした", + "backup_with_no_backup_script_for_app": "アプリ「{app}」にはバックアップスクリプトがありません。無視。", + "backup_with_no_restore_script_for_app": "{app}には復元スクリプトがないため、このアプリのバックアップを自動的に復元することはできません。", + "certmanager_acme_not_configured_for_domain": "ACMEチャレンジは、nginx confに対応するコードスニペットがないため、現在{domain}実行できません...'yunohost tools regen-conf nginx --dry-run --with-diff' を使用して、nginx の設定が最新であることを確認してください。", + "certmanager_attempt_to_renew_nonLE_cert": "ドメイン '{domain}' の証明書は、Let's Encryptによって発行されていません。自動的に更新できません!", + "certmanager_attempt_to_renew_valid_cert": "ドメイン '{domain}' の証明書の有効期限が近づいていません。(あなたが何をしているのかわかっている場合は、--forceを使用できます)", + "certmanager_cert_renew_failed": "{domains}のLet’s Encrypt 証明書更新に失敗しました", + "certmanager_cert_renew_success": "{domains}のLet’s Encrypt 証明書が更新されました", + "certmanager_cert_signing_failed": "新しい証明書に署名できませんでした", + "certmanager_certificate_fetching_or_enabling_failed": "{domain}に新しい証明書を使用しようとしましたが、機能しませんでした...", + "certmanager_domain_cert_not_selfsigned": "ドメイン {domain} の証明書は自己署名されていません。置き換えてよろしいですか(これを行うには '--force' を使用してください)?", + "certmanager_hit_rate_limit": "最近{domain}、この正確なドメインのセットに対して既に発行されている証明書が多すぎます。しばらくしてからもう一度お試しください。詳細については、https://letsencrypt.org/docs/rate-limits/ を参照してください。", + "certmanager_no_cert_file": "ドメイン {domain} (ファイル: {file}) の証明書ファイルを読み取れませんでした。", + "certmanager_self_ca_conf_file_not_found": "自己署名機関の設定ファイルが見つかりませんでした(ファイル:{file})", + "config_forbidden_readonly_type": "型 '{type}' は読み取り専用として設定できず、別の型を使用してこの値をレンダリングします (関連する引数 ID: '{id}')。", + "config_no_panel": "設定パネルが見つかりません。", + "config_unknown_filter_key": "フィルター キー '{filter_key}' が正しくありません。", + "config_validate_color": "有効な RGB 16 進色である必要があります", + "config_validate_date": "YYYY-MM-DD の形式のような有効な日付である必要があります。", + "config_validate_email": "有効なメールアドレスである必要があります", + "config_action_failed": "アクション '{action}' の実行に失敗しました: {error}", + "config_apply_failed": "新しい構成の適用に失敗しました: {error}", + "config_cant_set_value_on_section": "構成セクション全体に 1 つの値を設定することはできません。", + "config_forbidden_keyword": "キーワード '{keyword}' は予約されており、この ID の質問を含む設定パネルを作成または使用することはできません。", + "config_validate_time": "HH:MM のような有効な時刻である必要があります", + "config_validate_url": "有効なウェブ URL である必要があります", + "confirm_app_install_danger": "危険!このアプリはまだ実験的であることが知られています(明示的に動作していない場合)!あなたが何をしているのかわからない限り、おそらくそれをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません...とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", + "confirm_app_install_thirdparty": "危険!このアプリはYunoHostのアプリカタログの一部ではありません。サードパーティのアプリをインストールすると、システムの整合性とセキュリティが損なわれる可能性があります。あなたが何をしているのかわからない限り、おそらくそれをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません...とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", + "confirm_app_install_warning": "警告:このアプリは動作する可能性がありますが、YunoHostにうまく統合されていません。シングル サインオンやバックアップ/復元などの一部の機能は使用できない場合があります。とにかくインストールしますか?[{answers}] ", + "diagnosis_apps_allgood": "インストールされているすべてのアプリは、基本的なパッケージ化プラクティスを尊重します", + "diagnosis_apps_bad_quality": "このアプリケーションは現在、YunoHostのアプリケーションカタログで壊れているとフラグが付けられています。これは、メンテナが問題を修正しようとしている間の一時的な問題である可能性があります。それまでの間、このアプリのアップグレードは無効になります。", + "diagnosis_apps_broken": "このアプリケーションは現在、YunoHostのアプリケーションカタログで壊れているとフラグが付けられています。これは、メンテナが問題を修正しようとしている間の一時的な問題である可能性があります。それまでの間、このアプリのアップグレードは無効になります。", + "diagnosis_apps_deprecated_practices": "このアプリのインストール済みバージョンでは、非常に古い非推奨のパッケージ化プラクティスがまだ使用されています。あなたは本当にそれをアップグレードすることを検討する必要があります。", + "diagnosis_basesystem_hardware": "サーバーのハードウェア アーキテクチャが{virt} {arch}", + "diagnosis_basesystem_hardware_model": "サーバーモデルが{model}", + "diagnosis_apps_issue": "アプリ '{app}' をアップグレードする", + "diagnosis_apps_not_in_app_catalog": "このアプリケーションは、YunoHostのアプリケーションカタログにはありません。過去に存在し、削除された場合は、アップグレードを受け取らず、システムの整合性とセキュリティが損なわれる可能性があるため、このアプリのアンインストールを検討する必要があります。", + "diagnosis_apps_outdated_ynh_requirement": "このアプリのインストール済みバージョンには、yunohost >= 2.xまたは3.xのみが必要であり、推奨されるパッケージングプラクティスとヘルパーが最新ではないことを示す傾向があります。あなたは本当にそれをアップグレードすることを検討する必要があります。", + "diagnosis_backports_in_sources_list": "apt(パッケージマネージャー)はバックポートリポジトリを使用するように構成されているようです。あなたが何をしているのか本当にわからない限り、バックポートからパッケージをインストールすることは、システムに不安定性や競合を引き起こす可能性があるため、強くお勧めしません。", + "diagnosis_basesystem_host": "サーバは Debian {debian_version} を実行しています", + "diagnosis_basesystem_kernel": "サーバーはLinuxカーネル{kernel_version}を実行しています", + "diagnosis_basesystem_ynh_inconsistent_versions": "一貫性のないバージョンのYunoHostパッケージを実行しています...ほとんどの場合、アップグレードの失敗または部分的なことが原因です。", + "diagnosis_basesystem_ynh_main_version": "サーバーがYunoHost{main_version}を実行しています({repo})", + "diagnosis_basesystem_ynh_single_version": "{package}バージョン:{version}({repo})", + "diagnosis_cache_still_valid": "(キャッシュは{category}診断に有効です。まだ再診断しません!", + "diagnosis_description_regenconf": "システム設定", + "diagnosis_description_services": "サービスステータスチェック", + "diagnosis_description_systemresources": "システムリソース", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "ストレージ<0>(デバイス<1>上)には、( )残りの領域({free_percent} )しかありません{free}。{total}注意してください。", + "diagnosis_diskusage_ok": "ストレージ<0>(デバイス<1>上)にはまだ({free_percent}%)スペースが{free}残っています(から{total})!", + "diagnosis_diskusage_verylow": "ストレージ<0>(デバイス<1>上)には、( )残りの領域({free_percent} )しかありません{free}。{total}あなたは本当にいくつかのスペースをきれいにすることを検討する必要があります!", + "diagnosis_display_tip": "見つかった問題を確認するには、ウェブ管理者の診断セクションに移動するか、コマンドラインから「yunohost診断ショー--問題--人間が読める」を実行します。", + "diagnosis_dns_bad_conf": "一部の DNS レコードが見つからないか、ドメイン {domain} (カテゴリ {category}) が正しくない", + "diagnosis_dns_discrepancy": "次の DNS レコードは、推奨される構成に従っていないようです。
種類: <0>
名前: <1>
現在の値: <2>
期待値: <3>", + "diagnosis_dns_good_conf": "DNS レコードがドメイン {domain} (カテゴリ {category}) 用に正しく構成されている", + "diagnosis_dns_missing_record": "推奨される DNS 構成に従って、次の情報を含む DNS レコードを追加する必要があります。
種類: <0>
名前: <1>
価値: <2>", + "diagnosis_dns_point_to_doc": "DNS レコードの構成についてサポートが必要な場合は 、https://yunohost.org/dns_config のドキュメントを確認してください。", + "diagnosis_dns_specialusedomain": "ドメイン {domain} は、.local や .test などの特殊な用途のトップレベル ドメイン (TLD) に基づいているため、実際の DNS レコードを持つことは想定されていません。", + "diagnosis_dns_try_dyndns_update_force": "このドメインのDNS設定は、YunoHostによって自動的に管理されます。そうでない場合は、 yunohost dyndns update --force を使用して更新を強制することができます。", + "diagnosis_domain_expiration_error": "一部のドメインはすぐに期限切れになります!", + "diagnosis_failed_for_category": "カテゴリ '{category}' の診断に失敗しました: {error}", + "diagnosis_domain_expiration_not_found": "一部のドメインの有効期限を確認できない", + "diagnosis_domain_expiration_not_found_details": "ドメイン{domain}のWHOIS情報に有効期限に関する情報が含まれていないようですね?", + "diagnosis_found_errors": "{category}に関連する{errors}重大な問題が見つかりました!", + "diagnosis_domain_expiration_success": "ドメインは登録されており、すぐに期限切れになることはありません。", + "diagnosis_domain_expiration_warning": "一部のドメインはまもなく期限切れになります!", + "diagnosis_domain_expires_in": "{domain} の有効期限は {days}日です。", + "diagnosis_found_errors_and_warnings": "{category}に関連する重大な問題が{errors}(および{warnings}の警告)見つかりました!", + "diagnosis_found_warnings": "{category}{warnings}改善できるアイテムが見つかりました。", + "diagnosis_domain_not_found_details": "ドメイン{domain}がWHOISデータベースに存在しないか、有効期限が切れています!", + "diagnosis_everything_ok": "{category}はすべて大丈夫そうです!", + "diagnosis_failed": "カテゴリ '{category}' の診断結果を取得できませんでした: {error}", + "diagnosis_http_connection_error": "接続エラー: 要求されたドメインに接続できませんでした。到達できない可能性が非常に高いです。", + "diagnosis_http_could_not_diagnose": "ドメインが IPv{ipversion} の外部から到達可能かどうかを診断できませんでした。", + "diagnosis_http_could_not_diagnose_details": "エラー: {error}", + "diagnosis_http_hairpinning_issue": "ローカルネットワークでヘアピニングが有効になっていないようです。", + "diagnosis_http_nginx_conf_not_up_to_date": "このドメインのnginx設定は手動で変更されたようで、YunoHostがHTTPで到達可能かどうかを診断できません。", + "diagnosis_http_nginx_conf_not_up_to_date_details": "状況を修正するには、コマンドラインからの違いを調べて、 yunohostツールregen-conf nginx --dry-run --with-diff を使用し、問題がない場合は、 yunohostツールregen-conf nginx --forceで変更を適用します。", + "diagnosis_http_ok": "ドメイン {domain} は、ローカル ネットワークの外部から HTTP 経由で到達できます。", + "diagnosis_http_partially_unreachable": "ドメイン {domain} は、IPv{passed} では機能しますが、IPv{failed} ではローカル ネットワークの外部から HTTP 経由で到達できないように見えます。", + "diagnosis_http_special_use_tld": "ドメイン {domain} は、.local や .test などの特殊な用途のトップレベル ドメイン (TLD) に基づいているため、ローカル ネットワークの外部に公開されることは想定されていません。", + "diagnosis_http_timeout": "外部からサーバーに接続しようとしているときにタイムアウトしました。到達できないようです。
1.この問題の最も一般的な原因は、ポート80(および443)が サーバーに正しく転送されていないことです。
2. サービスnginxが実行されていることも確認する必要があります
3.より複雑なセットアップでは、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_http_unreachable": "ドメイン {domain} は、ローカル ネットワークの外部から HTTP 経由で到達できないように見えます。", + "diagnosis_ip_broken_dnsresolution": "ドメイン名の解決が何らかの理由で壊れているようです...ファイアウォールはDNSリクエストをブロックしていますか?", + "diagnosis_ip_broken_resolvconf": "ドメインの名前解決がサーバー上で壊れているようですが、これは/etc/resolv.conf127.0.0.1を指定していないことに関連しているようです。", + "diagnosis_ip_connected_ipv4": "サーバーはIPv4経由でインターネットに接続されています!", + "diagnosis_ip_connected_ipv6": "サーバーはIPv6経由でインターネットに接続されています!", + "diagnosis_ip_global": "グローバルIP: {global}", + "diagnosis_ip_local": "ローカル IP: {local}", + "diagnosis_ip_no_ipv4": "サーバーに機能している IPv4 がありません。", + "diagnosis_ip_no_ipv6": "サーバーに機能している IPv6 がありません。", + "diagnosis_ip_no_ipv6_tip": "IPv6を機能させることは、サーバーが機能するために必須ではありませんが、インターネット全体の健全性にとってはより良いことです。IPv6 は通常、システムまたはプロバイダー (使用可能な場合) によって自動的に構成されます。それ以外の場合は、こちらのドキュメントで説明されているように、いくつかのことを手動で構成する必要があります。 https://yunohost.org/#/ipv6。IPv6を有効にできない場合、または技術的に難しすぎると思われる場合は、この警告を無視しても問題ありません。", + "diagnosis_mail_blacklist_reason": "ブラックリストの登録理由は次のとおりです: {reason}", + "diagnosis_mail_blacklist_website": "リストされている理由を特定して修正した後、IPまたはドメインを削除するように依頼してください: {blacklist_website}", + "diagnosis_mail_ehlo_bad_answer": "SMTP 以外のサービスが IPv{ipversion} のポート 25 で応答しました", + "diagnosis_mail_ehlo_bad_answer_details": "あなたのサーバーの代わりに別のマシンが応答していることが原因である可能性があります。", + "diagnosis_mail_ehlo_could_not_diagnose": "メール サーバ(postfix)が IPv{ipversion} の外部から到達可能かどうかを診断できませんでした。", + "diagnosis_mail_ehlo_ok": "SMTPメールサーバーは外部から到達可能であるため、電子メールを受信できます!", + "diagnosis_mail_ehlo_unreachable": "SMTP メール サーバは、IPv{ipversion} の外部から到達できません。メールを受信できません。", + "diagnosis_mail_ehlo_unreachable_details": "ポート 25 で IPv{ipversion} のサーバーへの接続を開くことができませんでした。到達できないようです。
1.この問題の最も一般的な原因は、ポート25 がサーバーに正しく転送されていないことです。
2. また、サービス接尾辞が実行されていることも確認する必要があります。
3.より複雑なセットアップでは、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_mail_ehlo_wrong": "別の SMTP メール サーバーが IPv{ipversion} で応答します。サーバーはおそらく電子メールを受信できないでしょう。", + "diagnosis_mail_ehlo_wrong_details": "リモート診断ツールが IPv{ipversion} で受信した EHLO は、サーバーのドメインとは異なります。
受信したEHLO: <1>
期待: <2>
この問題の最も一般的な原因は、ポート 25 が サーバーに正しく転送されていないことです。または、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "逆引き DNS が IPv{ipversion} 用に正しく構成されていません。一部のメールは配信されないか、スパムとしてフラグが立てられる場合があります。", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "現在の逆引きDNS: <0>
期待値: <1>", + "diagnosis_mail_fcrdns_dns_missing": "IPv{ipversion} では逆引き DNS は定義されていません。一部のメールは配信されないか、スパムとしてフラグが立てられる場合があります。", + "diagnosis_mail_fcrdns_nok_alternatives_6": "一部のプロバイダーでは、逆引きDNSを構成できません(または機能が壊れている可能性があります...)。逆引きDNSがIPv4用に正しく設定されている場合は、 yunohost設定email.smtp.smtp_allow_ipv6-vオフに設定して、メールを送信するときにIPv6の使用を無効にしてみてください。注:この最後の解決策は、そこにあるいくつかのIPv6専用サーバーから電子メールを送受信できないことを意味します。", + "diagnosis_mail_fcrdns_nok_details": "まず、インターネットルーターインターフェイスまたはホスティングプロバイダーインターフェイスで <0> 逆引きDNSを構成してみてください。(一部のホスティングプロバイダーでは、このためのサポートチケットを送信する必要がある場合があります)。", + "diagnosis_mail_outgoing_port_25_blocked": "送信ポート 25 が IPv{ipversion} でブロックされているため、SMTP メール サーバーは他のサーバーに電子メールを送信できません。", + "diagnosis_mail_outgoing_port_25_blocked_details": "まず、インターネットルーターインターフェイスまたはホスティングプロバイダーインターフェイスの送信ポート25のブロックを解除する必要があります。(一部のホスティングプロバイダーでは、このために問い合わせを行う必要がある場合があります)。", + "diagnosis_never_ran_yet": "このサーバーは最近セットアップされたようで、表示する診断レポートはまだありません。Web管理画面またはコマンドラインから ’yunohost diagnosis run’ を実行して、完全な診断を実行することから始める必要があります。", + "diagnosis_package_installed_from_sury": "一部のシステムパッケージはダウングレードする必要があります", + "diagnosis_processes_killed_by_oom_reaper": "一部のプロセスは、メモリが不足したため、最近システムによって強制終了されました。これは通常、システム上のメモリ不足、またはプロセスがメモリを消費しすぎていることを示しています。強制終了されたプロセスの概要:\n{kills_summary}", + "diagnosis_ram_low": "システムには{available}({available_percent}%)の使用可能なRAMがあります({total}のうち)。注意してください。", + "diagnosis_package_installed_from_sury_details": "一部のパッケージは、Suryと呼ばれるサードパーティのリポジトリから誤ってインストールされました。YunoHostチームはこれらのパッケージを処理する戦略を改善しましたが、Stretchを使用している間にPHP7.3アプリをインストールした一部のセットアップには、いくつかの矛盾が残っていると予想されます。この状況を修正するには、次のコマンドを実行してみてください。 {cmd_to_fix}", + "diagnosis_ports_could_not_diagnose": "IPv{ipversion} で外部からポートに到達できるかどうかを診断できませんでした。", + "diagnosis_ports_could_not_diagnose_details": "エラー: {error}", + "diagnosis_ports_unreachable": "ポート {port} は外部から到達できません。", + "diagnosis_regenconf_allgood": "すべての構成ファイルは、推奨される構成と一致しています!", + "diagnosis_regenconf_manually_modified": "{file} 構成ファイルが手動で変更されたようです。", + "diagnosis_regenconf_manually_modified_details": "あなたが何をしているのかを知っていれば、これはおそらく大丈夫です!YunoHostはこのファイルの自動更新を停止します... ただし、YunoHostのアップグレードには重要な推奨変更が含まれている可能性があることに注意してください。必要に応じて、yunohost tools regen-conf {category} --dry-run --with-diffで違いを調べ、yunohost tools regen-conf {category} --forceを使用して推奨構成に強制的にリセットすることができます", + "diagnosis_rootfstotalspace_critical": "ルートファイルシステムには合計{space}しかありませんが、これは非常に心配な値です!ディスク容量がすぐに枯渇する可能性があります。ルートファイルシステム用には少なくとも16GBを用意することをお勧めします。", + "diagnosis_ram_ok": "システムには、{total}のうち{available} ({available_percent}%) の RAM がまだ使用可能です。", + "diagnosis_ram_verylow": "システムには{available}({available_percent}%)のRAMしか使用できません。({total}のうち)", + "diagnosis_rootfstotalspace_warning": "ルートファイルシステムには合計{space}しかありません。これは問題ないかもしれませんが、最終的にはディスク容量がすぐに枯渇する可能性があるため、注意してください... ルートファイルシステム用に少なくとも16GBを用意することをお勧めします。", + "diagnosis_security_vulnerable_to_meltdown_details": "これを修正するには、システムをアップグレードして再起動し、新しいLinuxカーネルをロードする必要があります(または、これが機能しない場合はサーバープロバイダーに連絡してください)。詳細については、https://meltdownattack.com/ を参照してください。", + "diagnosis_services_bad_status": "サービス{service} のステータスは {status} です :(", + "diagnosis_services_bad_status_tip": "サービスの再起動を試みることができ、それが機能しない場合は、webadminのサービスログを確認してください(コマンドラインから、yunohostサービスの再起動{service}とyunohostサービスログ{service}を使用してこれを行うことができます)。。", + "diagnosis_sshd_config_inconsistent_details": "security.ssh.ssh_port -v YOUR_SSH_PORT に設定された yunohost 設定を実行して SSH ポートを定義し、yunohost tools regen-conf ssh --dry-run --with-diff および yunohost tools regen-conf ssh --force をチェックして、会議を YunoHost の推奨事項にリセットしてください。", + "diagnosis_sshd_config_insecure": "SSH構成は手動で変更されたようで、許可されたユーザーへのアクセスを制限するための「許可グループ」または「許可ユーザー」ディレクティブが含まれていないため、安全ではありません。", + "diagnosis_swap_tip": "サーバーがSDカードまたはSSDストレージでスワップをホストしている場合、デバイスの平均寿命が大幅に短くなる可能性があることに注意してください。", + "diagnosis_unknown_categories": "次のカテゴリは不明です: {categories}", + "diagnosis_using_stable_codename": "apt (システムのパッケージマネージャ) は現在、現在の Debian バージョン (bullseye) のコードネームではなく、コードネーム 'stable' からパッケージをインストールするように設定されています。", + "disk_space_not_sufficient_install": "このアプリケーションをインストールするのに十分なディスク領域が残っていません", + "diagnosis_using_stable_codename_details": "これは通常、ホスティングプロバイダーからの構成が正しくないことが原因です。なぜなら、Debian の次のバージョンが新しい「安定版」になるとすぐに、apt は適切な移行手順を経ずにすべてのシステムパッケージをアップグレードしたくなるからです。ベース Debian リポジトリの apt ソースを編集してこれを修正し、安定版キーワードを bullseye に置き換えることをお勧めします。対応する設定ファイルは /etc/apt/sources.list、または /etc/apt/sources.list.d/ 内のファイルでなければなりません。", + "diagnosis_using_yunohost_testing": "apt (システムのパッケージマネージャー)は現在、YunoHostコアの「テスト」アップグレードをインストールするように構成されています。", + "diagnosis_using_yunohost_testing_details": "自分が何をしているのかを知っていれば、これはおそらく問題ありませんが、YunoHostのアップグレードをインストールする前にリリースノートに注意してください!「テスト版」のアップグレードを無効にしたい場合は、/etc/apt/sources.list.d/yunohost.list から testing キーワードを削除する必要があります。", + "disk_space_not_sufficient_update": "このアプリケーションを更新するのに十分なディスク領域が残っていません", + "domain_cannot_add_muc_upload": "「muc.」で始まるドメインを追加することはできません。この種の名前は、YunoHostに統合されたXMPPマルチユーザーチャット機能のために予約されています。", + "domain_cannot_add_xmpp_upload": "「xmpp-upload」で始まるドメインを追加することはできません。この種の名前は、YunoHostに統合されたXMPPアップロード機能のために予約されています。", + "domain_cannot_remove_main": "'{domain}'はメインドメインなので削除できないので、まず「yunohost domain main-domain -n」を使用して別のドメインをメインドメインとして設定する必要があります。 候補 ドメインのリストは次のとおりです。 {other_domains}", + "domain_config_api_protocol": "API プロトコル", + "domain_cannot_remove_main_add_new_one": "「{domain}」はメインドメインであり唯一のドメインであるため、最初に「yunohostドメイン追加」を使用して別のドメインを追加し、次に「yunohostドメインメインドメイン-n 」を使用してメインドメインとして設定し、「yunohostドメイン削除{domain}」を使用してドメイン「{domain}」を削除する必要があります。", + "domain_config_acme_eligible_explain": "このドメインは、Let's Encrypt証明書の準備ができていないようです。DNS 構成と HTTP サーバーの到達可能性を確認してください。 診断ページの 「DNSレコード」と「Web」セクションは、何が誤って構成されているかを理解するのに役立ちます。", + "domain_config_auth_application_key": "アプリケーションキー", + "domain_config_auth_application_secret": "アプリケーション秘密鍵", + "domain_config_auth_consumer_key": "消費者キー", + "domain_config_auth_entrypoint": "API エントリ ポイント", + "domain_config_default_app": "デフォルトのアプリ", + "domain_config_default_app_help": "このドメインを開くと、ユーザーは自動的にこのアプリにリダイレクトされます。アプリが指定されていない場合、ユーザーはユーザーポータルのログインフォームにリダイレクトされます。", + "domain_config_mail_in": "受信メール", + "domain_config_auth_key": "認証キー", + "domain_config_auth_secret": "認証シークレット", + "domain_config_auth_token": "認証トークン", + "domain_config_cert_install": "Let's Encrypt証明書をインストールする", + "domain_config_cert_issuer": "証明機関", + "domain_config_cert_no_checks": "診断チェックを無視する", + "domain_config_cert_renew": "Let’s Encrypt証明書を更新する", + "domain_config_cert_renew_help": "証明書は、有効期間の最後の 15 日間に自動的に更新されます。必要に応じて手動で更新できます(推奨されません)。", + "domain_config_cert_summary_letsencrypt": "やった!有効なLet's Encrypt証明書を使用しています!", + "domain_config_cert_summary_ok": "さて、現在の証明書は良さそうです!", + "domain_config_cert_summary_selfsigned": "警告: 現在の証明書は自己署名です。ブラウザは新しい訪問者に不気味な警告を表示します!", + "domain_config_mail_out": "送信メール", + "domain_config_xmpp_help": "注意: 一部のXMPP機能では、DNSレコードを更新し、Lets Encrypt 証明書を再生成して有効にする必要があります", + "domain_created": "作成されたドメイン", + "domain_creation_failed": "ドメイン {domain}を作成できません: {error}", + "domain_deleted": "ドメインが削除されました", + "domain_deletion_failed": "ドメイン {domain}を削除できません: {error}", + "domain_dns_push_failed_to_authenticate": "ドメイン '{domain}' のレジストラーの API で認証に失敗しました。おそらく資格情報が正しくないようです?(エラー: {error})", + "domain_dns_push_failed_to_list": "レジストラの API を使用して現在のレコードを一覧表示できませんでした: {error}", + "domain_dns_push_not_applicable": "自動 DNS 構成機能は、ドメイン {domain}には適用されません。https://yunohost.org/dns_config のドキュメントに従って、DNS レコードを手動で構成する必要があります。", + "domain_dns_push_managed_in_parent_domain": "自動 DNS 構成機能は、親ドメイン {parent_domain}で管理されます。", + "domain_dns_push_partial_failure": "DNS レコードが部分的に更新されました: いくつかの警告/エラーが報告されました。", + "domain_dns_push_record_failed": "{action} {type}/{name} の記録に失敗しました: {error}", + "domain_dns_push_success": "DNS レコードが更新されました!", + "domain_dns_pushing": "DNS レコードをプッシュしています...", + "domain_dns_registrar_experimental": "これまでのところ、**{registrar}**のAPIとのインターフェースは、YunoHostコミュニティによって適切にテストおよびレビューされていません。サポートは**非常に実験的**です-注意してください!", + "domain_dns_registrar_managed_in_parent_domain": "このドメインは{parent_domain_link}のサブドメインです。DNS レジストラーの構成は、{parent_domain}の設定パネルで管理する必要があります。", + "domain_dns_registrar_not_supported": "YunoHost は、このドメインを処理するレジストラを自動的に検出できませんでした。DNS レコードは、https://yunohost.org/dns のドキュメントに従って手動で構成する必要があります。", + "domain_dns_registrar_supported": "YunoHost は、このドメインがレジストラ**{registrar}**によって処理されていることを自動的に検出しました。必要に応じて、適切なAPI資格情報を提供すると、YunoHostはこのDNSゾーンを自動的に構成します。API 資格情報の取得方法に関するドキュメントは、https://yunohost.org/registar_api_{registrar} ページにあります。(https://yunohost.org/dns のドキュメントに従ってDNSレコードを手動で構成することもできます)", + "domain_dns_registrar_yunohost": "このドメインは nohost.me / nohost.st / ynh.fr であるため、そのDNS構成は、それ以上の構成なしでYunoHostによって自動的に処理されます。(「YunoHost Dyndns Update」コマンドを参照)", + "domain_dyndns_root_unknown": "'{domain}' ドメインのルートを '{name}' にリダイレクト", + "domain_exists": "この名前のバックアップアーカイブはすでに存在します。", + "domain_hostname_failed": "新しいホスト名を設定できません。これにより、後で問題が発生する可能性があります(問題ない可能性があります)。", + "domain_registrar_is_not_configured": "レジストラーは、ドメイン {domain} 用にまだ構成されていません。", + "domain_remove_confirm_apps_removal": "このドメインを削除すると、これらのアプリケーションが削除されます。\n{apps}\n\nよろしいですか?[{answers}]", + "domain_uninstall_app_first": "これらのアプリケーションは、ドメインに引き続きインストールされます。\n{apps}\n\nドメインの削除に進む前に、「yunohostアプリ削除the_app_id」を使用してアンインストールするか、「yunohostアプリ変更URL the_app_id」を使用して別のドメインに移動してください。", + "domain_unknown": "ドメイン {domain}を作成できません: {error}", + "domains_available": "ドメイン管理", + "done": "完了", + "downloading": "ダウンロード中...", + "dpkg_is_broken": "dpkg / APT(システムパッケージマネージャー)が壊れた状態にあるように見えるため、現在はこれを行うことができません...SSH経由で接続し、 'sudo apt install --fix-broken'および/または 'sudo dpkg --configure -a'および/または 'sudo dpkg --audit'を実行することで、この問題を解決しようとすることができます。", + "dpkg_lock_not_available": "別のプログラムがdpkg(システムパッケージマネージャー)のロックを使用しているように見えるため、このコマンドは現在実行できません", + "dyndns_could_not_check_available": "{domain}{provider}で利用できるかどうかを確認できませんでした。", + "dyndns_ip_update_failed": "IP アドレスを DynDNS に更新できませんでした", + "dyndns_ip_updated": "DynDNSでIPを更新しました", + "dyndns_no_domain_registered": "このカテゴリーにログが登録されていません", + "dyndns_provider_unreachable": "DynDNSプロバイダー{provider}に到達できません:YunoHostがインターネットに正しく接続されていないか、ダイネットサーバーがダウンしています。", + "dyndns_registered": "このカテゴリーにログが登録されていません", + "dyndns_registration_failed": "DynDNS ドメインを登録できませんでした: {error}", + "dyndns_unavailable": "ドメイン {domain}を作成できません: {error}", + "dyndns_domain_not_provided": "DynDNS プロバイダー{provider}ドメイン{domain}を提供できません。", + "extracting": "抽出。。。", + "field_invalid": "フィールドは必要です。", + "file_does_not_exist": "ファイル {path}が存在しません。", + "firewall_reloaded": "ファイアウォールがリロードされました", + "firewall_rules_cmd_failed": "一部のファイアウォール規則コマンドが失敗しました。ログの詳細情報。", + "global_settings_reset_success": "グローバルIP: {global}", + "global_settings_setting_admin_strength": "管理者パスワードの強度要件", + "global_settings_setting_admin_strength_help": "これらの要件は、パスワードを初期化または変更する場合にのみ適用されます", + "global_settings_setting_backup_compress_tar_archives": "バックアップの圧縮", + "global_settings_setting_backup_compress_tar_archives_help": "新しいバックアップを作成するときは、圧縮されていないアーカイブ (.tar) ではなく、アーカイブを圧縮 (.tar.gz) します。注意:このオプションを有効にすると、バックアップアーカイブの作成が軽くなりますが、最初のバックアップ手順が大幅に長くなり、CPUに負担がかかります。", + "global_settings_setting_dns_exposure": "DNS の構成と診断で考慮すべき IP バージョン", + "global_settings_setting_dns_exposure_help": "注意:これは、推奨されるDNS構成と診断チェックにのみ影響します。これはシステム構成には影響しません。", + "global_settings_setting_nginx_compatibility": "NGINXの互換性", + "global_settings_setting_nginx_compatibility_help": "WebサーバーNGINXの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します", + "global_settings_setting_nginx_redirect_to_https": "HTTPSを強制", + "global_settings_setting_nginx_redirect_to_https_help": "デフォルトでHTTPリクエストをHTTPにリダイレクトします(あなたが何をしているのか本当にわからない限り、オフにしないでください!", + "global_settings_setting_passwordless_sudo": "管理者がパスワードを再入力せずに「sudo」を使用できるようにする", + "global_settings_setting_portal_theme_help": "カスタム ポータル テーマの作成の詳細については、https://yunohost.org/theming を参照してください。", + "global_settings_setting_postfix_compatibility": "後置の互換性", + "global_settings_setting_pop3_enabled": "POP3 を有効にする", + "global_settings_setting_pop3_enabled_help": "メール サーバーの POP3 プロトコルを有効にする", + "global_settings_setting_portal_theme": "ユーザーポータルでタイルに表示する", + "global_settings_setting_root_access_explain": "Linux システムでは、「ルート」が絶対管理者です。YunoHost のコンテキストでは、サーバーのローカルネットワークからを除き、直接の「ルート」SSH ログインはデフォルトで無効になっています。'admins' グループのメンバーは、sudo コマンドを使用して、コマンドラインから root として動作できます。ただし、何らかの理由で通常の管理者がログインできなくなった場合に、システムをデバッグするための(堅牢な)rootパスワードがあると便利です。", + "global_settings_setting_security_experimental_enabled": "実験的なセキュリティ機能", + "global_settings_setting_security_experimental_enabled_help": "実験的なセキュリティ機能を有効にします(何をしているのかわからない場合は有効にしないでください)。", + "global_settings_setting_smtp_allow_ipv6_help": "IPv6 を使用したメールの送受信を許可する", + "global_settings_setting_smtp_relay_enabled": "SMTP リレーを有効にする", + "global_settings_setting_smtp_relay_enabled_help": "この yunohost インスタンスの代わりにメールを送信するために使用する SMTP リレーを有効にします。このような状況のいずれかにある場合に便利です:25ポートがISPまたはVPSプロバイダーによってブロックされている、DUHLにリストされている住宅用IPがある、逆引きDNSを構成できない、またはこのサーバーがインターネットに直接公開されておらず、他のものを使用してメールを送信したい。", + "global_settings_setting_smtp_relay_host": "SMTP リレー ホスト", + "global_settings_setting_smtp_relay_password": "SMTP リレー パスワード", + "global_settings_setting_smtp_relay_port": "SMTP リレー ポート", + "global_settings_setting_smtp_relay_user": "SMTP リレー ユーザー", + "global_settings_setting_ssh_compatibility": "SSH の互換性", + "global_settings_setting_ssh_compatibility_help": "SSHサーバーの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します。詳細については、https://infosec.mozilla.org/guidelines/openssh を参照してください。", + "global_settings_setting_ssh_password_authentication": "パスワード認証", + "global_settings_setting_ssh_password_authentication_help": "SSH のパスワード認証を許可する", + "global_settings_setting_ssh_port": "SSH ポート", + "global_settings_setting_ssowat_panel_overlay_enabled": "アプリで小さな「YunoHost」ポータルショートカットの正方形を有効にします", + "global_settings_setting_user_strength": "ユーザー パスワードの強度要件", + "global_settings_setting_webadmin_allowlist_help": "ウェブ管理者へのアクセスを許可されたIPアドレス。", + "global_settings_setting_webadmin_allowlist": "ウェブ管理者 IP 許可リスト", + "global_settings_setting_webadmin_allowlist_enabled": "ウェブ管理 IP 許可リストを有効にする", + "global_settings_setting_webadmin_allowlist_enabled_help": "一部の IP のみにウェブ管理者へのアクセスを許可します。", + "good_practices_about_admin_password": "次に、新しい管理パスワードを定義しようとしています。パスワードは8文字以上である必要がありますが、より長いパスワード(パスフレーズなど)を使用したり、さまざまな文字(大文字、小文字、数字、特殊文字)を使用したりすることをお勧めします。", + "good_practices_about_user_password": "次に、新しいユーザー・パスワードを定義しようとしています。パスワードは少なくとも8文字の長さである必要がありますが、より長いパスワード(パスフレーズなど)や、さまざまな文字(大文字、小文字、数字、特殊文字)を使用することをお勧めします。", + "group_already_exist": "グループ {group} は既に存在します", + "group_already_exist_on_system": "グループ {group} はシステム グループに既に存在します。", + "group_already_exist_on_system_but_removing_it": "グループ{group}はすでにシステムグループに存在しますが、YunoHostはそれを削除します...", + "group_cannot_edit_all_users": "グループ 'all_users' は手動で編集できません。これは、YunoHostに登録されているすべてのユーザーを含むことを目的とした特別なグループです", + "invalid_shell": "無効なシェル: {shell}", + "ip6tables_unavailable": "ここではip6tablesを使うことはできません。あなたはコンテナ内にいるか、カーネルがサポートしていません", + "group_cannot_edit_primary_group": "グループ '{group}' を手動で編集することはできません。これは、特定のユーザーを 1 人だけ含むためのプライマリ グループです。", + "group_cannot_edit_visitors": "グループの「訪問者」を手動で編集することはできません。匿名の訪問者を代表する特別なグループです", + "group_creation_failed": "グループ '{group}' を作成できませんでした: {error}", + "group_deleted": "グループ '{group}' が削除されました", + "group_deletion_failed": "グループ '{group}' を削除できませんでした: {error}", + "group_update_aliases": "グループ '{group}' のエイリアスの更新", + "group_update_failed": "グループ '{group}' を更新できませんでした: {error}", + "group_updated": "グループ '{group}' が更新されました", + "group_user_add": "ユーザー '{user}' がグループ '{group}' に追加されます。", + "hook_json_return_error": "フック{path}からリターンを読み取れませんでした。エラー: {msg}. 生のコンテンツ: {raw_content}", + "hook_list_by_invalid": "このプロパティは、フックを一覧表示するために使用することはできません", + "hook_name_unknown": "不明なフック名 '{name}'", + "installation_complete": "インストールが完了しました", + "invalid_credentials": "無効なパスワードまたはユーザー名", + "invalid_number": "数値にする必要があります", + "invalid_number_max": "{max}より小さくする必要があります", + "invalid_number_min": "{min}より大きい値にする必要があります", + "invalid_regex": "無効な正規表現: '{regex}'", + "iptables_unavailable": "ここではiptablesを使うことはできません。あなたはコンテナ内にいるか、カーネルがサポートしていません", + "ldap_attribute_already_exists": "LDAP 属性 '{attribute}' は、値 '{value}' で既に存在します。", + "ldap_server_down": "LDAP サーバーに到達できません", + "ldap_server_is_down_restart_it": "LDAP サービスがダウンしています。再起動を試みます...", + "log_app_action_run": "{} アプリのアクションの実行", + "log_app_change_url": "{} アプリのアクセスURLを変更", + "log_app_config_set": "‘{}’ アプリに設定を適用する", + "log_app_makedefault": "‘{}’ をデフォルトのアプリにする", + "log_app_remove": "「{}」アプリを削除する", + "log_app_upgrade": "「{}」アプリをアップグレードする", + "log_available_on_yunopaste": "このログは、{url}", + "log_backup_create": "バックアップアーカイブを作成する", + "log_backup_restore_app": "バックアップを復元する ‘{name}’", + "log_backup_restore_system": "収集したファイルからバックアップアーカイブを作成しています...", + "log_corrupted_md_file": "ログに関連付けられている YAML メタデータ ファイルが破損しています: '{md_file}\nエラー: {error}'", + "log_does_exists": "「{log}」という名前の操作ログはありません。「yunohostログリスト」を使用して、利用可能なすべての操作ログを表示します", + "log_domain_add": "ドメイン ‘{name}’ を追加する", + "log_domain_config_set": "ドメイン '{}' の構成を更新する", + "log_domain_dns_push": "‘{name}’ DNSレコードを登録する", + "log_domain_main_domain": "「{}」をメインドメインにする", + "log_domain_remove": "システム構成から「{}」ドメインを削除する", + "log_dyndns_subscribe": "YunoHostコアのアップグレードを開始しています...", + "log_dyndns_update": "YunoHostサブドメイン「{}」に関連付けられているIPを更新します", + "log_help_to_get_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、「yunohostログ共有{name}」コマンドを使用してこの操作の完全なログを共有してください", + "log_help_to_get_log": "操作「{desc}」のログを表示するには、「yunohostログショー{name}」コマンドを使用します。", + "log_letsencrypt_cert_install": "「{}」ドメインにLet's Encrypt証明書をインストールする", + "log_letsencrypt_cert_renew": "Let’s Encrypt証明書を更新する", + "log_link_to_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、 ここをクリックして この操作の完全なログを提供してください", + "log_link_to_log": "この操作の完全なログ: ''{desc}", + "log_operation_unit_unclosed_properly": "操作ユニットが正しく閉じられていません", + "log_permission_create": "作成権限 '{}'", + "log_permission_delete": "削除権限 '{}'", + "log_permission_url": "権限 '{}' に関連する URL を更新する", + "log_regen_conf": "システム設定", + "log_remove_on_failed_install": "インストールに失敗した後に「{}」を削除します", + "log_resource_snippet": "リソースのプロビジョニング/プロビジョニング解除/更新", + "log_selfsigned_cert_install": "「{}」ドメインに自己署名証明書をインストールする", + "log_user_create": "「{}」ユーザーを追加する", + "log_user_delete": "「{}」ユーザーの削除", + "log_user_group_create": "「{}」グループの作成", + "log_settings_reset": "設定をリセット", + "log_settings_reset_all": "すべての設定をリセット", + "log_settings_set": "設定を適用", + "log_tools_migrations_migrate_forward": "移行を実行する", + "log_tools_postinstall": "YunoHostサーバーをポストインストールします", + "log_tools_reboot": "サーバーを再起動", + "log_tools_shutdown": "サーバーをシャットダウン", + "log_tools_upgrade": "システムパッケージのアップグレード", + "log_user_group_delete": "「{}」グループの削除", + "log_user_group_update": "'{}' グループを更新", + "log_user_import": "ユーザーのインポート", + "mailbox_used_space_dovecot_down": "使用済みメールボックススペースをフェッチする場合は、Dovecotメールボックスサービスが稼働している必要があります", + "log_user_permission_reset": "アクセス許可 '{}' をリセットします", + "mailbox_disabled": "ユーザーの{user}に対して電子メールがオフになっている", + "main_domain_change_failed": "メインドメインを変更できません", + "main_domain_changed": "メインドメインが変更されました", + "migration_0021_cleaning_up": "キャッシュとパッケージのクリーンアップはもう役に立たなくなりました...", + "migration_0021_general_warning": "この移行はデリケートな操作であることに注意してください。YunoHostチームはそれをレビューしてテストするために最善を尽くしましたが、移行によってシステムまたはそのアプリの一部が破損する可能性があります。\n\nしたがって、次のことをお勧めします。\n - 重要なデータやアプリのバックアップを実行します。関する詳細情報: https://yunohost.org/backup\n - 移行を開始した後はしばらくお待ちください: インターネット接続とハードウェアによっては、すべてがアップグレードされるまでに最大数時間かかる場合があります。", + "migration_0021_main_upgrade": "メインアップグレードを開始しています...", + "migration_0021_not_enough_free_space": "/var/の空き容量はかなり少ないです!この移行を実行するには、少なくとも 1 GB の空き容量が必要です。", + "migration_0021_modified_files": "次のファイルは手動で変更されていることが判明し、アップグレード後に上書きされる可能性があることに注意してください: {manually_modified_files}", + "migration_0021_not_buster2": "現在の Debian ディストリビューションは Buster ではありません!すでにBuster->Bullseyeの移行を実行している場合、このエラーは移行手順が100% s成功しなかったという事実の兆候です(そうでなければ、YunoHostは完了のフラグを立てます)。Webadminのツール>ログにある移行の**完全な**ログを必要とするサポートチームで何が起こったのかを調査することをお勧めします。", + "migration_0021_patch_yunohost_conflicts": "競合の問題を回避するためにパッチを適用しています...", + "migration_0021_patching_sources_list": "sources.listsにパッチを適用しています...", + "migration_0021_problematic_apps_warning": "以下の問題のあるインストール済みアプリが検出されました。これらはYunoHostアプリカタログからインストールされていないか、「working」としてフラグが立てられていないようです。したがって、アップグレード後も動作することを保証することはできません: {problematic_apps}", + "migration_0021_still_on_buster_after_main_upgrade": "メインのアップグレード中に問題が発生しましたが、システムはまだDebian Busterです", + "migration_0021_system_not_fully_up_to_date": "システムが完全に最新ではありません。Bullseyeへの移行を実行する前に、まずは通常のアップグレードを実行してください。", + "migration_0023_not_enough_space": "移行を実行するのに十分な領域を {path} で使用できるようにします。", + "migration_0023_postgresql_11_not_installed": "PostgreSQL がシステムにインストールされていません。何もすることはありません。", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11はインストールされていますが、PostgreSQL 13はインストールされてい!?:(システムで何か奇妙なことが起こった可能性があります...", + "migration_0024_rebuild_python_venv_broken_app": "このアプリ用にvirtualenvを簡単に再構築できないため、{app}スキップします。代わりに、「yunohostアプリのアップグレード-{app}を強制」を使用してこのアプリを強制的にアップグレードして、状況を修正する必要があります。", + "migration_0024_rebuild_python_venv_disclaimer_base": "Debian Bullseye へのアップグレード後、Debian に同梱されている新しい Python バージョンに変換するために、いくつかの Python アプリケーションを部分的に再構築する必要があります (技術的には、「virtualenv」と呼ばれるものを再作成する必要があります)。それまでの間、これらのPythonアプリケーションは機能しない可能性があります。YunoHostは、以下に詳述するように、それらのいくつかについて仮想環境の再構築を試みることができます。他のアプリの場合、または再構築の試行が失敗した場合は、それらのアプリのアップグレードを手動で強制する必要があります。", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "これらのアプリに対して Virtualenvs を自動的に再構築することはできません。あなたはそれらのアップグレードを強制する必要があります、それはコマンドラインから行うことができます: 'yunohostアプリのアップグレード - -force APP':{ignored_apps}", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "virtualenvの再構築は、次のアプリに対して試行されます(注意:操作には時間がかかる場合があります)。 {rebuild_apps}", + "migration_0024_rebuild_python_venv_failed": "{app} の Python virtualenv の再構築に失敗しました。これが解決されない限り、アプリは機能しない場合があります。「yunohostアプリのアップグレード--強制{app}」を使用してこのアプリのアップグレードを強制して、状況を修正する必要があります。", + "migration_0024_rebuild_python_venv_in_progress": "現在、 '{app}'のPython仮想環境を再構築しようとしています", + "migration_description_0021_migrate_to_bullseye": "システムを Debian ブルズアイと YunoHost 11.x にアップグレードする", + "migration_description_0022_php73_to_php74_pools": "php7.3-fpm 'pool' conf ファイルを php7.4 に移行します。", + "migration_description_0023_postgresql_11_to_13": "PostgreSQL 11 から 13 へのデータベースの移行", + "migration_description_0024_rebuild_python_venv": "ブルズアイ移行後にPythonアプリを修復する", + "migration_description_0025_global_settings_to_configpanel": "従来のグローバル設定の命名法を新しい最新の命名法に移行する", + "migration_ldap_rollback_success": "システムがロールバックされました。", + "migrations_already_ran": "これらの移行は既に完了しています: {ids}", + "migrations_dependencies_not_satisfied": "移行{id}の前に、次の移行を実行します: '{dependencies_id}'。", + "migrations_exclusive_options": "'--auto'、'--skip'、および '--force-rerun' は相互に排他的なオプションです。", + "migrations_failed_to_load_migration": "移行{id}を読み込めませんでした: {error}", + "migrations_list_conflict_pending_done": "'--previous' と '--done' の両方を同時に使用することはできません。", + "migrations_loading_migration": "移行{id}を読み込んでいます...", + "migrations_migration_has_failed": "移行{id}が完了しなかったため、中止されました。エラー: {exception}", + "migrations_must_provide_explicit_targets": "'--skip' または '--force-rerun' を使用する場合は、明示的なターゲットを指定する必要があります。", + "migrations_need_to_accept_disclaimer": "移行{id}を実行するには、次の免責事項に同意する必要があります。\n---\n{disclaimer}\n---\n移行の実行に同意する場合は、'--accept-disclaimer' オプションを指定してコマンドを再実行してください。", + "migrations_running_forward": "移行{id}を実行しています...", + "migrations_skip_migration": "移行{id}スキップしています...", + "migrations_success_forward": "移行{id}完了しました", + "migrations_to_be_ran_manually": "移行{id}は手動で実行する必要があります。ウェブ管理ページの移行→ツールに移動するか、「yunohostツールの移行実行」を実行してください。", + "not_enough_disk_space": "'{path}'に十分な空き容量がありません", + "operation_interrupted": "操作は手動で中断されたようですね?", + "migrations_no_migrations_to_run": "実行する移行はありません", + "migrations_no_such_migration": "「{id}」と呼ばれる移行はありません", + "other_available_options": "...および{n}個の表示されない他の使用可能なオプション", + "migrations_not_pending_cant_skip": "これらの移行は保留中ではないため、スキップすることはできません。 {ids}", + "migrations_pending_cant_rerun": "これらの移行はまだ保留中であるため、再度実行することはできません{ids}", + "password_confirmation_not_the_same": "パスワードが一致しません", + "password_listed": "このパスワードは、世界で最も使用されているパスワードの1つです。もっとユニークなものを選んでください。", + "password_too_long": "127文字未満のパスワードを選択してください", + "password_too_simple_2": "パスワードは8文字以上で、数字、大文字、小文字を含める必要があります", + "password_too_simple_3": "パスワードは8文字以上で、数字、大文字、小文字、特殊文字を含める必要があります", + "password_too_simple_4": "パスワードは12文字以上で、数字、大文字、小文字、特殊文字を含める必要があります", + "pattern_backup_archive_name": "最大 30 文字、英数字、-_ を含む有効なファイル名である必要があります。文字のみ", + "pattern_domain": "有効なドメイン名である必要があります(例:my-domain.org)", + "pattern_email": "「+」記号のない有効な電子メールアドレスである必要があります(例:someone@example.com)", + "pattern_email_forward": "有効な電子メールアドレスである必要があり、「+」記号が受け入れられます(例:someone+tag@example.com)", + "pattern_firstname": "有効な名前(3 文字以上)である必要があります。", + "pattern_fullname": "有効なフルネーム (3 文字以上) である必要があります。", + "pattern_lastname": "有効な姓 (3 文字以上) である必要があります。", + "pattern_mailbox_quota": "クォータを持たない場合は、接尾辞が b/k/M/G/T または 0 を含むサイズである必要があります", + "pattern_password": "3 文字以上である必要があります", + "pattern_password_app": "申し訳ありませんが、パスワードに次の文字を含めることはできません: {forbidden_chars}", + "pattern_port_or_range": "有効なポート番号(例:0-65535)またはポート範囲(例:100:200)である必要があります", + "pattern_username": "小文字の英数字とアンダースコア(_)のみにする必要があります", + "permission_already_allowed": "グループ '{group}' には既にアクセス許可 '{permission}' が有効になっています", + "permission_already_disallowed": "グループ '{group}' には既にアクセス許可 '{permission}' が無効になっています", + "permission_already_exist": "アクセス許可 '{permission}' は既に存在します", + "permission_already_up_to_date": "追加/削除要求が既に現在の状態と一致しているため、アクセス許可は更新されませんでした。", + "permission_cannot_remove_main": "メイン権限の削除は許可されていません", + "permission_cant_add_to_all_users": "権限{permission}すべてのユーザーに追加することはできません。", + "permission_created": "アクセス許可 '{permission}' が作成されました", + "permission_creation_failed": "アクセス許可 '{permission}' を作成できませんでした: {error}", + "permission_currently_allowed_for_all_users": "このアクセス許可は現在、他のユーザーに加えてすべてのユーザーに付与されています。「all_users」権限を削除するか、現在付与されている他のグループを削除することをお勧めします。", + "permission_deleted": "権限 '{permission}' が削除されました", + "permission_deletion_failed": "アクセス許可 '{permission}' を削除できませんでした: {error}", + "permission_not_found": "アクセス許可 '{permission}' が見つかりません", + "permission_protected": "アクセス許可{permission}は保護されています。このアクセス許可に対して訪問者グループを追加または削除することはできません。", + "permission_require_account": "権限{permission}は、アカウントを持つユーザーに対してのみ意味があるため、訪問者に対して有効にすることはできません。", + "permission_update_failed": "アクセス許可 '{permission}' を更新できませんでした: {error}", + "port_already_closed": "ポート {port} は既に{ip_version}接続のために閉じられています", + "port_already_opened": "ポート {port} は既に{ip_version}接続用に開かれています", + "postinstall_low_rootfsspace": "ルートファイルシステムの総容量は10GB未満で、かなり気になります。ディスク容量がすぐに不足する可能性があります。ルートファイルシステム用に少なくとも16GBを用意することをお勧めします。この警告にもかかわらずYunoHostをインストールする場合は、--force-diskspaceを使用してポストインストールを再実行してください", + "regenconf_dry_pending_applying": "カテゴリ '{category}' に適用された保留中の構成を確認しています...", + "regenconf_failed": "カテゴリの設定を再生成できませんでした: {categories}", + "regenconf_file_backed_up": "構成ファイル '{conf}' が '{backup}' にバックアップされました", + "regenconf_file_copy_failed": "新しい構成ファイル '{new}' を '{conf}' にコピーできませんでした", + "regenconf_file_kept_back": "設定ファイル '{conf}' は regen-conf (カテゴリ {category}) によって削除される予定でしたが、元に戻されました。", + "regenconf_file_manually_modified": "構成ファイル '{conf}' は手動で変更されており、更新されません", + "regenconf_file_manually_removed": "構成ファイル '{conf}' は手動で削除され、作成されません", + "regenconf_file_remove_failed": "構成ファイル '{conf}' を削除できませんでした", + "regenconf_file_removed": "構成ファイル '{conf}' が削除されました", + "regenconf_file_updated": "構成ファイル '{conf}' が更新されました", + "regenconf_need_to_explicitly_specify_ssh": "ssh構成は手動で変更されていますが、実際に変更を適用するには、--forceでカテゴリ「ssh」を明示的に指定する必要があります。", + "regenconf_now_managed_by_yunohost": "設定ファイル '{conf}' が YunoHost (カテゴリ {category}) によって管理されるようになりました。", + "regenconf_pending_applying": "カテゴリ '{category}' に保留中の構成を適用しています...", + "regenconf_up_to_date": "カテゴリ '{category}' の設定は既に最新です", + "regenconf_updated": "このカテゴリーにログが登録されていません", + "regenconf_would_be_updated": "カテゴリ '{category}' の構成が更新されているはずです。", + "regex_incompatible_with_tile": "パッケージャー!アクセス許可 '{permission}' show_tile が 'true' に設定されているため、正規表現 URL をメイン URL として定義できません", + "regex_with_only_domain": "ドメインに正規表現を使用することはできませんが、パスにのみ使用できます", + "registrar_infos": "レジストラ情報", + "restore_already_installed_app": "'{name}' の ‘{id}’ パネル設定をアップデートする", + "restore_already_installed_apps": "次のアプリは既にインストールされているため復元できません。 {apps}", + "restore_backup_too_old": "このバックアップアーカイブは、古すぎるYunoHostバージョンからのものであるため、復元できません。", + "restore_cleaning_failed": "一時復元ディレクトリをクリーンアップできませんでした", + "restore_complete": "復元が完了しました", + "restore_may_be_not_enough_disk_space": "システムに十分なスペースがないようです(空き:{free_space} B、必要なスペース:{needed_space} B、セキュリティマージン:{margin} B)", + "root_password_desynchronized": "管理者パスワードが変更されましたが、YunoHostはこれをrootパスワードに伝播できませんでした!", + "server_reboot_confirm": "サーバーはすぐに再起動しますが、よろしいですか?[{answers}]", + "server_shutdown": "サーバーがシャットダウンします", + "service_already_stopped": "サービス '{service}' は既に停止されています", + "service_cmd_exec_failed": "コマンド '{command}' を実行できませんでした", + "service_description_nginx": "サーバーでホストされているすべてのWebサイトへのアクセスを提供または提供します", + "service_description_redis-server": "高速データ・アクセス、タスク・キュー、およびプログラム間の通信に使用される特殊なデータベース", + "service_description_rspamd": "スパムやその他の電子メール関連機能をフィルタリングします", + "service_description_slapd": "ユーザー、ドメイン、関連情報を格納します", + "service_description_ssh": "ターミナル経由でサーバーにリモート接続できます(SSHプロトコル)", + "service_description_yunohost-api": "YunoHostウェブインターフェイスとシステム間の相互作用を管理します", + "service_description_yunohost-firewall": "サービスへの接続ポートの開閉を管理", + "service_description_yunomdns": "ローカルネットワークで「yunohost.local」を使用してサーバーに到達できます", + "service_disable_failed": "起動時にサービス '{service}' を開始できませんでした。\n\n最近のサービスログ:{logs}", + "service_disabled": "システムの起動時にサービス '{service}' は開始されなくなります。", + "service_reload_failed": "サービス '{service}' をリロードできませんでした\n\n最近のサービスログ:{logs}", + "service_reload_or_restart_failed": "サービス '{service}' をリロードまたは再起動できませんでした\n\n最近のサービスログ:{logs}", + "service_reloaded_or_restarted": "サービス '{service}' が再読み込みまたは再起動されました", + "service_remove_failed": "サービス '{service}' を削除できませんでした", + "service_removed": "サービス '{service}' が削除されました", + "service_restart_failed": "サービス '{service}' を再起動できませんでした\n\n最近のサービスログ:{logs}", + "service_restarted": "サービス '{service}' が再起動しました", + "service_start_failed": "サービス '{service}' を開始できませんでした\n\n最近のサービスログ:{logs}", + "service_started": "サービス '{service}' が開始されました", + "service_stop_failed": "サービス '{service}' を停止できません\n\n最近のサービスログ:{logs}", + "service_stopped": "サービス '{service}' が停止しました", + "service_unknown": "不明なサービス '{service}'", + "system_username_exists": "ユーザー名はシステムユーザーのリストにすでに存在します", + "this_action_broke_dpkg": "このアクションはdpkg / APT(システムパッケージマネージャ)を壊しました...SSH経由で接続し、「sudo apt install --fix-broken」および/または「sudo dpkg --configure -a」を実行することで、この問題を解決できます。", + "tools_upgrade": "システムパッケージのアップグレード", + "tools_upgrade_failed": "パッケージをアップグレードできませんでした: {packages_list}", + "unbackup_app": "{app}は保存されません", + "unexpected_error": "予期しない問題が発生しました:{error}", + "unknown_main_domain_path": "'{app}' の不明なドメインまたはパス。アクセス許可の URL を指定できるようにするには、ドメインとパスを指定する必要があります。", + "unrestore_app": "{app}は復元されません", + "updating_apt_cache": "システムパッケージの利用可能なアップグレードを取得しています...", + "upgrade_complete": "アップグレート完了", + "upgrading_packages": "パッケージをアップグレードしています...", + "upnp_dev_not_found": "UPnP デバイスが見つかりません", + "upnp_disabled": "UPnP がオフになりました", + "upnp_enabled": "UPnP がオンになりました", + "upnp_port_open_failed": "UPnP 経由でポートを開けませんでした", + "user_already_exists": "ユーザー '{user}' は既に存在します", + "user_created": "ユーザーが作成されました。", + "user_creation_failed": "ユーザー {user}を作成できませんでした: {error}", + "user_deleted": "ユーザーが削除されました", + "user_deletion_failed": "ユーザー {user}を削除できませんでした: {error}", + "user_home_creation_failed": "ユーザーのホームフォルダ '{home}' を作成できませんでした", + "user_import_bad_file": "CSVファイルが正しくフォーマットされていないため、データ損失の可能性を回避するために無視されます", + "user_import_bad_line": "行{line}が正しくありません: {details}", + "user_import_failed": "ユーザーのインポート操作が完全に失敗しました", + "user_import_missing_columns": "次の列がありません: {columns}", + "user_import_nothing_to_do": "インポートする必要があるユーザーはいません", + "user_import_partial_failed": "ユーザーのインポート操作が部分的に失敗しました", + "user_import_success": "ユーザーが正常にインポートされました", + "user_unknown": "不明なユーザー: {user}", + "user_update_failed": "ユーザー {user}を更新できませんでした: {error}", + "user_updated": "ユーザー情報が変更されました", + "visitors": "訪問者", + "yunohost_already_installed": "YunoHostはすでにインストールされています", + "yunohost_configured": "YunoHost が構成されました", + "yunohost_installing": "YunoHostをインストールしています...", + "yunohost_not_installed": "YunoHostが正しくインストールされていません。’yunohost tools postinstall’ を実行してください", + "yunohost_postinstall_end_tip": "インストール後処理が完了しました!セットアップを完了するには、次の点を考慮してください。\n - ウェブ管理画面の「診断」セクション(またはコマンドラインで’yunohost diagnosis run’)を通じて潜在的な問題を診断します。\n - 管理ドキュメントの「セットアップの最終処理」と「YunoHostを知る」の部分を読む: https://yunohost.org/admindoc。", + "additional_urls_already_removed": "アクセス許可 ‘{permission}’ に対する追加URLで ‘{url}’ は既に削除されています" } From 0d0740826d104ec71f544587ba51a2fe9a2b8157 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 17:43:31 +0200 Subject: [PATCH 0158/1116] Revert "apps: fix version.parse now refusing to parse legacy version numbers" This reverts commit b98ac21a0663b5e1078d7505deb51d114b32e5c5. --- src/app.py | 65 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/src/app.py b/src/app.py index 64bb8c530..cce0aa51c 100644 --- a/src/app.py +++ b/src/app.py @@ -241,8 +241,8 @@ def _app_upgradable(app_infos): # Determine upgradability app_in_catalog = app_infos.get("from_catalog") - installed_version = _parse_app_version(app_infos.get("version", "0~ynh0")) - version_in_catalog = _parse_app_version( + installed_version = version.parse(app_infos.get("version", "0~ynh0")) + version_in_catalog = version.parse( app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0") ) @@ -257,7 +257,25 @@ def _app_upgradable(app_infos): ): return "bad_quality" - if installed_version < version_in_catalog: + # If the app uses the standard version scheme, use it to determine + # upgradability + if "~ynh" in str(installed_version) and "~ynh" in str(version_in_catalog): + if installed_version < version_in_catalog: + return "yes" + else: + return "no" + + # Legacy stuff for app with old / non-standard version numbers... + + # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded + if not app_infos["from_catalog"].get("lastUpdate") or not app_infos[ + "from_catalog" + ].get("git"): + return "url_required" + + settings = app_infos["settings"] + local_update_time = settings.get("update_time", settings.get("install_time", 0)) + if app_infos["from_catalog"]["lastUpdate"] > local_update_time: return "yes" else: return "no" @@ -602,11 +620,9 @@ def app_upgrade( # Manage upgrade type and avoid any upgrade if there is nothing to do upgrade_type = "UNKNOWN" # Get current_version and new version - app_new_version_raw = manifest.get("version", "?") - app_current_version_raw = app_dict.get("version", "?") - app_new_version = _parse_app_version(app_new_version_raw) - app_current_version = _parse_app_version(app_current_version_raw) - if "~ynh" in str(app_current_version_raw) and "~ynh" in str(app_new_version_raw): + app_new_version = version.parse(manifest.get("version", "?")) + app_current_version = version.parse(app_dict.get("version", "?")) + if "~ynh" in str(app_current_version) and "~ynh" in str(app_new_version): if app_current_version >= app_new_version and not force: # In case of upgrade from file or custom repository # No new version available @@ -626,10 +642,10 @@ def app_upgrade( upgrade_type = "UPGRADE_FORCED" else: app_current_version_upstream, app_current_version_pkg = str( - app_current_version_raw + app_current_version ).split("~ynh") app_new_version_upstream, app_new_version_pkg = str( - app_new_version_raw + app_new_version ).split("~ynh") if app_current_version_upstream == app_new_version_upstream: upgrade_type = "UPGRADE_PACKAGE" @@ -659,7 +675,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["PRE_UPGRADE"], - current_version=app_current_version_raw, + current_version=app_current_version, data=settings, ) _display_notifications(notifications, force=force) @@ -716,8 +732,8 @@ def app_upgrade( env_dict_more = { "YNH_APP_UPGRADE_TYPE": upgrade_type, - "YNH_APP_MANIFEST_VERSION": str(app_new_version_raw), - "YNH_APP_CURRENT_VERSION": str(app_current_version_raw), + "YNH_APP_MANIFEST_VERSION": str(app_new_version), + "YNH_APP_CURRENT_VERSION": str(app_current_version), } if manifest["packaging_format"] < 2: @@ -900,7 +916,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["POST_UPGRADE"], - current_version=app_current_version_raw, + current_version=app_current_version, data=settings, ) if Moulinette.interface.type == "cli": @@ -2038,20 +2054,6 @@ def _set_app_settings(app, settings): yaml.safe_dump(settings, f, default_flow_style=False) -def _parse_app_version(v): - - if v == "?": - return (0,0) - - try: - if "~" in v: - return (version.parse(v.split("~")[0]), int(v.split("~")[1].replace("ynh", ""))) - else: - return (version.parse(v), 0) - except Exception as e: - raise YunohostError(f"Failed to parse app version '{v}' : {e}", raw_msg=True) - - def _get_manifest_of_app(path): "Get app manifest stored in json or in toml" @@ -3156,7 +3158,12 @@ def _notification_is_dismissed(name, settings): def _filter_and_hydrate_notifications(notifications, current_version=None, data={}): def is_version_more_recent_than_current_version(name, current_version): current_version = str(current_version) - return _parse_app_version(name) > _parse_app_version(current_version) + # Boring code to handle the fact that "0.1 < 9999~ynh1" is False + + if "~" in name: + return version.parse(name) > version.parse(current_version) + else: + return version.parse(name) > version.parse(current_version.split("~")[0]) return { # Should we render the markdown maybe? idk From af93524c362abf67e23b81457215157081fd964d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 17:55:21 +0200 Subject: [PATCH 0159/1116] regenconf: fix a stupid bug using chown instead of chmod ... --- hooks/conf_regen/43-dnsmasq | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hooks/conf_regen/43-dnsmasq b/hooks/conf_regen/43-dnsmasq index 648a128c2..90e3ed2d7 100755 --- a/hooks/conf_regen/43-dnsmasq +++ b/hooks/conf_regen/43-dnsmasq @@ -62,7 +62,8 @@ do_post_regen() { regen_conf_files=$1 # Force permission (to cover some edge cases where root's umask is like 027 and then dnsmasq cant read this file) - chown 644 /etc/resolv.dnsmasq.conf + chown root /etc/resolv.dnsmasq.conf + chmod 644 /etc/resolv.dnsmasq.conf # Fuck it, those domain/search entries from dhclient are usually annoying # lying shit from the ISP trying to MiTM From 5b726bb8c00a0eb4d463eb803595495d6015b9dc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 17:56:33 +0200 Subject: [PATCH 0160/1116] Update changelog for 11.1.22 --- debian/changelog | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/debian/changelog b/debian/changelog index 2c33e3917..428d02b05 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,21 @@ +yunohost (11.1.22) stable; urgency=low + + - security: replace $http_host by $host in nginx conf, cf https://github.com/yandex/gixy/blob/master/docs/en/plugins/hostspoofing.md / Credit to A.Wolski (3957b10e) + - security: keep fail2ban rule when reloading firewall ([#1661](https://github.com/yunohost/yunohost/pull/1661)) + - regenconf: fix a stupid bug using chown instead of chmod ... (af93524c) + - postinstall: crash early if the username already exists on the system (e87ee09b) + - diagnosis: Support multiple TXT entries for TLD ([#1680](https://github.com/yunohost/yunohost/pull/1680)) + - apps: Support gitea's URL format ([#1683](https://github.com/yunohost/yunohost/pull/1683)) + - apps: fix a bug where YunoHost would complain that 'it needs X RAM but only Y left' with Y > X because some apps have a higher runtime RAM requirement than build time ... (4152cb0d) + - apps: Enhance app_shell() : prevent from taking the lock + improve php context with a 'phpflags' setting ([#1681](https://github.com/yunohost/yunohost/pull/1681)) + - apps resources: Allow passing an actual list in the manifest.toml for the apt resource packages ([#1670](https://github.com/yunohost/yunohost/pull/1670)) + - apps resources: fix a bug where port automigration between v1->v2 wouldnt work (36a17dfd) + - i18n: Translations updated for Basque, Galician, Japanese, Polish + + Thanks to all contributors <3 ! (Félix Piédallu, Grzegorz Cichocki, José M, Kayou, motcha, Nicolas Palix, orhtej2, tituspijean, xabirequejo, Yann Autissier) + + -- Alexandre Aubin Mon, 10 Jul 2023 17:43:56 +0200 + yunohost (11.1.21.4) stable; urgency=low - regenconf: Get rid of previous tmp hack about /dev/null for people that went through the very first 11.1.21, because it's causing issue in unpriviledged LXC or similar context (8242cab7) From 739e02eaf8ce99d6cd834aa1cd9a934a7ffa5ce0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 18:22:48 +0200 Subject: [PATCH 0161/1116] Typo/wording --- share/actionsmap.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 689c3da86..e64045367 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1596,7 +1596,7 @@ dyndns: full: --recovery-password nargs: "?" const: 0 - help: Password used to later delete the domain + help: Password used to later recover the domain if needed extra: pattern: *pattern_password @@ -1687,14 +1687,14 @@ tools: pattern: *pattern_password required: True comment: good_practices_about_admin_password - --ingnore-dyndns: + --ignore-dyndns: help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true --dyndns-recovery-password: metavar: PASSWORD nargs: "?" const: 0 - help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain + help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later recover the domain if needed extra: pattern: *pattern_password --force-diskspace: From 4a1b7c30ba68346225d80059a12f601ea9d379d1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 18:46:36 +0200 Subject: [PATCH 0162/1116] dyndns update is not deprecated because 'dns push' is not ready for dyndns ... --- share/actionsmap.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 97d2b5387..3624a9011 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1610,8 +1610,7 @@ dyndns: ### dyndns_update() update: - action_help: Update IP on DynDNS platform ( deprecated, use 'yunohost domain dns push DOMAIN' instead ) - deprecated: true + action_help: Update IP on DynDNS platform arguments: -d: full: --domain From 14040b8fd2ee18e93e051202a8dd3ee3cc9b8fe2 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Mon, 10 Jul 2023 17:05:52 +0000 Subject: [PATCH 0163/1116] [CI] Format code with Black --- src/app.py | 9 ++++++--- src/diagnosers/12-dnsrecords.py | 2 +- src/tests/test_apps.py | 4 +++- src/tests/test_appurl.py | 10 +++++++--- src/utils/resources.py | 22 +++++++++++++--------- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/app.py b/src/app.py index cce0aa51c..a77cf51b8 100644 --- a/src/app.py +++ b/src/app.py @@ -2784,9 +2784,12 @@ def _check_manifest_requirements( # Some apps have a higher runtime value than build ... if ram_requirement["build"] != "?" and ram_requirement["runtime"] != "?": - max_build_runtime = (ram_requirement["build"] - if human_to_binary(ram_requirement["build"]) > human_to_binary(ram_requirement["runtime"]) - else ram_requirement["runtime"]) + max_build_runtime = ( + ram_requirement["build"] + if human_to_binary(ram_requirement["build"]) + > human_to_binary(ram_requirement["runtime"]) + else ram_requirement["runtime"] + ) else: max_build_runtime = ram_requirement["build"] diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py index be9bf5418..196a2e1f9 100644 --- a/src/diagnosers/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -182,7 +182,7 @@ class MyDiagnoser(Diagnoser): if success != "ok": return None else: - if type_ == "TXT" and isinstance(answers,list): + if type_ == "TXT" and isinstance(answers, list): for part in answers: if part.startswith('"v=spf1'): return part diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index e6e1342ba..d7a591a36 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -339,7 +339,9 @@ def test_app_from_catalog(): assert app_map_[main_domain]["/site"]["id"] == "my_webapp" assert app_is_installed(main_domain, "my_webapp") - assert app_is_exposed_on_http(main_domain, "/site", "you have just installed My Webapp") + assert app_is_exposed_on_http( + main_domain, "/site", "you have just installed My Webapp" + ) # Try upgrade, should do nothing app_upgrade("my_webapp") diff --git a/src/tests/test_appurl.py b/src/tests/test_appurl.py index 996a5a2c3..d0c55f732 100644 --- a/src/tests/test_appurl.py +++ b/src/tests/test_appurl.py @@ -71,10 +71,14 @@ def test_repo_url_definition(): ### Gitea assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh") - assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh/src/branch/branch_name") + assert _is_app_repo_url( + "https://gitea.instance.tld/user/repo_ynh/src/branch/branch_name" + ) assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh/src/tag/tag_name") - assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh/src/commit/abcd1234") - + assert _is_app_repo_url( + "https://gitea.instance.tld/user/repo_ynh/src/commit/abcd1234" + ) + ### Invalid patterns # no schema diff --git a/src/utils/resources.py b/src/utils/resources.py index 7f6f263de..8d33c3bac 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1065,9 +1065,11 @@ class AptDependenciesAppResource(AppResource): if isinstance(values.get("packages"), str): values["packages"] = [value.strip() for value in values["packages"].split(",")] # type: ignore - if not isinstance(values.get("repo"), str) \ - or not isinstance(values.get("key"), str) \ - or not isinstance(values.get("packages"), list): + if ( + not isinstance(values.get("repo"), str) + or not isinstance(values.get("key"), str) + or not isinstance(values.get("packages"), list) + ): raise YunohostError( "In apt resource in the manifest: 'extras' repo should have the keys 'repo', 'key' defined as strings and 'packages' defined as list", raw_msg=True, @@ -1076,12 +1078,14 @@ class AptDependenciesAppResource(AppResource): def provision_or_update(self, context: Dict = {}): script = " ".join(["ynh_install_app_dependencies", *self.packages]) for repo, values in self.extras.items(): - script += "\n" + " ".join([ - "ynh_install_extra_app_dependencies", - f"--repo='{values['repo']}'", - f"--key='{values['key']}'", - f"--package='{' '.join(values['packages'])}'" - ]) + script += "\n" + " ".join( + [ + "ynh_install_extra_app_dependencies", + f"--repo='{values['repo']}'", + f"--key='{values['key']}'", + f"--package='{' '.join(values['packages'])}'", + ] + ) # FIXME : we're feeding the raw value of values['packages'] to the helper .. if we want to be consistent, may they should be comma-separated, though in the majority of cases, only a single package is installed from an extra repo.. self._run_script("provision_or_update", script) From c0c0fcaf54520459cbe7517ca0cfa0a67dfe2e33 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 19:10:54 +0200 Subject: [PATCH 0164/1116] ocsp stapling: Use 1.1.1.1 and 9.9.9.9 instead of 8.8.8.8 --- conf/nginx/server.tpl.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index 183cce8b8..5103e9081 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -54,7 +54,7 @@ server { ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - resolver 8.8.8.8 valid=300s; + resolver 1.1.1.1 9.9.9.9 valid=300s; resolver_timeout 5s; {% endif %} @@ -110,7 +110,7 @@ server { ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - resolver 8.8.8.8 valid=300s; + resolver 1.1.1.1 9.9.9.9 valid=300s; resolver_timeout 5s; {% endif %} From 432a9ab544800782dcdaa0bef9ae84480c7d77ba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 19:19:28 +0200 Subject: [PATCH 0165/1116] regenconf/ssh: disable Banner by default --- conf/ssh/sshd_config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/ssh/sshd_config b/conf/ssh/sshd_config index eaa0c7380..4a239d2ad 100644 --- a/conf/ssh/sshd_config +++ b/conf/ssh/sshd_config @@ -64,7 +64,7 @@ PasswordAuthentication no {% endif %} # Post-login stuff -Banner /etc/issue.net +# Banner none PrintMotd no PrintLastLog yes ClientAliveInterval 60 From 1927875924b16b08f8f850142a5e17c0f08b3bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Th=C3=A9o=20LAURET?= <118362885+eldertek@users.noreply.github.com> Date: Mon, 10 Jul 2023 21:28:22 +0400 Subject: [PATCH 0166/1116] [fix/enh] Rewrite of yunopaste CLI tool (#1667) * rewrite python * Modify to pipe * alexAubin review * Fix "output" var not existing ... * yunopaste: anonymize_output is too harsh and not yunopaste's job + print_usage ain't called ... * yunopaste: return link to the raw version, less confusing than haste's ui ... --------- Co-authored-by: Alexandre Aubin --- bin/yunopaste | 93 ++++++++++++++------------------------------------- 1 file changed, 25 insertions(+), 68 deletions(-) diff --git a/bin/yunopaste b/bin/yunopaste index edf8d55c8..f6bdecae2 100755 --- a/bin/yunopaste +++ b/bin/yunopaste @@ -1,77 +1,34 @@ -#!/bin/bash +#!/usr/bin/env python3 -set -e -set -u +import sys +import requests +import json -PASTE_URL="https://paste.yunohost.org" +SERVER_URL = "https://paste.yunohost.org" +TIMEOUT = 3 -_die() { - printf "Error: %s\n" "$*" - exit 1 -} +def create_snippet(data): + try: + url = SERVER_URL + "/documents" + response = requests.post(url, data=data.encode('utf-8'), timeout=TIMEOUT) + response.raise_for_status() + dockey = json.loads(response.text)['key'] + return SERVER_URL + "/raw/" + dockey + except requests.exceptions.RequestException as e: + print("\033[31mError: {}\033[0m".format(e)) + sys.exit(1) -check_dependencies() { - curl -V > /dev/null 2>&1 || _die "This script requires curl." -} -paste_data() { - json=$(curl -X POST -s -d "$1" "${PASTE_URL}/documents") - [[ -z "$json" ]] && _die "Unable to post the data to the server." +def main(): + output = sys.stdin.read() - key=$(echo "$json" \ - | python3 -c 'import json,sys;o=json.load(sys.stdin);print(o["key"])' \ - 2>/dev/null) - [[ -z "$key" ]] && _die "Unable to parse the server response." + if not output: + print("\033[31mError: No input received from stdin.\033[0m") + sys.exit(1) - echo "${PASTE_URL}/${key}" -} + url = create_snippet(output) -usage() { - printf "Usage: ${0} [OPTION]... + print("\033[32mURL: {}\033[0m".format(url)) -Read from input stream and paste the data to the YunoHost -Haste server. - -For example, to paste the output of the YunoHost diagnosis, you -can simply execute the following: - yunohost diagnosis show | ${0} - -It will return the URL where you can access the pasted data. - -Options: - -h, --help show this help message and exit -" -} - -main() { - # parse options - while (( ${#} )); do - case "${1}" in - --help|-h) - usage - exit 0 - ;; - *) - echo "Unknown parameter detected: ${1}" >&2 - echo >&2 - usage >&2 - exit 1 - ;; - esac - - shift 1 - done - - # check input stream - read -t 0 || { - echo -e "Invalid usage: No input is provided.\n" >&2 - usage - exit 1 - } - - paste_data "$(cat)" -} - -check_dependencies - -main "${@}" +if __name__ == "__main__": + main() From dfc51ed7c525c61bd0a352002f0d8609da4a0c46 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 19:29:34 +0200 Subject: [PATCH 0167/1116] Revert "[fix/enh] Rewrite of yunopaste CLI tool (#1667)" This reverts commit 1927875924b16b08f8f850142a5e17c0f08b3bc3. --- bin/yunopaste | 93 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 25 deletions(-) diff --git a/bin/yunopaste b/bin/yunopaste index f6bdecae2..edf8d55c8 100755 --- a/bin/yunopaste +++ b/bin/yunopaste @@ -1,34 +1,77 @@ -#!/usr/bin/env python3 +#!/bin/bash -import sys -import requests -import json +set -e +set -u -SERVER_URL = "https://paste.yunohost.org" -TIMEOUT = 3 +PASTE_URL="https://paste.yunohost.org" -def create_snippet(data): - try: - url = SERVER_URL + "/documents" - response = requests.post(url, data=data.encode('utf-8'), timeout=TIMEOUT) - response.raise_for_status() - dockey = json.loads(response.text)['key'] - return SERVER_URL + "/raw/" + dockey - except requests.exceptions.RequestException as e: - print("\033[31mError: {}\033[0m".format(e)) - sys.exit(1) +_die() { + printf "Error: %s\n" "$*" + exit 1 +} +check_dependencies() { + curl -V > /dev/null 2>&1 || _die "This script requires curl." +} -def main(): - output = sys.stdin.read() +paste_data() { + json=$(curl -X POST -s -d "$1" "${PASTE_URL}/documents") + [[ -z "$json" ]] && _die "Unable to post the data to the server." - if not output: - print("\033[31mError: No input received from stdin.\033[0m") - sys.exit(1) + key=$(echo "$json" \ + | python3 -c 'import json,sys;o=json.load(sys.stdin);print(o["key"])' \ + 2>/dev/null) + [[ -z "$key" ]] && _die "Unable to parse the server response." - url = create_snippet(output) + echo "${PASTE_URL}/${key}" +} - print("\033[32mURL: {}\033[0m".format(url)) +usage() { + printf "Usage: ${0} [OPTION]... -if __name__ == "__main__": - main() +Read from input stream and paste the data to the YunoHost +Haste server. + +For example, to paste the output of the YunoHost diagnosis, you +can simply execute the following: + yunohost diagnosis show | ${0} + +It will return the URL where you can access the pasted data. + +Options: + -h, --help show this help message and exit +" +} + +main() { + # parse options + while (( ${#} )); do + case "${1}" in + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown parameter detected: ${1}" >&2 + echo >&2 + usage >&2 + exit 1 + ;; + esac + + shift 1 + done + + # check input stream + read -t 0 || { + echo -e "Invalid usage: No input is provided.\n" >&2 + usage + exit 1 + } + + paste_data "$(cat)" +} + +check_dependencies + +main "${@}" From ba2159de7358c3b36d30472c8742d932dcf5191d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Th=C3=A9o=20LAURET?= <118362885+eldertek@users.noreply.github.com> Date: Mon, 10 Jul 2023 21:28:22 +0400 Subject: [PATCH 0168/1116] [fix/enh] Rewrite of yunopaste CLI tool (#1667) * rewrite python * Modify to pipe * alexAubin review * Fix "output" var not existing ... * yunopaste: anonymize_output is too harsh and not yunopaste's job + print_usage ain't called ... * yunopaste: return link to the raw version, less confusing than haste's ui ... --------- Co-authored-by: Alexandre Aubin --- bin/yunopaste | 93 ++++++++++++++------------------------------------- 1 file changed, 25 insertions(+), 68 deletions(-) diff --git a/bin/yunopaste b/bin/yunopaste index edf8d55c8..f6bdecae2 100755 --- a/bin/yunopaste +++ b/bin/yunopaste @@ -1,77 +1,34 @@ -#!/bin/bash +#!/usr/bin/env python3 -set -e -set -u +import sys +import requests +import json -PASTE_URL="https://paste.yunohost.org" +SERVER_URL = "https://paste.yunohost.org" +TIMEOUT = 3 -_die() { - printf "Error: %s\n" "$*" - exit 1 -} +def create_snippet(data): + try: + url = SERVER_URL + "/documents" + response = requests.post(url, data=data.encode('utf-8'), timeout=TIMEOUT) + response.raise_for_status() + dockey = json.loads(response.text)['key'] + return SERVER_URL + "/raw/" + dockey + except requests.exceptions.RequestException as e: + print("\033[31mError: {}\033[0m".format(e)) + sys.exit(1) -check_dependencies() { - curl -V > /dev/null 2>&1 || _die "This script requires curl." -} -paste_data() { - json=$(curl -X POST -s -d "$1" "${PASTE_URL}/documents") - [[ -z "$json" ]] && _die "Unable to post the data to the server." +def main(): + output = sys.stdin.read() - key=$(echo "$json" \ - | python3 -c 'import json,sys;o=json.load(sys.stdin);print(o["key"])' \ - 2>/dev/null) - [[ -z "$key" ]] && _die "Unable to parse the server response." + if not output: + print("\033[31mError: No input received from stdin.\033[0m") + sys.exit(1) - echo "${PASTE_URL}/${key}" -} + url = create_snippet(output) -usage() { - printf "Usage: ${0} [OPTION]... + print("\033[32mURL: {}\033[0m".format(url)) -Read from input stream and paste the data to the YunoHost -Haste server. - -For example, to paste the output of the YunoHost diagnosis, you -can simply execute the following: - yunohost diagnosis show | ${0} - -It will return the URL where you can access the pasted data. - -Options: - -h, --help show this help message and exit -" -} - -main() { - # parse options - while (( ${#} )); do - case "${1}" in - --help|-h) - usage - exit 0 - ;; - *) - echo "Unknown parameter detected: ${1}" >&2 - echo >&2 - usage >&2 - exit 1 - ;; - esac - - shift 1 - done - - # check input stream - read -t 0 || { - echo -e "Invalid usage: No input is provided.\n" >&2 - usage - exit 1 - } - - paste_data "$(cat)" -} - -check_dependencies - -main "${@}" +if __name__ == "__main__": + main() From 81f269fc29ccd68fc3a79c482386e6ce9d7363e7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2023 19:35:17 +0200 Subject: [PATCH 0169/1116] Fix funky no_unsubscribe dyndns stuff in test_domains.py ... --- src/tests/test_domains.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index b7625ff7c..c5c1ab7ae 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -4,7 +4,6 @@ import time import random from moulinette.core import MoulinetteError -from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.domain import ( @@ -41,7 +40,7 @@ def setup_function(function): for domain in domains: if (domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]) and domain != TEST_DYNDNS_DOMAIN: # Clean domains not used for testing - domain_remove(domain, no_unsubscribe=is_yunohost_dyndns_domain(domain)) + domain_remove(domain) elif domain in TEST_DOMAINS: # Reset settings if any os.system(f"rm -rf {DOMAIN_SETTINGS_DIR}/{domain}.yml") From 7c1c147a74e5592f5e312419b0594bb477f18f9c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2023 15:46:35 +0200 Subject: [PATCH 0170/1116] quality: we don't really care about linter for the tests/ folder ... --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 49c78959d..c38df434b 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ deps = py39-black-{run,check}: black py39-mypy: mypy >= 0.900 commands = - py39-lint: flake8 src doc maintenance tests --ignore E402,E501,E203,W503,E741 --exclude src/vendor + py39-lint: flake8 src doc maintenance tests --ignore E402,E501,E203,W503,E741 --exclude src/tests,src/vendor py39-invalidcode: flake8 src bin maintenance --exclude src/tests,src/vendor --select F,E722,W605 py39-black-check: black --check --diff bin src doc maintenance tests py39-black-run: black bin src doc maintenance tests From e695c89ad05c8fdf618ef5c761d93dc25805d377 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2023 15:51:19 +0200 Subject: [PATCH 0171/1116] Typo in i18n key --- src/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index dca4e9c77..c3fa80d3a 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -237,7 +237,7 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): timeout=30, ) except Exception as e: - raise YunohostError("dyndns_unregistration_failed", error=str(e)) + raise YunohostError("dyndns_unsubscribe_failed", error=str(e)) if r.status_code == 200: # Deletion was successful for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key"): From 2f2ff6eb190cd9a825e88d91b3101f2e3fba6c96 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2023 15:58:59 +0200 Subject: [PATCH 0172/1116] Simplify fpm add config helper (Bookworm) (#1685) * Simplify ynh_add_fpm_config helper * helpers: drop dedicated_service option in ynh_add_fpm_config --- helpers/php | 97 ++++++----------------------------------------------- 1 file changed, 11 insertions(+), 86 deletions(-) diff --git a/helpers/php b/helpers/php index 5cfe521f1..21fb20e27 100644 --- a/helpers/php +++ b/helpers/php @@ -70,12 +70,11 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} ynh_add_fpm_config() { local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. - local legacy_args=vufd - local -A args_array=([v]=phpversion= [u]=usage= [f]=footprint= [d]=dedicated_service) + local legacy_args=vuf + local -A args_array=([v]=phpversion= [u]=usage= [f]=footprint=) local phpversion local usage local footprint - local dedicated_service # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -103,8 +102,6 @@ ynh_add_fpm_config() { fi fi - # Do not use a dedicated service by default - dedicated_service=${dedicated_service:-0} # Set the default PHP-FPM version by default if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then @@ -128,38 +125,16 @@ ynh_add_fpm_config() { fi fi - if [ $dedicated_service -eq 1 ]; then - ynh_print_warn --message "Argument --dedicated_service of ynh_add_fpm_config is deprecated and to be removed in the future" - local fpm_service="${app}-phpfpm" - local fpm_config_dir="/etc/php/$phpversion/dedicated-fpm" - else - local fpm_service="php${phpversion}-fpm" - local fpm_config_dir="/etc/php/$phpversion/fpm" - fi + local fpm_service="php${phpversion}-fpm" + local fpm_config_dir="/etc/php/$phpversion/fpm" # Create the directory for FPM pools mkdir --parents "$fpm_config_dir/pool.d" ynh_app_setting_set --app=$app --key=fpm_config_dir --value="$fpm_config_dir" ynh_app_setting_set --app=$app --key=fpm_service --value="$fpm_service" - ynh_app_setting_set --app=$app --key=fpm_dedicated_service --value="$dedicated_service" ynh_app_setting_set --app=$app --key=phpversion --value=$phpversion - # Migrate from mutual PHP service to dedicated one. - if [ $dedicated_service -eq 1 ]; then - local old_fpm_config_dir="/etc/php/$phpversion/fpm" - # If a config file exist in the common pool, move it. - if [ -e "$old_fpm_config_dir/pool.d/$app.conf" ]; then - ynh_print_info --message="Migrate to a dedicated php-fpm service for $app." - # Create a backup of the old file before migration - ynh_backup_if_checksum_is_different --file="$old_fpm_config_dir/pool.d/$app.conf" - # Remove the old PHP config file - ynh_secure_remove --file="$old_fpm_config_dir/pool.d/$app.conf" - # Reload PHP to release the socket and allow the dedicated service to use it - ynh_systemd_action --service_name=php${phpversion}-fpm --action=reload - fi - fi - if [ $autogenconf == "false" ]; then # Usage 1, use the template in conf/php-fpm.conf local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf" @@ -212,51 +187,13 @@ pm.process_idle_timeout = 10s local finalphpconf="$fpm_config_dir/pool.d/$app.conf" ynh_add_config --template="$phpfpm_path" --destination="$finalphpconf" - if [ $dedicated_service -eq 1 ]; then - # Create a dedicated php-fpm.conf for the service - local globalphpconf=$fpm_config_dir/php-fpm-$app.conf - - echo "[global] -pid = /run/php/php__PHPVERSION__-fpm-__APP__.pid -error_log = /var/log/php/fpm-php.__APP__.log -syslog.ident = php-fpm-__APP__ -include = __FINALPHPCONF__ -" >$YNH_APP_BASEDIR/conf/php-fpm-$app.conf - - ynh_add_config --template="php-fpm-$app.conf" --destination="$globalphpconf" - - # Create a config for a dedicated PHP-FPM service for the app - echo "[Unit] -Description=PHP __PHPVERSION__ FastCGI Process Manager for __APP__ -After=network.target - -[Service] -Type=notify -PIDFile=/run/php/php__PHPVERSION__-fpm-__APP__.pid -ExecStart=/usr/sbin/php-fpm__PHPVERSION__ --nodaemonize --fpm-config __GLOBALPHPCONF__ -ExecReload=/bin/kill -USR2 \$MAINPID - -[Install] -WantedBy=multi-user.target -" >$YNH_APP_BASEDIR/conf/$fpm_service - - # Create this dedicated PHP-FPM service - ynh_add_systemd_config --service=$fpm_service --template=$fpm_service - # Integrate the service in YunoHost admin panel - yunohost service add $fpm_service --log /var/log/php/fpm-php.$app.log --description "Php-fpm dedicated to $app" - # Configure log rotate - ynh_use_logrotate --logfile=/var/log/php - # Restart the service, as this service is either stopped or only for this app - ynh_systemd_action --service_name=$fpm_service --action=restart - else - # Validate that the new php conf doesn't break php-fpm entirely - if ! php-fpm${phpversion} --test 2>/dev/null; then - php-fpm${phpversion} --test || true - ynh_secure_remove --file="$finalphpconf" - ynh_die --message="The new configuration broke php-fpm?" - fi - ynh_systemd_action --service_name=$fpm_service --action=reload + # Validate that the new php conf doesn't break php-fpm entirely + if ! php-fpm${phpversion} --test 2>/dev/null; then + php-fpm${phpversion} --test || true + ynh_secure_remove --file="$finalphpconf" + ynh_die --message="The new configuration broke php-fpm?" fi + ynh_systemd_action --service_name=$fpm_service --action=reload } # Remove the dedicated PHP-FPM config @@ -267,8 +204,6 @@ WantedBy=multi-user.target ynh_remove_fpm_config() { local fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir) local fpm_service=$(ynh_app_setting_get --app=$app --key=fpm_service) - local dedicated_service=$(ynh_app_setting_get --app=$app --key=fpm_dedicated_service) - dedicated_service=${dedicated_service:-0} # Get the version of PHP used by this app local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) @@ -282,17 +217,7 @@ ynh_remove_fpm_config() { fi ynh_secure_remove --file="$fpm_config_dir/pool.d/$app.conf" - - if [ $dedicated_service -eq 1 ]; then - # Remove the dedicated service PHP-FPM service for the app - ynh_remove_systemd_config --service=$fpm_service - # Remove the global PHP-FPM conf - ynh_secure_remove --file="$fpm_config_dir/php-fpm-$app.conf" - # Remove the service from the list of services known by YunoHost - yunohost service remove $fpm_service - elif ynh_package_is_installed --package="php${phpversion}-fpm"; then - ynh_systemd_action --service_name=$fpm_service --action=reload - fi + ynh_systemd_action --service_name=$fpm_service --action=reload } # Define the values to configure PHP-FPM From 9a5080ea16b36e465587d4f0c9b2d531e9dfc6ce Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2023 17:49:25 +0200 Subject: [PATCH 0173/1116] portalapi: fix split or user/password in auth code --- src/authenticators/ldap_ynhuser.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index fe2a657f5..c8ba4ecf2 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -27,11 +27,10 @@ class Authenticator(BaseAuthenticator): def _authenticate_credentials(self, credentials=None): - # FIXME ':' should a legit char in the password ? shall we encode the password as base64 or something idk - if ":" not in credentials or len(credentials.split(":")) != 2: - raise YunohostError("invalid_credentials_format") - - username, password = credentials.split(":") + try: + username, password = credentials.split(":", 1) + except ValueError: + raise YunohostError("invalid_credentials") def _reconnect(): con = ldap.ldapobject.ReconnectLDAPObject( From f1200b81dc605a5320bfd5124b43c0ee02946d14 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2023 18:10:32 +0200 Subject: [PATCH 0174/1116] apt: always add yarn repo because it's annoying to have to deal with an extra repo in each nodejs app just to install a single package.. --- hooks/conf_regen/10-apt | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/hooks/conf_regen/10-apt b/hooks/conf_regen/10-apt index 93ff053b8..72c0773b9 100755 --- a/hooks/conf_regen/10-apt +++ b/hooks/conf_regen/10-apt @@ -23,19 +23,33 @@ Pin-Priority: 500" >>"${pending_dir}/etc/apt/preferences.d/extra_php_version" for package in $packages_to_refuse_from_sury; do echo " Package: $package -Pin: origin \"packages.sury.org\" +Pin: origin \"packages.sury.org\" Pin-Priority: -1" >>"${pending_dir}/etc/apt/preferences.d/extra_php_version" done + # Add yarn + echo "deb https://dl.yarnpkg.com/debian/ stable main" > "${pending_dir}/etc/apt/sources.list.d/yarn.list" + + # Ban everything from Yarn except Yarn + echo " +Package: * +Pin: origin \"dl.yarnpkg.com\" +Pin-Priority: -1 + +Package: yarn +Pin: origin \"dl.yarnpkg.com\" +Pin-Priority: 500" >>"${pending_dir}/etc/apt/preferences.d/yarn" + + # Ban apache2, bind9 echo " # PLEASE READ THIS WARNING AND DON'T EDIT THIS FILE -# You are probably reading this file because you tried to install apache2 or +# You are probably reading this file because you tried to install apache2 or # bind9. These 2 packages conflict with YunoHost. # Installing apache2 will break nginx and break the entire YunoHost ecosystem -# on your server, therefore don't remove those lines! +# on your server, therefore don't remove those lines! # You have been warned. @@ -69,6 +83,12 @@ do_post_regen() { wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/extra_php_version.gpg" fi + # Similar to Sury + if [[ ! -s /etc/apt/trusted.gpg.d/yarn.gpg ]] + then + wget --timeout 900 --quiet "https://dl.yarnpkg.com/debian/pubkey.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/yarn.gpg" + fi + # Make sure php7.4 is the default version when using php in cli if test -e /usr/bin/php$YNH_DEFAULT_PHP_VERSION then From 236e85eece3f4d6fa479c9d57de41710fc4c736c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2023 18:12:08 +0200 Subject: [PATCH 0175/1116] apt: add signed-by clause to sury and yarn repo --- hooks/conf_regen/10-apt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/conf_regen/10-apt b/hooks/conf_regen/10-apt index 72c0773b9..725fdde82 100755 --- a/hooks/conf_regen/10-apt +++ b/hooks/conf_regen/10-apt @@ -11,7 +11,7 @@ do_pre_regen() { # Add sury mkdir -p ${pending_dir}/etc/apt/sources.list.d/ - echo "deb https://packages.sury.org/php/ $(lsb_release --codename --short) main" > "${pending_dir}/etc/apt/sources.list.d/extra_php_version.list" + echo "deb [signed-by=/etc/apt/trusted.gpg.d/extra_php_version.gpg] https://packages.sury.org/php/ $(lsb_release --codename --short) main" > "${pending_dir}/etc/apt/sources.list.d/extra_php_version.list" # Ban some packages from sury echo " @@ -28,7 +28,7 @@ Pin-Priority: -1" >>"${pending_dir}/etc/apt/preferences.d/extra_php_version" done # Add yarn - echo "deb https://dl.yarnpkg.com/debian/ stable main" > "${pending_dir}/etc/apt/sources.list.d/yarn.list" + echo "deb [signed-by=/etc/apt/trusted.gpg.d/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > "${pending_dir}/etc/apt/sources.list.d/yarn.list" # Ban everything from Yarn except Yarn echo " From 6c6dd318fb8c5a31fe64fbeb78ed1d7304dd8a8c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2023 22:39:22 +0200 Subject: [PATCH 0176/1116] portalapi: implement encrypted password storage in the user's cookie using AES256 --- debian/control | 2 +- hooks/conf_regen/01-yunohost | 3 +- src/authenticators/ldap_ynhuser.py | 73 ++++++++++++++++++++++-------- src/portal.py | 13 ++---- 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/debian/control b/debian/control index 8880867a2..df9a6d2bd 100644 --- a/debian/control +++ b/debian/control @@ -15,7 +15,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , python3-miniupnpc, python3-dbus, python3-jinja2 , python3-toml, python3-packaging, python3-publicsuffix2 , python3-ldap, python3-zeroconf (>= 0.36), python3-lexicon, - , python3-jwt + , python3-cryptography, python3-jwt , python-is-python3 , nginx, nginx-extras (>=1.18) , apt, apt-transport-https, apt-utils, dirmngr diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 7bd835f8f..4d53997a5 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -177,7 +177,8 @@ do_post_regen() { getent passwd ynh-portal &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group ynh-portal if [ ! -e /etc/yunohost/.ssowat_cookie_secret ]; then - dd if=/dev/urandom bs=1 count=1000 2>/dev/null | tr --complement --delete 'A-Za-z0-9' | head -c 64 > /etc/yunohost/.ssowat_cookie_secret + # NB: we need this to be exactly 32 char long, because it is later used as a key for AES256 + dd if=/dev/urandom bs=1 count=1000 2>/dev/null | tr --complement --delete 'A-Za-z0-9' | head -c 32 > /etc/yunohost/.ssowat_cookie_secret fi chown ynh-portal:root /etc/yunohost/.ssowat_cookie_secret chmod 400 /etc/yunohost/.ssowat_cookie_secret diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index c8ba4ecf2..407277acf 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -5,6 +5,13 @@ import logging import ldap import ldap.sasl import datetime +import base64 +import os + +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.backends import default_backend + from moulinette import m18n from moulinette.authentication import BaseAuthenticator @@ -13,13 +20,52 @@ from yunohost.utils.error import YunohostError, YunohostAuthenticationError # FIXME : we shall generate this somewhere if it doesnt exists yet # FIXME : fix permissions -session_secret = open("/etc/yunohost/.ssowat_cookie_secret").read() +session_secret = open("/etc/yunohost/.ssowat_cookie_secret").read().strip() logger = logging.getLogger("yunohostportal.authenticators.ldap_ynhuser") URI = "ldap://localhost:389" USERDN = "uid={username},ou=users,dc=yunohost,dc=org" +# We want to save the password in the cookie, but we should do so in an encrypted fashion +# This is needed because the SSO later needs to possibly inject the Basic Auth header +# which includes the user's password +# It's also needed because we need to be able to open LDAP sessions, authenticated as the user, +# which requires the user's password +# +# To do so, we use AES-256-CBC. As it's a block encryption algorithm, it requires an IV, +# which we need to keep around for decryption on SSOwat'side. +# +# session_secret is used as the encryption key, which implies it must be exactly 32-char long (256/8) +# +# The result is a string formatted as | +# For example: ctl8kk5GevYdaA5VZ2S88Q==|yTAzCx0Gd1+MCit4EQl9lA== +def encrypt(data): + + alg = algorithms.AES(session_secret.encode()) + iv = os.urandom(int(alg.block_size / 8)) + + E = Cipher(alg, modes.CBC(iv), default_backend()).encryptor() + p = padding.PKCS7(alg.block_size).padder() + data_padded = p.update(data.encode()) + p.finalize() + data_enc = E.update(data_padded) + E.finalize() + data_enc_b64 = base64.b64encode(data_enc).decode() + iv_b64 = base64.b64encode(iv).decode() + return data_enc_b64 + "|" + iv_b64 + +def decrypt(data_enc_and_iv_b64): + + data_enc_b64, iv_b64 = data_enc_and_iv_b64.split("|") + data_enc = base64.b64decode(data_enc_b64) + iv = base64.b64decode(iv_b64) + + alg = algorithms.AES(session_secret.encode()) + D = Cipher(alg, modes.CBC(iv), default_backend()).decryptor() + p = padding.PKCS7(alg.block_size).unpadder() + data_padded = D.update(data_enc) + data = p.update(data_padded) + p.finalize() + return data.decode() + class Authenticator(BaseAuthenticator): @@ -64,23 +110,7 @@ class Authenticator(BaseAuthenticator): if con: con.unbind_s() - - - - - - - # FIXME FIXME FIXME : the password is to be encrypted to not expose it in the JWT cookie which is only signed and base64 encoded but not encrypted - - - - - - - - - - return {"user": username, "password": password} + return {"user": username, "pwd": encrypt(password)} def set_session_cookie(self, infos): @@ -101,7 +131,7 @@ class Authenticator(BaseAuthenticator): response.set_cookie( "yunohost.portal", - jwt.encode(new_infos, session_secret, algorithm="HS256").decode(), + jwt.encode(new_infos, session_secret, algorithm="HS256"), secure=True, httponly=True, path="/", @@ -109,7 +139,7 @@ class Authenticator(BaseAuthenticator): # FIXME : add Expire clause ) - def get_session_cookie(self, raise_if_no_session_exists=True): + def get_session_cookie(self, raise_if_no_session_exists=True, decrypt_pwd=False): from bottle import request @@ -127,6 +157,9 @@ class Authenticator(BaseAuthenticator): if "id" not in infos: infos["id"] = random_ascii() + if decrypt_pwd: + infos["pwd"] = decrypt(infos["pwd"]) + # FIXME: Here, maybe we want to re-authenticate the session via the authenticator # For example to check that the username authenticated is still in the admin group... diff --git a/src/portal.py b/src/portal.py index 2eaa59dd4..6aa7cba6d 100644 --- a/src/portal.py +++ b/src/portal.py @@ -32,24 +32,17 @@ logger = getActionLogger("yunohostportal.user") def portal_me(): """ Get user informations - - Keyword argument: - username -- Username to get informations - """ - import pdb; pdb.set_trace() - - auth = Auth().get_session_cookie() + auth = Auth().get_session_cookie(decrypt_pwd=True) username = auth["user"] - password = auth["password"] - ldap = LDAPInterface(username, password) + ldap = LDAPInterface(username, auth["pwd"]) user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"] filter = "uid=" + username - result = ldap.search("ou=users,dc=yunohost,dc=org", filter, user_attrs) + result = ldap.search("ou=users", filter, user_attrs) if result: user = result[0] From f3eef43d020c345b08e74406b7269c0b1e30fae8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2023 23:59:06 +0200 Subject: [PATCH 0177/1116] helpers: in ynh_systemd_action, check the actual timestamp when checking for timeout, because for some reason journalctl may take a ridiculous amount of time to run --- helpers/systemd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/helpers/systemd b/helpers/systemd index 761e818ad..765c575ef 100644 --- a/helpers/systemd +++ b/helpers/systemd @@ -128,6 +128,7 @@ ynh_systemd_action() { if [[ -n "${line_match:-}" ]]; then set +x local i=0 + local starttime=$(date +%s) for i in $(seq 1 $timeout); do # Read the log until the sentence is found, that means the app finished to start. Or run until the timeout if [ "$log_path" == "systemd" ]; then @@ -145,6 +146,14 @@ ynh_systemd_action() { if [ $i -eq 30 ]; then echo "(this may take some time)" >&2 fi + # Also check the timeout using actual timestamp, because sometimes for some reason, + # journalctl may take a huge time to run, and we end up waiting literally an entire hour + # instead of 5 min ... + if [[ "$(( $(date +%s) - $starttime))" -gt "$timeout" ]] + then + i=$timeout + break + fi sleep 1 done set -x From 2c0f49cef3fbf044e89ea5b43933c06dbbf8d956 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Jul 2023 04:44:03 +0200 Subject: [PATCH 0178/1116] portalapi: add groups and apps list in infos returned by GET /me --- src/portal.py | 65 ++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/src/portal.py b/src/portal.py index 6aa7cba6d..6a51f33d4 100644 --- a/src/portal.py +++ b/src/portal.py @@ -19,14 +19,14 @@ """ -# from moulinette import Moulinette, m18n from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_json from yunohost.authenticators.ldap_ynhuser import Authenticator as Auth from yunohost.utils.ldap import LDAPInterface from yunohost.utils.error import YunohostValidationError -logger = getActionLogger("yunohostportal.user") +logger = getActionLogger("portal") def portal_me(): @@ -39,48 +39,43 @@ def portal_me(): ldap = LDAPInterface(username, auth["pwd"]) - user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"] + user_attrs = ["cn", "mail", "maildrop", "mailuserquota", "memberOf", "permission"] - filter = "uid=" + username - result = ldap.search("ou=users", filter, user_attrs) + result = ldap.search("ou=users", f"uid={username}", user_attrs) if result: user = result[0] else: raise YunohostValidationError("user_unknown", user=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": [], + groups = [g.replace("cn=", "").replace(",ou=groups,dc=yunohost,dc=org", "") for g in user["memberOf"]] + groups = [g for g in groups if g not in [username, "all_users"]] + + permissions = [p.replace("cn=", "").replace(",ou=permission,dc=yunohost,dc=org", "") for p in user["permission"]] + + ssowat_conf = read_json("/etc/ssowat/conf.json") + apps = { + perm.replace(".main", ""): {"label": infos["label"], "url": infos["uris"][0]} + for perm, infos in ssowat_conf["permissions"].items() + if perm in permissions and infos["show_tile"] and username in infos["users"] } - if len(user["mail"]) > 1: - result_dict["mail-aliases"] = user["mail"][1:] + result_dict = { + "username": username, + "fullname": user["cn"][0], + "mail": user["mail"][0], + "mail-aliases": user["mail"][1:], + "mail-forward": user["maildrop"][1:], + "groups": groups, + "apps": apps + } - if len(user["maildrop"]) > 1: - result_dict["mail-forward"] = user["maildrop"][1:] - - if "mailuserquota" in user: - pass - # FIXME - # result_dict["mailbox-quota"] = { - # "limit": userquota if is_limited else m18n.n("unlimit"), - # "use": storage_use, - # } - - # FIXME : should also parse "permission" key in ldap maybe ? - # and list of groups / memberof ? - # (in particular to have e.g. the mail / xmpp / ssh / ... perms) + # FIXME / TODO : add mail quota status ? + # result_dict["mailbox-quota"] = { + # "limit": userquota if is_limited else m18n.n("unlimit"), + # "use": storage_use, + # } + # Could use : doveadm -c /dev/null -f flow quota recalc -u johndoe + # But this requires to be in the mail group ... return result_dict - - -def apps(username): - return {"foo": "bar"} - # FIXME: should list available apps and corresponding infos ? - # from /etc/ssowat/conf.json ? From d44b09cf12c13af3e46a8e7c1cc2a4e72be7dfa9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Jul 2023 15:20:13 +0200 Subject: [PATCH 0179/1116] quality: cleanup unused code --- src/app.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/app.py b/src/app.py index a77cf51b8..e5b1a1f6a 100644 --- a/src/app.py +++ b/src/app.py @@ -1964,31 +1964,6 @@ ynh_app_config_run $1 return values -def _get_app_actions(app_id): - "Get app config panel stored in json or in toml" - 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") - - if os.path.exists(actions_toml_path): - toml_actions = toml.load(open(actions_toml_path, "r"), _dict=OrderedDict) - - # transform toml format into json format - actions = [] - - for key, value in toml_actions.items(): - action = dict(**value) - action["id"] = key - action["arguments"] = value.get("arguments", {}) - actions.append(action) - - return actions - - elif os.path.exists(actions_json_path): - return json.load(open(actions_json_path)) - - return None - - def _get_app_settings(app): """ Get settings of an installed app From d2107278a70fe43379cd33b516d35d6c95da4b44 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Jul 2023 15:22:23 +0200 Subject: [PATCH 0180/1116] ci: for some reason the helper tests may be the reason the full-tests job sometimes crashes with no explanation ... let's try to keep it in a separate job to see if that indeed fixes the issue --- .gitlab/ci/test.gitlab-ci.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index b0ffd3db5..cded5bf7d 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -37,8 +37,6 @@ full-tests: - 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/ --junitxml=report.xml - - cd tests - - bash test_helpers.sh needs: - job: build-yunohost artifacts: true @@ -64,9 +62,9 @@ test-helpers: script: - cd tests - bash test_helpers.sh - only: - changes: - - helpers/* +# only: +# changes: +# - helpers/* test-domains: extends: .test-stage From 5e1d69a2cb09c8a125f6a6cce76a54c42eed9338 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Jul 2023 18:55:33 +0200 Subject: [PATCH 0181/1116] portalapi: harden systemd service configuration --- conf/yunohost/yunohost-portal-api.service | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/conf/yunohost/yunohost-portal-api.service b/conf/yunohost/yunohost-portal-api.service index 0ba6e8b3d..006af0080 100644 --- a/conf/yunohost/yunohost-portal-api.service +++ b/conf/yunohost/yunohost-portal-api.service @@ -4,11 +4,45 @@ After=network.target [Service] User=ynh-portal +Group=ynh-portal Type=simple ExecStart=/usr/bin/yunohost-portal-api Restart=always RestartSec=5 TimeoutStopSec=30 +# Sandboxing options to harden security +# Details for these options: https://www.freedesktop.org/software/systemd/man/systemd.exec.html +NoNewPrivileges=yes +PrivateTmp=yes +PrivateDevices=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +RestrictNamespaces=yes +RestrictRealtime=yes +DevicePolicy=closed +ProtectClock=yes +ProtectHostname=yes +ProtectProc=invisible +ProtectSystem=full +ProtectControlGroups=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +LockPersonality=yes +SystemCallArchitectures=native +SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap @cpu-emulation @privileged + +# Denying access to capabilities that should not be relevant +# Doc: https://man7.org/linux/man-pages/man7/capabilities.7.html +CapabilityBoundingSet=~CAP_RAWIO CAP_MKNOD +CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE +CapabilityBoundingSet=~CAP_SYS_BOOT CAP_SYS_TIME CAP_SYS_MODULE CAP_SYS_PACCT +CapabilityBoundingSet=~CAP_LEASE CAP_LINUX_IMMUTABLE CAP_IPC_LOCK +CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_WAKE_ALARM +CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG +CapabilityBoundingSet=~CAP_MAC_ADMIN CAP_MAC_OVERRIDE +CapabilityBoundingSet=~CAP_NET_ADMIN CAP_NET_BROADCAST CAP_NET_RAW +CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYSLOG + + [Install] WantedBy=multi-user.target From 5104c2a79f698bd359adc4d7c32ab9232c9c05b0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Jul 2023 19:11:32 +0200 Subject: [PATCH 0182/1116] portalapi: add CORS headers ... though gotta revisit this later, I don't know what I'm doing --- conf/nginx/yunohost_api.conf.inc | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/conf/nginx/yunohost_api.conf.inc b/conf/nginx/yunohost_api.conf.inc index b4567e0b8..a3a23f1a9 100644 --- a/conf/nginx/yunohost_api.conf.inc +++ b/conf/nginx/yunohost_api.conf.inc @@ -25,12 +25,25 @@ location = /yunohost/api/error/502 { } location /yunohost/portalapi/ { - proxy_read_timeout 3600s; + + # FIXME FIXME FIXME : we should think about what we really want here ... + more_set_headers "Access-Control-Allow-Origin: $http_origin"; + more_set_headers "Access-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, DELETE"; + more_set_headers "Access-Control-Allow-Headers: Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With"; + more_set_headers "Access-Control-Allow-Credentials: true"; + + if ($request_method = 'OPTIONS') { + more_set_headers "Content-Type: text/plain; charset=utf-8"; + more_set_headers "Content-Length: 0"; + return 204; + } + + proxy_read_timeout 5s; proxy_pass http://127.0.0.1:6788/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_set_header Host $http_host; + proxy_set_header Host $http; # Custom 502 error page error_page 502 /yunohost/portalapi/error/502; From a81a548f765800d664cd2fcc419eb8a39a30cb68 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Jul 2023 19:12:49 +0200 Subject: [PATCH 0183/1116] Unused imports --- src/app.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app.py b/src/app.py index e5b1a1f6a..43ce0ac52 100644 --- a/src/app.py +++ b/src/app.py @@ -19,8 +19,6 @@ import glob import os -import toml -import json import shutil import yaml import time @@ -28,7 +26,6 @@ import re import subprocess import tempfile import copy -from collections import OrderedDict from typing import List, Tuple, Dict, Any, Iterator, Optional from packaging import version From 0cb673c12504d386ec7341ff199440a216f8ffd4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Jul 2023 19:35:05 +0200 Subject: [PATCH 0184/1116] portalapi: woopsies --- conf/nginx/yunohost_api.conf.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/nginx/yunohost_api.conf.inc b/conf/nginx/yunohost_api.conf.inc index a3a23f1a9..8133624b3 100644 --- a/conf/nginx/yunohost_api.conf.inc +++ b/conf/nginx/yunohost_api.conf.inc @@ -43,7 +43,7 @@ location /yunohost/portalapi/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_set_header Host $http; + proxy_set_header Host $host; # Custom 502 error page error_page 502 /yunohost/portalapi/error/502; From f4dfb560068b1f952de86653427ded5cbc1206bb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 15 Jul 2023 16:01:03 +0200 Subject: [PATCH 0185/1116] portal refactoring: the 'yunohost tile' thingy won't work anymore, gotta discuss what we want to do exactly --- conf/nginx/plain/yunohost_panel.conf.inc | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/conf/nginx/plain/yunohost_panel.conf.inc b/conf/nginx/plain/yunohost_panel.conf.inc index 16a6e6b29..69ca48a62 100644 --- a/conf/nginx/plain/yunohost_panel.conf.inc +++ b/conf/nginx/plain/yunohost_panel.conf.inc @@ -1,8 +1,12 @@ -# Insert YunoHost button + portal overlay -sub_filter ''; -sub_filter_once on; -# Apply to other mime types than text/html -sub_filter_types application/xhtml+xml; -# Prevent YunoHost panel files from being blocked by specific app rules -location ~ (ynh_portal.js|ynh_overlay.css|ynh_userinfo.json|ynhtheme/custom_portal.js|ynhtheme/custom_overlay.css) { -} +# This is some old code that worked with the old portal +# We need to rethink wether we want to keep something similar, +# or drop the feature + +# # Insert YunoHost button + portal overlay +# sub_filter ''; +# sub_filter_once on; +# # Apply to other mime types than text/html +# sub_filter_types application/xhtml+xml; +# # Prevent YunoHost panel files from being blocked by specific app rules +# location ~ (ynh_portal.js|ynh_overlay.css|ynh_userinfo.json|ynhtheme/custom_portal.js|ynhtheme/custom_overlay.css) { +# } From dd73c7ba599243802a874fa5ac88c25e14d3fd7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=CC=81ric=20Gaspar?= <46165813+ericgaspar@users.noreply.github.com> Date: Sat, 15 Jul 2023 17:09:57 +0200 Subject: [PATCH 0186/1116] Fix typo --- locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index bfc564afd..152cf33e3 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,8 +1,8 @@ { "aborting": "Aborting.", "action_invalid": "Invalid action '{action}'", - "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}'", + "additional_urls_already_added": "Additional URL '{url}' already added in the additional URL for permission '{permission}'", + "additional_urls_already_removed": "Additional URL '{url}' already removed in the additional URL for permission '{permission}'", "admin_password": "Administration password", "admins": "Admins", "all_users": "All YunoHost users", From 91a564c3d10d28392a05d813fb5db330d4ce3b84 Mon Sep 17 00:00:00 2001 From: motcha Date: Tue, 11 Jul 2023 14:31:16 +0000 Subject: [PATCH 0187/1116] Translated using Weblate (Japanese) Currently translated at 77.2% (593 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ja/ --- locales/ja.json | 324 ++++++++++++++++++++++++------------------------ 1 file changed, 162 insertions(+), 162 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index 90645193b..479c8c07e 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1,25 +1,25 @@ { - "password_too_simple_1": "パスワードは少なくとも8文字必要です", + "password_too_simple_1": "パスワードは8文字以上である必要があります", "aborting": "中止します。", "action_invalid": "不正なアクション ’ {action}’", "additional_urls_already_added": "アクセス許可 '{permission}' に対する追加URLには ‘{url}’ が既に追加されています", "admin_password": "管理者パスワード", - "app_action_cannot_be_ran_because_required_services_down": "このアクションを実行するには、次の必要なサービスが実行されている必要があります: {services} 。続行するには再起動してみてください (そして何故ダウンしているのか調査してください)。", - "app_action_failed": "‘{name}’ アプリのアクション ’{action}' に失敗しました", + "app_action_cannot_be_ran_because_required_services_down": "このアクションを実行するには、次の必要なサービスが実行されている必要があります: {services} 。続行するには再起動してみてください (そして何故実行されていないのか調査してください)。", + "app_action_failed": "アプリ{app}のアクション{action}の実行に失敗しました", "app_argument_invalid": "引数 '{name}' の有効な値を選択してください: {error}", "app_argument_password_no_default": "パスワード引数 '{name}' の解析中にエラーが発生しました: セキュリティ上の理由から、パスワード引数にデフォルト値を設定することはできません", - "app_argument_required": "‘{name}’ は必要です。", + "app_argument_required": "引数 '{name}' が必要です", "app_change_url_failed": "{app}のURLを変更できませんでした:{error}", - "app_change_url_identical_domains": "古いドメインと新しいドメイン/url_pathは同一であり( '{domain}{path}')、何もしません。", + "app_change_url_identical_domains": "古いドメインと新しいドメイン/url_pathは同一であるため( '{domain}{path}')、何もしません。", "app_change_url_script_failed": "URL 変更スクリプト内でエラーが発生しました", - "app_failed_to_upgrade_but_continue": "アプリの{failed_app}アップグレードに失敗しました。要求に応じて次のアップグレードに進みます。「yunohostログショー{operation_logger_name}」を実行して失敗ログを表示します", + "app_failed_to_upgrade_but_continue": "アプリ {failed_app} のアップグレードに失敗しました。次のアップグレードに進むことも可能です。’yunohost log show {operation_logger_name}’ を実行して失敗ログを表示します", "app_full_domain_unavailable": "申し訳ありませんが、このアプリは独自のドメインにインストールする必要がありますが、他のアプリは既にドメイン '{domain}' にインストールされています。代わりに、このアプリ専用のサブドメインを使用できます。", "app_id_invalid": "不正なアプリID", "app_install_failed": "インストールできません {app}:{error}", "app_manifest_install_ask_password": "このアプリの管理パスワードを選択してください", "app_manifest_install_ask_path": "このアプリをインストールするURLパス(ドメインの後)を選択します", "app_not_properly_removed": "{app}が正しく削除されていません", - "app_not_upgraded": "アプリ「{failed_app}」のアップグレードに失敗したため、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_not_upgraded": "アプリ'{failed_app}'のアップグレードに失敗したため、次のアプリのアップグレードがキャンセルされました: {apps}", "app_start_remove": "‘{app}’ を削除しています…", "app_start_restore": "‘{app}’ をリストアしています…", "ask_main_domain": "メインドメイン", @@ -32,7 +32,7 @@ "backup_actually_backuping": "収集したファイルからバックアップアーカイブを作成しています...", "backup_archive_corrupted": "バックアップアーカイブ ’{archive}’ は破損しているようです: {error}", "backup_archive_name_exists": "この名前のバックアップアーカイブはすでに存在します。", - "backup_archive_name_unknown": "「{name}」という名前の不明なローカルバックアップアーカイブ", + "backup_archive_name_unknown": "‘{name}’ という不明なローカルバックアップアーカイブ", "backup_archive_open_failed": "バックアップアーカイブを開けませんでした", "backup_archive_system_part_not_available": "このバックアップでは、システム部分 '{part}' を使用できません", "backup_method_custom_finished": "カスタム バックアップ方法 '{method}' が完了しました", @@ -41,14 +41,14 @@ "certmanager_cert_install_failed": "{domains}のLet’s Encrypt 証明書のインストールに失敗しました", "certmanager_cert_install_failed_selfsigned": "{domains} ドメインの自己署名証明書のインストールに失敗しました", "certmanager_cert_install_success": "Let’s Encrypt 証明書が ‘{domain}’ にインストールされました", - "certmanager_cert_install_success_selfsigned": "ドメイン「{domain}」に自己署名証明書がインストールされました", - "certmanager_domain_dns_ip_differs_from_public_ip": "ドメイン '{domain}' の DNS レコードは、このサーバーの IP とは異なります。詳細については、診断の「DNSレコード」(基本)カテゴリを確認してください。最近 A レコードを変更した場合は、反映されるまでお待ちください (一部の DNS 伝達チェッカーはオンラインで入手できます)。(何をしているかがわかっている場合は、 '--no-checks'を使用してこれらのチェックをオフにします。", - "certmanager_domain_http_not_working": "ドメイン{domain}はHTTP経由でアクセスできないようです。詳細については、診断の「Web」カテゴリを確認してください。(何をしているかがわかっている場合は、 '--no-checks'を使用してこれらのチェックをオフにします。", - "certmanager_unable_to_parse_self_CA_name": "自己署名機関の名前を解析できませんでした (ファイル: {file})", - "certmanager_domain_not_diagnosed_yet": "ドメイン{domain}の診断結果はまだありません。診断セクションのカテゴリ「DNSレコード」と「Web」の診断を再実行して、ドメインが暗号化の準備ができているかどうかを確認してください。(または、何をしているかがわかっている場合は、「--no-checks」を使用してこれらのチェックをオフにします。", - "confirm_app_insufficient_ram": "危険!このアプリのインストール/アップグレードには{required}RAMが必要ですが、現在利用可能なのは{current}つだけです。このアプリを実行できたとしても、そのインストール/アップグレードプロセスには大量のRAMが必要なため、サーバーがフリーズして惨めに失敗する可能性があります。とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", - "confirm_notifications_read": "警告:続行する前に上記のアプリ通知を確認する必要があります、知っておくべき重要なことがあるかもしれません。[{answers}]", - "custom_app_url_required": "カスタム App をアップグレードするには URL を指定する必要があります{app}", + "certmanager_cert_install_success_selfsigned": "ドメイン'{domain}'に自己署名証明書がインストールされました", + "certmanager_domain_dns_ip_differs_from_public_ip": "ドメイン '{domain}' の DNS レコードは、このサーバーの IP とは異なります。詳細については、診断の'DNSレコード'(基本)カテゴリを確認してください。最近 A レコードを変更した場合は、反映されるまでお待ちください (一部の DNS プロパゲーション チェッカーはオンラインで入手できます)。(何をしているかがわかっている場合は、 '--no-checks'を使用してこれらのチェックをオフにします。", + "certmanager_domain_http_not_working": "ドメイン{domain}はHTTP経由でアクセスできないようです。詳細については、診断の'Web'カテゴリを確認してください。(何をしているかがわかっている場合は、 '--no-checks'を使用してこれらのチェックをオフにします。", + "certmanager_unable_to_parse_self_CA_name": "自己署名機関の名前をパースできませんでした (ファイル: {file})", + "certmanager_domain_not_diagnosed_yet": "ドメイン{domain}の診断結果はまだありません。診断セクションのカテゴリ'DNSレコード'と'Web'の診断を再実行して、ドメインの暗号化が準備できているかどうかを確認してください。(または、何をしているかがわかっている場合は、'--no-checks'を使用してこれらのチェックをオフにします。", + "confirm_app_insufficient_ram": "危険!このアプリのインストール/アップグレードには{required} のRAMが必要ですが、現在利用可能なのは{current} だけです。このアプリを実行できたとしても、インストール/アップグレードには大量のRAMが必要なため、サーバーがフリーズして惨めに失敗する可能性があります。とにかく、そのリスクを冒しても構わないと思っているなら'{answers}'と入力してください", + "confirm_notifications_read": "警告: 続行する前に、上記のアプリ通知を確認する必要があります。知っておくべき重要なことがあるかもしれません。[{answers}]", + "custom_app_url_required": "カスタム App {app} をアップグレードするには URL を指定する必要があります", "danger": "危険:", "diagnosis_cant_run_because_of_dep": "{dep}に関連する重要な問題がある間、{category}診断を実行できません。", "diagnosis_description_apps": "アプリケーション", @@ -85,8 +85,8 @@ "diagnosis_security_vulnerable_to_meltdown": "Meltdown(重大なセキュリティの脆弱性)に対して脆弱に見えます", "diagnosis_services_conf_broken": "サービス{service}の構成が壊れています!", "diagnosis_services_running": "サービス{service}が実行されています!", - "diagnosis_sshd_config_inconsistent": "SSHポートが/ etc / ssh / sshd_configで手動で変更されたようです。YunoHost 4.2以降、手動で構成を編集する必要がないように、新しいグローバル設定「security.ssh.ssh_port」を使用できます。", - "diagnosis_swap_none": "システムにスワップがまったくない。システムのメモリ不足の状況を回避するために、少なくとも {recommended} つのスワップを追加することを検討する必要があります。", + "diagnosis_sshd_config_inconsistent": "SSHポートが/etc/ssh/sshd_config で手動変更されたようです。YunoHost 4.2以降、手動で構成を編集する必要がないように、新しいグローバル設定'security.ssh.ssh_port'を使用できます。", + "diagnosis_swap_none": "システムにはスワップが {total} しかありません。システムのメモリ不足の状況を回避するために、少なくとも {recommended} のスワップを用意することを検討してください。", "diagnosis_swap_notsomuch": "システムにはスワップが {total} しかありません。システムのメモリ不足の状況を回避するために、少なくとも {recommended} のスワップを用意することを検討してください。", "diagnosis_swap_ok": "システムには {total} のスワップがあります!", "domain_cert_gen_failed": "証明書を生成できませんでした", @@ -100,13 +100,13 @@ "domain_dns_conf_special_use_tld": "このドメインは、.local や .test などの特殊な用途のトップレベル ドメイン (TLD) に基づいているため、実際の DNS レコードを持つことは想定されていません。", "domain_dns_push_already_up_to_date": "レコードはすでに最新であり、何もする必要はありません。", "domain_dns_push_failed": "DNS レコードの更新が失敗しました。", - "domain_dyndns_already_subscribed": "すでに DynDNS ドメインにサブスクライブしている", + "domain_dyndns_already_subscribed": "すでに DynDNS ドメインにサブスクライブしています", "dyndns_key_generating": "DNS キーを生成しています...しばらく時間がかかる場合があります。", "dyndns_key_not_found": "ドメインの DNS キーが見つかりません", - "firewall_reload_failed": "バックアップアーカイブを開けませんでした", - "global_settings_setting_postfix_compatibility_help": "Postfix サーバーの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します", - "global_settings_setting_root_password": "新しい管理者パスワード", - "global_settings_setting_root_password_confirm": "新しい管理者パスワード", + "firewall_reload_failed": "ファイアウォールをリロードできませんでした", + "global_settings_setting_postfix_compatibility_help": "Postfix サーバーの互換性とセキュリティにはトレードオフがあります。暗号(およびその他のセキュリティ関連の側面)に影響します", + "global_settings_setting_root_password": "新しいルートパスワード", + "global_settings_setting_root_password_confirm": "新しいルートパスワード(確認)", "global_settings_setting_smtp_allow_ipv6": "IPv6 を許可する", "global_settings_setting_user_strength_help": "これらの要件は、パスワードを初期化または変更する場合にのみ適用されます", "group_cannot_be_deleted": "グループ{group}を手動で削除することはできません。", @@ -122,37 +122,37 @@ "hook_exec_not_terminated": "スクリプトが正しく終了しませんでした: {path}", "log_app_install": "‘{}’ アプリをインストールする", "log_user_permission_update": "アクセス許可 '{}' のアクセスを更新する", - "log_user_update": "ユーザー '{}' の情報を更新する", + "log_user_update": "ユーザー ‘{name}’ を更新する", "mail_alias_remove_failed": "電子メール エイリアス '{mail}' を削除できませんでした", "mail_domain_unknown": "ドメイン '{domain}' の電子メール アドレスが無効です。このサーバーによって管理されているドメインを使用してください。", "mail_forward_remove_failed": "電子メール転送 '{mail}' を削除できませんでした", "mail_unavailable": "この電子メール アドレスは、管理者グループ用に予約されています", "migration_0021_start": "Bullseyeへの移行開始", "migration_0021_yunohost_upgrade": "YunoHostコアのアップグレードを開始しています...", - "migration_description_0026_new_admins_group": "新しい「複数の管理者」システムに移行する", + "migration_description_0026_new_admins_group": "新しい'複数の管理者'システムに移行する", "migration_ldap_backup_before_migration": "実際の移行の前に、LDAP データベースとアプリ設定のバックアップを作成します。", "migration_ldap_can_not_backup_before_migration": "移行が失敗する前に、システムのバックアップを完了できませんでした。エラー: {error}", "migration_ldap_migration_failed_trying_to_rollback": "移行できませんでした...システムをロールバックしようとしています。", "permission_updated": "アクセス許可 '{permission}' が更新されました", "restore_confirm_yunohost_installed": "すでにインストールされているシステムを復元しますか?[{answers}]", "restore_extracting": "アーカイブから必要なファイルを抽出しています...", - "restore_failed": "バックアップを復元する ‘{name}’", - "restore_hook_unavailable": "「{part}」の復元スクリプトは、システムで使用できず、アーカイブでも利用できません", + "restore_failed": "システムを復元できませんでした", + "restore_hook_unavailable": "'{part}'の復元スクリプトは、システムで使用できず、アーカイブでも利用できません", "restore_not_enough_disk_space": "十分なスペースがありません(スペース:{free_space} B、必要なスペース:{needed_space} B、セキュリティマージン:{margin} B)", "restore_nothings_done": "何も復元されませんでした", "restore_removing_tmp_dir_failed": "古い一時ディレクトリを削除できませんでした", - "restore_running_app_script": "アプリ「{app}」を復元しています...", + "restore_running_app_script": "アプリ'{app}'を復元しています...", "restore_running_hooks": "復元フックを実行しています...", - "restore_system_part_failed": "「{part}」システム部分を復元できませんでした", - "root_password_changed": "パスワード確認", + "restore_system_part_failed": "'{part}'システム部分を復元できませんでした", + "root_password_changed": "ルートのパスワードが変更されました", "server_reboot": "サーバーが再起動します", - "server_shutdown_confirm": "サーバーはすぐにシャットダウンしますが、よろしいですか?[{answers}]", + "server_shutdown_confirm": "サーバーはすぐにシャットダウンしますが、よろしいですか? [{answers}]", "service_add_failed": "サービス '{service}' を追加できませんでした", "service_added": "サービス '{service}' が追加されました", "service_already_started": "サービス '{service}' は既に実行されています", - "service_description_dnsmasq": "ドメイン名解決 (DNS) を処理します。", + "service_description_dnsmasq": "ドメイン名解決 (DNS) を処理します", "service_description_dovecot": "電子メールクライアントが電子メールにアクセス/フェッチすることを許可します(IMAPおよびPOP3経由)", - "service_description_fail2ban": "インターネットからのブルートフォース攻撃やその他の種類の攻撃から保護します", + "service_description_fail2ban": "インターネットからのブルートフォース攻撃やその他の攻撃から保護します", "service_description_metronome": "XMPP インスタント メッセージング アカウントを管理する", "service_description_mysql": "アプリ データの格納 (SQL データベース)", "service_description_postfix": "電子メールの送受信に使用", @@ -160,8 +160,8 @@ "service_enable_failed": "起動時にサービス '{service}' を自動的に開始できませんでした。\n\n最近のサービスログ:{logs}", "service_enabled": "サービス '{service}' は、システムの起動時に自動的に開始されるようになりました。", "service_reloaded": "サービス '{service}' がリロードされました", - "service_not_reloading_because_conf_broken": "構成が壊れているため、サービス「{name}」をリロード/再起動しません:{errors}", - "show_tile_cant_be_enabled_for_regex": "権限 '{permission}' の URL は正規表現であるため、現在 'show_tile' を有効にすることはできません。", + "service_not_reloading_because_conf_broken": "構成が壊れているため、サービス'{name}'をリロード/再起動しません: {errors}", + "show_tile_cant_be_enabled_for_regex": "権限 '{permission}' の URL は正規表現であるため、現在 'show_tile' を有効にすることはできません", "show_tile_cant_be_enabled_for_url_not_defined": "最初にアクセス許可 '{permission}' の URL を定義する必要があるため、現在 'show_tile' を有効にすることはできません。", "ssowat_conf_generated": "SSOワット構成の再生成", "system_upgraded": "システムのアップグレード", @@ -170,26 +170,26 @@ "update_apt_cache_warning": "APT(Debianのパッケージマネージャー)のキャッシュを更新中に問題が発生しました。問題のある行を特定するのに役立つ可能性のあるsources.list行のダンプを次に示します。\n{sourceslist}", "admins": "管理者", "all_users": "YunoHostの全ユーザー", - "already_up_to_date": "何もすることはありません。すべて最新です。", + "already_up_to_date": "何もすることはありません。すべてが最新です。", "app_action_broke_system": "このアクションは、これらの重要なサービスを壊したようです: {services}", - "app_already_installed": "アプリ '{app}' は既にインストール済み", + "app_already_installed": "{app}は既にインストールされています", "app_already_installed_cant_change_url": "このアプリは既にインストールされています。この機能だけではURLを変更することはできません。利用可能な場合は、`app changeurl`を確認してください。", "app_already_up_to_date": "{app} アプリは既に最新です", "app_arch_not_supported": "このアプリはアーキテクチャ {required} にのみインストールできますが、サーバーのアーキテクチャは{current} です", "app_argument_choice_invalid": "引数 '{name}' に有効な値を選択してください: '{value}' は使用可能な選択肢に含まれていません ({choices})", - "app_change_url_no_script": "アプリ「{app_name}」はまだURLの変更をサポートしていません。多分あなたはそれをアップグレードする必要があります。", + "app_change_url_no_script": "アプリ ‘{app_name}’ はまだURLの変更をサポートしていません。おそらく、あなたはそれをアップグレードする必要があります。", "app_change_url_require_full_domain": "{app}は完全なドメイン(つまり、path = /)を必要とするため、この新しいURLに移動できません。", - "app_change_url_success": "{app} URL が{domain}{path}されました", + "app_change_url_success": "{app} URL は{domain}{path}になりました", "app_config_unable_to_apply": "設定パネルの値を適用できませんでした。", "app_config_unable_to_read": "設定パネルの値の読み取りに失敗しました。", "app_corrupt_source": "YunoHost はアセット '{source_id}' ({url}) を {app} 用にダウンロードできましたが、アセットのチェックサムが期待されるものと一致しません。これは、あなたのサーバーで一時的なネットワーク障害が発生したか、もしくはアセットがアップストリームメンテナ(または悪意のあるアクター?)によって何らかの形で変更され、YunoHostパッケージャーがアプリマニフェストを調査/更新する必要があることを意味する可能性があります。\n 期待される sha256 チェックサム: {expected_sha256}\n ダウンロードしたsha256チェックサム: {computed_sha256}\n ダウンロードしたファイルサイズ: {size}", "app_extraction_failed": "インストール ファイルを抽出できませんでした", - "app_failed_to_download_asset": "{app}のアセット「{source_id}」({url})をダウンロードできませんでした:{out}", + "app_failed_to_download_asset": "{app}のアセット’{source_id}’ ({url}) をダウンロードできませんでした: {out}", "app_install_files_invalid": "これらのファイルはインストールできません", "app_install_script_failed": "アプリのインストールスクリプト内部でエラーが発生しました", "app_label_deprecated": "このコマンドは非推奨です。新しいコマンド ’yunohost user permission update’ を使用して、アプリラベルを管理してください。", "app_location_unavailable": "この URL は利用できないか、既にインストールされているアプリと競合しています。\n{apps}", - "app_make_default_location_already_used": "「{app}」をドメインのデフォルトアプリにすることはできません。「{domain}」は「{other_app}」によってすでに使用されています", + "app_make_default_location_already_used": "‘{app}’ をドメインのデフォルトアプリにすることはできません。’{domain}’ は ’{other_app}’ によってすでに使用されています", "app_manifest_install_ask_admin": "このアプリの管理者ユーザーを選択する", "app_manifest_install_ask_domain": "このアプリをインストールするドメインを選択してください", "app_manifest_install_ask_init_admin_permission": "このアプリの管理機能にアクセスできるのは誰ですか?(これは後で変更できます)", @@ -199,12 +199,12 @@ "app_not_enough_disk": "このアプリには{required}の空き容量が必要です。", "app_not_enough_ram": "このアプリのインストール/アップグレードには{required} のRAMが必要ですが、現在利用可能なのは {current} だけです。", "app_not_installed": "インストールされているアプリのリストに{app}が見つかりませんでした: {all_apps}", - "app_not_upgraded_broken_system": "アプリ「{failed_app}」はアップグレードに失敗し、システムを壊れた状態にしたため、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_not_upgraded_broken_system": "アプリ'{failed_app}'はアップグレードに失敗し、システムを壊れた状態にしたため、次のアプリのアップグレードがキャンセルされました: {apps}", "app_not_upgraded_broken_system_continue": "アプリ ’{failed_app}’ はアップグレードに失敗し、システムを壊れた状態にした(そのためcontinue-on-failureは無視されます)ので、次のアプリのアップグレードがキャンセルされました: {apps}", "app_restore_failed": "{app}を復元できませんでした: {error}", "app_restore_script_failed": "アプリのリストアスクリプト内でエラーが発生しました", "app_sources_fetch_failed": "ソースファイルをフェッチできませんでしたが、URLは正しいですか?", - "app_packaging_format_not_supported": "このアプリは、パッケージ形式がYunoHostバージョンでサポートされていないため、インストールできません。おそらく、システムのアップグレードを検討する必要があります。", + "app_packaging_format_not_supported": "このアプリは、パッケージ形式がこのYunoHostバージョンではサポートされていないため、インストールできません。おそらく、システムのアップグレードを検討する必要があります。", "app_remove_after_failed_install": "インストールの失敗後にアプリを削除しています...", "app_removed": "'{app}' はアンインストール済", "app_requirements_checking": "{app} の依存関係を確認しています…", @@ -224,7 +224,7 @@ "app_upgrade_some_app_failed": "一部のアプリをアップグレードできませんでした", "app_upgraded": "'{app}' アップグレード済", "app_yunohost_version_not_supported": "このアプリは YunoHost >= {required} を必要としますが、現在インストールされているバージョンは{current} です", - "apps_already_up_to_date": "全てのアプリが最新になりました!", + "apps_already_up_to_date": "全てのアプリが最新になりました", "apps_catalog_failed_to_download": "{apps_catalog} アプリ カタログをダウンロードできません: {error}", "apps_failed_to_upgrade": "これらのアプリケーションのアップグレードに失敗しました: {apps}", "apps_failed_to_upgrade_line": "\n * {app_id} (対応するログを表示するには、’yunohost log show {operation_logger_name}’ を実行してください)", @@ -239,7 +239,7 @@ "backup_archive_broken_link": "バックアップアーカイブにアクセスできませんでした({path}へのリンクが壊れています)", "backup_archive_cant_retrieve_info_json": "アーカイブ '{archive}' の情報を読み込めませんでした... info.json ファイルを取得できません (または有効な json ではありません)。", "backup_archive_writing_error": "圧縮アーカイブ '{archive}' にバックアップするファイル '{source}' (アーカイブ '{dest}' で指定) を追加できませんでした", - "backup_ask_for_copying_if_needed": "一時的に{size}MBを使用してバックアップを実行しますか?(この方法は、より効率的な方法で準備できなかったファイルがあるため、この方法が使用されます。", + "backup_ask_for_copying_if_needed": "一時的に{size}MBを使用してバックアップを実行しますか?(より効率的な方法で準備できなかったファイルがあるため、この方法が使用されます)", "backup_cant_mount_uncompress_archive": "非圧縮アーカイブを書き込み保護としてマウントできませんでした", "backup_cleaning_failed": "一時バックアップフォルダをクリーンアップできませんでした", "backup_copying_to_organize_the_archive": "アーカイブを整理するために{size}MBをコピーしています", @@ -249,8 +249,8 @@ "backup_creation_failed": "バックアップ作成できませんでした", "backup_csv_addition_failed": "バックアップするファイルをCSVファイルに追加できませんでした", "backup_csv_creation_failed": "復元に必要な CSV ファイルを作成できませんでした", - "backup_custom_backup_error": "カスタムバックアップ方法は「バックアップ」ステップを通過できませんでした", - "backup_custom_mount_error": "カスタムバックアップ方法は「マウント」ステップを通過できませんでした", + "backup_custom_backup_error": "カスタムバックアップ方法は'バックアップ'ステップを通過できませんでした", + "backup_custom_mount_error": "カスタムバックアップ方法は'マウント'ステップを通過できませんでした", "backup_delete_error": "‘{path}’ を削除する", "backup_deleted": "バックアップは削除されました: {name}", "backup_nothings_done": "保存するものがありません", @@ -260,50 +260,50 @@ "backup_hook_unknown": "バックアップ フック '{hook}' が不明です", "backup_method_copy_finished": "バックアップコピーがファイナライズされました", "backup_method_tar_finished": "TARバックアップアーカイブが作成されました", - "backup_output_symlink_dir_broken": "アーカイブディレクトリ '{path}' は壊れたシンボリックリンクです。たぶん、あなたはそれが指す記憶媒体を再/マウントまたは差し込むのを忘れました。", + "backup_output_symlink_dir_broken": "アーカイブディレクトリ '{path}' は壊れたシンボリックリンクです。おそらく、リンク先の記憶媒体をマウント/再マウントし忘れたか、差し込むのを忘れたのではないかと。", "backup_mount_archive_for_restore": "復元のためにアーカイブを準備しています...", "backup_no_uncompress_archive_dir": "そのような圧縮されていないアーカイブディレクトリはありません", "certmanager_warning_subdomain_dns_record": "サブドメイン '{subdomain}' は '{domain}' と同じ IP アドレスに解決されません。一部の機能は、これを修正して証明書を再生成するまで使用できません。", "config_action_disabled": "アクション '{action}' は無効になっているため実行できませんでした。制約を満たしていることを確認してください。ヘルプ: {help}", "backup_permission": "{app}のバックアップ権限", "backup_running_hooks": "バックアップフックを実行しています...", - "backup_system_part_failed": "「{part}」システム部分をバックアップできませんでした", - "backup_unable_to_organize_files": "簡単な方法を使用してアーカイブ内のファイルを整理できませんでした", - "backup_with_no_backup_script_for_app": "アプリ「{app}」にはバックアップスクリプトがありません。無視。", + "backup_system_part_failed": "‘{part}’ システム部分をバックアップできませんでした", + "backup_unable_to_organize_files": "急速な方法を使用してアーカイブ内のファイルを整理できませんでした", + "backup_with_no_backup_script_for_app": "アプリ ’{app}’ にはバックアップスクリプトがありません。無視します。", "backup_with_no_restore_script_for_app": "{app}には復元スクリプトがないため、このアプリのバックアップを自動的に復元することはできません。", - "certmanager_acme_not_configured_for_domain": "ACMEチャレンジは、nginx confに対応するコードスニペットがないため、現在{domain}実行できません...'yunohost tools regen-conf nginx --dry-run --with-diff' を使用して、nginx の設定が最新であることを確認してください。", + "certmanager_acme_not_configured_for_domain": "{domain}に対するACMEチャレンジは、nginx confに対応するコードスニペットがないため現在実行できません... 'yunohost tools regen-conf nginx --dry-run --with-diff' を使用して、nginx の設定が最新であることを確認してください。", "certmanager_attempt_to_renew_nonLE_cert": "ドメイン '{domain}' の証明書は、Let's Encryptによって発行されていません。自動的に更新できません!", "certmanager_attempt_to_renew_valid_cert": "ドメイン '{domain}' の証明書の有効期限が近づいていません。(あなたが何をしているのかわかっている場合は、--forceを使用できます)", "certmanager_cert_renew_failed": "{domains}のLet’s Encrypt 証明書更新に失敗しました", - "certmanager_cert_renew_success": "{domains}のLet’s Encrypt 証明書が更新されました", + "certmanager_cert_renew_success": "{domain}のLet’s Encrypt 証明書が更新されました", "certmanager_cert_signing_failed": "新しい証明書に署名できませんでした", "certmanager_certificate_fetching_or_enabling_failed": "{domain}に新しい証明書を使用しようとしましたが、機能しませんでした...", "certmanager_domain_cert_not_selfsigned": "ドメイン {domain} の証明書は自己署名されていません。置き換えてよろしいですか(これを行うには '--force' を使用してください)?", - "certmanager_hit_rate_limit": "最近{domain}、この正確なドメインのセットに対して既に発行されている証明書が多すぎます。しばらくしてからもう一度お試しください。詳細については、https://letsencrypt.org/docs/rate-limits/ を参照してください。", + "certmanager_hit_rate_limit": "直近でドメイン {domain} に対して発行されている証明書が多すぎます。しばらくしてからもう一度お試しください。詳細については、https://letsencrypt.org/docs/rate-limits/ を参照してください。", "certmanager_no_cert_file": "ドメイン {domain} (ファイル: {file}) の証明書ファイルを読み取れませんでした。", "certmanager_self_ca_conf_file_not_found": "自己署名機関の設定ファイルが見つかりませんでした(ファイル:{file})", - "config_forbidden_readonly_type": "型 '{type}' は読み取り専用として設定できず、別の型を使用してこの値をレンダリングします (関連する引数 ID: '{id}')。", + "config_forbidden_readonly_type": "型 '{type}' は読み取り専用として設定できず、別の型を使用してこの値をレンダリングしてください (関連する引数 ID: '{id}')。", "config_no_panel": "設定パネルが見つかりません。", "config_unknown_filter_key": "フィルター キー '{filter_key}' が正しくありません。", "config_validate_color": "有効な RGB 16 進色である必要があります", - "config_validate_date": "YYYY-MM-DD の形式のような有効な日付である必要があります。", + "config_validate_date": "YYYY-MM-DD のような形式の有効な日付である必要があります", "config_validate_email": "有効なメールアドレスである必要があります", "config_action_failed": "アクション '{action}' の実行に失敗しました: {error}", - "config_apply_failed": "新しい構成の適用に失敗しました: {error}", - "config_cant_set_value_on_section": "構成セクション全体に 1 つの値を設定することはできません。", - "config_forbidden_keyword": "キーワード '{keyword}' は予約されており、この ID の質問を含む設定パネルを作成または使用することはできません。", + "config_apply_failed": "新しい設定の適用に失敗しました: {error}", + "config_cant_set_value_on_section": "設定セクション全体に 1 つの値を設定することはできません。", + "config_forbidden_keyword": "キーワード '{keyword}' は予約されており、この ID を持つ質問を含む設定パネルを作成または使用することはできません。", "config_validate_time": "HH:MM のような有効な時刻である必要があります", "config_validate_url": "有効なウェブ URL である必要があります", - "confirm_app_install_danger": "危険!このアプリはまだ実験的であることが知られています(明示的に動作していない場合)!あなたが何をしているのかわからない限り、おそらくそれをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません...とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", - "confirm_app_install_thirdparty": "危険!このアプリはYunoHostのアプリカタログの一部ではありません。サードパーティのアプリをインストールすると、システムの整合性とセキュリティが損なわれる可能性があります。あなたが何をしているのかわからない限り、おそらくそれをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません...とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", - "confirm_app_install_warning": "警告:このアプリは動作する可能性がありますが、YunoHostにうまく統合されていません。シングル サインオンやバックアップ/復元などの一部の機能は使用できない場合があります。とにかくインストールしますか?[{answers}] ", + "confirm_app_install_danger": "危険!このアプリはまだ実験的であることが知られています(明示的に動作しないとされていない場合)! 自分で何をしているのかわからない限り、それをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません... それでも、とにかくそのリスクを冒しても構わないと思っているなら、'{answers}'と入力してください", + "confirm_app_install_thirdparty": "危険!このアプリはYunoHostのアプリカタログの一部ではありません。サードパーティのアプリをインストールすると、システムの整合性とセキュリティが損なわれる可能性があります。あなたが何をしているのかわからない限り、それをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません... それでもとにかくそのリスクを冒しても構わないと思っているなら、'{answers}'と入力してください", + "confirm_app_install_warning": "警告:このアプリは動作する可能性がありますが、YunoHostにうまく統合されていません。シングル サインオンやバックアップ/復元などの一部の機能は使用できない場合があります。とにかくインストールしますか? [{answers}] ", "diagnosis_apps_allgood": "インストールされているすべてのアプリは、基本的なパッケージ化プラクティスを尊重します", "diagnosis_apps_bad_quality": "このアプリケーションは現在、YunoHostのアプリケーションカタログで壊れているとフラグが付けられています。これは、メンテナが問題を修正しようとしている間の一時的な問題である可能性があります。それまでの間、このアプリのアップグレードは無効になります。", "diagnosis_apps_broken": "このアプリケーションは現在、YunoHostのアプリケーションカタログで壊れているとフラグが付けられています。これは、メンテナが問題を修正しようとしている間の一時的な問題である可能性があります。それまでの間、このアプリのアップグレードは無効になります。", "diagnosis_apps_deprecated_practices": "このアプリのインストール済みバージョンでは、非常に古い非推奨のパッケージ化プラクティスがまだ使用されています。あなたは本当にそれをアップグレードすることを検討する必要があります。", "diagnosis_basesystem_hardware": "サーバーのハードウェア アーキテクチャが{virt} {arch}", "diagnosis_basesystem_hardware_model": "サーバーモデルが{model}", - "diagnosis_apps_issue": "アプリ '{app}' をアップグレードする", + "diagnosis_apps_issue": "アプリ{app}で問題が見つかりました", "diagnosis_apps_not_in_app_catalog": "このアプリケーションは、YunoHostのアプリケーションカタログにはありません。過去に存在し、削除された場合は、アップグレードを受け取らず、システムの整合性とセキュリティが損なわれる可能性があるため、このアプリのアンインストールを検討する必要があります。", "diagnosis_apps_outdated_ynh_requirement": "このアプリのインストール済みバージョンには、yunohost >= 2.xまたは3.xのみが必要であり、推奨されるパッケージングプラクティスとヘルパーが最新ではないことを示す傾向があります。あなたは本当にそれをアップグレードすることを検討する必要があります。", "diagnosis_backports_in_sources_list": "apt(パッケージマネージャー)はバックポートリポジトリを使用するように構成されているようです。あなたが何をしているのか本当にわからない限り、バックポートからパッケージをインストールすることは、システムに不安定性や競合を引き起こす可能性があるため、強くお勧めしません。", @@ -320,7 +320,7 @@ "diagnosis_diskusage_low": "ストレージ<0>(デバイス<1>上)には、( )残りの領域({free_percent} )しかありません{free}。{total}注意してください。", "diagnosis_diskusage_ok": "ストレージ<0>(デバイス<1>上)にはまだ({free_percent}%)スペースが{free}残っています(から{total})!", "diagnosis_diskusage_verylow": "ストレージ<0>(デバイス<1>上)には、( )残りの領域({free_percent} )しかありません{free}。{total}あなたは本当にいくつかのスペースをきれいにすることを検討する必要があります!", - "diagnosis_display_tip": "見つかった問題を確認するには、ウェブ管理者の診断セクションに移動するか、コマンドラインから「yunohost診断ショー--問題--人間が読める」を実行します。", + "diagnosis_display_tip": "見つかった問題を確認するには、ウェブ管理者の診断セクションに移動するか、コマンドラインから'yunohost診断ショー--問題--人間が読める'を実行します。", "diagnosis_dns_bad_conf": "一部の DNS レコードが見つからないか、ドメイン {domain} (カテゴリ {category}) が正しくない", "diagnosis_dns_discrepancy": "次の DNS レコードは、推奨される構成に従っていないようです。
種類: <0>
名前: <1>
現在の値: <2>
期待値: <3>", "diagnosis_dns_good_conf": "DNS レコードがドメイン {domain} (カテゴリ {category}) 用に正しく構成されている", @@ -382,7 +382,7 @@ "diagnosis_package_installed_from_sury": "一部のシステムパッケージはダウングレードする必要があります", "diagnosis_processes_killed_by_oom_reaper": "一部のプロセスは、メモリが不足したため、最近システムによって強制終了されました。これは通常、システム上のメモリ不足、またはプロセスがメモリを消費しすぎていることを示しています。強制終了されたプロセスの概要:\n{kills_summary}", "diagnosis_ram_low": "システムには{available}({available_percent}%)の使用可能なRAMがあります({total}のうち)。注意してください。", - "diagnosis_package_installed_from_sury_details": "一部のパッケージは、Suryと呼ばれるサードパーティのリポジトリから誤ってインストールされました。YunoHostチームはこれらのパッケージを処理する戦略を改善しましたが、Stretchを使用している間にPHP7.3アプリをインストールした一部のセットアップには、いくつかの矛盾が残っていると予想されます。この状況を修正するには、次のコマンドを実行してみてください。 {cmd_to_fix}", + "diagnosis_package_installed_from_sury_details": "一部のパッケージは、Suryと呼ばれるサードパーティのリポジトリから誤ってインストールされました。YunoHostチームはこれらのパッケージを処理する戦略を改善しましたが、Debian Stretchを使用してPHP7.3アプリをインストールした一部のセットアップには、いくつかの点で一貫性のない状態であることが予想されます。この状況を修正するには、次のコマンドを実行してみてください: {cmd_to_fix}", "diagnosis_ports_could_not_diagnose": "IPv{ipversion} で外部からポートに到達できるかどうかを診断できませんでした。", "diagnosis_ports_could_not_diagnose_details": "エラー: {error}", "diagnosis_ports_unreachable": "ポート {port} は外部から到達できません。", @@ -397,21 +397,21 @@ "diagnosis_services_bad_status": "サービス{service} のステータスは {status} です :(", "diagnosis_services_bad_status_tip": "サービスの再起動を試みることができ、それが機能しない場合は、webadminのサービスログを確認してください(コマンドラインから、yunohostサービスの再起動{service}とyunohostサービスログ{service}を使用してこれを行うことができます)。。", "diagnosis_sshd_config_inconsistent_details": "security.ssh.ssh_port -v YOUR_SSH_PORT に設定された yunohost 設定を実行して SSH ポートを定義し、yunohost tools regen-conf ssh --dry-run --with-diff および yunohost tools regen-conf ssh --force をチェックして、会議を YunoHost の推奨事項にリセットしてください。", - "diagnosis_sshd_config_insecure": "SSH構成は手動で変更されたようで、許可されたユーザーへのアクセスを制限するための「許可グループ」または「許可ユーザー」ディレクティブが含まれていないため、安全ではありません。", + "diagnosis_sshd_config_insecure": "SSH構成は手動で変更されたようで、許可されたユーザーへのアクセスを制限するための'許可グループ'または'許可ユーザー'ディレクティブが含まれていないため、安全ではありません。", "diagnosis_swap_tip": "サーバーがSDカードまたはSSDストレージでスワップをホストしている場合、デバイスの平均寿命が大幅に短くなる可能性があることに注意してください。", "diagnosis_unknown_categories": "次のカテゴリは不明です: {categories}", "diagnosis_using_stable_codename": "apt (システムのパッケージマネージャ) は現在、現在の Debian バージョン (bullseye) のコードネームではなく、コードネーム 'stable' からパッケージをインストールするように設定されています。", "disk_space_not_sufficient_install": "このアプリケーションをインストールするのに十分なディスク領域が残っていません", - "diagnosis_using_stable_codename_details": "これは通常、ホスティングプロバイダーからの構成が正しくないことが原因です。なぜなら、Debian の次のバージョンが新しい「安定版」になるとすぐに、apt は適切な移行手順を経ずにすべてのシステムパッケージをアップグレードしたくなるからです。ベース Debian リポジトリの apt ソースを編集してこれを修正し、安定版キーワードを bullseye に置き換えることをお勧めします。対応する設定ファイルは /etc/apt/sources.list、または /etc/apt/sources.list.d/ 内のファイルでなければなりません。", - "diagnosis_using_yunohost_testing": "apt (システムのパッケージマネージャー)は現在、YunoHostコアの「テスト」アップグレードをインストールするように構成されています。", - "diagnosis_using_yunohost_testing_details": "自分が何をしているのかを知っていれば、これはおそらく問題ありませんが、YunoHostのアップグレードをインストールする前にリリースノートに注意してください!「テスト版」のアップグレードを無効にしたい場合は、/etc/apt/sources.list.d/yunohost.list から testing キーワードを削除する必要があります。", + "diagnosis_using_stable_codename_details": "これは通常、ホスティングプロバイダーからの構成が正しくないことが原因です。なぜなら、Debian の次のバージョンが新しい'安定版'になるとすぐに、apt は適切な移行手順を経ずにすべてのシステムパッケージをアップグレードしたくなるからです。ベース Debian リポジトリの apt ソースを編集してこれを修正し、安定版キーワードを bullseye に置き換えることをお勧めします。対応する設定ファイルは /etc/apt/sources.list、または /etc/apt/sources.list.d/ 内のファイルでなければなりません。", + "diagnosis_using_yunohost_testing": "apt (システムのパッケージマネージャー)は現在、YunoHostコアの'テスト'アップグレードをインストールするように構成されています。", + "diagnosis_using_yunohost_testing_details": "自分が何をしているのかを知っていれば、これはおそらく問題ありませんが、YunoHostのアップグレードをインストールする前にリリースノートに注意してください!'テスト版'のアップグレードを無効にしたい場合は、/etc/apt/sources.list.d/yunohost.list から testing キーワードを削除する必要があります。", "disk_space_not_sufficient_update": "このアプリケーションを更新するのに十分なディスク領域が残っていません", - "domain_cannot_add_muc_upload": "「muc.」で始まるドメインを追加することはできません。この種の名前は、YunoHostに統合されたXMPPマルチユーザーチャット機能のために予約されています。", - "domain_cannot_add_xmpp_upload": "「xmpp-upload」で始まるドメインを追加することはできません。この種の名前は、YunoHostに統合されたXMPPアップロード機能のために予約されています。", - "domain_cannot_remove_main": "'{domain}'はメインドメインなので削除できないので、まず「yunohost domain main-domain -n」を使用して別のドメインをメインドメインとして設定する必要があります。 候補 ドメインのリストは次のとおりです。 {other_domains}", + "domain_cannot_add_muc_upload": "'muc.'で始まるドメインを追加することはできません。この種の名前は、YunoHostに統合されたXMPPマルチユーザーチャット機能のために予約されています。", + "domain_cannot_add_xmpp_upload": "'xmpp-upload'で始まるドメインを追加することはできません。この種の名前は、YunoHostに統合されたXMPPアップロード機能のために予約されています。", + "domain_cannot_remove_main": "'{domain}'はメインドメインなので削除できないので、まず'yunohost domain main-domain -n'を使用して別のドメインをメインドメインとして設定する必要があります。 候補 ドメインのリストは次のとおりです。 {other_domains}", "domain_config_api_protocol": "API プロトコル", - "domain_cannot_remove_main_add_new_one": "「{domain}」はメインドメインであり唯一のドメインであるため、最初に「yunohostドメイン追加」を使用して別のドメインを追加し、次に「yunohostドメインメインドメイン-n 」を使用してメインドメインとして設定し、「yunohostドメイン削除{domain}」を使用してドメイン「{domain}」を削除する必要があります。", - "domain_config_acme_eligible_explain": "このドメインは、Let's Encrypt証明書の準備ができていないようです。DNS 構成と HTTP サーバーの到達可能性を確認してください。 診断ページの 「DNSレコード」と「Web」セクションは、何が誤って構成されているかを理解するのに役立ちます。", + "domain_cannot_remove_main_add_new_one": "'{domain}'はメインドメインであり唯一のドメインであるため、最初に'yunohostドメイン追加'を使用して別のドメインを追加し、次に'yunohostドメインメインドメイン-n 'を使用してメインドメインとして設定し、'yunohostドメイン削除{domain}'を使用してドメイン'{domain}'を削除する必要があります。", + "domain_config_acme_eligible_explain": "このドメインは、Let's Encrypt証明書の準備ができていないようです。DNS 構成と HTTP サーバーの到達可能性を確認してください。 診断ページの 'DNSレコード'と'Web'セクションは、何が誤って構成されているかを理解するのに役立ちます。", "domain_config_auth_application_key": "アプリケーションキー", "domain_config_auth_application_secret": "アプリケーション秘密鍵", "domain_config_auth_consumer_key": "消費者キー", @@ -447,57 +447,57 @@ "domain_dns_registrar_experimental": "これまでのところ、**{registrar}**のAPIとのインターフェースは、YunoHostコミュニティによって適切にテストおよびレビューされていません。サポートは**非常に実験的**です-注意してください!", "domain_dns_registrar_managed_in_parent_domain": "このドメインは{parent_domain_link}のサブドメインです。DNS レジストラーの構成は、{parent_domain}の設定パネルで管理する必要があります。", "domain_dns_registrar_not_supported": "YunoHost は、このドメインを処理するレジストラを自動的に検出できませんでした。DNS レコードは、https://yunohost.org/dns のドキュメントに従って手動で構成する必要があります。", - "domain_dns_registrar_supported": "YunoHost は、このドメインがレジストラ**{registrar}**によって処理されていることを自動的に検出しました。必要に応じて、適切なAPI資格情報を提供すると、YunoHostはこのDNSゾーンを自動的に構成します。API 資格情報の取得方法に関するドキュメントは、https://yunohost.org/registar_api_{registrar} ページにあります。(https://yunohost.org/dns のドキュメントに従ってDNSレコードを手動で構成することもできます)", - "domain_dns_registrar_yunohost": "このドメインは nohost.me / nohost.st / ynh.fr であるため、そのDNS構成は、それ以上の構成なしでYunoHostによって自動的に処理されます。(「YunoHost Dyndns Update」コマンドを参照)", - "domain_dyndns_root_unknown": "'{domain}' ドメインのルートを '{name}' にリダイレクト", - "domain_exists": "この名前のバックアップアーカイブはすでに存在します。", - "domain_hostname_failed": "新しいホスト名を設定できません。これにより、後で問題が発生する可能性があります(問題ない可能性があります)。", + "domain_dns_registrar_supported": "YunoHost は、このドメインがレジストラ **{registrar}** によって処理されていることを自動的に検出しました。必要に応じて適切なAPI資格情報を提供すると、YunoHostはこのDNSゾーンを自動的に構成します。API 資格情報の取得方法に関するドキュメントは、https://yunohost.org/registar_api_{registrar} ページにあります。(https://yunohost.org/dns のドキュメントに従ってDNSレコードを手動で構成することもできます)", + "domain_dns_registrar_yunohost": "このドメインは nohost.me / nohost.st / ynh.fr であるため、DNS構成は特別な構成なしでYunoHostによって自動的に処理されます。(‘yunohost dyndns update’ コマンドを参照)", + "domain_dyndns_root_unknown": "不明な DynDNS ルートドメイン", + "domain_exists": "この名前のバックアップアーカイブはすでに存在します", + "domain_hostname_failed": "新しいホスト名を設定できません。これにより、後で問題が発生する可能性があります(問題ない可能性もあります)。", "domain_registrar_is_not_configured": "レジストラーは、ドメイン {domain} 用にまだ構成されていません。", - "domain_remove_confirm_apps_removal": "このドメインを削除すると、これらのアプリケーションが削除されます。\n{apps}\n\nよろしいですか?[{answers}]", - "domain_uninstall_app_first": "これらのアプリケーションは、ドメインに引き続きインストールされます。\n{apps}\n\nドメインの削除に進む前に、「yunohostアプリ削除the_app_id」を使用してアンインストールするか、「yunohostアプリ変更URL the_app_id」を使用して別のドメインに移動してください。", - "domain_unknown": "ドメイン {domain}を作成できません: {error}", - "domains_available": "ドメイン管理", + "domain_remove_confirm_apps_removal": "このドメインを削除すると、これらのアプリケーションが削除されます。\n{apps}\n\nよろしいですか? [{answers}]", + "domain_uninstall_app_first": "これらのアプリケーションは、ドメインにインストールされたままです。\n{apps}\n\nドメインの削除に進む前に、’yunohost app remove ’ を実行してアンインストールするか、’yunohost app change-url ’ を実行してアプリケーションを別のドメインに移動してください", + "domain_unknown": "ドメイン '{domain}' は不明です", + "domains_available": "利用可能なドメイン:", "done": "完了", "downloading": "ダウンロード中...", - "dpkg_is_broken": "dpkg / APT(システムパッケージマネージャー)が壊れた状態にあるように見えるため、現在はこれを行うことができません...SSH経由で接続し、 'sudo apt install --fix-broken'および/または 'sudo dpkg --configure -a'および/または 'sudo dpkg --audit'を実行することで、この問題を解決しようとすることができます。", + "dpkg_is_broken": "dpkg / APT(システムパッケージマネージャー)が壊れた状態にあるように見えるため、現在はこれを行うことができません... SSH経由で接続し、 'sudo apt install --fix-broken' および/または 'sudo dpkg --configure -a' および/または 'sudo dpkg --audit' を実行することで、この問題を解決できるかもしれません。", "dpkg_lock_not_available": "別のプログラムがdpkg(システムパッケージマネージャー)のロックを使用しているように見えるため、このコマンドは現在実行できません", - "dyndns_could_not_check_available": "{domain}{provider}で利用できるかどうかを確認できませんでした。", - "dyndns_ip_update_failed": "IP アドレスを DynDNS に更新できませんでした", + "dyndns_could_not_check_available": "{domain} が {provider}で利用できるかどうかを確認できませんでした。", + "dyndns_ip_update_failed": "IP アドレスを DynDNS で更新できませんでした", "dyndns_ip_updated": "DynDNSでIPを更新しました", - "dyndns_no_domain_registered": "このカテゴリーにログが登録されていません", - "dyndns_provider_unreachable": "DynDNSプロバイダー{provider}に到達できません:YunoHostがインターネットに正しく接続されていないか、ダイネットサーバーがダウンしています。", - "dyndns_registered": "このカテゴリーにログが登録されていません", + "dyndns_no_domain_registered": "DynDNS に登録されているドメインがありません", + "dyndns_provider_unreachable": "DynDNSプロバイダー {provider} に到達できません: YunoHostがインターネットに正しく接続されていないか、dynetteサーバーがダウンしています。", + "dyndns_registered": "登録されている DynDNS ドメイン", "dyndns_registration_failed": "DynDNS ドメインを登録できませんでした: {error}", - "dyndns_unavailable": "ドメイン {domain}を作成できません: {error}", - "dyndns_domain_not_provided": "DynDNS プロバイダー{provider}ドメイン{domain}を提供できません。", - "extracting": "抽出。。。", - "field_invalid": "フィールドは必要です。", + "dyndns_unavailable": "ドメイン '{domain}' は使用できません。", + "dyndns_domain_not_provided": "DynDNS プロバイダー{provider} はドメイン{domain}を提供できません。", + "extracting": "抽出中…", + "field_invalid": "無効なフィールド '{}'", "file_does_not_exist": "ファイル {path}が存在しません。", "firewall_reloaded": "ファイアウォールがリロードされました", - "firewall_rules_cmd_failed": "一部のファイアウォール規則コマンドが失敗しました。ログの詳細情報。", - "global_settings_reset_success": "グローバルIP: {global}", + "firewall_rules_cmd_failed": "一部のファイアウォール ルール コマンドが失敗しました。詳細情報はログに残されています。", + "global_settings_reset_success": "グローバル設定をリセットする", "global_settings_setting_admin_strength": "管理者パスワードの強度要件", "global_settings_setting_admin_strength_help": "これらの要件は、パスワードを初期化または変更する場合にのみ適用されます", "global_settings_setting_backup_compress_tar_archives": "バックアップの圧縮", - "global_settings_setting_backup_compress_tar_archives_help": "新しいバックアップを作成するときは、圧縮されていないアーカイブ (.tar) ではなく、アーカイブを圧縮 (.tar.gz) します。注意:このオプションを有効にすると、バックアップアーカイブの作成が軽くなりますが、最初のバックアップ手順が大幅に長くなり、CPUに負担がかかります。", + "global_settings_setting_backup_compress_tar_archives_help": "新しいバックアップを作成するとき、圧縮されていないアーカイブ (.tar) ではなく、アーカイブを圧縮 (.tar.gz) します。注意: このオプションを有効にすると、バックアップアーカイブの容量は小さくなりますが、最初のバックアップ処理が大幅に長くなり、CPUに負担がかかります。", "global_settings_setting_dns_exposure": "DNS の構成と診断で考慮すべき IP バージョン", - "global_settings_setting_dns_exposure_help": "注意:これは、推奨されるDNS構成と診断チェックにのみ影響します。これはシステム構成には影響しません。", + "global_settings_setting_dns_exposure_help": "注意: これは、推奨されるDNS構成と診断チェックにのみ影響します。これはシステム構成には影響しません。", "global_settings_setting_nginx_compatibility": "NGINXの互換性", - "global_settings_setting_nginx_compatibility_help": "WebサーバーNGINXの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します", + "global_settings_setting_nginx_compatibility_help": "WebサーバーNGINXの互換性とセキュリティの間にはトレードオフがあります。これは暗号(およびその他のセキュリティ関連の側面)に影響します", "global_settings_setting_nginx_redirect_to_https": "HTTPSを強制", - "global_settings_setting_nginx_redirect_to_https_help": "デフォルトでHTTPリクエストをHTTPにリダイレクトします(あなたが何をしているのか本当にわからない限り、オフにしないでください!", - "global_settings_setting_passwordless_sudo": "管理者がパスワードを再入力せずに「sudo」を使用できるようにする", - "global_settings_setting_portal_theme_help": "カスタム ポータル テーマの作成の詳細については、https://yunohost.org/theming を参照してください。", - "global_settings_setting_postfix_compatibility": "後置の互換性", + "global_settings_setting_nginx_redirect_to_https_help": "デフォルトでHTTPリクエストをHTTPにリダイレクトします(あなたが何をしているのか本当に本当にわかっていると自信を持てないのであれば、オフにしないでください!)", + "global_settings_setting_passwordless_sudo": "管理者がパスワードを再入力せずに'sudo'を使用できるようにする", + "global_settings_setting_portal_theme_help": "カスタム ポータル テーマの作成の詳細については、https://yunohost.org/theming を参照してください", + "global_settings_setting_postfix_compatibility": "Postfixの互換性", "global_settings_setting_pop3_enabled": "POP3 を有効にする", "global_settings_setting_pop3_enabled_help": "メール サーバーの POP3 プロトコルを有効にする", - "global_settings_setting_portal_theme": "ユーザーポータルでタイルに表示する", - "global_settings_setting_root_access_explain": "Linux システムでは、「ルート」が絶対管理者です。YunoHost のコンテキストでは、サーバーのローカルネットワークからを除き、直接の「ルート」SSH ログインはデフォルトで無効になっています。'admins' グループのメンバーは、sudo コマンドを使用して、コマンドラインから root として動作できます。ただし、何らかの理由で通常の管理者がログインできなくなった場合に、システムをデバッグするための(堅牢な)rootパスワードがあると便利です。", + "global_settings_setting_portal_theme": "ポータルのテーマ", + "global_settings_setting_root_access_explain": "Linux システムでは'root'が絶対に管理者です。YunoHost のコンテキストでは、'root'ユーザーでのSSH ログインは(サーバーのローカルネットワークからのSSHである場合を除き)デフォルトで無効になっています。'admins' グループのメンバーは、sudo コマンドを使用することで、root としてコマンドを実行できます。ただし、何らかの理由で通常の管理者がログインできなくなった場合には、システムをデバッグするための(堅牢な)rootパスワードがあると便利です。", "global_settings_setting_security_experimental_enabled": "実験的なセキュリティ機能", - "global_settings_setting_security_experimental_enabled_help": "実験的なセキュリティ機能を有効にします(何をしているのかわからない場合は有効にしないでください)。", + "global_settings_setting_security_experimental_enabled_help": "実験的なセキュリティ機能を有効にします(何をしているのかわからない場合は有効にしないでください)", "global_settings_setting_smtp_allow_ipv6_help": "IPv6 を使用したメールの送受信を許可する", "global_settings_setting_smtp_relay_enabled": "SMTP リレーを有効にする", - "global_settings_setting_smtp_relay_enabled_help": "この yunohost インスタンスの代わりにメールを送信するために使用する SMTP リレーを有効にします。このような状況のいずれかにある場合に便利です:25ポートがISPまたはVPSプロバイダーによってブロックされている、DUHLにリストされている住宅用IPがある、逆引きDNSを構成できない、またはこのサーバーがインターネットに直接公開されておらず、他のものを使用してメールを送信したい。", + "global_settings_setting_smtp_relay_enabled_help": "SMTP リレーを有効にすることで、この yunohost サーバー以外の\nサーバーが(代わりに)メールを送信するようになります。この設定は次の状態にある場合に便利です: 25ポートがISPまたはVPSプロバイダーによってブロックされている / DUHLリスト(電子メール拒否リスト)にお住まいのIPが登録されている / 逆引きDNSを構成できない / このサーバーがインターネットに直接公開されておらず、他のサーバーを使用してメールを送信したい。", "global_settings_setting_smtp_relay_host": "SMTP リレー ホスト", "global_settings_setting_smtp_relay_password": "SMTP リレー パスワード", "global_settings_setting_smtp_relay_port": "SMTP リレー ポート", @@ -507,7 +507,7 @@ "global_settings_setting_ssh_password_authentication": "パスワード認証", "global_settings_setting_ssh_password_authentication_help": "SSH のパスワード認証を許可する", "global_settings_setting_ssh_port": "SSH ポート", - "global_settings_setting_ssowat_panel_overlay_enabled": "アプリで小さな「YunoHost」ポータルショートカットの正方形を有効にします", + "global_settings_setting_ssowat_panel_overlay_enabled": "アプリで小さな'YunoHost'ポータルショートカットの正方形を有効にします", "global_settings_setting_user_strength": "ユーザー パスワードの強度要件", "global_settings_setting_webadmin_allowlist_help": "ウェブ管理者へのアクセスを許可されたIPアドレス。", "global_settings_setting_webadmin_allowlist": "ウェブ管理者 IP 許可リスト", @@ -522,7 +522,7 @@ "invalid_shell": "無効なシェル: {shell}", "ip6tables_unavailable": "ここではip6tablesを使うことはできません。あなたはコンテナ内にいるか、カーネルがサポートしていません", "group_cannot_edit_primary_group": "グループ '{group}' を手動で編集することはできません。これは、特定のユーザーを 1 人だけ含むためのプライマリ グループです。", - "group_cannot_edit_visitors": "グループの「訪問者」を手動で編集することはできません。匿名の訪問者を代表する特別なグループです", + "group_cannot_edit_visitors": "グループの'訪問者'を手動で編集することはできません。匿名の訪問者を代表する特別なグループです", "group_creation_failed": "グループ '{group}' を作成できませんでした: {error}", "group_deleted": "グループ '{group}' が削除されました", "group_deletion_failed": "グループ '{group}' を削除できませんでした: {error}", @@ -547,24 +547,24 @@ "log_app_change_url": "{} アプリのアクセスURLを変更", "log_app_config_set": "‘{}’ アプリに設定を適用する", "log_app_makedefault": "‘{}’ をデフォルトのアプリにする", - "log_app_remove": "「{}」アプリを削除する", - "log_app_upgrade": "「{}」アプリをアップグレードする", + "log_app_remove": "'{}'アプリを削除する", + "log_app_upgrade": "アプリ '{app}' をアップグレードする", "log_available_on_yunopaste": "このログは、{url}", - "log_backup_create": "バックアップアーカイブを作成する", - "log_backup_restore_app": "バックアップを復元する ‘{name}’", - "log_backup_restore_system": "収集したファイルからバックアップアーカイブを作成しています...", + "log_backup_create": "バックアップ作成できませんでした", + "log_backup_restore_app": "バックアップアーカイブから'{}'を復元する", + "log_backup_restore_system": "バックアップアーカイブからシステムを復元する", "log_corrupted_md_file": "ログに関連付けられている YAML メタデータ ファイルが破損しています: '{md_file}\nエラー: {error}'", - "log_does_exists": "「{log}」という名前の操作ログはありません。「yunohostログリスト」を使用して、利用可能なすべての操作ログを表示します", - "log_domain_add": "ドメイン ‘{name}’ を追加する", + "log_does_exists": "'{log}'という名前の操作ログはありません。'yunohostログリスト'を使用して、利用可能なすべての操作ログを表示します", + "log_domain_add": "'{}'ドメインをシステム構成に追加する", "log_domain_config_set": "ドメイン '{}' の構成を更新する", - "log_domain_dns_push": "‘{name}’ DNSレコードを登録する", - "log_domain_main_domain": "「{}」をメインドメインにする", - "log_domain_remove": "システム構成から「{}」ドメインを削除する", - "log_dyndns_subscribe": "YunoHostコアのアップグレードを開始しています...", - "log_dyndns_update": "YunoHostサブドメイン「{}」に関連付けられているIPを更新します", - "log_help_to_get_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、「yunohostログ共有{name}」コマンドを使用してこの操作の完全なログを共有してください", - "log_help_to_get_log": "操作「{desc}」のログを表示するには、「yunohostログショー{name}」コマンドを使用します。", - "log_letsencrypt_cert_install": "「{}」ドメインにLet's Encrypt証明書をインストールする", + "log_domain_dns_push": "ドメイン '{}' の DNS レコードをプッシュする", + "log_domain_main_domain": "'{}'をメインドメインにする", + "log_domain_remove": "システム構成から'{}'ドメインを削除する", + "log_dyndns_subscribe": "YunoHostサブドメイン'{}'を購読する", + "log_dyndns_update": "YunoHostサブドメイン'{}'に関連付けられているIPを更新します", + "log_help_to_get_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、'yunohostログ共有{name}'コマンドを使用してこの操作の完全なログを共有してください", + "log_help_to_get_log": "操作'{desc}'のログを表示するには、'yunohostログショー{name}'コマンドを使用します。", + "log_letsencrypt_cert_install": "'{}'ドメインにLet's Encrypt証明書をインストールする", "log_letsencrypt_cert_renew": "Let’s Encrypt証明書を更新する", "log_link_to_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、 ここをクリックして この操作の完全なログを提供してください", "log_link_to_log": "この操作の完全なログ: ''{desc}", @@ -572,13 +572,13 @@ "log_permission_create": "作成権限 '{}'", "log_permission_delete": "削除権限 '{}'", "log_permission_url": "権限 '{}' に関連する URL を更新する", - "log_regen_conf": "システム設定", - "log_remove_on_failed_install": "インストールに失敗した後に「{}」を削除します", + "log_regen_conf": "システム構成 '{}' を再生成する", + "log_remove_on_failed_install": "インストールに失敗した後に'{}'を削除します", "log_resource_snippet": "リソースのプロビジョニング/プロビジョニング解除/更新", - "log_selfsigned_cert_install": "「{}」ドメインに自己署名証明書をインストールする", - "log_user_create": "「{}」ユーザーを追加する", - "log_user_delete": "「{}」ユーザーの削除", - "log_user_group_create": "「{}」グループの作成", + "log_selfsigned_cert_install": "'{}'ドメインに自己署名証明書をインストールする", + "log_user_create": "‘{}’ ユーザーを追加", + "log_user_delete": "‘{}’ ユーザーを削除", + "log_user_group_create": "‘{}’ グループを作成", "log_settings_reset": "設定をリセット", "log_settings_reset_all": "すべての設定をリセット", "log_settings_set": "設定を適用", @@ -587,9 +587,9 @@ "log_tools_reboot": "サーバーを再起動", "log_tools_shutdown": "サーバーをシャットダウン", "log_tools_upgrade": "システムパッケージのアップグレード", - "log_user_group_delete": "「{}」グループの削除", - "log_user_group_update": "'{}' グループを更新", - "log_user_import": "ユーザーのインポート", + "log_user_group_delete": "‘{}’ グループを削除", + "log_user_group_update": "‘{}’ グループを更新", + "log_user_import": "ユーザーをインポート", "mailbox_used_space_dovecot_down": "使用済みメールボックススペースをフェッチする場合は、Dovecotメールボックスサービスが稼働している必要があります", "log_user_permission_reset": "アクセス許可 '{}' をリセットします", "mailbox_disabled": "ユーザーの{user}に対して電子メールがオフになっている", @@ -600,25 +600,25 @@ "migration_0021_main_upgrade": "メインアップグレードを開始しています...", "migration_0021_not_enough_free_space": "/var/の空き容量はかなり少ないです!この移行を実行するには、少なくとも 1 GB の空き容量が必要です。", "migration_0021_modified_files": "次のファイルは手動で変更されていることが判明し、アップグレード後に上書きされる可能性があることに注意してください: {manually_modified_files}", - "migration_0021_not_buster2": "現在の Debian ディストリビューションは Buster ではありません!すでにBuster->Bullseyeの移行を実行している場合、このエラーは移行手順が100% s成功しなかったという事実の兆候です(そうでなければ、YunoHostは完了のフラグを立てます)。Webadminのツール>ログにある移行の**完全な**ログを必要とするサポートチームで何が起こったのかを調査することをお勧めします。", + "migration_0021_not_buster2": "現在の Debian ディストリビューションは Buster ではありません! すでにBuster->Bullseyeの移行を実行している場合、このエラーは移行手順が100% 完璧に成功しなかったという事実を意味します(そうでなければ、YunoHostは完了のフラグを立てます)。Web管理画面のツール>ログにある移行の**完全な**ログを取得し、サポートチームと共に何が起こったのか調査することをお勧めします。", "migration_0021_patch_yunohost_conflicts": "競合の問題を回避するためにパッチを適用しています...", "migration_0021_patching_sources_list": "sources.listsにパッチを適用しています...", - "migration_0021_problematic_apps_warning": "以下の問題のあるインストール済みアプリが検出されました。これらはYunoHostアプリカタログからインストールされていないか、「working」としてフラグが立てられていないようです。したがって、アップグレード後も動作することを保証することはできません: {problematic_apps}", + "migration_0021_problematic_apps_warning": "以下の問題のあるインストール済みアプリが検出されました。これらはYunoHostアプリカタログからインストールされていないか、'working'としてフラグが立てられていないようです。したがって、アップグレード後も動作することを保証することはできません: {problematic_apps}", "migration_0021_still_on_buster_after_main_upgrade": "メインのアップグレード中に問題が発生しましたが、システムはまだDebian Busterです", "migration_0021_system_not_fully_up_to_date": "システムが完全に最新ではありません。Bullseyeへの移行を実行する前に、まずは通常のアップグレードを実行してください。", "migration_0023_not_enough_space": "移行を実行するのに十分な領域を {path} で使用できるようにします。", "migration_0023_postgresql_11_not_installed": "PostgreSQL がシステムにインストールされていません。何もすることはありません。", "migration_0023_postgresql_13_not_installed": "PostgreSQL 11はインストールされていますが、PostgreSQL 13はインストールされてい!?:(システムで何か奇妙なことが起こった可能性があります...", - "migration_0024_rebuild_python_venv_broken_app": "このアプリ用にvirtualenvを簡単に再構築できないため、{app}スキップします。代わりに、「yunohostアプリのアップグレード-{app}を強制」を使用してこのアプリを強制的にアップグレードして、状況を修正する必要があります。", - "migration_0024_rebuild_python_venv_disclaimer_base": "Debian Bullseye へのアップグレード後、Debian に同梱されている新しい Python バージョンに変換するために、いくつかの Python アプリケーションを部分的に再構築する必要があります (技術的には、「virtualenv」と呼ばれるものを再作成する必要があります)。それまでの間、これらのPythonアプリケーションは機能しない可能性があります。YunoHostは、以下に詳述するように、それらのいくつかについて仮想環境の再構築を試みることができます。他のアプリの場合、または再構築の試行が失敗した場合は、それらのアプリのアップグレードを手動で強制する必要があります。", + "migration_0024_rebuild_python_venv_broken_app": "このアプリ用にvirtualenvを簡単に再構築できないため、{app}スキップします。代わりに、'yunohostアプリのアップグレード-{app}を強制'を使用してこのアプリを強制的にアップグレードして、状況を修正する必要があります。", + "migration_0024_rebuild_python_venv_disclaimer_base": "Debian Bullseye へのアップグレード後、Debian に同梱されている新しい Python バージョンに変換するために、いくつかの Python アプリケーションを部分的に再構築する必要があります (技術的には、'virtualenv'と呼ばれるものを再作成する必要があります)。それまでの間、これらのPythonアプリケーションは機能しない可能性があります。YunoHostは、以下に詳述するように、それらのいくつかについて仮想環境の再構築を試みることができます。他のアプリの場合、または再構築の試行が失敗した場合は、それらのアプリのアップグレードを手動で強制する必要があります。", "migration_0024_rebuild_python_venv_disclaimer_ignored": "これらのアプリに対して Virtualenvs を自動的に再構築することはできません。あなたはそれらのアップグレードを強制する必要があります、それはコマンドラインから行うことができます: 'yunohostアプリのアップグレード - -force APP':{ignored_apps}", "migration_0024_rebuild_python_venv_disclaimer_rebuild": "virtualenvの再構築は、次のアプリに対して試行されます(注意:操作には時間がかかる場合があります)。 {rebuild_apps}", - "migration_0024_rebuild_python_venv_failed": "{app} の Python virtualenv の再構築に失敗しました。これが解決されない限り、アプリは機能しない場合があります。「yunohostアプリのアップグレード--強制{app}」を使用してこのアプリのアップグレードを強制して、状況を修正する必要があります。", + "migration_0024_rebuild_python_venv_failed": "{app} の Python virtualenv の再構築に失敗しました。これが解決されない限り、アプリは機能しない場合があります。'yunohostアプリのアップグレード--強制{app}'を使用してこのアプリのアップグレードを強制して、状況を修正する必要があります。", "migration_0024_rebuild_python_venv_in_progress": "現在、 '{app}'のPython仮想環境を再構築しようとしています", - "migration_description_0021_migrate_to_bullseye": "システムを Debian ブルズアイと YunoHost 11.x にアップグレードする", - "migration_description_0022_php73_to_php74_pools": "php7.3-fpm 'pool' conf ファイルを php7.4 に移行します。", + "migration_description_0021_migrate_to_bullseye": "システムを Debian Bullseyeと YunoHost 11.x にアップグレードする", + "migration_description_0022_php73_to_php74_pools": "php7.3-fpm 'pool' conf ファイルを php7.4 に移行します", "migration_description_0023_postgresql_11_to_13": "PostgreSQL 11 から 13 へのデータベースの移行", - "migration_description_0024_rebuild_python_venv": "ブルズアイ移行後にPythonアプリを修復する", + "migration_description_0024_rebuild_python_venv": "Bullseye移行後にPythonアプリを修復する", "migration_description_0025_global_settings_to_configpanel": "従来のグローバル設定の命名法を新しい最新の命名法に移行する", "migration_ldap_rollback_success": "システムがロールバックされました。", "migrations_already_ran": "これらの移行は既に完了しています: {ids}", @@ -633,24 +633,24 @@ "migrations_running_forward": "移行{id}を実行しています...", "migrations_skip_migration": "移行{id}スキップしています...", "migrations_success_forward": "移行{id}完了しました", - "migrations_to_be_ran_manually": "移行{id}は手動で実行する必要があります。ウェブ管理ページの移行→ツールに移動するか、「yunohostツールの移行実行」を実行してください。", + "migrations_to_be_ran_manually": "移行{id}は手動で実行する必要があります。Web管理ページの移行→ツールに移動するか、'yunohost tools migrations run'を実行してください。", "not_enough_disk_space": "'{path}'に十分な空き容量がありません", "operation_interrupted": "操作は手動で中断されたようですね?", "migrations_no_migrations_to_run": "実行する移行はありません", - "migrations_no_such_migration": "「{id}」と呼ばれる移行はありません", + "migrations_no_such_migration": "'{id}'と呼ばれる移行はありません", "other_available_options": "...および{n}個の表示されない他の使用可能なオプション", "migrations_not_pending_cant_skip": "これらの移行は保留中ではないため、スキップすることはできません。 {ids}", "migrations_pending_cant_rerun": "これらの移行はまだ保留中であるため、再度実行することはできません{ids}", "password_confirmation_not_the_same": "パスワードが一致しません", - "password_listed": "このパスワードは、世界で最も使用されているパスワードの1つです。もっとユニークなものを選んでください。", - "password_too_long": "127文字未満のパスワードを選択してください", + "password_listed": "このパスワードは、世界で最も使用されているパスワードの1つです。もっと他の人と被っていないものを選んでください。", + "password_too_long": "127文字未満のパスワードを使用してください", "password_too_simple_2": "パスワードは8文字以上で、数字、大文字、小文字を含める必要があります", "password_too_simple_3": "パスワードは8文字以上で、数字、大文字、小文字、特殊文字を含める必要があります", "password_too_simple_4": "パスワードは12文字以上で、数字、大文字、小文字、特殊文字を含める必要があります", - "pattern_backup_archive_name": "最大 30 文字、英数字、-_ を含む有効なファイル名である必要があります。文字のみ", + "pattern_backup_archive_name": "有効なファイル名は最大 30 文字、英数字、-_. のみで構成されたものである必要があります。", "pattern_domain": "有効なドメイン名である必要があります(例:my-domain.org)", - "pattern_email": "「+」記号のない有効な電子メールアドレスである必要があります(例:someone@example.com)", - "pattern_email_forward": "有効な電子メールアドレスである必要があり、「+」記号が受け入れられます(例:someone+tag@example.com)", + "pattern_email": "'+'記号のない有効な電子メールアドレスである必要があります(例:someone@example.com)", + "pattern_email_forward": "有効な電子メールアドレスである必要があり、'+'記号が受け入れられます(例:someone+tag@example.com)", "pattern_firstname": "有効な名前(3 文字以上)である必要があります。", "pattern_fullname": "有効なフルネーム (3 文字以上) である必要があります。", "pattern_lastname": "有効な姓 (3 文字以上) である必要があります。", @@ -667,7 +667,7 @@ "permission_cant_add_to_all_users": "権限{permission}すべてのユーザーに追加することはできません。", "permission_created": "アクセス許可 '{permission}' が作成されました", "permission_creation_failed": "アクセス許可 '{permission}' を作成できませんでした: {error}", - "permission_currently_allowed_for_all_users": "このアクセス許可は現在、他のユーザーに加えてすべてのユーザーに付与されています。「all_users」権限を削除するか、現在付与されている他のグループを削除することをお勧めします。", + "permission_currently_allowed_for_all_users": "このアクセス許可は現在、他のユーザーに加えてすべてのユーザーに付与されています。'all_users'権限を削除するか、現在付与されている他のグループを削除することをお勧めします。", "permission_deleted": "権限 '{permission}' が削除されました", "permission_deletion_failed": "アクセス許可 '{permission}' を削除できませんでした: {error}", "permission_not_found": "アクセス許可 '{permission}' が見つかりません", @@ -687,36 +687,36 @@ "regenconf_file_remove_failed": "構成ファイル '{conf}' を削除できませんでした", "regenconf_file_removed": "構成ファイル '{conf}' が削除されました", "regenconf_file_updated": "構成ファイル '{conf}' が更新されました", - "regenconf_need_to_explicitly_specify_ssh": "ssh構成は手動で変更されていますが、実際に変更を適用するには、--forceでカテゴリ「ssh」を明示的に指定する必要があります。", + "regenconf_need_to_explicitly_specify_ssh": "ssh構成は手動で変更されていますが、実際に変更を適用するには、--forceでカテゴリ'ssh'を明示的に指定する必要があります。", "regenconf_now_managed_by_yunohost": "設定ファイル '{conf}' が YunoHost (カテゴリ {category}) によって管理されるようになりました。", "regenconf_pending_applying": "カテゴリ '{category}' に保留中の構成を適用しています...", "regenconf_up_to_date": "カテゴリ '{category}' の設定は既に最新です", - "regenconf_updated": "このカテゴリーにログが登録されていません", + "regenconf_updated": "'{category}' の構成が更新されました", "regenconf_would_be_updated": "カテゴリ '{category}' の構成が更新されているはずです。", "regex_incompatible_with_tile": "パッケージャー!アクセス許可 '{permission}' show_tile が 'true' に設定されているため、正規表現 URL をメイン URL として定義できません", "regex_with_only_domain": "ドメインに正規表現を使用することはできませんが、パスにのみ使用できます", "registrar_infos": "レジストラ情報", - "restore_already_installed_app": "'{name}' の ‘{id}’ パネル設定をアップデートする", + "restore_already_installed_app": "ID が'{app}'のアプリが既にインストールされている", "restore_already_installed_apps": "次のアプリは既にインストールされているため復元できません。 {apps}", "restore_backup_too_old": "このバックアップアーカイブは、古すぎるYunoHostバージョンからのものであるため、復元できません。", "restore_cleaning_failed": "一時復元ディレクトリをクリーンアップできませんでした", "restore_complete": "復元が完了しました", "restore_may_be_not_enough_disk_space": "システムに十分なスペースがないようです(空き:{free_space} B、必要なスペース:{needed_space} B、セキュリティマージン:{margin} B)", - "root_password_desynchronized": "管理者パスワードが変更されましたが、YunoHostはこれをrootパスワードに伝播できませんでした!", - "server_reboot_confirm": "サーバーはすぐに再起動しますが、よろしいですか?[{answers}]", + "root_password_desynchronized": "管理者パスワードが変更されましたが、YunoHostはこれをrootパスワードに反映できませんでした!", + "server_reboot_confirm": "サーバーはすぐに再起動しますが、よろしいですか? [{answers}]", "server_shutdown": "サーバーがシャットダウンします", "service_already_stopped": "サービス '{service}' は既に停止されています", "service_cmd_exec_failed": "コマンド '{command}' を実行できませんでした", - "service_description_nginx": "サーバーでホストされているすべてのWebサイトへのアクセスを提供または提供します", + "service_description_nginx": "サーバーでホストされているすべてのWebサイトへのアクセスを提供します", "service_description_redis-server": "高速データ・アクセス、タスク・キュー、およびプログラム間の通信に使用される特殊なデータベース", - "service_description_rspamd": "スパムやその他の電子メール関連機能をフィルタリングします", + "service_description_rspamd": "スパムフィルタリングやその他の電子メール関連機能", "service_description_slapd": "ユーザー、ドメイン、関連情報を格納します", "service_description_ssh": "ターミナル経由でサーバーにリモート接続できます(SSHプロトコル)", - "service_description_yunohost-api": "YunoHostウェブインターフェイスとシステム間の相互作用を管理します", + "service_description_yunohost-api": "YunoHostウェブインターフェイスとシステム間の連携を管理します", "service_description_yunohost-firewall": "サービスへの接続ポートの開閉を管理", - "service_description_yunomdns": "ローカルネットワークで「yunohost.local」を使用してサーバーに到達できます", + "service_description_yunomdns": "ローカルネットワークで'yunohost.local'を使用してサーバーに到達できます", "service_disable_failed": "起動時にサービス '{service}' を開始できませんでした。\n\n最近のサービスログ:{logs}", - "service_disabled": "システムの起動時にサービス '{service}' は開始されなくなります。", + "service_disabled": "システムの起動時にサービス '{service}' は自動開始されなくなります。", "service_reload_failed": "サービス '{service}' をリロードできませんでした\n\n最近のサービスログ:{logs}", "service_reload_or_restart_failed": "サービス '{service}' をリロードまたは再起動できませんでした\n\n最近のサービスログ:{logs}", "service_reloaded_or_restarted": "サービス '{service}' が再読み込みまたは再起動されました", @@ -730,12 +730,12 @@ "service_stopped": "サービス '{service}' が停止しました", "service_unknown": "不明なサービス '{service}'", "system_username_exists": "ユーザー名はシステムユーザーのリストにすでに存在します", - "this_action_broke_dpkg": "このアクションはdpkg / APT(システムパッケージマネージャ)を壊しました...SSH経由で接続し、「sudo apt install --fix-broken」および/または「sudo dpkg --configure -a」を実行することで、この問題を解決できます。", + "this_action_broke_dpkg": "このアクションはdpkg / APT(システムパッケージマネージャ)を壊しました... SSH経由で接続し、’sudo apt install --fix-broken’ および/または ’sudo dpkg --configure -a’ を実行することで、この問題を解決できるかもしれません。", "tools_upgrade": "システムパッケージのアップグレード", "tools_upgrade_failed": "パッケージをアップグレードできませんでした: {packages_list}", "unbackup_app": "{app}は保存されません", - "unexpected_error": "予期しない問題が発生しました:{error}", - "unknown_main_domain_path": "'{app}' の不明なドメインまたはパス。アクセス許可の URL を指定できるようにするには、ドメインとパスを指定する必要があります。", + "unexpected_error": "予期しない問題が発生しました: {error}", + "unknown_main_domain_path": "'{app}' のドメインまたはパスが不明です。アクセス許可の URL を指定できるようにするには、ドメインとパスを指定する必要があります。", "unrestore_app": "{app}は復元されません", "updating_apt_cache": "システムパッケージの利用可能なアップグレードを取得しています...", "upgrade_complete": "アップグレート完了", @@ -765,6 +765,6 @@ "yunohost_configured": "YunoHost が構成されました", "yunohost_installing": "YunoHostをインストールしています...", "yunohost_not_installed": "YunoHostが正しくインストールされていません。’yunohost tools postinstall’ を実行してください", - "yunohost_postinstall_end_tip": "インストール後処理が完了しました!セットアップを完了するには、次の点を考慮してください。\n - ウェブ管理画面の「診断」セクション(またはコマンドラインで’yunohost diagnosis run’)を通じて潜在的な問題を診断します。\n - 管理ドキュメントの「セットアップの最終処理」と「YunoHostを知る」の部分を読む: https://yunohost.org/admindoc。", + "yunohost_postinstall_end_tip": "インストール後処理が完了しました!セットアップを完了するには、次の点を考慮してください。\n - ウェブ管理画面の'診断'セクション(またはコマンドラインで’yunohost diagnosis run’)を通じて潜在的な問題を診断します。\n - 管理ドキュメントの'セットアップの最終処理'と'YunoHostを知る'の部分を読む: https://yunohost.org/admindoc。", "additional_urls_already_removed": "アクセス許可 ‘{permission}’ に対する追加URLで ‘{url}’ は既に削除されています" } From 5bb36ee0604ee6a8dc402a9dad2536466b9bcc08 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 14 Jul 2023 10:20:18 +0000 Subject: [PATCH 0188/1116] Translated using Weblate (German) Currently translated at 90.1% (692 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/locales/de.json b/locales/de.json index b61d0a431..e9ea0162f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -704,5 +704,14 @@ "app_not_enough_ram": "Diese App benötigt {required} RAM um installiert/aktualisiert zu werden, aber es sind aktuell nur {current} verfügbar.", "app_change_url_failed": "Kann die URL für {app} nicht ändern: {error}", "app_change_url_script_failed": "Es ist ein Fehler im URL-Änderungs-Script aufgetreten", - "app_resource_failed": "Automatische Ressourcen-Allokation (provisioning), die Unterbindung des Zugriffts auf Ressourcen (deprovisioning) oder die Aktualisierung der Ressourcen für {app} schlug fehl: {error}" -} \ No newline at end of file + "app_resource_failed": "Automatische Ressourcen-Allokation (provisioning), die Unterbindung des Zugriffts auf Ressourcen (deprovisioning) oder die Aktualisierung der Ressourcen für {app} schlug fehl: {error}", + "domain_config_cert_validity": "Validität", + "confirm_notifications_read": "WARNUNG: Sie sollten die App-Benachrichtigungen anschauen bevor sie weitermachen. Es könnte da Dinge geben, die gut zu wissen sein könnten. [{answers}]", + "domain_cannot_add_muc_upload": "Domänen, welche mit 'muc.' beginnen, können/dürfen Sie nicht hinzufügen. Dieser Namens-Typ ist reserviert für das in YunoHost integrierte XMPP-Multiuser-Chat-Feature.", + "domain_config_cert_summary_selfsigned": "WARNUNG: Aktuelles Zertifikat ist selbstssigniert. Browser werden neuen Besuchern eine furchteinflössende Warnung anzeigen!", + "app_failed_to_download_asset": "Konnte die Ressource '{source_id}' ({url}) für {app} nicht herunterladen: {out}", + "apps_failed_to_upgrade_line": "\n * {app_id} (um den zugehörigen Log anzuzeigen, führen Sie ein 'yunohost log show {operation_logger_name}' aus)", + "confirm_app_insufficient_ram": "GEFAHR! Diese App braucht {required} RAM um zu installieren/upgraden wobei momentan aber nur {current} vorhanden sind. Auch wenn diese App laufen könnte, würde ihr Installations- bzw. ihr Upgrade-Prozess eine grosse Menge an RAM brauchen, so dass Ihr Server anhalten und schrecklich versagen würde. Wenn Sie dieses Risiko einfach hinnehmen möchten, tippen Sie '{answers}'", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 sollte, sofern verfügbar, üblicherweise automatisch durch das System oder Ihren Provider konfiguriert werden. Andernfalls kann es notwendig sein, dass Sie ein paar Dinge selbst, händisch konfigurieren, wie es die Dokumentation erklärt: https://yunohost.org/#/ipv6.", + "app_corrupt_source": "YunoHost konnte die Ressource '{source_id}' ({url}) für {app} herunterladen, aber die Ressource stimmt mit der erwarteten Checksum nicht überein. Dies könnte entweder bedeuten, dass Ihr Server einfach ein vorübergehendes Netzwerkproblem hatte ODER dass der Upstream-Betreuer (oder ein schädlicher/arglistiger Akteur) die Ressource auf eine bestimmte Art verändert hat und dass die YunoHost-Paketierer das App-Manifest untersuchen und so aktualisieren müssen, dass es diese Veränderung reflektiert.\n Erwartete sha256-Prüfsumme: {expected_sha256}\n Heruntergeladene sha256-Prüfsumme: {computed_sha256}\n Heruntergeladene Dateigrösse: {size}" +} From 972e98d66fb1ae6cd21e26e1b6b86c7bc450bc3b Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 14 Jul 2023 12:36:44 +0000 Subject: [PATCH 0189/1116] Translated using Weblate (German) Currently translated at 96.3% (740 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 67 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/locales/de.json b/locales/de.json index e9ea0162f..f0f93e04c 100644 --- a/locales/de.json +++ b/locales/de.json @@ -23,9 +23,9 @@ "backup_archive_name_unknown": "Unbekanntes lokale Datensicherung mit Namen '{name}' gefunden", "backup_archive_open_failed": "Kann Sicherungsarchiv nicht öfnen", "backup_cleaning_failed": "Temporäres Sicherungsverzeichnis konnte nicht geleert werden", - "backup_created": "Datensicherung komplett", + "backup_created": "Datensicherung vollständig: {name}", "backup_delete_error": "Pfad '{path}' konnte nicht gelöscht werden", - "backup_deleted": "Backup wurde entfernt", + "backup_deleted": "Backup gelöscht: {name}", "backup_hook_unknown": "Der Datensicherungshook '{hook}' unbekannt", "backup_nothings_done": "Keine Änderungen zur Speicherung", "backup_output_directory_forbidden": "Wähle ein anderes Ausgabeverzeichnis. Datensicherungen können nicht in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var oder in Unterordnern von /home/yunohost.backup/archives erstellt werden", @@ -222,7 +222,7 @@ "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", - "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.", + "dpkg_is_broken": "Sie können dies gerade nicht machen, weil dpkg/APT (der Paketmanager des Systems) in einem defekten Zustand zu sein scheint... Sie können versuchen, dieses Problem zu lösen, indem Sie sich über SSH mit dem Server verbinden und `sudo apt install --fix-broken` und/oder `sudo dpkg --configure -a` und/oder `sudo dpkg --audit`ausführen.", "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}'", "log_app_remove": "Entferne die Applikation '{}'", @@ -314,7 +314,7 @@ "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.", "diagnosis_swap_ok": "Das System hat {total} Swap!", - "diagnosis_swap_tip": "Bitte beachte, dass das Betreiben der Swap-Partition auf einer SD-Karte oder SSD die Lebenszeit dieser drastisch reduziert.", + "diagnosis_swap_tip": "Bitte wahren Sie Vorsicht und Aufmerksamkeit, dass das Betreiben der Swap-Partition auf einer SD-Karte oder einer SSD die Lebenszeit dieses Geräts drastisch reduzieren kann.", "diagnosis_mail_outgoing_port_25_ok": "Der SMTP-Server ist in der Lage E-Mails zu versenden (der ausgehende Port 25 ist nicht blockiert).", "diagnosis_mail_outgoing_port_25_blocked": "Der SMTP-Server kann keine E-Mails an andere Server senden, weil der ausgehende Port 25 per IPv{ipversion} blockiert ist. Du kannst versuchen, diesen in der Konfigurations-Oberfläche deines Internet-Anbieters (oder Hosters) zu öffnen.", "diagnosis_mail_ehlo_unreachable": "Der SMTP-Server ist von außen nicht erreichbar per IPv{ipversion}. Er wird nicht in der Lage sein E-Mails zu empfangen.", @@ -352,7 +352,7 @@ "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": "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_fcrdns_nok_alternatives_4": "Einige Provider werden Ihnen nicht erlauben, den Reverse-DNS zu konfigurieren (oder deren Funktionalität ist defekt...). Falls Sie deswegen auf Probleme stossen sollten, ziehen Sie 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 könnte.
- Eine privatsphärenfreundlichere Alternative ist die Benutzung eines VPN *mit einer dedizierten öffentlichen IP* um Einschränkungen dieser Art zu umgehen. Schauen Sie hier nach https://yunohost.org/#/vpn_advantage
- Schließlich ist es auch möglich, zu einem anderen Provider 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", @@ -364,10 +364,10 @@ "diagnosis_http_connection_error": "Verbindungsfehler: konnte nicht zur angeforderten Domäne verbinden, es ist sehr wahrscheinlich, dass sie nicht erreichbat ist.", "diagnosis_http_could_not_diagnose_details": "Fehler: {error}", "diagnosis_http_could_not_diagnose": "Konnte nicht diagnostizieren, ob die Domäne von aussen per IPv{ipversion} erreichbar ist.", - "diagnosis_ports_partially_unreachable": "Port {port} ist von aussen per IPv{failed} nicht erreichbar.", - "diagnosis_ports_unreachable": "Port {port} ist von aussen nicht erreichbar.", + "diagnosis_ports_partially_unreachable": "Port {port} ist von Aussen her per IPv{failed} nicht erreichbar.", + "diagnosis_ports_unreachable": "Port {port} ist von Aussen her nicht erreichbar.", "diagnosis_ports_could_not_diagnose_details": "Fehler: {error}", - "diagnosis_security_vulnerable_to_meltdown_details": "Um dieses Problem zu beheben, solltest du dein System upgraden und neustarten um den neuen Linux-Kernel zu laden (oder deinen Server-Anbieter kontaktieren, falls das nicht funktionieren sollte). Besuche https://meltdownattack.com/ für weitere Informationen.", + "diagnosis_security_vulnerable_to_meltdown_details": "Um dieses Problem zu beheben, solltest Sie Ihr System upgraden und neustarten um den neuen Linux-Kernel zu laden (oder Ihren Server-Anbieter kontaktieren, falls das nicht funktionieren sollte). Besuchen Sie https://meltdownattack.com/ für weitere Informationen.", "diagnosis_ports_could_not_diagnose": "Konnte nicht diagnostizieren, ob die Ports von aussen per IPv{ipversion} erreichbar sind.", "diagnosis_description_regenconf": "Systemkonfiguration", "diagnosis_description_mail": "E-Mail", @@ -377,9 +377,9 @@ "diagnosis_description_dnsrecords": "DNS-Einträge", "diagnosis_description_ip": "Internetkonnektivität", "diagnosis_description_basesystem": "Grundsystem", - "diagnosis_security_vulnerable_to_meltdown": "Es scheint, als ob du durch die kritische Meltdown-Sicherheitslücke verwundbar bist", + "diagnosis_security_vulnerable_to_meltdown": "Es scheint als ob Sie durch die kritische Meltdown-Verwundbarkeit verwundbar sind", "diagnosis_regenconf_manually_modified": "Die Konfigurationsdatei {file} scheint manuell verändert worden zu sein.", - "diagnosis_regenconf_allgood": "Alle Konfigurationsdateien stimmen mit der empfohlenen Konfiguration überein!", + "diagnosis_regenconf_allgood": "Alle Konfigurationsdateien sind in Übereinstimmung mit der empfohlenen Konfiguration!", "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", @@ -389,10 +389,10 @@ "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 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.", + "domain_cannot_add_xmpp_upload": "Sie können keine Domänen hinzufügen, welche mit 'xmpp-upload.' beginnen. Diese Art von Namen ist für die in YunoHost integrierte XMPP-Upload-Feature 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.", - "diagnosis_processes_killed_by_oom_reaper": "Das System hat einige Prozesse beendet, weil ihm der Arbeitsspeicher ausgegangen ist. Das passiert normalerweise, wenn das System ingesamt nicht genügend Arbeitsspeicher zur Verfügung hat oder wenn ein einzelner Prozess zu viel Speicher verbraucht. Zusammenfassung der beendeten Prozesse: \n{kills_summary}", + "diagnosis_processes_killed_by_oom_reaper": "Das System hat ein paar Prozesse abgewürgt, da ihm der Speicher ausgegangen ist. Dies ist typischerweise sympomatisch eines ungenügenden Vorhandenseins des Arbeitsspeichers oder eines einzelnen Prozesses, der zu viel Speicher verbraucht. Zusammenfassung der abgewürgtenProzesse: \n{kills_summary}", "diagnosis_description_ports": "Geöffnete Ports", "additional_urls_already_added": "Zusätzliche URL '{url}' bereits hinzugefügt in der zusätzlichen URL für Berechtigung '{permission}'", "additional_urls_already_removed": "Zusätzliche URL '{url}' bereits entfernt in der zusätzlichen URL für Berechtigung '{permission}'", @@ -411,8 +411,8 @@ "group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber YunoHost wird sie entfernen...", "group_already_exist_on_system": "Die Gruppe {group} existiert bereits in den Systemgruppen", "group_already_exist": "Die Gruppe {group} existiert bereits", - "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_password": "SMTP-Relais-Passwort", + "global_settings_setting_smtp_relay_user": "SMTP-Relais-Benutzeraccount", "global_settings_setting_smtp_relay_port": "SMTP Relay Port", "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.", @@ -512,7 +512,7 @@ "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.", + "diagnosis_sshd_config_inconsistent": "Es scheint wie wenn der SSH-Port in /etc/ssh/sshd_config manuell verändert wurde. Seit YunoHost 4.2 ist eine neue globale Einstellung 'security.ssh.ssh_port' verfügbar, um zu verhindern, dass die Konfiguration händisch verändert wird.", "diagnosis_sshd_config_insecure": "Die SSH-Konfiguration wurde scheinbar manuell geändert und ist unsicher, weil sie keine 'AllowGroups'- oder 'AllowUsers' -Direktiven für die Beschränkung des Zugriffs durch autorisierte Benutzer enthält.", "backup_create_size_estimation": "Das Archiv wird etwa {size} an Daten enthalten.", "app_restore_script_failed": "Im Wiederherstellungsskript der Applikation ist ein Fehler aufgetreten", @@ -520,8 +520,8 @@ "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", - "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.", + "global_settings_setting_ssowat_panel_overlay_enabled": "Das 'YunoHost'-Portalverknüpfungsquadrätchen bei den Apps aktivieren", + "diagnosis_sshd_config_inconsistent_details": "Bitte führen Sie yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT aus, um den SSH-Port festzulegen, und überprüfen Sie yunohost tools regen-conf ssh --dry-run --with-diff und yunohost tools regen-conf ssh --force um Ihre 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} 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}", @@ -713,5 +713,36 @@ "apps_failed_to_upgrade_line": "\n * {app_id} (um den zugehörigen Log anzuzeigen, führen Sie ein 'yunohost log show {operation_logger_name}' aus)", "confirm_app_insufficient_ram": "GEFAHR! Diese App braucht {required} RAM um zu installieren/upgraden wobei momentan aber nur {current} vorhanden sind. Auch wenn diese App laufen könnte, würde ihr Installations- bzw. ihr Upgrade-Prozess eine grosse Menge an RAM brauchen, so dass Ihr Server anhalten und schrecklich versagen würde. Wenn Sie dieses Risiko einfach hinnehmen möchten, tippen Sie '{answers}'", "diagnosis_ip_no_ipv6_tip_important": "IPv6 sollte, sofern verfügbar, üblicherweise automatisch durch das System oder Ihren Provider konfiguriert werden. Andernfalls kann es notwendig sein, dass Sie ein paar Dinge selbst, händisch konfigurieren, wie es die Dokumentation erklärt: https://yunohost.org/#/ipv6.", - "app_corrupt_source": "YunoHost konnte die Ressource '{source_id}' ({url}) für {app} herunterladen, aber die Ressource stimmt mit der erwarteten Checksum nicht überein. Dies könnte entweder bedeuten, dass Ihr Server einfach ein vorübergehendes Netzwerkproblem hatte ODER dass der Upstream-Betreuer (oder ein schädlicher/arglistiger Akteur) die Ressource auf eine bestimmte Art verändert hat und dass die YunoHost-Paketierer das App-Manifest untersuchen und so aktualisieren müssen, dass es diese Veränderung reflektiert.\n Erwartete sha256-Prüfsumme: {expected_sha256}\n Heruntergeladene sha256-Prüfsumme: {computed_sha256}\n Heruntergeladene Dateigrösse: {size}" + "app_corrupt_source": "YunoHost konnte die Ressource '{source_id}' ({url}) für {app} herunterladen, aber die Ressource stimmt mit der erwarteten Checksum nicht überein. Dies könnte entweder bedeuten, dass Ihr Server einfach ein vorübergehendes Netzwerkproblem hatte ODER dass der Upstream-Betreuer (oder ein schädlicher/arglistiger Akteur) die Ressource auf eine bestimmte Art verändert hat und dass die YunoHost-Paketierer das App-Manifest untersuchen und so aktualisieren müssen, dass es diese Veränderung reflektiert.\n Erwartete sha256-Prüfsumme: {expected_sha256}\n Heruntergeladene sha256-Prüfsumme: {computed_sha256}\n Heruntergeladene Dateigrösse: {size}", + "global_settings_reset_success": "Reinitialisieren der globalen Einstellungen", + "global_settings_setting_root_password_confirm": "Neues root-Passwort (Bestätigung)", + "global_settings_setting_ssh_compatibility": "SSH-Kompatibilität", + "group_mailalias_remove": "Der E-Mail-Alias '{mail}' wird von der Gruppe '{group}' entfernt", + "group_user_add": "Der Benutzer '{user}' wird der Gruppe '{group}' hinzugefügt werden", + "global_settings_setting_nginx_compatibility": "NGINX-Kompatibilität", + "global_settings_setting_passwordless_sudo": "Erlauben Sie Administratoren 'sudo' zu benützen, ohne das Passwort erneut einzugeben", + "global_settings_setting_smtp_allow_ipv6": "Autorisiere das IPv6", + "domain_config_default_app_help": "Personen werden automatisch zu dieser App weitergeleitet, wenn sie diese Domäne öffnen. Wenn keine App spezifiziert wurde, werden Personen zum Benutzerportal-Login-Formular weitergeleitet.", + "domain_config_xmpp_help": "NB: ein paar XMPP-Features werden voraussetzen, dass Sie Ihre DNS-Einträge aktualisieren und Ihr Lets-Encrypt-Zertifikat neu erstellen, um eingeschaltet zu werden", + "global_settings_setting_smtp_relay_host": "Adresse des SMTP-Relais", + "global_settings_setting_nginx_redirect_to_https": "HTTPS erzwingen", + "group_no_change": "Nichts zu ändern für die Gruppe '{group}'", + "global_settings_setting_admin_strength_help": "Diese Parameter werden nur bei einer Initiailisierung oder einer Passwortänderung anwandt", + "global_settings_setting_backup_compress_tar_archives": "Datensicherungen komprimieren", + "global_settings_setting_dns_exposure_help": "NB: Dies beinflusst nur die vorgeschlagenen DNS-Konfigurations- und -Diagnose-Überprüfungen. Dies beeinflusst keine Systemkonfigurationen.", + "global_settings_setting_pop3_enabled": "POP3 einschalten", + "global_settings_setting_pop3_enabled_help": "POP3-Protokoll für den Mail-Server aktivieren", + "global_settings_setting_portal_theme_help": "Weitere Informationen dazu, wie Custom-Portal-Themes kreiert werden können unter https://yunohost.org/theming", + "global_settings_setting_postfix_compatibility": "Postfix-Kompatibilität", + "global_settings_setting_root_password": "Neues root-Passwort", + "global_settings_setting_user_strength_help": "Diese Parameter werden nur bei einer Initialisierung des oder Änderung des Passworts angewandt", + "global_settings_setting_webadmin_allowlist": "Allowlist für die Webadmin-IPs", + "global_settings_setting_webadmin_allowlist_enabled": "Webadmin-IP-Allowlist aktivieren", + "group_update_aliases": "Aktualisieren der Aliase für die Gruppe '{group}'", + "global_settings_setting_portal_theme": "Portal-Theme", + "global_settings_setting_smtp_relay_enabled": "Aktiviere das SMTP-Relais", + "global_settings_setting_dns_exposure": "Bei DNS-Konfiguration und -Diagnose zu berücksichtigende IP-Versionen", + "global_settings_setting_root_access_explain": "Auf Linux-Systemen ist 'root' der absolute Administrator. Im Kontext von YunoHost ist der direkte 'root'-SSH-Login standardmässig deaktiviert - ausgenommen des lokalen Netzwerks des Servers. Mitglieder der 'admins'-Gruppe sind in der Lage mit dem 'sudo'-Befehl in der Kommandozeile (CLI) als root zu agieren. Nun kann es hilfreich sein, ein (robustes) root-Passwort zu haben um das System zu debuggen oder für den Fall, dass sich die regulären Administratoren nicht mehr einloggen können.", + "global_settings_setting_ssh_password_authentication": "Authentifizieren mit Passwort", + "group_mailalias_add": "Der E-Mail-Alias '{mail}' wird der Gruppe '{group}' hinzugefügt" } From a81d688dc1a423e1da6d8d524def7f8b9531ecf7 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 14 Jul 2023 13:20:30 +0000 Subject: [PATCH 0190/1116] Translated using Weblate (German) Currently translated at 96.7% (743 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index f0f93e04c..6df5a4ebe 100644 --- a/locales/de.json +++ b/locales/de.json @@ -744,5 +744,8 @@ "global_settings_setting_dns_exposure": "Bei DNS-Konfiguration und -Diagnose zu berücksichtigende IP-Versionen", "global_settings_setting_root_access_explain": "Auf Linux-Systemen ist 'root' der absolute Administrator. Im Kontext von YunoHost ist der direkte 'root'-SSH-Login standardmässig deaktiviert - ausgenommen des lokalen Netzwerks des Servers. Mitglieder der 'admins'-Gruppe sind in der Lage mit dem 'sudo'-Befehl in der Kommandozeile (CLI) als root zu agieren. Nun kann es hilfreich sein, ein (robustes) root-Passwort zu haben um das System zu debuggen oder für den Fall, dass sich die regulären Administratoren nicht mehr einloggen können.", "global_settings_setting_ssh_password_authentication": "Authentifizieren mit Passwort", - "group_mailalias_add": "Der E-Mail-Alias '{mail}' wird der Gruppe '{group}' hinzugefügt" + "group_mailalias_add": "Der E-Mail-Alias '{mail}' wird der Gruppe '{group}' hinzugefügt", + "group_user_remove": "Der Benutzer '{user}' wird von der Gruppe '{group}' entfernt werden", + "invalid_credentials": "Ungültiges Passwort oder Benutzername", + "invalid_shell": "Ungültiger Shell: {shell}" } From 373dabbcb08c1707ea585a4ce57065e71ee1a8c7 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 14 Jul 2023 14:14:25 +0000 Subject: [PATCH 0191/1116] Translated using Weblate (German) Currently translated at 100.0% (768 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/locales/de.json b/locales/de.json index 6df5a4ebe..1e2bd5c38 100644 --- a/locales/de.json +++ b/locales/de.json @@ -67,11 +67,11 @@ "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": "Es muss ein gültiger Dateiname mit maximal 30 Zeichen sein, nur alphanumerische Zeichen und -_.", + "pattern_backup_archive_name": "Muss ein gültiger Dateiname mit maximal 30 Zeichen sein, ausschliesslich alphanumerische Zeichen und -_.", "pattern_domain": "Muss ein gültiger Domainname sein (z.B. meine-domain.org)", "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_firstname": "Muss ein gültiger Vorname sein (mindestens 3 Zeichen)", + "pattern_lastname": "Muss ein gültiger Nachname sein (mindestens 3 Zeichen)", "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", @@ -228,12 +228,12 @@ "log_app_remove": "Entferne die Applikation '{}'", "log_app_install": "Installiere die Applikation '{}'", "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.", + "good_practices_about_admin_password": "Die sind 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}''", "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 '{}'", - "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.", + "good_practices_about_user_password": "Sie sind nun dabei, ein neues Benutzerpasswort 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", @@ -282,7 +282,7 @@ "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 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", + "mail_unavailable": "Diese E-Mail-Adresse ist für die Administratoren-Gruppe reserviert", "diagnosis_services_conf_broken": "Die Konfiguration für den Dienst {service} ist fehlerhaft!", "diagnosis_services_running": "Dienst {service} läuft!", "diagnosis_domain_expires_in": "{domain} läuft in {days} Tagen ab.", @@ -310,7 +310,7 @@ "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": "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_ok": "Der SMTP-Server ist von Aussen 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.", "diagnosis_swap_ok": "Das System hat {total} Swap!", @@ -362,7 +362,7 @@ "diagnosis_http_partially_unreachable": "Die Domäne {domain} scheint von aussen via HTTP per IPv{failed} nicht erreichbar zu sein, obwohl es per IPv{passed} funktioniert.", "diagnosis_http_unreachable": "Die Domäne {domain} scheint von aussen per HTTP nicht erreichbar zu sein.", "diagnosis_http_connection_error": "Verbindungsfehler: konnte nicht zur angeforderten Domäne verbinden, es ist sehr wahrscheinlich, dass sie nicht erreichbat ist.", - "diagnosis_http_could_not_diagnose_details": "Fehler: {error}", + "diagnosis_http_could_not_diagnose_details": "Fehler: {error}", "diagnosis_http_could_not_diagnose": "Konnte nicht diagnostizieren, ob die Domäne von aussen per IPv{ipversion} erreichbar ist.", "diagnosis_ports_partially_unreachable": "Port {port} ist von Aussen her per IPv{failed} nicht erreichbar.", "diagnosis_ports_unreachable": "Port {port} ist von Aussen her nicht erreichbar.", @@ -446,8 +446,8 @@ "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}]", - "migrations_pending_cant_rerun": "Diese Migrationen sind immer noch anstehend und können deshalb nicht erneut durchgeführt werden: {ids}", - "migrations_not_pending_cant_skip": "Diese Migrationen sind nicht anstehend und können deshalb nicht übersprungen werden: {ids}", + "migrations_pending_cant_rerun": "Diese Migrationen sind immer noch ausstehend und können deshalb nicht erneut durchgeführt werden: {ids}", + "migrations_not_pending_cant_skip": "Diese Migrationen sind nicht ausstehend und können deshalb nicht übersprungen werden: {ids}", "migrations_success_forward": "Migration {id} abgeschlossen", "migrations_dependencies_not_satisfied": "Führe diese Migrationen aus: '{dependencies_id}', bevor du {id} migrierst.", "migrations_failed_to_load_migration": "Konnte Migration nicht laden {id}: {error}", @@ -548,7 +548,7 @@ "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 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.", + "yunohost_postinstall_end_tip": "Post-Installation ist fertig! Um das Setup abzuschliessen, wird folgendes empfohlen:\n - mögliche Fehler diagnostizieren im Bereich 'Diagnose' des Adminbereichs (oder mittels 'yunohost diagnosis run' in der Kommandozeile;\n - Die Abschnitte 'Install YunoHost' und 'Geführte Tour' im Administratorenhandbuch lesen: https://yunohost.org/admindoc.", "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}", "disk_space_not_sufficient_update": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu aktualisieren", @@ -654,7 +654,7 @@ "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_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den SSH-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte). Bei Bedarf können Sie in https://infosec.mozilla.org/guidelines/openssh die Informationen nachlesen.", "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.", @@ -677,7 +677,7 @@ "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_stable_codename_details": "Dies hat meistens eine fehlerhafte Konfiguration seitens Hosting-Provider zur Ursache. Dies stellt eine Gefahr dar, da sobald die nächste Debian-Version zum neuen 'stable' wird, führt apt eine Aktualisierung aller System-Pakete durch, ohne eine ordnungsgemässe Migration zu durchlaufen. Es wird dringlich darauf hingewiesen, dies zu berichtigen indem Sie die Datei der apt-Quellen des Debian-Base-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", @@ -747,5 +747,24 @@ "group_mailalias_add": "Der E-Mail-Alias '{mail}' wird der Gruppe '{group}' hinzugefügt", "group_user_remove": "Der Benutzer '{user}' wird von der Gruppe '{group}' entfernt werden", "invalid_credentials": "Ungültiges Passwort oder Benutzername", - "invalid_shell": "Ungültiger Shell: {shell}" + "invalid_shell": "Ungültiger Shell: {shell}", + "migration_description_0025_global_settings_to_configpanel": "Migrieren der Legacy-Global-Einstellungsnomenklatur zur neuen, modernen Nomenklatur", + "root_password_changed": "Das root-Passwort wurde geändert", + "password_too_long": "Bitte wählen Sie ein Passwort aus, das weniger als 127 Zeichen hat", + "registrar_infos": "Registrar-Informationen (Herausgeber der Domainnamen/Domänennamen)", + "migration_0024_rebuild_python_venv_in_progress": "Probiere die Erneuerung der Python-virtualenv für `{app}`", + "migration_description_0024_rebuild_python_venv": "Reparieren der Python-Applikation nach Bullseye-Migration", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Erneuerung der virtualenv wird für die folgenden Applikationen versucht (NB: die Operation kann einige Zeit in Anspruch nehmen!): {rebuild_apps}", + "log_resource_snippet": "Provisioning/Deprovisioning/Aktualisieren einer Ressource", + "log_settings_reset_all": "alle Parameter rücksetzen", + "log_settings_set": "Parameter anwenden", + "password_confirmation_not_the_same": "Das Passwort und die Bestätigung stimmen nicht überein", + "log_settings_reset": "Einstellungen rücksetzen", + "migration_0024_rebuild_python_venv_broken_app": "Ignoriere {app} weil virtualenv für diese Applikation nicht auf einfache Weise neu gebaut werden kann. Stattdessen sollten Sie die Situation berichtigen indem Sie das Aktualisieren der Applikation erzwingen indem Sie `yunohost app upgrade --force {app}`nutzen.", + "migration_0024_rebuild_python_venv_failed": "Fehler aufgetreten bei der Erneuerung der Python-virtualenv für Applikation {app}. Die Applikation kann nicht mehr funktionieren solange die Situation nicht behoben worden ist. Sie sollten diesen Umstand durch eine erzwungene Aktualisierung für diese Applikation beheben indem Sie `yunohost app upgrade --force {app}`benützen.", + "migration_description_0026_new_admins_group": "Migrieren in das neue 'Multiple-Administratoren'-Managementsystem (mehrere Benutzer können in der Gruppe 'Administratoren' präsent sein, mit allen Rechten von Administratoren auf der ganzen YunoHost-Instanz)", + "pattern_fullname": "Muss ein gültiger voller Name sein (mindestens 3 Zeichen)", + "migration_0021_not_buster2": "Die aktuelle Debian-Distribution ist nicht Buster! Wenn Sie bereits eine Buster->Bullseye-Migration durchgeführt haben, dann ist dieser Fehler symptomatisch für den Umstand, dass das Migrationsprozedere nicht zu 100% erfolgreich war (andernfalls hätte Yunohost es als vollständig gekennzeichnet). Es ist empfehlenswert, sich der Geschehnisse zusammen mit dem Support-Team anzunehmen, das einen Bedarf an einem **vollständigen** Log der Migration haben wird, das in Werkzeuge > Logs im Adminpanel auffindbar ist.", + "migration_0024_rebuild_python_venv_disclaimer_base": "Der Aktualisierung zu Debian Bullseye folgend ist es nötig, dass ein paar Python-Applikationen partiell neu gebaut und in die neue, mit Debian mitgelieferte Python-Version konvertiert werden. (in technischen Begrifflichkeiten: das, was wir die 'virtualenv' nennen, muss erneuert werden). In der Zwischenzeit kann es sein, dass diese Python-Applikationen nicht funktionieren. YunoHost kann versuchen die virtualenv für ein paar davon zu erneuern, wie untenstehend detailliert beschrieben wird. Für die anderen Applikationen, oder für den Fall, dass die Erneuerung fehlschlägt, werden eine erzwungene Aktualisierung für diese Applikationen durchführen müssen.", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs können für diese Applikationen nicht automatisch erneuert werden. Für diejenigen werden Sie eine erzwungene Aktualisierung durchführen müssen, was in der Kommandozeile bewerkstelligt werden kann mit: `yunohost app upgrade --force APP`: {ignored_apps}" } From 606335a47484c9e9941d50b15a0bfabe4ab69504 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 14 Jul 2023 18:15:23 +0000 Subject: [PATCH 0192/1116] Translated using Weblate (German) Currently translated at 100.0% (768 of 768 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 66 ++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/locales/de.json b/locales/de.json index 1e2bd5c38..634107b1f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -32,7 +32,7 @@ "backup_output_directory_not_empty": "Der gewählte Ausgabeordner sollte leer sein", "backup_output_directory_required": "Für die Datensicherung muss ein Zielverzeichnis angegeben werden", "backup_running_hooks": "Datensicherunghook wird ausgeführt...", - "custom_app_url_required": "Du musst eine URL angeben, um deine benutzerdefinierte App {app} zu aktualisieren", + "custom_app_url_required": "Sie müssen eine URL angeben, um Ihre benutzerdefinierte App {app} zu aktualisieren", "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", "domain_created": "Domäne erstellt", "domain_creation_failed": "Konnte Domäne nicht erzeugen", @@ -79,11 +79,11 @@ "port_already_closed": "Der Port {port} wurde bereits für {ip_version} Verbindungen geschlossen", "port_already_opened": "Der Port {port} wird bereits von {ip_version} benutzt", "restore_already_installed_app": "Eine Applikation mit der ID '{app}' ist bereits installiert", - "restore_cleaning_failed": "Das temporäre Dateiverzeichnis für Systemrestaurierung konnte nicht gelöscht werden", + "restore_cleaning_failed": "Das temporäre Dateiverzeichnis für die Systemwiederherstellung konnte nicht gelöscht werden", "restore_complete": "Vollständig wiederhergestellt", - "restore_confirm_yunohost_installed": "Möchtest du die Wiederherstellung wirklich starten? [{answers}]", + "restore_confirm_yunohost_installed": "Möchten Sie die Wiederherstellung wirklich starten? [{answers}]", "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_hook_unavailable": "Das Wiederherstellungsskript für '{part}' steht weder in Ihrem System noch im Archiv zur Verfügung", "restore_nothings_done": "Nichts wurde wiederhergestellt", "restore_running_app_script": "App '{app}' wird wiederhergestellt...", "restore_running_hooks": "Wiederherstellung wird gestartet...", @@ -140,8 +140,8 @@ "dyndns_key_not_found": "DNS-Schlüssel für die Domain wurde nicht gefunden", "dyndns_no_domain_registered": "Keine Domain mit DynDNS registriert", "mailbox_used_space_dovecot_down": "Der Dovecot-Mailbox-Dienst muss aktiv sein, wenn du den von der Mailbox belegten Speicher abrufen willst", - "certmanager_attempt_to_replace_valid_cert": "Du versuchst gerade eine richtiges und gültiges Zertifikat der Domain {domain} zu überschreiben! (Benutze --force , um diese Nachricht zu umgehen)", - "certmanager_domain_cert_not_selfsigned": "Das Zertifikat der Domain {domain} ist kein selbstsigniertes Zertifikat. Bist du sich sicher, dass du es ersetzen willst? (Benutze dafür '--force')", + "certmanager_attempt_to_replace_valid_cert": "Sie versuchen gerade ein gutes und gültiges Zertifikat der Domäne {domain} zu überschreiben! (Benutzen Sie --force , um diese Nachricht zu umgehen)", + "certmanager_domain_cert_not_selfsigned": "Das Zertifikat der Domäne {domain} ist kein selbstsigniertes Zertifikat. Sind Sie sicher, dass Sie es ersetzen möchten? (Verwenden Sie dafür '--force')", "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)", @@ -159,7 +159,7 @@ "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.", + "app_already_installed_cant_change_url": "Diese Applikation ist bereits installiert. Die URL kann durch diese Funktion nicht modifiziert werden. Überprüfen Sie ob `app changeurl` verfügbar ist.", "app_change_url_identical_domains": "Die alte und neue domain/url_path sind identisch: ('{domain} {path}'). Es gibt nichts zu tun.", "app_already_up_to_date": "{app} ist bereits aktuell", "backup_abstract_method": "Diese Backup-Methode wird noch nicht unterstützt", @@ -177,14 +177,14 @@ "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 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_danger": "WARNUNG! Diese Applikation ist noch experimentell (wenn nicht sogar ausdrücklich nicht funktionsfähig)! Sie sollten sie wahrscheinlich NICHT installieren, es sei denn, Sie wissen, was Sie tun. Es wird keine Unterstützung angeboten, falls diese Applikation nicht funktionieren oder Ihr System beschädigen sollte... Falls Sie bereit sind, dieses Risiko einzugehen, tippen Sie '{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", "backup_system_part_failed": "Der Systemteil '{part}' konnte nicht gesichert werden", "backup_permission": "Sicherungsberechtigung für {app}", - "backup_output_symlink_dir_broken": "Dein Archivverzeichnis '{path}' ist ein fehlerhafter Symlink. Vielleicht hast du vergessen, das Speichermedium, auf das er verweist, neu zu mounten oder einzustecken.", + "backup_output_symlink_dir_broken": "Ihr Archivverzeichnis '{path}' ist ein fehlerhafter Symlink. Vielleicht haben Sie vergessen, das Speichermedium, auf das er verweist, neu zu mounten oder einzustecken.", "backup_mount_archive_for_restore": "Archiv für Wiederherstellung vorbereiten...", "backup_method_tar_finished": "Tar-Backup-Archiv erstellt", "backup_method_custom_finished": "Benutzerdefinierte Sicherungsmethode '{method}' beendet", @@ -193,7 +193,7 @@ "backup_custom_backup_error": "Bei der benutzerdefinierten Sicherungsmethode ist beim Arbeitsschritt \"Sicherung\" ein Fehler aufgetreten", "backup_csv_creation_failed": "Die zur Wiederherstellung erforderliche CSV-Datei kann nicht erstellt werden", "backup_couldnt_bind": "{src} konnte nicht an {dest} angebunden werden.", - "backup_ask_for_copying_if_needed": "Möchtest du die Sicherung mit {size}MB temporär durchführen? (Dieser Weg wird verwendet, da einige Dateien nicht mit einer effizienteren Methode vorbereitet werden konnten.)", + "backup_ask_for_copying_if_needed": "Möchten Sie die Datensicherung mit {size}MB temporär durchführen? (Dieser Weg wird verwendet, da einige Dateien nicht mit einer effizienteren Methode vorbereitet werden konnten.)", "backup_actually_backuping": "Erstellt ein Backup-Archiv aus den gesammelten Dateien...", "ask_new_path": "Neuer Pfad", "ask_new_domain": "Neue Domain", @@ -207,7 +207,7 @@ "app_not_upgraded": "Die App '{failed_app}' konnte nicht aktualisiert werden. Infolgedessen wurden die folgenden App-Upgrades abgebrochen: {apps}", "app_make_default_location_already_used": "Die App \"{app}\" kann nicht als Standard für die Domain \"{domain}\" festgelegt werden. Sie wird bereits von \"{other_app}\" verwendet", "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).", + "app_action_cannot_be_ran_because_required_services_down": "Diese erforderlichen Dienste sollten zur Durchführung dieser Aktion laufen: {services}. Versuchen Sie, 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.", "app_action_broke_system": "Diese Aktion scheint diese wichtigen Dienste unterbrochen zu haben: {services}", "apps_already_up_to_date": "Alle Apps sind bereits aktuell", @@ -248,7 +248,7 @@ "diagnosis_basesystem_kernel": "Server läuft unter Linux-Kernel {kernel_version}", "diagnosis_basesystem_ynh_single_version": "{package} Version: {version} ({repo})", "diagnosis_basesystem_ynh_main_version": "Server läuft YunoHost {main_version} ({repo})", - "diagnosis_basesystem_ynh_inconsistent_versions": "Du verwendest inkonsistente Versionen der YunoHost-Pakete... wahrscheinlich wegen eines fehlgeschlagenen oder teilweisen Upgrades.", + "diagnosis_basesystem_ynh_inconsistent_versions": "Sie verwenden inkonsistente Versionen der YunoHost-Pakete... wahrscheinlich wegen eines fehlgeschlagenen oder teilweisen Upgrades.", "apps_catalog_init_success": "App-Katalogsystem initialisiert!", "apps_catalog_updating": "Aktualisierung des Applikationskatalogs...", "apps_catalog_failed_to_download": "Der {apps_catalog} App-Katalog kann nicht heruntergeladen werden: {error}", @@ -264,23 +264,23 @@ "diagnosis_ip_no_ipv6": "Der Server verfügt nicht über eine funktionierende IPv6-Adresse.", "diagnosis_ip_not_connected_at_all": "Der Server scheint überhaupt nicht mit dem Internet verbunden zu sein!?", "diagnosis_failed_for_category": "Diagnose fehlgeschlagen für die Kategorie '{category}': {error}", - "diagnosis_cache_still_valid": "(Cache noch gültig für {category} Diagnose. Es wird keine neue Diagnose durchgeführt!)", + "diagnosis_cache_still_valid": "(Der Cache für die {category} Diagnose ist noch gültig. 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_found_errors_and_warnings": "{errors} erhebliche(s) Problem(e) (und {warnings} Warnung(en)) in Verbindung mit {category} gefunden!", "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})", "diagnosis_ignored_issues": "(+ {nb_ignored} ignorierte(s) Problem(e))", "diagnosis_basesystem_hardware": "Server Hardware Architektur ist {virt} {arch}", - "diagnosis_found_errors": "Habe {errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!", + "diagnosis_found_errors": "{errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!", "diagnosis_found_warnings": "Habe {warnings} Ding(e) gefunden, die verbessert werden könnten für {category}.", "diagnosis_ip_dnsresolution_working": "Domänen-Namens-Auflösung funktioniert!", "diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber sei vorsichtig wenn du deine eigene /etc/resolv.conf verwendest.", - "diagnosis_display_tip": "Um die gefundenen Probleme zu sehen, kannst du zum Diagnose-Bereich des webadmin gehen, oder 'yunohost diagnosis show --issues --human-readable' in der Kommandozeile ausführen.", + "diagnosis_display_tip": "Damit Sie die gefundenen Probleme anschauen können, gehen Sie zum Diagnose-Bereich des Admin-Panels, oder führen Sie 'yunohost diagnosis show --issues --human-readable' in der Kommandozeile aus.", "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.", + "app_packaging_format_not_supported": "Diese App kann nicht installiert werden da das Paketformat nicht von der YunoHost-Version unterstützt wird. Am besten sollten Sie Ihr System aktualisieren.", "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 für die Administratoren-Gruppe reserviert", "diagnosis_services_conf_broken": "Die Konfiguration für den Dienst {service} ist fehlerhaft!", @@ -290,23 +290,23 @@ "diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.", "diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!", "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden", - "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost dyndns update --force ein Update erzwingen.", + "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls können Sie mittels yunohost dyndns update --force ein Update erzwingen.", "diagnosis_dns_point_to_doc": "Bitte schauen Sie in der Dokumentation unter https://yunohost.org/dns_config nach, wenn Sie Hilfe bei der Konfiguration der DNS-Einträge benötigen.", "diagnosis_dns_discrepancy": "Der folgende DNS Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}", - "diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration solltest du einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}", + "diagnosis_dns_missing_record": "Gemäss der empfohlenen DNS-Konfiguration sollten Sie 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": "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_diskusage_verylow": "Der Speicher {mountpoint} (auf Gerät {device}) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von ingesamt {total}). Sie sollten 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 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.", + "certmanager_warning_subdomain_dns_record": "Die Subdomäne \"{subdomain}\" löst nicht zur gleichen IP Adresse auf wie \"{domain}\". Einige Funktionen werden nicht verfügbar sein bis Sie dies behoben und die Zertifikate neu erzeugt haben.", "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": "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).", @@ -397,9 +397,9 @@ "additional_urls_already_added": "Zusätzliche URL '{url}' bereits hinzugefügt in der zusätzlichen URL für Berechtigung '{permission}'", "additional_urls_already_removed": "Zusätzliche URL '{url}' bereits entfernt in der zusätzlichen URL für Berechtigung '{permission}'", "app_label_deprecated": "Dieser Befehl ist veraltet! Bitte nutze den neuen Befehl 'yunohost user permission update' um das Applabel zu verwalten.", - "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_hairpinning_issue_details": "Das liegt wahrscheinlich an Ihrem Router. Dadurch können Personen von ausserhalb deines Netzwerkes, aber nicht von innerhalb deines lokalen Netzwerkes (wie wahrscheinlich Sie selbst), auf Ihren Server zugreifen, wenn dazu die Domäne oder öffentliche IP verwendet wird. Sie können das Problem eventuell beheben, indem Sie einen Blick auf https://yunohost.org/dns_local_network werfen", "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_http_bad_status_code": "Es sieht so aus als ob ein anderes Gerät (vielleicht dein Router/Modem) anstelle Ihres 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üfen Sie ob Ihre Firewall oder Reverse-Proxy die Verbindung stören.", "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.", @@ -505,12 +505,12 @@ "restore_system_part_failed": "Die Systemteile '{part}' konnten nicht wiederhergestellt werden", "restore_removing_tmp_dir_failed": "Ein altes, temporäres Directory konnte nicht entfernt werden", "restore_not_enough_disk_space": "Nicht genug Speicher (Speicher: {free_space} B, benötigter Speicher: {needed_space} B, Sicherheitspuffer: {margin} B)", - "restore_may_be_not_enough_disk_space": "Dein System scheint nicht genug Speicherplatz zu haben (frei: {free_space} B, benötigter Platz: {needed_space} B, Sicherheitspuffer: {margin} B)", - "restore_extracting": "Packe die benötigten Dateien aus dem Archiv aus...", + "restore_may_be_not_enough_disk_space": "Ihr System scheint nicht genug Speicherplatz zu haben (frei: {free_space} B, benötigter Platz: {needed_space} B, Sicherheitspuffer: {margin} B)", + "restore_extracting": "Auspacken der benötigten Dateien aus dem Archiv...", "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", + "regex_with_only_domain": "Sie können regex nicht als Domain verwenden, sondern nur als Pfad", "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.", + "regenconf_need_to_explicitly_specify_ssh": "Die SSH-Konfiguration wurde manuell modifiziert, aber Sie müssen 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 scheint wie wenn der SSH-Port in /etc/ssh/sshd_config manuell verändert wurde. Seit YunoHost 4.2 ist eine neue globale Einstellung 'security.ssh.ssh_port' verfügbar, um zu verhindern, dass die Konfiguration händisch verändert wird.", "diagnosis_sshd_config_insecure": "Die SSH-Konfiguration wurde scheinbar manuell geändert und ist unsicher, weil sie keine 'AllowGroups'- oder 'AllowUsers' -Direktiven für die Beschränkung des Zugriffs durch autorisierte Benutzer enthält.", @@ -522,15 +522,15 @@ "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 'YunoHost'-Portalverknüpfungsquadrätchen bei den Apps aktivieren", "diagnosis_sshd_config_inconsistent_details": "Bitte führen Sie yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT aus, um den SSH-Port festzulegen, und überprüfen Sie yunohost tools regen-conf ssh --dry-run --with-diff und yunohost tools regen-conf ssh --force um Ihre 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", + "regex_incompatible_with_tile": "/!\\ Packagers! Für Berechtigung '{permission}' ist show_tile auf 'true' gesetzt und deshalb können Sie keine regex-URL als Hauptdomäne setzen", "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)", + "service_description_dnsmasq": "Verwaltet die Auflösung des Domainnamens (DNS)", "restore_backup_too_old": "Dieses Backup kann nicht wieder hergestellt werden, weil es von einer zu alten YunoHost Version stammt.", "service_description_slapd": "Speichert Konten, Domänen und verbundene Informationen", - "service_description_rspamd": "Spamfilter und andere E-Mail-Merkmale", + "service_description_rspamd": "Spamfilter und andere E-Mail-Funktionen", "service_description_redis-server": "Eine spezialisierte Datenbank für den schnellen Datenzugriff, die Aufgabenwarteschlange und die Kommunikation zwischen Programmen", "service_description_postfix": "Wird benutzt, um E-Mails zu senden und zu empfangen", "service_description_nginx": "Stellt Daten aller Websiten auf dem Server bereit", @@ -558,7 +558,7 @@ "config_apply_failed": "Anwenden der neuen Konfiguration fehlgeschlagen: {error}", "config_validate_date": "Sollte ein zulässiges Datum in folgendem Format sein: YYYY-MM-DD", "config_validate_email": "Sollte eine zulässige eMail sein", - "config_forbidden_keyword": "Das Schlüsselwort '{keyword}' ist reserviert. Du kannst kein Konfigurationspanel mit einer Frage erstellen, die diese ID verwendet.", + "config_forbidden_keyword": "Das Schlüsselwort '{keyword}' ist reserviert. Sie können kein Konfigurationspanel mit einer Frage erstellen, das diese ID verwendet.", "config_no_panel": "Kein Konfigurationspanel gefunden.", "config_validate_color": "Sollte eine zulässige RGB hexadezimal Farbe sein", "diagnosis_apps_issue": "Ein Problem für die App {app} ist aufgetreten", @@ -569,7 +569,7 @@ "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.", + "config_cant_set_value_on_section": "Sie können einen einzelnen Wert nicht auf einen gesamten Konfigurationsbereich anwenden.", "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.", @@ -624,7 +624,7 @@ "domain_dns_push_failed_to_authenticate": "Die Authentifizierung bei der API des Registrars für die Domäne '{domain}' ist fehlgeschlagen. Wahrscheinlich sind die Anmeldedaten falsch? (Fehler: {error})", "log_domain_config_set": "Konfiguration für die Domäne '{}' aktualisieren", "log_domain_dns_push": "DNS-Einträge für die Domäne '{}' übertragen", - "service_description_yunomdns": "Ermöglicht es dir, deinen Server über 'yunohost.local' in deinem lokalen Netzwerk zu erreichen", + "service_description_yunomdns": "Ermöglicht es Ihnen, den Server über 'yunohost.local' in Ihrem lokalen Netzwerk zu erreichen", "migration_0021_start": "Beginnen von Migration zu Bullseye", "migration_0021_patching_sources_list": "Aktualisieren der sources.lists...", "migration_0021_main_upgrade": "Starte Hauptupdate...", From ec96558c8126a60bf45e14583cff0ce428ae578d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 15 Jul 2023 20:07:18 +0200 Subject: [PATCH 0193/1116] portalapi: add FIXMEs about auth layer --- src/authenticators/ldap_ynhuser.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index 407277acf..e8cfaf108 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -88,6 +88,7 @@ class Authenticator(BaseAuthenticator): try: con = _reconnect() except ldap.INVALID_CREDENTIALS: + # FIXME FIXME FIXME : this should be properly logged and caught by Fail2ban ! ! ! ! ! ! ! raise YunohostError("invalid_password") except ldap.SERVER_DOWN: logger.warning(m18n.n("ldap_server_down")) @@ -125,7 +126,7 @@ class Authenticator(BaseAuthenticator): # See https://pyjwt.readthedocs.io/en/latest/usage.html#registered-claim-names # for explanations regarding nbf, exp "nbf": int(datetime.datetime.now().timestamp()), - "exp": int(datetime.datetime.now().timestamp()) + (7 * 24 * 3600) # One week validity + "exp": int(datetime.datetime.now().timestamp()) + (7 * 24 * 3600) # One week validity # FIXME : does it mean the session suddenly expires after a week ? Can we somehow auto-renew it at every usage or something ? } new_infos.update(infos) @@ -149,6 +150,7 @@ class Authenticator(BaseAuthenticator): except Exception: if not raise_if_no_session_exists: return {"id": random_ascii()} + # FIXME FIXME FIXME : we might also want this to be caught by fail2ban ? Idk ... raise YunohostAuthenticationError("unable_authenticate") if not infos and raise_if_no_session_exists: @@ -160,8 +162,9 @@ class Authenticator(BaseAuthenticator): if decrypt_pwd: infos["pwd"] = decrypt(infos["pwd"]) - # FIXME: Here, maybe we want to re-authenticate the session via the authenticator - # For example to check that the username authenticated is still in the admin group... + # FIXME : maybe check expiration here ? Or is it already done in jwt.decode ? + + # FIXME: also a valid cookie ain't everything ... i.e. maybe we should validate that the user still exists return infos From 4561f900df20897cffef8633e89e492f45551465 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 15 Jul 2023 21:20:15 +0200 Subject: [PATCH 0194/1116] portal refactoring: update ssowat conf format with a dict mapping domains to portal urls. For now, let's have one portal per main/parent domain (which is anyway imposed by cookie management unless we reintroduce complex cross-domain authentication...) --- src/app.py | 14 ++++---------- src/domain.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/app.py b/src/app.py index 069134798..31108fde5 100644 --- a/src/app.py +++ b/src/app.py @@ -1712,7 +1712,7 @@ def app_ssowatconf(): """ - from yunohost.domain import domain_list, _get_maindomain, domain_config_get + from yunohost.domain import domain_list, _get_maindomain, domain_config_get, _get_domain_portal_dict from yunohost.permission import user_permission_list from yunohost.settings import settings_get @@ -1740,6 +1740,8 @@ def app_ssowatconf(): ], } } + + # FIXME : what's the reason we do this only for the maindomain ? x_X redirected_regex = { main_domain + r"/yunohost[\/]?$": "https://" + main_domain + "/yunohost/sso/" } @@ -1808,17 +1810,9 @@ def app_ssowatconf(): "cookie_secret_file": "/etc/yunohost/.ssowat_cookie_secret", "cookie_name": "yunohost.portal", "theme": settings_get("misc.portal.portal_theme"), - "portal_domain": main_domain, - "portal_path": "/yunohost/sso/", - "additional_headers": { - "Auth-User": "uid", - "Remote-User": "uid", - "Name": "cn", - "Email": "mail", - }, - "domains": domains, "redirected_urls": redirected_urls, "redirected_regex": redirected_regex, + "domain_portal_urls": _get_domain_portal_dict(), "permissions": permissions, } diff --git a/src/domain.py b/src/domain.py index 4f96d08c4..ecb1cc5ea 100644 --- a/src/domain.py +++ b/src/domain.py @@ -99,6 +99,26 @@ def _get_domains(exclude_subdomains=False): return domain_list_cache +def _get_domain_portal_dict(): + + domains = _get_domains() + out = OrderedDict() + + for domain in domains: + + parent = None + + # Use the topest parent domain if any + for d in out.keys(): + if domain.endswith(f".{d}"): + parent = d + break + + out[domain] = f'{parent or domain}/yunohost/sso' + + return dict(out) + + def domain_list(exclude_subdomains=False, tree=False, features=[]): """ List domains From 419a32bf153f30f78f3cf68af2700d94be9d2a63 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 17 Jul 2023 15:24:58 +0200 Subject: [PATCH 0195/1116] dyndns: remove 'domain dyndns list' command because this gives the false impression that there can be several of them... --- share/actionsmap.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 3624a9011..48e12ba0f 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -691,10 +691,6 @@ domain: extra: pattern: *pattern_password - ### domain_dyndns_list() - list: - action_help: List all subscribed DynDNS domains - ### domain_dyndns_set_recovery_password() set-recovery-password: action_help: Set recovery password From 036119d9bad04545aeb2fb63eec20daf33a333fe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 17 Jul 2023 16:23:33 +0200 Subject: [PATCH 0196/1116] Update changelog for 11.2 --- debian/changelog | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/debian/changelog b/debian/changelog index 428d02b05..ab5003b88 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,19 @@ +yunohost (11.2) stable; urgency=low + + - dyndns: add support for recovery passwords ([#1475](https://github.com/YunoHost/yunohost/pull/1475)) + - mail/apps: allow system users to auth on the mail stack and send emails ([#815](https://github.com/YunoHost/yunohost/pull/815)) + - nginx: fix OCSP stapling errors ([#1543](https://github.com/YunoHost/yunohost/pull/1534)) + - ssh: disable banner by default ([#1605](https://github.com/YunoHost/yunohost/pull/1605)) + - configpanels: another partial refactoring of config panels / questions, paving the way for Pydantic ([#1676](https://github.com/YunoHost/yunohost/pull/1676)) + - misc: rewrite the `yunopaste` tool ([#1667](https://github.com/YunoHost/yunohost/pull/1667)) + - apps: simplify the use of `ynh_add_fpm_config` ([#1684](https://github.com/YunoHost/yunohost/pull/1684)) + - apps: in ynh_systemd_action, check the actual timestamp when checking for timeout, because for some reason journalctl may take a ridiculous amount of time to run (f3eef43d) + - i18n: Translations updated for German, Japanese + + Thanks to all contributors <3 ! (André Théo LAURET, axolotle, Christian Wehrli, Éric Gaspar, ljf, motcha, theo-is-taken) + + -- Alexandre Aubin Mon, 17 Jul 2023 16:14:58 +0200 + yunohost (11.1.22) stable; urgency=low - security: replace $http_host by $host in nginx conf, cf https://github.com/yandex/gixy/blob/master/docs/en/plugins/hostspoofing.md / Credit to A.Wolski (3957b10e) From 6e63b6fc53e6e95fff1c936898fca436d62ef105 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Mon, 17 Jul 2023 16:00:11 +0000 Subject: [PATCH 0197/1116] [CI] Format code with Black --- src/app.py | 14 +++++------ src/dns.py | 1 + src/domain.py | 34 ++++++++++++++++++++----- src/dyndns.py | 49 +++++++++++++++++++++++++------------ src/tests/test_domains.py | 12 ++++++--- src/tests/test_questions.py | 2 +- src/tools.py | 10 ++++++-- src/utils/resources.py | 25 +++++++++++++------ 8 files changed, 104 insertions(+), 43 deletions(-) diff --git a/src/app.py b/src/app.py index ab27f8818..6f18d341f 100644 --- a/src/app.py +++ b/src/app.py @@ -2360,9 +2360,7 @@ def _set_default_ask_questions(questions, script_name="install"): 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['id']}" - ) + question["ask"] = m18n.n(f"app_manifest_{script_name}_ask_{question['id']}") # Also it in fact doesn't make sense for any of those questions to have an example value nor a default value... if question.get("type") in ["domain", "user", "password"]: @@ -3212,7 +3210,6 @@ def _ask_confirmation( def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): - dovecot = True if only in [None, "dovecot"] else False postfix = True if only in [None, "postfix"] else False @@ -3221,7 +3218,6 @@ def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): postfix_map = [] dovecot_passwd = [] for app in _installed_apps(): - settings = _get_app_settings(app) if "domain" not in settings or "mail_pwd" not in settings: @@ -3229,7 +3225,9 @@ def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): if dovecot: hashed_password = _hash_user_password(settings["mail_pwd"]) - dovecot_passwd.append(f"{app}:{hashed_password}::::::allow_nets=127.0.0.1/24") + dovecot_passwd.append( + f"{app}:{hashed_password}::::::allow_nets=127.0.0.1/24" + ) if postfix: mail_user = settings.get("mail_user", app) mail_domain = settings.get("mail_domain", settings["domain"]) @@ -3238,7 +3236,7 @@ def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): if dovecot: app_senders_passwd = "/etc/dovecot/app-senders-passwd" content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!" - content += '\n' + '\n'.join(dovecot_passwd) + content += "\n" + "\n".join(dovecot_passwd) write_to_file(app_senders_passwd, content) chmod(app_senders_passwd, 0o440) chown(app_senders_passwd, "root", "dovecot") @@ -3246,7 +3244,7 @@ def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): if postfix: app_senders_map = "/etc/postfix/app_senders_login_maps" content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!" - content += '\n' + '\n'.join(postfix_map) + content += "\n" + "\n".join(postfix_map) write_to_file(app_senders_map, content) chmod(app_senders_map, 0o440) chown(app_senders_map, "postfix", "root") diff --git a/src/dns.py b/src/dns.py index d514d1b17..e25d0f3ec 100644 --- a/src/dns.py +++ b/src/dns.py @@ -641,6 +641,7 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= # FIXME: in the future, properly unify this with yunohost dyndns update if registrar == "yunohost": from yunohost.dyndns import dyndns_update + dyndns_update(domain=domain, force=force) return {} diff --git a/src/domain.py b/src/domain.py index 3c648ad4f..8fc9799cd 100644 --- a/src/domain.py +++ b/src/domain.py @@ -212,7 +212,9 @@ def _get_parent_domain_of(domain, return_self=False, topest=False): @is_unit_operation(exclude=["dyndns_recovery_password"]) -def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_dyndns=False): +def domain_add( + operation_logger, domain, dyndns_recovery_password=None, ignore_dyndns=False +): """ Create a custom domain @@ -252,9 +254,14 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_d domain = domain.encode("idna").decode("utf-8") # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) - dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 + dyndns = ( + not ignore_dyndns + and is_yunohost_dyndns_domain(domain) + and len(domain.split(".")) == 3 + ) if dyndns: from yunohost.dyndns import is_subscribing_allowed + # Do not allow to subscribe to multiple dyndns domains... if not is_subscribing_allowed(): raise YunohostValidationError("domain_dyndns_already_subscribed") @@ -264,7 +271,9 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_d operation_logger.start() if dyndns: - domain_dyndns_subscribe(domain=domain, recovery_password=dyndns_recovery_password) + domain_dyndns_subscribe( + domain=domain, recovery_password=dyndns_recovery_password + ) _certificate_install_selfsigned([domain], True) @@ -314,7 +323,14 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_d @is_unit_operation(exclude=["dyndns_recovery_password"]) -def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_recovery_password=None, ignore_dyndns=False): +def domain_remove( + operation_logger, + domain, + remove_apps=False, + force=False, + dyndns_recovery_password=None, + ignore_dyndns=False, +): """ Delete domains @@ -394,7 +410,11 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd ) # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) - dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3 + dyndns = ( + not ignore_dyndns + and is_yunohost_dyndns_domain(domain) + and len(domain.split(".")) == 3 + ) operation_logger.start() @@ -445,7 +465,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd # If a password is provided, delete the DynDNS record if dyndns: # Actually unsubscribe - domain_dyndns_unsubscribe(domain=domain, recovery_password=dyndns_recovery_password) + domain_dyndns_unsubscribe( + domain=domain, recovery_password=dyndns_recovery_password + ) logger.success(m18n.n("domain_deleted")) diff --git a/src/dyndns.py b/src/dyndns.py index c3fa80d3a..a3afd655f 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -106,9 +106,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): if not recovery_password and Moulinette.interface.type == "cli": logger.warning(m18n.n("ask_dyndns_recovery_password_explain")) recovery_password = Moulinette.prompt( - m18n.n("ask_dyndns_recovery_password"), - is_password=True, - confirm=True + m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True ) if not recovery_password: @@ -116,6 +114,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): if recovery_password: from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough("admin", recovery_password) operation_logger.data_to_redact.append(recovery_password) @@ -158,7 +157,9 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): b64encoded_key = base64.b64encode(secret.encode()).decode() data = {"subdomain": domain} if recovery_password: - data["recovery_password"] = hashlib.sha256((domain + ":" + recovery_password.strip()).encode('utf-8')).hexdigest() + data["recovery_password"] = hashlib.sha256( + (domain + ":" + recovery_password.strip()).encode("utf-8") + ).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", data=data, @@ -214,18 +215,23 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): # Otherwise, ask for the recovery password else: if Moulinette.interface.type == "cli" and not recovery_password: - logger.warning(m18n.n("ask_dyndns_recovery_password_explain_during_unsubscribe")) + logger.warning( + m18n.n("ask_dyndns_recovery_password_explain_during_unsubscribe") + ) recovery_password = Moulinette.prompt( - m18n.n("ask_dyndns_recovery_password"), - is_password=True + m18n.n("ask_dyndns_recovery_password"), is_password=True ) if not recovery_password: - logger.error(f"Cannot unsubscribe the domain {domain}: no credential provided") + logger.error( + f"Cannot unsubscribe the domain {domain}: no credential provided" + ) return secret = str(domain) + ":" + str(recovery_password).strip() - credential = {"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()} + credential = { + "recovery_password": hashlib.sha256(secret.encode("utf-8")).hexdigest() + } operation_logger.start() @@ -250,19 +256,22 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): elif r.status_code == 409: raise YunohostError("dyndns_unsubscribe_already_unsubscribed") else: - raise YunohostError("dyndns_unsubscribe_failed", error=f"The server returned code {r.status_code}") + raise YunohostError( + "dyndns_unsubscribe_failed", + error=f"The server returned code {r.status_code}", + ) logger.success(m18n.n("dyndns_unsubscribed")) def dyndns_set_recovery_password(domain, recovery_password): - keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") if not keys: raise YunohostValidationError("dyndns_key_not_found") from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough("admin", recovery_password) secret = str(domain) + ":" + str(recovery_password).strip() @@ -277,7 +286,10 @@ def dyndns_set_recovery_password(domain, recovery_password): try: r = requests.put( f"https://{DYNDNS_PROVIDER}/domains/{domain}/recovery_password", - data={"key": base64key, "recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, + data={ + "key": base64key, + "recovery_password": hashlib.sha256(secret.encode("utf-8")).hexdigest(), + }, timeout=30, ) except Exception as e: @@ -292,7 +304,10 @@ def dyndns_set_recovery_password(domain, recovery_password): elif r.status_code == 409: raise YunohostError("dyndns_set_recovery_password_invalid_password") else: - raise YunohostError("dyndns_set_recovery_password_failed", error=f"The server returned code {r.status_code}") + raise YunohostError( + "dyndns_set_recovery_password_failed", + error=f"The server returned code {r.status_code}", + ) def dyndns_list(): @@ -303,7 +318,12 @@ def dyndns_list(): from yunohost.domain import domain_list domains = domain_list(exclude_subdomains=True)["domains"] - dyndns_domains = [d for d in domains if is_yunohost_dyndns_domain(d) and glob.glob(f"/etc/yunohost/dyndns/K{d}.+*.key")] + dyndns_domains = [ + d + for d in domains + if is_yunohost_dyndns_domain(d) + and glob.glob(f"/etc/yunohost/dyndns/K{d}.+*.key") + ] return {"domains": dyndns_domains} @@ -330,7 +350,6 @@ def dyndns_update( # If domain is not given, update all DynDNS domains if domain is None: - dyndns_domains = dyndns_list()["domains"] if not dyndns_domains: diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index c5c1ab7ae..1bbbb7890 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -18,7 +18,11 @@ from yunohost.domain import ( ) TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"] -TEST_DYNDNS_DOMAIN = "ci-test-" + "".join(chr(random.randint(ord("a"), ord("z"))) for x in range(12)) + random.choice([".noho.st", ".ynh.fr", ".nohost.me"]) +TEST_DYNDNS_DOMAIN = ( + "ci-test-" + + "".join(chr(random.randint(ord("a"), ord("z"))) for x in range(12)) + + random.choice([".noho.st", ".ynh.fr", ".nohost.me"]) +) TEST_DYNDNS_PASSWORD = "astrongandcomplicatedpassphrasethatisverysecure" @@ -38,7 +42,9 @@ def setup_function(function): # Clear other domains for domain in domains: - if (domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]) and domain != TEST_DYNDNS_DOMAIN: + if ( + domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2] + ) and domain != TEST_DYNDNS_DOMAIN: # Clean domains not used for testing domain_remove(domain) elif domain in TEST_DOMAINS: @@ -70,7 +76,6 @@ def test_domain_add(): def test_domain_add_subscribe(): - time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) @@ -90,7 +95,6 @@ def test_domain_remove(): def test_domain_remove_unsubscribe(): - time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index a9b61aad9..a695e834d 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -1968,7 +1968,7 @@ def test_option_default_type_with_choices_is_select(): "some_choices": {"choices": ["a", "b"]}, # LEGACY (`choices` in option `string` used to be valid) # make sure this result as a `select` option - "some_legacy": {"type": "string", "choices": ["a", "b"]} + "some_legacy": {"type": "string", "choices": ["a", "b"]}, } answers = {"some_choices": "a", "some_legacy": "a"} diff --git a/src/tools.py b/src/tools.py index 7704a99b5..cd48f00ee 100644 --- a/src/tools.py +++ b/src/tools.py @@ -213,7 +213,9 @@ def tools_postinstall( # connectivity or something. Assume that this domain isn't manageable # and inform the user that we could not contact the dyndns host server. except Exception: - raise YunohostValidationError("dyndns_provider_unreachable", provider="dyndns.yunohost.org") + raise YunohostValidationError( + "dyndns_provider_unreachable", provider="dyndns.yunohost.org" + ) else: if not available: raise YunohostValidationError("dyndns_unavailable", domain=domain) @@ -228,7 +230,11 @@ def tools_postinstall( logger.info(m18n.n("yunohost_installing")) # New domain config - domain_add(domain, dyndns_recovery_password=dyndns_recovery_password, ignore_dyndns=ignore_dyndns) + domain_add( + domain, + dyndns_recovery_password=dyndns_recovery_password, + ignore_dyndns=ignore_dyndns, + ) domain_main_domain(domain) # First user diff --git a/src/utils/resources.py b/src/utils/resources.py index be7f9fba5..60a5f44f6 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -716,7 +716,6 @@ class SystemuserAppResource(AppResource): home: str = "" def provision_or_update(self, context: Dict = {}): - from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix # FIXME : validate that no yunohost user exists with that name? @@ -773,13 +772,19 @@ class SystemuserAppResource(AppResource): regen_mail_app_user_config_for_dovecot_and_postfix() else: self.delete_setting("mail_pwd") - if os.system(f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps") == 0 \ - or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") == 0: + if ( + os.system( + f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps" + ) + == 0 + or os.system( + f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd" + ) + == 0 + ): regen_mail_app_user_config_for_dovecot_and_postfix() - def deprovision(self, context: Dict = {}): - from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: @@ -797,8 +802,14 @@ class SystemuserAppResource(AppResource): ) self.delete_setting("mail_pwd") - if os.system(f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps") == 0 \ - or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") == 0: + if ( + os.system( + f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps" + ) + == 0 + or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") + == 0 + ): regen_mail_app_user_config_for_dovecot_and_postfix() # FIXME : better logging and error handling, add stdout/stderr from the deluser/delgroup commands... From ced222eaa5505acae36c2a1e4ff9cddfa14657c7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 17 Jul 2023 19:09:04 +0200 Subject: [PATCH 0198/1116] doc: fix resource doc generation .. not sure why this line that removed legit indent was there --- doc/generate_resource_doc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/generate_resource_doc.py b/doc/generate_resource_doc.py index 201d25265..30a5273a1 100644 --- a/doc/generate_resource_doc.py +++ b/doc/generate_resource_doc.py @@ -62,7 +62,6 @@ for c in ResourceClasses: for resource_id, doc in sorted(ResourceDocString.items()): - doc = doc.replace("\n ", "\n") print("----------------") print("") From ae37b5fc248c600e9be6f1c404c3383a8a0e258d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 17 Jul 2023 19:47:24 +0200 Subject: [PATCH 0199/1116] portalapi: Add new yunohost-portal-api to yunohost services --- conf/yunohost/services.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conf/yunohost/services.yml b/conf/yunohost/services.yml index 45621876e..693793465 100644 --- a/conf/yunohost/services.yml +++ b/conf/yunohost/services.yml @@ -51,6 +51,9 @@ ssh: test_conf: sshd -t needs_exposed_ports: [22] category: admin +yunohost-portal-api: + log: /var/log/yunohost-portal-api.log + category: userportal yunohost-api: log: /var/log/yunohost/yunohost-api.log category: admin From 781f924e30d8864e3908cdf402de8911aad42a41 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 17 Jul 2023 21:13:34 +0200 Subject: [PATCH 0200/1116] apps: hotfix for funky issue, apps getting named 'undefined' --- src/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app.py b/src/app.py index 6f18d341f..75d336241 100644 --- a/src/app.py +++ b/src/app.py @@ -1157,6 +1157,10 @@ def app_install( recursive=True, ) + # Hotfix for bug in the webadmin while we fix the actual issue :D + if label == "undefined": + label = None + # Override manifest name by given label # This info is also later picked-up by the 'permission' resource initialization if label: From 1eb208db23dff63f35926635aa9ed457be885999 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 17 Jul 2023 21:19:50 +0200 Subject: [PATCH 0201/1116] Update changelog for 11.2.1 --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index ab5003b88..3a2747bec 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (11.2.1) stable; urgency=low + + - doc: fix resource doc generation .. not sure why this line that removed legit indent was there (ced222ea) + - apps: hotfix for funky issue, apps getting named 'undefined' (781f924e) + + -- Alexandre Aubin Mon, 17 Jul 2023 21:13:54 +0200 + yunohost (11.2) stable; urgency=low - dyndns: add support for recovery passwords ([#1475](https://github.com/YunoHost/yunohost/pull/1475)) From dd93df45e6331a2188d09f8f6d7e2e49b0957403 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Mon, 17 Jul 2023 20:34:49 +0000 Subject: [PATCH 0202/1116] [CI] Format code with Black --- doc/generate_resource_doc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/generate_resource_doc.py b/doc/generate_resource_doc.py index 30a5273a1..4c60d7950 100644 --- a/doc/generate_resource_doc.py +++ b/doc/generate_resource_doc.py @@ -62,7 +62,6 @@ for c in ResourceClasses: for resource_id, doc in sorted(ResourceDocString.items()): - print("----------------") print("") print(f"## {resource_id.replace('_', ' ').title()}") From 2ece3b65f6c6e51cdbb240bdb24cb74d8c21802b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 18 Jul 2023 00:19:16 +0200 Subject: [PATCH 0203/1116] Moulinette logging is an unecessarily complex mess, episode 57682 --- src/__init__.py | 13 +++---------- src/app.py | 4 ++-- src/app_catalog.py | 4 ++-- src/backup.py | 4 ++-- src/certificate.py | 5 ++--- src/diagnosis.py | 3 ++- src/dns.py | 5 ++--- src/domain.py | 4 ++-- src/dyndns.py | 4 ++-- src/firewall.py | 4 ++-- src/hook.py | 4 ++-- src/log.py | 3 +-- src/migrations/0021_migrate_to_bullseye.py | 4 ++-- src/migrations/0022_php73_to_php74_pools.py | 5 ++--- src/migrations/0023_postgresql_11_to_13.py | 4 ++-- src/migrations/0024_rebuild_python_venv.py | 4 ++-- .../0025_global_settings_to_configpanel.py | 4 ++-- src/migrations/0026_new_admins_group.py | 4 ++-- src/permission.py | 4 ++-- src/regenconf.py | 14 +++++++------- src/service.py | 5 ++--- src/settings.py | 4 ++-- src/tools.py | 4 ++-- src/user.py | 4 ++-- src/utils/configpanel.py | 4 ++-- src/utils/form.py | 4 ++-- src/utils/legacy.py | 5 +++-- src/utils/resources.py | 4 ++-- 28 files changed, 62 insertions(+), 72 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index d13d61089..e23b62219 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -115,17 +115,11 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun "version": 1, "disable_existing_loggers": True, "formatters": { - "console": { - "format": "%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s" + "tty-debug": { + "format": "%(relativeCreated)-4d %(level_with_color)s %(message)s" }, - "tty-debug": {"format": "%(relativeCreated)-4d %(fmessage)s"}, "precise": { - "format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s" - }, - }, - "filters": { - "action": { - "()": "moulinette.utils.log.ActionFilter", + "format": "%(asctime)-15s %(levelname)-8s %(name)s.%(funcName)s - %(message)s" }, }, "handlers": { @@ -142,7 +136,6 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun "class": "logging.FileHandler", "formatter": "precise", "filename": logfile, - "filters": ["action"], }, }, "loggers": { diff --git a/src/app.py b/src/app.py index 75d336241..483d34153 100644 --- a/src/app.py +++ b/src/app.py @@ -28,9 +28,9 @@ import tempfile import copy from typing import List, Tuple, Dict, Any, Iterator, Optional from packaging import version +from logging import getLogger from moulinette import Moulinette, m18n -from moulinette.utils.log import getActionLogger from moulinette.utils.process import run_commands, check_output from moulinette.utils.filesystem import ( read_file, @@ -71,7 +71,7 @@ from yunohost.app_catalog import ( # noqa APPS_CATALOG_LOGOS, ) -logger = getActionLogger("yunohost.app") +logger = getLogger("yunohost.app") APPS_SETTING_PATH = "/etc/yunohost/apps/" APP_TMP_WORKDIRS = "/var/cache/yunohost/app_tmp_work_dirs" diff --git a/src/app_catalog.py b/src/app_catalog.py index 9fb662845..2a50a0f82 100644 --- a/src/app_catalog.py +++ b/src/app_catalog.py @@ -19,9 +19,9 @@ import os import re import hashlib +from logging import getLogger from moulinette import m18n -from moulinette.utils.log import getActionLogger from moulinette.utils.network import download_json from moulinette.utils.filesystem import ( read_json, @@ -34,7 +34,7 @@ from moulinette.utils.filesystem import ( from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError -logger = getActionLogger("yunohost.app_catalog") +logger = getLogger("yunohost.app_catalog") APPS_CATALOG_CACHE = "/var/cache/yunohost/repo" APPS_CATALOG_LOGOS = "/usr/share/yunohost/applogos" diff --git a/src/backup.py b/src/backup.py index ce1e8ba2c..505e071a1 100644 --- a/src/backup.py +++ b/src/backup.py @@ -30,10 +30,10 @@ from glob import glob from collections import OrderedDict from functools import reduce from packaging import version +from logging import getLogger from moulinette import Moulinette, m18n from moulinette.utils.text import random_ascii -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import ( read_file, mkdir, @@ -84,7 +84,7 @@ APP_MARGIN_SPACE_SIZE = 100 # In MB CONF_MARGIN_SPACE_SIZE = 10 # IN MB POSTINSTALL_ESTIMATE_SPACE_SIZE = 5 # In MB MB_ALLOWED_TO_ORGANIZE = 10 -logger = getActionLogger("yunohost.backup") +logger = getLogger("yunohost.backup") class BackupRestoreTargetsManager: diff --git a/src/certificate.py b/src/certificate.py index 76d3f32b7..c3aaacc87 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -21,11 +21,10 @@ import sys import shutil import subprocess from glob import glob - +from logging import getLogger from datetime import datetime from moulinette import m18n -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, chown, chmod from moulinette.utils.process import check_output @@ -38,7 +37,7 @@ from yunohost.service import _run_service_command from yunohost.regenconf import regen_conf from yunohost.log import OperationLogger -logger = getActionLogger("yunohost.certmanager") +logger = getLogger("yunohost.certmanager") CERT_FOLDER = "/etc/yunohost/certs/" TMP_FOLDER = "/var/www/.well-known/acme-challenge-private/" diff --git a/src/diagnosis.py b/src/diagnosis.py index 02047c001..be3208b02 100644 --- a/src/diagnosis.py +++ b/src/diagnosis.py @@ -21,6 +21,7 @@ import os import time import glob from importlib import import_module +from logging import getLogger from moulinette import m18n, Moulinette from moulinette.utils import log @@ -33,7 +34,7 @@ from moulinette.utils.filesystem import ( from yunohost.utils.error import YunohostError, YunohostValidationError -logger = log.getActionLogger("yunohost.diagnosis") +logger = getLogger("yunohost.diagnosis") DIAGNOSIS_CACHE = "/var/cache/yunohost/diagnosis/" DIAGNOSIS_CONFIG_FILE = "/etc/yunohost/diagnosis.yml" diff --git a/src/dns.py b/src/dns.py index e25d0f3ec..9a081e228 100644 --- a/src/dns.py +++ b/src/dns.py @@ -19,12 +19,11 @@ import os import re import time - +from logging import getLogger from difflib import SequenceMatcher from collections import OrderedDict from moulinette import m18n, Moulinette -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, write_to_file, read_toml, mkdir from yunohost.domain import ( @@ -42,7 +41,7 @@ from yunohost.settings import settings_get from yunohost.log import is_unit_operation from yunohost.hook import hook_callback -logger = getActionLogger("yunohost.domain") +logger = getLogger("yunohost.domain") DOMAIN_REGISTRAR_LIST_PATH = "/usr/share/yunohost/registrar_list.toml" diff --git a/src/domain.py b/src/domain.py index 8fc9799cd..2d36fdfa4 100644 --- a/src/domain.py +++ b/src/domain.py @@ -20,10 +20,10 @@ import os import time from typing import List, Optional from collections import OrderedDict +from logging import getLogger from moulinette import m18n, Moulinette from moulinette.core import MoulinetteError -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file, read_yaml, write_to_yaml, rm from yunohost.app import ( @@ -39,7 +39,7 @@ from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.log import is_unit_operation -logger = getActionLogger("yunohost.domain") +logger = getLogger("yunohost.domain") DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" diff --git a/src/dyndns.py b/src/dyndns.py index a3afd655f..5c9e2a36e 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -22,10 +22,10 @@ import glob import base64 import subprocess import hashlib +from logging import getLogger from moulinette import Moulinette, m18n from moulinette.core import MoulinetteError -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file, rm, chown, chmod from moulinette.utils.network import download_json @@ -36,7 +36,7 @@ from yunohost.utils.dns import dig, is_yunohost_dyndns_domain from yunohost.log import is_unit_operation from yunohost.regenconf import regen_conf -logger = getActionLogger("yunohost.dyndns") +logger = getLogger("yunohost.dyndns") DYNDNS_PROVIDER = "dyndns.yunohost.org" DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"] diff --git a/src/firewall.py b/src/firewall.py index 392678fe1..9375aded0 100644 --- a/src/firewall.py +++ b/src/firewall.py @@ -19,16 +19,16 @@ import os import yaml import miniupnpc +from logging import getLogger from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils import process -from moulinette.utils.log import getActionLogger FIREWALL_FILE = "/etc/yunohost/firewall.yml" UPNP_CRON_JOB = "/etc/cron.d/yunohost-firewall-upnp" -logger = getActionLogger("yunohost.firewall") +logger = getLogger("yunohost.firewall") def firewall_allow( diff --git a/src/hook.py b/src/hook.py index 4b07d1c17..60abb98d4 100644 --- a/src/hook.py +++ b/src/hook.py @@ -23,16 +23,16 @@ import tempfile import mimetypes from glob import iglob from importlib import import_module +from logging import getLogger from moulinette import m18n, Moulinette from yunohost.utils.error import YunohostError, YunohostValidationError -from moulinette.utils import log from moulinette.utils.filesystem import read_yaml, cp HOOK_FOLDER = "/usr/share/yunohost/hooks/" CUSTOM_HOOK_FOLDER = "/etc/yunohost/hooks.d/" -logger = log.getActionLogger("yunohost.hook") +logger = getLogger("yunohost.hook") def hook_add(app, file): diff --git a/src/log.py b/src/log.py index 5ab918e76..13683d8ef 100644 --- a/src/log.py +++ b/src/log.py @@ -32,10 +32,9 @@ from moulinette import m18n, Moulinette from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.system import get_ynh_package_version -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, read_yaml -logger = getActionLogger("yunohost.log") +logger = getLogger("yunohost.log") CATEGORIES_PATH = "/var/log/yunohost/categories/" OPERATIONS_PATH = "/var/log/yunohost/categories/operation/" diff --git a/src/migrations/0021_migrate_to_bullseye.py b/src/migrations/0021_migrate_to_bullseye.py index f320577e1..eee8cc667 100644 --- a/src/migrations/0021_migrate_to_bullseye.py +++ b/src/migrations/0021_migrate_to_bullseye.py @@ -1,9 +1,9 @@ import glob import os +from logging import getLogger from moulinette import m18n from yunohost.utils.error import YunohostError -from moulinette.utils.log import getActionLogger from moulinette.utils.process import check_output, call_async_output from moulinette.utils.filesystem import read_file, rm, write_to_file @@ -22,7 +22,7 @@ from yunohost.utils.system import ( ) from yunohost.service import _get_services, _save_services -logger = getActionLogger("yunohost.migration") +logger = getLogger("yunohost.migration") N_CURRENT_DEBIAN = 10 N_CURRENT_YUNOHOST = 4 diff --git a/src/migrations/0022_php73_to_php74_pools.py b/src/migrations/0022_php73_to_php74_pools.py index dc428e504..23ecc5b89 100644 --- a/src/migrations/0022_php73_to_php74_pools.py +++ b/src/migrations/0022_php73_to_php74_pools.py @@ -1,15 +1,14 @@ import os import glob from shutil import copy2 - -from moulinette.utils.log import getActionLogger +from logging import getLogger from yunohost.app import _is_installed from yunohost.utils.legacy import _patch_legacy_php_versions_in_settings from yunohost.tools import Migration from yunohost.service import _run_service_command -logger = getActionLogger("yunohost.migration") +logger = getLogger("yunohost.migration") OLDPHP_POOLS = "/etc/php/7.3/fpm/pool.d" NEWPHP_POOLS = "/etc/php/7.4/fpm/pool.d" diff --git a/src/migrations/0023_postgresql_11_to_13.py b/src/migrations/0023_postgresql_11_to_13.py index 6d37ffa74..086b17af7 100644 --- a/src/migrations/0023_postgresql_11_to_13.py +++ b/src/migrations/0023_postgresql_11_to_13.py @@ -1,15 +1,15 @@ import subprocess import time import os +from logging import getLogger 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.system import free_space_in_directory, space_used_by_directory -logger = getActionLogger("yunohost.migration") +logger = getLogger("yunohost.migration") class MyMigration(Migration): diff --git a/src/migrations/0024_rebuild_python_venv.py b/src/migrations/0024_rebuild_python_venv.py index 01a229b87..5b77e69a0 100644 --- a/src/migrations/0024_rebuild_python_venv.py +++ b/src/migrations/0024_rebuild_python_venv.py @@ -1,14 +1,14 @@ import os +from logging import getLogger 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") +logger = getLogger("yunohost.migration") VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bullseye_upgrade.txt" diff --git a/src/migrations/0025_global_settings_to_configpanel.py b/src/migrations/0025_global_settings_to_configpanel.py index 3a8818461..76289e608 100644 --- a/src/migrations/0025_global_settings_to_configpanel.py +++ b/src/migrations/0025_global_settings_to_configpanel.py @@ -1,13 +1,13 @@ import os +from logging import getLogger 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") +logger = getLogger("yunohost.migration") SETTINGS_PATH = "/etc/yunohost/settings.yml" OLD_SETTINGS_PATH = "/etc/yunohost/settings.json" diff --git a/src/migrations/0026_new_admins_group.py b/src/migrations/0026_new_admins_group.py index 43f10a7b6..30237e720 100644 --- a/src/migrations/0026_new_admins_group.py +++ b/src/migrations/0026_new_admins_group.py @@ -1,8 +1,8 @@ -from moulinette.utils.log import getActionLogger +from logging import getLogger from yunohost.tools import Migration -logger = getActionLogger("yunohost.migration") +logger = getLogger("yunohost.migration") ################################################### # Tools used also for restoration diff --git a/src/permission.py b/src/permission.py index 72975561f..7ec6f17bc 100644 --- a/src/permission.py +++ b/src/permission.py @@ -20,13 +20,13 @@ import re import copy import grp import random +from logging import getLogger from moulinette import m18n -from moulinette.utils.log import getActionLogger from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation -logger = getActionLogger("yunohost.user") +logger = getLogger("yunohost.user") SYSTEM_PERMS = ["mail", "xmpp", "sftp", "ssh"] diff --git a/src/regenconf.py b/src/regenconf.py index 74bbdb17c..f180368ad 100644 --- a/src/regenconf.py +++ b/src/regenconf.py @@ -20,12 +20,12 @@ import os import yaml import shutil import hashlib - +from logging import getLogger from difflib import unified_diff from datetime import datetime from moulinette import m18n -from moulinette.utils import log, filesystem +from moulinette.utils.filesystem import mkdir from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError @@ -37,7 +37,7 @@ BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, "backup") PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, "pending") REGEN_CONF_FILE = "/etc/yunohost/regenconf.yml" -logger = log.getActionLogger("yunohost.regenconf") +logger = getLogger("yunohost.regenconf") # FIXME : those ain't just services anymore ... what are we supposed to do with this ... @@ -102,7 +102,7 @@ def regen_conf( for name in names: shutil.rmtree(os.path.join(PENDING_CONF_DIR, name), ignore_errors=True) else: - filesystem.mkdir(PENDING_CONF_DIR, 0o755, True) + mkdir(PENDING_CONF_DIR, 0o755, True) # Execute hooks for pre-regen # element 2 and 3 with empty string is because of legacy... @@ -111,7 +111,7 @@ def regen_conf( def _pre_call(name, priority, path, args): # create the pending conf directory for the category category_pending_path = os.path.join(PENDING_CONF_DIR, name) - filesystem.mkdir(category_pending_path, 0o755, True, uid="root") + mkdir(category_pending_path, 0o755, True, uid="root") # return the arguments to pass to the script return pre_args + [ @@ -622,7 +622,7 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): backup_dir = os.path.dirname(backup_path) if not os.path.isdir(backup_dir): - filesystem.mkdir(backup_dir, 0o755, True) + mkdir(backup_dir, 0o755, True) shutil.copy2(system_conf, backup_path) logger.debug( @@ -637,7 +637,7 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): system_dir = os.path.dirname(system_conf) if not os.path.isdir(system_dir): - filesystem.mkdir(system_dir, 0o755, True) + mkdir(system_dir, 0o755, True) shutil.copyfile(new_conf, system_conf) logger.debug(m18n.n("regenconf_file_updated", conf=system_conf)) diff --git a/src/service.py b/src/service.py index 47bc1903a..6bb61d841 100644 --- a/src/service.py +++ b/src/service.py @@ -21,14 +21,13 @@ import os import time import yaml import subprocess - +from logging import getLogger from glob import glob from datetime import datetime from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils.process import check_output -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import ( read_file, append_to_file, @@ -42,7 +41,7 @@ MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" SERVICES_CONF = "/etc/yunohost/services.yml" SERVICES_CONF_BASE = "/usr/share/yunohost/conf/yunohost/services.yml" -logger = getActionLogger("yunohost.service") +logger = getLogger("yunohost.service") def service_add( diff --git a/src/settings.py b/src/settings.py index 6690ab3fd..e2f34bda9 100644 --- a/src/settings.py +++ b/src/settings.py @@ -18,18 +18,18 @@ # import os import subprocess +from logging import getLogger from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.configpanel import ConfigPanel from yunohost.utils.form import BaseOption -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") +logger = getLogger("yunohost.settings") SETTINGS_PATH = "/etc/yunohost/settings.yml" diff --git a/src/tools.py b/src/tools.py index cd48f00ee..23edf1004 100644 --- a/src/tools.py +++ b/src/tools.py @@ -24,9 +24,9 @@ import time from importlib import import_module from packaging import version from typing import List +from logging import getLogger 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, chown @@ -55,7 +55,7 @@ from yunohost.log import is_unit_operation, OperationLogger MIGRATIONS_STATE_PATH = "/etc/yunohost/migrations.yaml" -logger = getActionLogger("yunohost.tools") +logger = getLogger("yunohost.tools") def tools_versions(): diff --git a/src/user.py b/src/user.py index 00876854e..780797a61 100644 --- a/src/user.py +++ b/src/user.py @@ -25,9 +25,9 @@ import random import string import subprocess import copy +from logging import getLogger from moulinette import Moulinette, m18n -from moulinette.utils.log import getActionLogger from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError, YunohostValidationError @@ -35,7 +35,7 @@ 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") +logger = getLogger("yunohost.user") FIELDS_FOR_IMPORT = { "username": r"^[a-z0-9_]+$", diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 42a030cbc..56e071956 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -22,11 +22,11 @@ import re import urllib.parse from collections import OrderedDict from typing import Union +from logging import getLogger from moulinette import Moulinette, m18n from moulinette.interfaces.cli import colorize from moulinette.utils.filesystem import mkdir, read_toml, read_yaml, write_to_yaml -from moulinette.utils.log import getActionLogger from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.form import ( OPTIONS, @@ -40,7 +40,7 @@ from yunohost.utils.form import ( ) from yunohost.utils.i18n import _value_for_locale -logger = getActionLogger("yunohost.configpanel") +logger = getLogger("yunohost.configpanel") CONFIG_PANEL_VERSION_SUPPORTED = 1.0 diff --git a/src/utils/form.py b/src/utils/form.py index 1ca03373e..e2e01ca12 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -25,16 +25,16 @@ import tempfile import urllib.parse from enum import Enum from typing import Any, Callable, Dict, List, Literal, Mapping, Optional, Union +from logging import getLogger from moulinette import Moulinette, m18n from moulinette.interfaces.cli import colorize from moulinette.utils.filesystem import read_file, write_to_file -from moulinette.utils.log import getActionLogger from yunohost.log import OperationLogger from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.i18n import _value_for_locale -logger = getActionLogger("yunohost.form") +logger = getLogger("yunohost.form") Context = dict[str, Any] diff --git a/src/utils/legacy.py b/src/utils/legacy.py index 82507d64d..8b44fb3fb 100644 --- a/src/utils/legacy.py +++ b/src/utils/legacy.py @@ -19,8 +19,9 @@ import os import re import glob +from logging import getLogger + from moulinette.core import MoulinetteError -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import ( read_file, write_to_file, @@ -32,7 +33,7 @@ from moulinette.utils.filesystem import ( from yunohost.utils.error import YunohostValidationError -logger = getActionLogger("yunohost.legacy") +logger = getLogger("yunohost.utils.legacy") LEGACY_PERMISSION_LABEL = { ("nextcloud", "skipped"): "api", # .well-known diff --git a/src/utils/resources.py b/src/utils/resources.py index 60a5f44f6..5e3f2c569 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -23,11 +23,11 @@ import random import tempfile import subprocess from typing import Dict, Any, List, Union +from logging import getLogger from moulinette import m18n from moulinette.utils.text import random_ascii from moulinette.utils.process import check_output -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import mkdir, chown, chmod, write_to_file from moulinette.utils.filesystem import ( rm, @@ -35,7 +35,7 @@ from moulinette.utils.filesystem import ( from yunohost.utils.system import system_arch from yunohost.utils.error import YunohostError, YunohostValidationError -logger = getActionLogger("yunohost.app_resources") +logger = getLogger("yunohost.utils.resources") class AppResourceManager: From 6b5c9a2a8bc0b628a9f45773e7375f51c4bc5782 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 18 Jul 2023 11:25:08 +0200 Subject: [PATCH 0204/1116] Gandi's `api_protocol` field should be a `select` type --- share/registrar_list.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/registrar_list.toml b/share/registrar_list.toml index 3f478a03f..47218c9e3 100644 --- a/share/registrar_list.toml +++ b/share/registrar_list.toml @@ -227,7 +227,7 @@ redact = true [gandi.api_protocol] - type = "string" + type = "select" choices.rpc = "RPC" choices.rest = "REST" default = "rest" From e1ceb084c3df75e37be4fbde97c075ee75d775cf Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Jul 2023 15:05:07 +0200 Subject: [PATCH 0205/1116] fix config panel readonly .value --- src/utils/configpanel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 42a030cbc..ee9019303 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -609,7 +609,7 @@ class ConfigPanel: { question.id: question.value for question in questions - if question.value is not None + if not question.readonly and question.value is not None } ) From 2d54be6e8d64a3f19c215ed3553d928a1548b156 Mon Sep 17 00:00:00 2001 From: ppr Date: Tue, 18 Jul 2023 20:04:22 +0000 Subject: [PATCH 0206/1116] Translated using Weblate (French) Currently translated at 98.4% (768 of 780 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index f98470c99..2cc0f612c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -527,7 +527,7 @@ "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 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}'", + "additional_urls_already_added": "L'URL supplémentaire '{url}' a déjà été 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.", "show_tile_cant_be_enabled_for_regex": "Vous ne pouvez pas activer 'show_tile' pour le moment, cela car l'URL de l'autorisation '{permission}' est une expression régulière", "show_tile_cant_be_enabled_for_url_not_defined": "Vous ne pouvez pas activer 'show_tile' pour le moment, car vous devez d'abord définir une URL pour l'autorisation '{permission}'", @@ -536,7 +536,7 @@ "permission_protected": "L'autorisation {permission} est protégée. Vous ne pouvez pas ajouter ou supprimer le groupe visiteurs à/de cette autorisation.", "invalid_regex": "Regex non valide : '{regex}'", "app_label_deprecated": "Cette commande est obsolète ! Veuillez utiliser la nouvelle commande 'yunohost user permission update' pour gérer l'étiquette de l'application.", - "additional_urls_already_removed": "URL supplémentaire '{url}' déjà supprimées pour la permission '{permission}'", + "additional_urls_already_removed": "L'URL supplémentaire '{url}' a déjà été supprimée 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 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.", @@ -766,5 +766,21 @@ "group_mailalias_add": "L'alias de courrier électronique '{mail}' sera ajouté au groupe '{group}'", "group_user_add": "L'utilisateur '{user}' sera ajouté au groupe '{group}'", "group_user_remove": "L'utilisateur '{user}' sera retiré du groupe '{group}'", - "group_mailalias_remove": "L'alias de courrier électronique '{mail}' sera supprimé du groupe '{group}'" + "group_mailalias_remove": "L'alias de courrier électronique '{mail}' sera supprimé du groupe '{group}'", + "ask_dyndns_recovery_password_explain": "Veuillez choisir un mot de passe de récupération pour votre domaine DynDNS, au cas où vous devriez le réinitialiser plus tard.", + "ask_dyndns_recovery_password": "Mot de passe de récupération DynDNS", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Veuillez saisir le mot de passe de récupération pour ce domaine DynDNS.", + "dyndns_no_recovery_password": "Aucun mot de passe de récupération n'a été spécifié ! Si vous perdez le contrôle de ce domaine, vous devrez contacter un administrateur de l'équipe YunoHost !", + "dyndns_subscribed": "Domaine DynDNS souscrit/enregistré", + "dyndns_subscribe_failed": "Impossible de souscrire/de s'enregistrer au domaine DynDNS : {erreur}", + "dyndns_unsubscribe_failed": "Impossible de se désinscrire du domaine DynDNS : {erreur}", + "dyndns_unsubscribed": "Désinscription du domaine DynDNS", + "dyndns_unsubscribe_denied": "Échec de la désinscription du domaine : informations d'identification non valides", + "dyndns_unsubscribe_already_unsubscribed": "Le domaine est déjà désabonné/retiré", + "dyndns_set_recovery_password_denied": "Échec de la définition du mot de passe de récupération : mot de passe non valide", + "dyndns_set_recovery_password_unknown_domain": "Échec de la définition du mot de passe de récupération : le domaine n'est pas enregistré", + "dyndns_set_recovery_password_invalid_password": "Échec de la définition du mot de passe de récupération : le mot de passe n'est pas assez fort/solide", + "dyndns_set_recovery_password_failed": "Échec de la définition du mot de passe de récupération : {erreur}", + "dyndns_set_recovery_password_success": "Mot de passe de récupération défini/configuré !", + "log_dyndns_unsubscribe": "Se désabonner d'un sous-domaine YunoHost '{}'" } From e4462556488444f8f9ed70385a1d711eda85be5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Tue, 18 Jul 2023 03:49:58 +0000 Subject: [PATCH 0207/1116] Translated using Weblate (Galician) Currently translated at 100.0% (780 of 780 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/locales/gl.json b/locales/gl.json index 3aaacd9c9..1926f6148 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -766,5 +766,21 @@ "group_mailalias_add": "Vaise engadir o alias de correo '{mail}' ao grupo '{group}'", "group_mailalias_remove": "Vaise quitar o alias de email '{mail}' do grupo '{group}'", "group_user_add": "Vaise engadir a '{user}' ao grupo '{grupo}'", - "group_user_remove": "Vaise quitar a '{user}' do grupo '{grupo}'" + "group_user_remove": "Vaise quitar a '{user}' do grupo '{grupo}'", + "ask_dyndns_recovery_password_explain": "Elixe un contrasinal de recuperación para o teu dominio DynDNS, por se precisas restablecelo no futuro.", + "ask_dyndns_recovery_password": "Contrasinal de recuperación DynDNS", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Escribe o contrasinal de recuperación para este dominio DynDNS.", + "dyndns_no_recovery_password": "Non se estableceu un contrasinal de recuperación! Se perdes o control sobre dominio precisarás contactar coa administración do equipo YunoHost!", + "dyndns_subscribed": "Tes unha subscrición a un dominio DynDNS", + "dyndns_subscribe_failed": "Non te subscribiches ao dominio DynDNS: {error}", + "dyndns_unsubscribe_failed": "Non se retirou a subscrición ao dominio DynDNS: {error}", + "dyndns_unsubscribed": "Retirada a subscrición ao dominio DynDNS", + "dyndns_unsubscribe_denied": "Fallo ao intentar retirar subscrición: credenciais incorrectas", + "dyndns_unsubscribe_already_unsubscribed": "Non tes unha subscrición ao dominio", + "dyndns_set_recovery_password_denied": "Fallou o establecemento do contrasinal de recuperación: chave non válida", + "dyndns_set_recovery_password_unknown_domain": "Fallo ao establecer o contrasinal de recuperación: dominio non rexistrado", + "dyndns_set_recovery_password_invalid_password": "Fallo ao establecer contrasinal de recuperación: o contrasinal non é suficientemente forte", + "dyndns_set_recovery_password_failed": "Fallo ao establecer o contrasinal de recuperación: {error}", + "dyndns_set_recovery_password_success": "Estableceuse o contrasinal de recuperación!", + "log_dyndns_unsubscribe": "Retirar subscrición para o subdominio YunoHost '{}'" } From 53bc30b9fb1ca698b971ae10e579abce605b56b0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 19 Jul 2023 02:36:39 +0200 Subject: [PATCH 0208/1116] Update changelog for 11.2.2 --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 3a2747bec..103495d72 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (11.2.2) stable; urgency=low + + - domains: Gandi's `api_protocol` field should be a `select` type ([#1693](https://github.com/yunohost/yunohost/pull/1693)) + - configpanel: fix .value call for readonly-type options (e1ceb084) + - i18n: Translations updated for French, Galician + + Thanks to all contributors <3 ! (axolotle, José M, ppr, tituspijean) + + -- Alexandre Aubin Wed, 19 Jul 2023 02:35:28 +0200 + yunohost (11.2.1) stable; urgency=low - doc: fix resource doc generation .. not sure why this line that removed legit indent was there (ced222ea) From 4fda8ed49fbf97c10343c15e794e74683a727785 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 21 Jul 2023 22:02:56 +0200 Subject: [PATCH 0209/1116] apps: fix another case of no attribute 'value' due to config panels/questions refactoring --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 75d336241..7f4acdc1a 100644 --- a/src/app.py +++ b/src/app.py @@ -1098,7 +1098,7 @@ def app_install( args = { question.id: question.value for question in questions - if question.value is not None + if not question.readonly and question.value is not None } # Validate domain / path availability for webapps From 465f6da5cd4d716bbcb802dfd742114083034235 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 22 Jul 2023 16:48:48 +0200 Subject: [PATCH 0210/1116] Update changelog for 11.2.3 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 103495d72..586f8387b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (11.2.3) stable; urgency=low + + - apps: fix another case of no attribute 'value' due to config panels/questions refactoring (4fda8ed49) + + -- Alexandre Aubin Sat, 22 Jul 2023 16:48:22 +0200 + yunohost (11.2.2) stable; urgency=low - domains: Gandi's `api_protocol` field should be a `select` type ([#1693](https://github.com/yunohost/yunohost/pull/1693)) From 704e42a6af530c4816a50c9cb93655a14ed4cbe8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 29 Jul 2023 19:13:00 +0200 Subject: [PATCH 0211/1116] portalapi: fix cookie not being deleted because maxage=-1 or something --- src/authenticators/ldap_ynhuser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index e8cfaf108..9702693ed 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -172,5 +172,5 @@ class Authenticator(BaseAuthenticator): from bottle import response - response.set_cookie("yunohost.portal", "", max_age=-1) + response.set_cookie("yunohost.portal", "") response.delete_cookie("yunohost.portal") From 09c5a4cfb91cdede55956bb2e3cf747a0a2c6e18 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 29 Jul 2023 19:15:30 +0200 Subject: [PATCH 0212/1116] admin and portalapi: propagate new configurable CORS mechanism from moulinette --- conf/nginx/yunohost_api.conf.inc | 12 ------------ src/__init__.py | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/conf/nginx/yunohost_api.conf.inc b/conf/nginx/yunohost_api.conf.inc index 8133624b3..9cb4ff00d 100644 --- a/conf/nginx/yunohost_api.conf.inc +++ b/conf/nginx/yunohost_api.conf.inc @@ -26,18 +26,6 @@ location = /yunohost/api/error/502 { location /yunohost/portalapi/ { - # FIXME FIXME FIXME : we should think about what we really want here ... - more_set_headers "Access-Control-Allow-Origin: $http_origin"; - more_set_headers "Access-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, DELETE"; - more_set_headers "Access-Control-Allow-Headers: Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With"; - more_set_headers "Access-Control-Allow-Credentials: true"; - - if ($request_method = 'OPTIONS') { - more_set_headers "Content-Type: text/plain; charset=utf-8"; - more_set_headers "Content-Length: 0"; - return 204; - } - proxy_read_timeout 5s; proxy_pass http://127.0.0.1:6788/; proxy_http_version 1.1; diff --git a/src/__init__.py b/src/__init__.py index 146485d2d..99f3739bf 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -50,6 +50,13 @@ def cli(debug, quiet, output_as, timeout, args, parser): def api(debug, host, port): + + allowed_cors_origins = [] + allowed_cors_origins_file = "/etc/yunohost/.admin-api-allowed-cors-origins" + + if os.path.exists(allowed_cors_origins_file): + allowed_cors_origins = open(allowed_cors_origins_file).read().strip().split(",") + init_logging(interface="api", debug=debug) def is_installed_api(): @@ -64,12 +71,19 @@ def api(debug, host, port): actionsmap="/usr/share/yunohost/actionsmap.yml", locales_dir="/usr/share/yunohost/locales/", routes={("GET", "/installed"): is_installed_api}, + allowed_cors_origins=allowed_cors_origins, ) sys.exit(ret) def portalapi(debug, host, port): + allowed_cors_origins = [] + allowed_cors_origins_file = "/etc/yunohost/.portal-api-allowed-cors-origins" + + if os.path.exists(allowed_cors_origins_file): + allowed_cors_origins = open(allowed_cors_origins_file).read().strip().split(",") + # FIXME : is this the logdir we want ? (yolo to work around permission issue) init_logging(interface="portalapi", debug=debug, logdir="/var/log") @@ -77,7 +91,8 @@ def portalapi(debug, host, port): host=host, port=port, actionsmap="/usr/share/yunohost/actionsmap-portal.yml", - locales_dir="/usr/share/yunohost/locales/" + locales_dir="/usr/share/yunohost/locales/", + allowed_cors_origins=allowed_cors_origins, ) sys.exit(ret) From 5fd1850f19c020115298d7595fc0212a8fb0b9be Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 30 Jul 2023 23:53:04 +0200 Subject: [PATCH 0213/1116] Add dependency to new yunohost-portal debian package --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index df9a6d2bd..121ac3f6f 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Package: yunohost Essential: yes Architecture: all Depends: ${python3:Depends}, ${misc:Depends} - , moulinette (>= 11.1), ssowat (>= 11.1) + , moulinette (>= 11.1), ssowat (>= 11.1), yunohost-portal (>= 11.1) , python3-psutil, python3-requests, python3-dnspython, python3-openssl , python3-miniupnpc, python3-dbus, python3-jinja2 , python3-toml, python3-packaging, python3-publicsuffix2 From afd7b37ebcabfd53e6fbfe2dc64503aab25d5f1a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 30 Jul 2023 23:53:43 +0200 Subject: [PATCH 0214/1116] Tweak nginx portal conf to serve html/css/js/assets from /usr/share/yunohost/portal, similar to webadmin --- conf/nginx/plain/yunohost_sso.conf.inc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/conf/nginx/plain/yunohost_sso.conf.inc b/conf/nginx/plain/yunohost_sso.conf.inc index 984440679..578a75e52 100644 --- a/conf/nginx/plain/yunohost_sso.conf.inc +++ b/conf/nginx/plain/yunohost_sso.conf.inc @@ -2,6 +2,16 @@ rewrite ^/yunohost/sso$ /yunohost/sso/ permanent; location /yunohost/sso/ { - alias /usr/share/ssowat/portal/; + alias /usr/share/yunohost/portal/; + default_type text/html; index index.html; + try_files $uri $uri/ /index.html; + + location = /yunohost/sso/index.html { + etag off; + expires off; + more_set_headers "Cache-Control: no-store, no-cache, must-revalidate"; + } + + more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; img-src 'self' data:;"; } From ca6eb2cbaf8e65abc95c78e2223a24b36c9005e3 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 1 Aug 2023 15:15:52 +0200 Subject: [PATCH 0215/1116] lint --- src/portal.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/portal.py b/src/portal.py index 6a51f33d4..f102f1515 100644 --- a/src/portal.py +++ b/src/portal.py @@ -48,10 +48,16 @@ def portal_me(): else: raise YunohostValidationError("user_unknown", user=username) - groups = [g.replace("cn=", "").replace(",ou=groups,dc=yunohost,dc=org", "") for g in user["memberOf"]] + groups = [ + g.replace("cn=", "").replace(",ou=groups,dc=yunohost,dc=org", "") + for g in user["memberOf"] + ] groups = [g for g in groups if g not in [username, "all_users"]] - permissions = [p.replace("cn=", "").replace(",ou=permission,dc=yunohost,dc=org", "") for p in user["permission"]] + permissions = [ + p.replace("cn=", "").replace(",ou=permission,dc=yunohost,dc=org", "") + for p in user["permission"] + ] ssowat_conf = read_json("/etc/ssowat/conf.json") apps = { @@ -67,7 +73,7 @@ def portal_me(): "mail-aliases": user["mail"][1:], "mail-forward": user["maildrop"][1:], "groups": groups, - "apps": apps + "apps": apps, } # FIXME / TODO : add mail quota status ? From c3a4b7dabb9946bc6ed1d12aa7ba50fc85c255cb Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 1 Aug 2023 15:18:48 +0200 Subject: [PATCH 0216/1116] add _get_user_infos helper --- src/portal.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/portal.py b/src/portal.py index f102f1515..7e67ff35d 100644 --- a/src/portal.py +++ b/src/portal.py @@ -29,24 +29,25 @@ from yunohost.utils.error import YunohostValidationError logger = getActionLogger("portal") +def _get_user_infos(user_attrs: list[str]): + auth = Auth().get_session_cookie(decrypt_pwd=True) + username = auth["user"] + ldap = LDAPInterface(username, auth["pwd"]) + result = ldap.search("ou=users", f"uid={username}", user_attrs) + if not result: + raise YunohostValidationError("user_unknown", user=username) + + return username, result[0], ldap + + def portal_me(): """ Get user informations """ - auth = Auth().get_session_cookie(decrypt_pwd=True) - username = auth["user"] - - ldap = LDAPInterface(username, auth["pwd"]) - - user_attrs = ["cn", "mail", "maildrop", "mailuserquota", "memberOf", "permission"] - - result = ldap.search("ou=users", f"uid={username}", user_attrs) - - if result: - user = result[0] - else: - raise YunohostValidationError("user_unknown", user=username) + username, user, ldap = _get_user_infos( + ["cn", "mail", "maildrop", "mailuserquota", "memberOf", "permission"] + ) groups = [ g.replace("cn=", "").replace(",ou=groups,dc=yunohost,dc=org", "") From c9092b2aadd82f3bc7c57ce626248f6a9983fba6 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 1 Aug 2023 15:29:09 +0200 Subject: [PATCH 0217/1116] add portal_update to update user infos --- share/actionsmap-portal.yml | 28 +++++++++++- src/portal.py | 85 +++++++++++++++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/share/actionsmap-portal.yml b/share/actionsmap-portal.yml index 761d5a6ce..268a420b3 100644 --- a/share/actionsmap-portal.yml +++ b/share/actionsmap-portal.yml @@ -23,8 +23,32 @@ portal: ### portal_update() update: action_help: Allow user to update their infos (display name, mail aliases/forward, password, ...) - api: PUT /me - # FIXME: add args etc + api: PUT /update + arguments: + -F: + full: --fullname + help: The full name of the user. For example 'Camille Dupont' + extra: + pattern: &pattern_fullname + - !!str ^([^\W_]{1,30}[ ,.'-]{0,3})+$ + - "pattern_fullname" + --mailforward: + help: Mailforward addresses to add + nargs: "*" + metavar: MAIL + extra: + pattern: &pattern_email_forward + - !!str ^[\w\+.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ + - "pattern_email_forward" + --mailalias: + help: Mail aliases to add + nargs: "*" + metavar: MAIL + extra: + pattern: &pattern_email + - !!str ^[\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ + - "pattern_email" + ### portal_reset_password() reset_password: diff --git a/src/portal.py b/src/portal.py index 7e67ff35d..2e234ec73 100644 --- a/src/portal.py +++ b/src/portal.py @@ -18,16 +18,19 @@ along with this program; if not, see http://www.gnu.org/licenses """ +from typing import Union from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_json from yunohost.authenticators.ldap_ynhuser import Authenticator as Auth from yunohost.utils.ldap import LDAPInterface -from yunohost.utils.error import YunohostValidationError +from yunohost.utils.error import YunohostError, YunohostValidationError logger = getActionLogger("portal") +ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] + def _get_user_infos(user_attrs: list[str]): auth = Auth().get_session_cookie(decrypt_pwd=True) @@ -71,8 +74,8 @@ def portal_me(): "username": username, "fullname": user["cn"][0], "mail": user["mail"][0], - "mail-aliases": user["mail"][1:], - "mail-forward": user["maildrop"][1:], + "mailalias": user["mail"][1:], + "mailforward": user["maildrop"][1:], "groups": groups, "apps": apps, } @@ -86,3 +89,79 @@ def portal_me(): # But this requires to be in the mail group ... return result_dict + + +def portal_update( + fullname: Union[str, None] = None, + mailforward: Union[list[str], None] = None, + mailalias: Union[list[str], None] = None, +): + from yunohost.domain import domain_list + + domains = domain_list()["domains"] + username, current_user, ldap = _get_user_infos( + ["givenName", "sn", "cn", "mail", "maildrop", "memberOf"] + ) + new_attr_dict = {} + + if fullname is not None and fullname != current_user["cn"]: + 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... + new_attr_dict["givenName"] = [firstname] # TODO: Validate + new_attr_dict["sn"] = [lastname] # TODO: Validate + new_attr_dict["cn"] = new_attr_dict["displayName"] = [ + (firstname + " " + lastname).strip() + ] + + if mailalias is not None: + mailalias = [mail.strip() for mail in mailalias if mail and mail.strip()] + # keep first current mail unaltered + mails = [current_user["mail"][0]] + + for index, mail in enumerate(mailalias): + if mail in current_user["mail"]: + if mail != current_user["mail"][0]: + mails.append(mail) + continue # already in mails, skip validation + + local_part, domain = mail.strip().split("@") + if local_part in ADMIN_ALIASES: + raise YunohostValidationError( + "mail_unavailable", path="mailalias", index=index + ) + + try: + ldap.validate_uniqueness({"mail": mail}) + except Exception as e: + raise YunohostError("user_update_failed", user=username, error=e) + + if domain not in domains: + raise YunohostError("mail_domain_unknown", domain=domain) + + mails.append(mail) + + new_attr_dict["mail"] = mails + + if mailforward is not None: + new_attr_dict["maildrop"] = [current_user["maildrop"][0]] + [ + mail.strip() + for mail in mailforward + if mail and mail.strip() and mail != current_user["maildrop"][0] + ] + + try: + ldap.update(f"uid={username},ou=users", new_attr_dict) + except Exception as e: + raise YunohostError("user_update_failed", user=username, error=e) + + # FIXME: Here we could want to trigger "post_user_update" hook but hooks has to + # be run as root + + return { + "fullname": new_attr_dict["cn"][0], + "mailalias": new_attr_dict["mail"][1:], + "mailforward": new_attr_dict["maildrop"][1:], + } From db1670ca5d8cec856cda93ed3b5e06d4c03e6dfe Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 1 Aug 2023 16:28:25 +0200 Subject: [PATCH 0218/1116] add temp portal_update_password --- share/actionsmap-portal.yml | 11 +++++++++++ src/portal.py | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/share/actionsmap-portal.yml b/share/actionsmap-portal.yml index 268a420b3..2673fc226 100644 --- a/share/actionsmap-portal.yml +++ b/share/actionsmap-portal.yml @@ -49,6 +49,17 @@ portal: - !!str ^[\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ - "pattern_email" + ### portal_update_password() + update_password: + action_help: Allow user to change their password + api: PUT /me/update_password + arguments: + -c: + full: --current + help: Current password + -p: + full: --password + help: New password to set ### portal_reset_password() reset_password: diff --git a/src/portal.py b/src/portal.py index 2e234ec73..fa2d1a30a 100644 --- a/src/portal.py +++ b/src/portal.py @@ -26,6 +26,11 @@ from moulinette.utils.filesystem import read_json from yunohost.authenticators.ldap_ynhuser import Authenticator as Auth from yunohost.utils.ldap import LDAPInterface from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.password import ( + assert_password_is_compatible, + assert_password_is_strong_enough, +) +from yunohost.user import _hash_user_password logger = getActionLogger("portal") @@ -165,3 +170,22 @@ def portal_update( "mailalias": new_attr_dict["mail"][1:], "mailforward": new_attr_dict["maildrop"][1:], } + + +def portal_update_password(current: str, password: str): + username, current_user, ldap = _get_user_infos(["userPassword", "memberOf"]) + is_admin = "cn=admins,ou=groups,dc=yunohost,dc=org" in current_user["memberOf"] + + # FIXME: Verify current password ? + + # Ensure compatibility and sufficiently complex password + assert_password_is_compatible(password) + assert_password_is_strong_enough("admin" if is_admin else "user", password) + + try: + ldap.update( + f"uid={username},ou=users", + {"userPassword": [_hash_user_password(password)]}, + ) + except Exception as e: + raise YunohostError("user_update_failed", user=username, error=e) From 6f8b3fd57feebbbf987a02605fd842f9324ff646 Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Sun, 13 Aug 2023 23:11:31 +0200 Subject: [PATCH 0219/1116] Handle both cookies in the same way (please let me logout) --- src/authenticators/ldap_admin.py | 4 ++-- src/authenticators/ldap_ynhuser.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/authenticators/ldap_admin.py b/src/authenticators/ldap_admin.py index b1b550bc0..155e84127 100644 --- a/src/authenticators/ldap_admin.py +++ b/src/authenticators/ldap_admin.py @@ -138,6 +138,7 @@ class Authenticator(BaseAuthenticator): secure=True, secret=session_secret, httponly=True, + path="/" # samesite="strict", # Bottle 0.12 doesn't support samesite, to be added in next versions ) @@ -172,5 +173,4 @@ class Authenticator(BaseAuthenticator): def delete_session_cookie(self): from bottle import response - response.set_cookie("yunohost.admin", "", max_age=-1) - response.delete_cookie("yunohost.admin") + response.delete_cookie("yunohost.admin", path="/") diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index 9702693ed..08138f1b5 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -172,5 +172,4 @@ class Authenticator(BaseAuthenticator): from bottle import response - response.set_cookie("yunohost.portal", "") - response.delete_cookie("yunohost.portal") + response.delete_cookie("yunohost.portal", path="/") From 101b5704c491de6df80f236a10c4fc865fdc9963 Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Tue, 15 Aug 2023 12:23:56 +0200 Subject: [PATCH 0220/1116] Serialize the JWT token to a cookie string instead of failing --- src/authenticators/ldap_ynhuser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index 08138f1b5..2add68cab 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -132,7 +132,7 @@ class Authenticator(BaseAuthenticator): response.set_cookie( "yunohost.portal", - jwt.encode(new_infos, session_secret, algorithm="HS256"), + jwt.encode(new_infos, session_secret, algorithm="HS256").decode(), secure=True, httponly=True, path="/", From 26d4d9420c6e129e3de869f85cd308e087d25d5b Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Tue, 15 Aug 2023 14:12:08 +0200 Subject: [PATCH 0221/1116] Allow inline scripts for yunohost-portal (nginx CSP) --- conf/nginx/plain/yunohost_sso.conf.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/nginx/plain/yunohost_sso.conf.inc b/conf/nginx/plain/yunohost_sso.conf.inc index 578a75e52..fb5406cfc 100644 --- a/conf/nginx/plain/yunohost_sso.conf.inc +++ b/conf/nginx/plain/yunohost_sso.conf.inc @@ -13,5 +13,5 @@ location /yunohost/sso/ { more_set_headers "Cache-Control: no-store, no-cache, must-revalidate"; } - more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; img-src 'self' data:;"; + more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; object-src 'none'; img-src 'self' data:;"; } From d2113b243ebc4f5ac3ffcce34e821f47bea27ba1 Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Tue, 15 Aug 2023 15:35:36 +0200 Subject: [PATCH 0222/1116] Add information in yunohost app install --help --- share/actionsmap.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 48e12ba0f..0c3301874 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -934,14 +934,14 @@ app: help: Custom name for the app -a: full: --args - help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") + help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path&init_main_permission=visitors") -n: full: --no-remove-on-failure help: Debug option to avoid removing the app on a failed installation action: store_true -f: full: --force - help: Do not ask confirmation if the app is not safe to use (low quality, experimental or 3rd party) + help: Do not ask confirmation if the app is not safe to use (low quality, experimental or 3rd party), or when the app displays a post-install notification action: store_true ### app_remove() From 0d88978c2a31e967570b04e14ab747956b2fb9b5 Mon Sep 17 00:00:00 2001 From: massyas Date: Wed, 16 Aug 2023 17:25:23 +0200 Subject: [PATCH 0223/1116] Fix typo in app_upgrade argument help --- share/actionsmap.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 0c3301874..e44a72125 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -980,7 +980,7 @@ app: action: store_true -c: full: --continue-on-failure - help: Continue to upgrade apps event if one or more upgrade failed + help: Continue to upgrade apps even if one or more upgrade failed action: store_true ### app_change_url() From d716746f281957fec6054744a13ce499420e76e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Wed, 19 Jul 2023 12:09:22 +0000 Subject: [PATCH 0224/1116] Translated using Weblate (Galician) Currently translated at 100.0% (780 of 780 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/gl.json b/locales/gl.json index 1926f6148..93f73b528 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -612,7 +612,7 @@ "domain_config_auth_consumer_key": "Chave consumidora", "log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'", "other_available_options": "... e outras {n} opcións dispoñibles non mostradas", - "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Este dominio é un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost sen máis requisitos. (mira o comando 'yunohost dyndns update')", "domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registrar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registrar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )", "domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.", "domain_config_auth_token": "Token de autenticación", @@ -654,7 +654,7 @@ "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_compatibility_help": "Compromiso entre compatibilidade e seguridade para o servidor SSH. Aféctalle ao cifrado (e outros aspectos da seguridade). Le https://infosec.mozilla.org/guidelines/openssh for more info.", "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.", From 9fdbc5532f8f5144602f3430e2f9ef07f96dbc0e Mon Sep 17 00:00:00 2001 From: Suleyman Harmandar Date: Sat, 22 Jul 2023 05:45:21 +0000 Subject: [PATCH 0225/1116] Translated using Weblate (Turkish) Currently translated at 2.5% (20 of 780 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/tr/ --- locales/tr.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/locales/tr.json b/locales/tr.json index 1af0ffd54..3c15591f3 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -15,5 +15,10 @@ "additional_urls_already_added": "Ek URL '{url}' zaten '{permission}' izni için ek URL'ye eklendi", "additional_urls_already_removed": "Ek URL '{url}', '{permission}' izni için ek URL'de zaten kaldırıldı", "app_action_cannot_be_ran_because_required_services_down": "Bu eylemi gerçekleştirmek için şu servisler çalışıyor olmalıdır: {services}. Devam etmek için onları yeniden başlatın (ve muhtemelen neden çalışmadığını araştırın).", - "app_arch_not_supported": "Bu uygulama yalnızca {required} işlemci mimarisi üzerine kurulabilir ancak sunucunuzun işlemci mimarisi {current}." -} \ No newline at end of file + "app_arch_not_supported": "Bu uygulama yalnızca {required} işlemci mimarisi üzerine kurulabilir ancak sunucunuzun işlemci mimarisi {current}.", + "app_argument_choice_invalid": "'{name}'' için geçerli bir değer giriniz '{value}' mevcut seçimlerin arasında değil ({choices})", + "app_change_url_failed": "{app}: {error} için url değiştirilemedi", + "app_argument_required": "'{name}' değeri gerekli", + "app_argument_invalid": "'{name}': {error} için geçerli bir değer giriniz", + "app_argument_password_no_default": "'{name}': çözümlenirken bir hata meydana geldi. Parola argümanı güvenlik nedeniyle varsayılan değer alamaz" +} From 5eecfcae6755c517cdc73943a981544d11f3d094 Mon Sep 17 00:00:00 2001 From: Kuba Bazan Date: Sun, 23 Jul 2023 22:20:41 +0000 Subject: [PATCH 0226/1116] Translated using Weblate (Polish) Currently translated at 34.8% (272 of 780 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index 52f2de3ca..78d295f61 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -103,7 +103,7 @@ "backup_applying_method_custom": "Wywołuję niestandardową metodę tworzenia kopii zapasowych '{method}'...", "app_remove_after_failed_install": "Usuwanie aplikacji po niepowodzeniu instalacji...", "app_upgrade_script_failed": "Wystąpił błąd w skrypcie aktualizacji aplikacji", - "apps_catalog_init_success": "Zainicjowano system katalogu aplikacji!", + "apps_catalog_init_success": "System katalogu aplikacji został zainicjowany!", "apps_catalog_obsolete_cache": "Pamięć podręczna katalogu aplikacji jest pusta lub przestarzała.", "app_extraction_failed": "Nie można wyodrębnić plików instalacyjnych", "app_packaging_format_not_supported": "Ta aplikacja nie może zostać zainstalowana, ponieważ jej format opakowania nie jest obsługiwany przez twoją wersję YunoHost. Prawdopodobnie powinieneś rozważyć aktualizację swojego systemu.", @@ -183,7 +183,7 @@ "app_failed_to_download_asset": "Nie udało się pobrać zasobu '{source_id}' ({url}) dla {app}: {out}", "backup_with_no_backup_script_for_app": "Aplikacja '{app}' nie posiada skryptu kopii zapasowej. Ignorowanie.", "backup_with_no_restore_script_for_app": "Aplikacja {app} nie posiada skryptu przywracania, co oznacza, że nie będzie można automatycznie przywrócić kopii zapasowej tej aplikacji.", - "certmanager_acme_not_configured_for_domain": "Wyzwanie ACME nie może zostać uruchomione dla domeny {domain}, ponieważ jej konfiguracja nginx nie zawiera odpowiedniego fragmentu kodu... Upewnij się, że konfiguracja nginx jest aktualna, używając polecenia yunohost tools regen-conf nginx --dry-run --with-diff.", + "certmanager_acme_not_configured_for_domain": "Wyzwanie ACME nie może być teraz uruchomione dla {domain}, ponieważ jego konfiguracja nginx nie zawiera odpowiedniego fragmentu kodu… Upewnij się, że twoja konfiguracja nginx jest aktualna, używając `yunohost tools regen-conf nginx --dry-run --with-diff`.", "certmanager_domain_dns_ip_differs_from_public_ip": "Rekordy DNS dla domeny '{domain}' różnią się od adresu IP tego serwera. Sprawdź kategorię 'Rekordy DNS' (podstawowe) w diagnozie, aby uzyskać więcej informacji. Jeśli niedawno dokonałeś zmiany rekordu A, poczekaj, aż zostanie on zaktualizowany (można skorzystać z narzędzi online do sprawdzania propagacji DNS). (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", "confirm_app_install_danger": "UWAGA! Ta aplikacja jest wciąż w fazie eksperymentalnej (jeśli nie działa jawnie)! Prawdopodobnie NIE powinieneś jej instalować, chyba że wiesz, co robisz. NIE ZOSTANIE udzielone wsparcie, jeśli ta aplikacja nie będzie działać poprawnie lub spowoduje uszkodzenie systemu... Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}", "confirm_app_install_thirdparty": "UWAGA! Ta aplikacja nie jest częścią katalogu aplikacji YunoHost. Instalowanie aplikacji innych firm może naruszyć integralność i bezpieczeństwo systemu. Prawdopodobnie NIE powinieneś jej instalować, chyba że wiesz, co robisz. NIE ZOSTANIE udzielone wsparcie, jeśli ta aplikacja nie będzie działać poprawnie lub spowoduje uszkodzenie systemu... Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'", @@ -192,19 +192,19 @@ "config_no_panel": "Nie znaleziono panelu konfiguracji.", "config_unknown_filter_key": "Klucz filtru '{filter_key}' jest niepoprawny.", "config_validate_email": "Proszę podać poprawny adres e-mail", - "backup_hook_unknown": "Nieznany jest hook kopii zapasowej '{hook}'.", + "backup_hook_unknown": "Nieznany jest hook kopii zapasowej '{hook}'", "backup_no_uncompress_archive_dir": "Nie istnieje taki katalog nieskompresowanego archiwum.", - "backup_output_symlink_dir_broken": "Twój katalog archiwum '{path}' to uszkodzony dowiązanie symboliczne. Być może zapomniałeś o ponownym zamontowaniu lub podłączeniu nośnika przechowującego, do którego on wskazuje.", - "backup_system_part_failed": "Nie można wykonać kopii zapasowej części systemu '{part}'", + "backup_output_symlink_dir_broken": "Twój katalog archiwum ‘{path}’ to uszkodzony symlink. Być może zapomniałeś o ponownym zamontowaniu lub podłączeniu nośnika przechowującego, do którego on wskazuje.", + "backup_system_part_failed": "Nie udało się wykonać kopii zapasowej części systemu ‘{part}’", "config_validate_color": "Powinien być poprawnym szesnastkowym kodem koloru RGB.", "config_validate_date": "Data powinna być poprawna w formacie RRRR-MM-DD", "config_validate_time": "Podaj poprawny czas w formacie GG:MM", "certmanager_domain_not_diagnosed_yet": "Nie ma jeszcze wyników diagnozy dla domeny {domain}. Proszę ponownie uruchomić diagnozę dla kategorii 'Rekordy DNS' i 'Strona internetowa' w sekcji diagnozy, aby sprawdzić, czy domena jest gotowa do użycia Let's Encrypt. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", "certmanager_cannot_read_cert": "Wystąpił problem podczas próby otwarcia bieżącego certyfikatu dla domeny {domain} (plik: {file}), przyczyna: {reason}", - "certmanager_no_cert_file": "Nie można odczytać pliku certyfikatu dla domeny {domain} (plik: {file}).", - "certmanager_self_ca_conf_file_not_found": "Nie można znaleźć pliku konfiguracyjnego dla autorytetu samopodpisującego (plik: {file})", - "backup_running_hooks": "Uruchamianie hooków kopii zapasowej...", - "backup_permission": "Uprawnienia kopii zapasowej dla aplikacji {app}", + "certmanager_no_cert_file": "Nie można odczytać pliku certyfikatu dla domeny {domain} (plik: {file})", + "certmanager_self_ca_conf_file_not_found": "Nie można znaleźć pliku konfiguracyjnego dla samodzielnie podpisanego upoważnienia do (file: {file})", + "backup_running_hooks": "Uruchamianie kopii zapasowej hooków...", + "backup_permission": "Uprawnienia do tworzenia kopii zapasowej dla aplikacji {app}", "certmanager_domain_cert_not_selfsigned": "Certyfikat dla domeny {domain} nie jest samopodpisany. Czy na pewno chcesz go zastąpić? (Użyj opcji '--force', aby to zrobić.)", "config_action_disabled": "Nie można uruchomić akcji '{action}', ponieważ jest ona wyłączona. Upewnij się, że spełnione są jej ograniczenia. Pomoc: {help}", "config_action_failed": "Nie udało się uruchomić akcji '{action}': {error}", @@ -214,7 +214,7 @@ "confirm_app_insufficient_ram": "UWAGA! Ta aplikacja wymaga {required} pamięci RAM do zainstalowania/aktualizacji, a obecnie dostępne jest tylko {current}. Nawet jeśli aplikacja mogłaby działać, proces instalacji/aktualizacji wymaga dużej ilości pamięci RAM, więc serwer może się zawiesić i niepowodzenie może być katastrofalne. Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'", "app_not_upgraded_broken_system": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu. W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}", "app_not_upgraded_broken_system_continue": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu (parametr --continue-on-failure jest ignorowany). W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}", - "certmanager_domain_http_not_working": "Domena {domain} wydaje się niedostępna przez HTTP. Sprawdź kategorię 'Strona internetowa' diagnostyki, aby uzyskać więcej informacji. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", + "certmanager_domain_http_not_working": "Domena {domain} nie wydaje się być dostępna przez HTTP. Sprawdź kategorię 'Strona internetowa' diagnostyki, aby uzyskać więcej informacji. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", "migration_0021_system_not_fully_up_to_date": "Twój system nie jest w pełni zaktualizowany! Proszę, wykonaj zwykłą aktualizację oprogramowania zanim rozpoczniesz migrację na system Bullseye.", "global_settings_setting_smtp_relay_port": "Port przekaźnika SMTP", "domain_config_cert_renew": "Odnów certyfikat Let's Encrypt", @@ -274,5 +274,12 @@ "global_settings_setting_smtp_allow_ipv6_help": "Zezwól na wykorzystywanie IPv7 do odbierania i wysyłania maili", "global_settings_setting_ssh_password_authentication": "Logowanie hasłem", "diagnosis_backports_in_sources_list": "Wygląda na to że apt (menedżer pakietów) został skonfigurowany tak, aby wykorzystywać repozytorium backported. Nie zalecamy wykorzystywania repozytorium backported, ponieważ może powodować problemy ze stabilnością i/lub konflikty z konfiguracją. No chyba, że wiesz co robisz.", - "domain_config_xmpp_help": "Uwaga: niektóre funkcje XMPP będą wymagały aktualizacji rekordów DNS i odnowienia certyfikatu Lets Encrypt w celu ich włączenia" + "domain_config_xmpp_help": "Uwaga: niektóre funkcje XMPP będą wymagały aktualizacji rekordów DNS i odnowienia certyfikatu Lets Encrypt w celu ich włączenia", + "ask_dyndns_recovery_password_explain": "Proszę wybrać hasło odzyskiwania dla swojej domeny DynDNS, na wypadek gdybyś musiał go później zresetować.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Proszę wprowadzić hasło odzyskiwania dla tej domeny DynDNS.", + "certmanager_unable_to_parse_self_CA_name": "Nie można spasować nazwy organu samopodpisywanego (pliku: {file})", + "app_corrupt_source": "YunoHost był w stanie pobrać zasób ‘{source_id}’ ({url}) dla {app}, ale zasób nie pasuje do oczekiwanego sumy kontrolnej. Może to oznaczać, że na twoim serwerze wystąpiła tymczasowa awaria sieci, LUB zasób został jakoś zmieniony przez dostawcę usługi (lub złośliwego aktora?) i pakowacze YunoHost muszą zbadać sprawę i zaktualizować manifest aplikacji, aby odzwierciedlić tę zmianę. \nOczekiwana suma kontrolna sha256: {expected_sha256} \nPobrana suma kontrolna sha256: {computed_sha256} \nRozmiar pobranego pliku: {size}”", + "ask_dyndns_recovery_password": "Hasło odzyskiwania DynDNS", + "certmanager_hit_rate_limit": "Zbyt wiele certyfikatów zostało ostatnio wydanych dla tej dokładnej grupy domen {domain}. Spróbuj ponownie później. Zobacz https://letsencrypt.org/docs/rate-limits/ aby uzyskać więcej informacji", + "apps_failed_to_upgrade_line": "\n * {app_id} (aby zobaczyć odpowiedni dziennik, wykonaj ‘yunohost log show {operation_logger_name}’)" } From 927a17cf3046d49023e9cd8ac92be5a02978eb2a Mon Sep 17 00:00:00 2001 From: taco Date: Wed, 2 Aug 2023 18:07:56 +0000 Subject: [PATCH 0227/1116] Translated using Weblate (Spanish) Currently translated at 95.8% (748 of 780 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/es/ --- locales/es.json | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/locales/es.json b/locales/es.json index 85d7b1f43..5003c305c 100644 --- a/locales/es.json +++ b/locales/es.json @@ -522,8 +522,8 @@ "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": "URL adicional '{url}' ya eliminada en la URL adicional para permiso «{permission}»", + "additional_urls_already_added": "URL adicional '{url}' ya añadida en la URL adicional para 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.", @@ -747,5 +747,10 @@ "global_settings_setting_smtp_relay_host": "Host de retransmisión SMTP", "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Se intentará reconstruir el virtualenv para las siguientes apps (NB: ¡la operación puede llevar algún tiempo!): {rebuild_apps}", "migration_description_0025_global_settings_to_configpanel": "Migración de la nomenclatura de ajustes globales heredada a la nomenclatura nueva y moderna", - "registrar_infos": "Información sobre el registrador" -} \ No newline at end of file + "registrar_infos": "Información sobre el registrador", + "app_failed_to_download_asset": "Error al descargar el recurso '{source_id}' ({url}) para {app}: {out}", + "app_corrupt_source": "YunoHost ha podido descargar el recurso '{source_id}' ({url}) para {app}, pero no coincide con la suma de comprobación esperada. Esto puede significar que ocurrió un fallo de red en tu servidor, o que el recurso ha sido modificado por el responsable de la aplicación (¿o un actor malicioso?) y los responsables de empaquetar esta aplicación para YunoHost necesitan investigar y actualizar el manifesto de la aplicación para reflejar estos cambios. \n Suma de control sha256 esperada: {expected_sha256}\n Suma de control sha256 descargada: {computed_sha256}\n Tamaño del archivo descargado: {size}", + "app_change_url_failed": "No es possible cambiar la URL para {app}: {error}", + "app_change_url_require_full_domain": "{app} no se puede mover a esta nueva URL porque requiere un dominio completo (es decir, con una ruta = /)", + "app_change_url_script_failed": "Se ha producido un error en el script de modificación de la url" +} From f46dc30783e7b19295f3e5b36c7c85e34832cf5e Mon Sep 17 00:00:00 2001 From: Grzegorz Cichocki Date: Sat, 5 Aug 2023 22:19:12 +0000 Subject: [PATCH 0228/1116] Translated using Weblate (Polish) Currently translated at 35.0% (273 of 780 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/pl.json b/locales/pl.json index 78d295f61..17a9fa572 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -193,7 +193,7 @@ "config_unknown_filter_key": "Klucz filtru '{filter_key}' jest niepoprawny.", "config_validate_email": "Proszę podać poprawny adres e-mail", "backup_hook_unknown": "Nieznany jest hook kopii zapasowej '{hook}'", - "backup_no_uncompress_archive_dir": "Nie istnieje taki katalog nieskompresowanego archiwum.", + "backup_no_uncompress_archive_dir": "Nie istnieje taki katalog nieskompresowanego archiwum", "backup_output_symlink_dir_broken": "Twój katalog archiwum ‘{path}’ to uszkodzony symlink. Być może zapomniałeś o ponownym zamontowaniu lub podłączeniu nośnika przechowującego, do którego on wskazuje.", "backup_system_part_failed": "Nie udało się wykonać kopii zapasowej części systemu ‘{part}’", "config_validate_color": "Powinien być poprawnym szesnastkowym kodem koloru RGB.", From c4b3068d3abf3c4a1518dbd0d4df004441629673 Mon Sep 17 00:00:00 2001 From: Neko Nekowazarashi Date: Tue, 8 Aug 2023 15:12:19 +0000 Subject: [PATCH 0229/1116] Translated using Weblate (Indonesian) Currently translated at 52.9% (413 of 780 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/id/ --- locales/id.json | 51 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/locales/id.json b/locales/id.json index c6b023102..2cc7c56dc 100644 --- a/locales/id.json +++ b/locales/id.json @@ -391,5 +391,54 @@ "log_letsencrypt_cert_renew": "Memperbarui sertifikat Let's Encrypt '{}'", "log_selfsigned_cert_install": "Memasang sertifikat ditandai sendiri pada domain '{}'", "log_user_permission_reset": "Mengatur ulang izin '{}'", - "domain_config_xmpp": "Pesan Langsung (XMPP)" + "domain_config_xmpp": "Pesan Langsung (XMPP)", + "diagnosis_http_connection_error": "Masalah jaringan: tidak dapat terhubung dengan domain yang diminta, sangat mungkin terputus.", + "dyndns_ip_updated": "IP Anda diperbarui di DynDNS", + "ask_dyndns_recovery_password_explain": "Pilih kata sandi pemulihan untuk domain DynDNS Anda.", + "ask_dyndns_recovery_password": "Kata sandi pemulihan DynDNS", + "backup_output_directory_not_empty": "Anda harus memilih direktori yang kosong", + "service_reload_or_restart_failed": "Tidak dapat memuat atau memulai ulang layanan '{service}'\n\nLog layanan baru-baru ini:{logs}", + "service_reload_failed": "Tidak dapat memuat ulang layanan '{service}'\n\nLog layanan baru-baru ini:{logs}", + "service_start_failed": "Tidak dapat memulai layanan '{service}'\n\nLog layanan baru-baru ini: {logs}", + "diagnosis_apps_deprecated_practices": "Versi aplikasi yang dipasang ini masih menggunakan praktik pengemasan yang lama. Anda lebih baik untuk memperbarui aplikasi tersebut.", + "diagnosis_dns_bad_conf": "Beberapa rekaman DNS untuk domain {domain} ada yang tidak ada atau salah (kategori {category})", + "diagnosis_dns_good_conf": "Rekaman DNS untuk domain {domain} sudah diatur dengan benar (kategori {category})", + "dyndns_unavailable": "Domain '{domain}' tidak tersedia.", + "dyndns_set_recovery_password_denied": "Tidak dapat menyetel kata sandi pemulihan: tidak valid", + "dyndns_set_recovery_password_unknown_domain": "Tidak dapat menyetel kata sandi pemulihan: domain belum terdaftar", + "dyndns_set_recovery_password_invalid_password": "Tidak dapat menyetel kata sandi pemulihan: kata sandi tidak cukup kuat", + "dyndns_set_recovery_password_failed": "Tidak dapat menyetel kata sandi pemulihan: {error}", + "dyndns_set_recovery_password_success": "Kata sandi pemulihan berhasil disetel!", + "file_does_not_exist": "Berkas {path} tidak ada.", + "firewall_reload_failed": "Tidak dapat memuat ulang tembok api", + "firewall_reloaded": "Tembok api dimuat ulang", + "migration_description_0023_postgresql_11_to_13": "Migrasi basis data dari PostgreSQL 11 ke 13", + "service_enabled": "Layanan '{service}' akan secara mandiri dimulai saat pemulaian.", + "service_reloaded_or_restarted": "Layanan {service} dimuat atau dimulai ulang", + "service_stopped": "Layanan '{service}' diberhentikan", + "service_unknown": "Layanan yang tidak diketahui: '{service}'", + "updating_apt_cache": "Mengambil pembaruan yang tersedia untuk paket sistem...", + "group_mailalias_remove": "Alias surel '{mail}' akan dihapus dari kelompok '{group}'", + "migration_description_0021_migrate_to_bullseye": "Peningkatan sistem ke Debian Bullseye dan YunoHost 11.x", + "migration_description_0024_rebuild_python_venv": "Memperbaiki aplikasi Python setelah migrasi Bullseye", + "service_disable_failed": "Tidak dapat membuat layanan '{service}' dimulai saat pemulaian.\n\nLog layanan baru-baru ini:{logs}", + "service_disabled": "Layanan '{service}' tidak akan dimulai kembali saat pemulaian.", + "tools_upgrade_failed": "Tidak dapat memperbarui paket: {packages_list}", + "global_settings_setting_nginx_redirect_to_https": "Paksa HTTPS", + "backup_archive_system_part_not_available": "Segmen '{part}' tidak tersedia di cadangan ini", + "backup_output_directory_forbidden": "Pilih direktori yang berbeda. Cadangan tidak dapat dibuat di /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var, atau subfolder dari /home/yunohost.backup/archives", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Masukkan kata sandi pemulihan untuk domain DynDNS ini.", + "backup_output_symlink_dir_broken": "Direktori arsip Anda '{path}' rusak penautannya. Mungkin Anda lupa untuk menambatkan ulang atau memasukkan kembali penyimpanan tujuan penautan direktori arsip tersebut.", + "diagnosis_apps_not_in_app_catalog": "Aplikasi ini tidak ada di katalog aplikasi YunoHost. Jika aplikasi ini ada di sana sebelumnya dan dihapus, Anda disarankan untuk melepas aplikasi ini dikarenakan ini tidak akan menerima pembaruan dan mungkin bisa menghancurkan integritas dan keamanan sistem Anda.", + "dyndns_ip_update_failed": "Tidak dapat memperbarui IP Anda di DynDNS", + "service_restarted": "Layanan {service} dimulai ulang", + "service_started": "Layanan '{service}' dimulai", + "service_stop_failed": "Tidak dapat menghentikan layanan '{service}'\n\nLog layanan baru-baru ini: {logs}", + "apps_catalog_failed_to_download": "Tidak dapat mengunduh katalog aplikasi {apps_catalog}: {error}", + "backup_archive_corrupted": "Sepertinya arsip cadangan '{archive}' rusak: {error}", + "diagnosis_found_errors": "{errors} masalah signifikan ditemukan terkait dengan {category}!", + "restore_system_part_failed": "Tidak dapat memulihkan segmen '{part}'", + "service_enable_failed": "Tidak dapat membuat layanan '{service}' dimulai mandiri saat pemulaian.\n\nLog layanan baru-baru ini:{logs}", + "service_not_reloading_because_conf_broken": "Tidak memuat atau memulai ulang layanan '{name}' karena konfigurasinya rusak: {errors}", + "service_reloaded": "Layanan {service} dimuat ulang" } From 576992899cdb4785980dd5916295953593923879 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 16 Aug 2023 19:18:37 +0200 Subject: [PATCH 0230/1116] apps: allow to use jinja {% if foobar %} blocks in their notifications/doc pages --- src/app.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/app.py b/src/app.py index 7f4acdc1a..0b1be18a6 100644 --- a/src/app.py +++ b/src/app.py @@ -186,11 +186,17 @@ def app_info(app, full=False, upgradable=False): ret["from_catalog"] = from_catalog # Hydrate app notifications and doc + rendered_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 - ) + rendered_content = _hydrate_app_template(content, settings) + # Rendered content may be empty because of conditional blocks + if not rendered_content: + continue + if pagename not in rendered_doc: + rendered_doc[pagename] = {} + rendered_doc[pagename][lang] = rendered_content + ret["manifest"]["doc"] = rendered_doc # Filter dismissed notification ret["manifest"]["notifications"] = { @@ -201,9 +207,16 @@ def app_info(app, full=False, upgradable=False): # Hydrate notifications (also filter uneeded post_upgrade notification based on version) for step, notifications in ret["manifest"]["notifications"].items(): + rendered_notifications = {} for name, content_per_lang in notifications.items(): for lang, content in content_per_lang.items(): - notifications[name][lang] = _hydrate_app_template(content, settings) + rendered_content = _hydrate_app_template(content, settings) + if not rendered_content: + continue + if name not in rendered_notifications: + rendered_notifications[name] = {} + rendered_notifications[name][lang] = rendered_content + ret["manifest"]["notifications"][step] = rendered_notifications ret["is_webapp"] = "domain" in settings and "path" in settings @@ -2230,6 +2243,13 @@ def _parse_app_doc_and_notifications(path): def _hydrate_app_template(template, data): + + # Apply jinja for stuff like {% if .. %} blocks, + # but only if there's indeed an if block (to try to reduce overhead or idk) + if "{%" in template: + from jinja2 import Template + template = Template(template).render(**data) + stuff_to_replace = set(re.findall(r"__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__", template)) for stuff in stuff_to_replace: @@ -2238,7 +2258,7 @@ def _hydrate_app_template(template, data): if varname in data: template = template.replace(stuff, str(data[varname])) - return template + return template.strip() def _convert_v1_manifest_to_v2(manifest): @@ -3145,7 +3165,7 @@ def _filter_and_hydrate_notifications(notifications, current_version=None, data= else: return version.parse(name) > version.parse(current_version.split("~")[0]) - return { + out = { # 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() @@ -3154,6 +3174,9 @@ def _filter_and_hydrate_notifications(notifications, current_version=None, data= or is_version_more_recent_than_current_version(name, current_version) } + # Filter out empty notifications (notifications may be empty because of if blocks) + return {name:content for name, content in out.items() if content and content.strip()} + def _display_notifications(notifications, force=False): if not notifications: From 32376cf18ffce2cffb54e4adf30856cab39e842c Mon Sep 17 00:00:00 2001 From: Tagada <36127788+Tagadda@users.noreply.github.com> Date: Mon, 21 Aug 2023 16:39:54 +0200 Subject: [PATCH 0231/1116] Use the existing db_name setting for database provising This should help upgrading an app from packaging v1 when db_name was different from `app` --- src/utils/resources.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 60a5f44f6..ba02930c9 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1337,8 +1337,8 @@ class DatabaseAppResource(AppResource): def provision_or_update(self, context: Dict = {}): # This is equivalent to ynh_sanitize_dbid - db_name = self.app.replace("-", "_").replace(".", "_") - db_user = db_name + db_user = self.app.replace("-", "_").replace(".", "_") + db_name = self.get_setting("db_name") || db_user self.set_setting("db_name", db_name) self.set_setting("db_user", db_user) @@ -1372,8 +1372,8 @@ class DatabaseAppResource(AppResource): ) def deprovision(self, context: Dict = {}): - db_name = self.app.replace("-", "_").replace(".", "_") - db_user = db_name + db_user = self.app.replace("-", "_").replace(".", "_") + db_name = self.get_setting("db_name") || db_user if self.dbtype == "mysql": self._run_script( From 07daa68770df82f92c1539228d5d003b9570fa29 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Aug 2023 15:37:47 +0200 Subject: [PATCH 0232/1116] apps: BACKUP_CORE_ONLY was not set for pre-upgrade safety backups, resulting in unecessarily large pre-upgrade backups --- src/app.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app.py b/src/app.py index 0b1be18a6..94485b176 100644 --- a/src/app.py +++ b/src/app.py @@ -707,9 +707,17 @@ def app_upgrade( 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], system=None - ) + tweaked_backup_core_only = False + if "BACKUP_CORE_ONLY" not in os.environ: + tweaked_backup_core_only = True + os.environ["BACKUP_CORE_ONLY"] = "1" + try: + backup_create( + name=safety_backup_name, apps=[app_instance_name], system=None + ) + finally: + if tweaked_backup_core_only: + del os.environ["BACKUP_CORE_ONLY"] if safety_backup_name in backup_list()["archives"]: # if the backup suceeded, delete old safety backup to save space From ee4d94d3829192029e8ece8d87529a005280eea3 Mon Sep 17 00:00:00 2001 From: Tagada <36127788+Tagadda@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:11:56 +0200 Subject: [PATCH 0233/1116] Update src/utils/resources.py Co-authored-by: Alexandre Aubin --- src/utils/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index ba02930c9..fd476d9bd 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1338,7 +1338,7 @@ class DatabaseAppResource(AppResource): def provision_or_update(self, context: Dict = {}): # This is equivalent to ynh_sanitize_dbid db_user = self.app.replace("-", "_").replace(".", "_") - db_name = self.get_setting("db_name") || db_user + db_name = self.get_setting("db_name") or db_user self.set_setting("db_name", db_name) self.set_setting("db_user", db_user) From 73a144fa4623f36f6f4b105801509cd5d183fec1 Mon Sep 17 00:00:00 2001 From: Tagada <36127788+Tagadda@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:12:03 +0200 Subject: [PATCH 0234/1116] Update src/utils/resources.py Co-authored-by: Alexandre Aubin --- src/utils/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index fd476d9bd..69b260334 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1373,7 +1373,7 @@ class DatabaseAppResource(AppResource): def deprovision(self, context: Dict = {}): db_user = self.app.replace("-", "_").replace(".", "_") - db_name = self.get_setting("db_name") || db_user + db_name = self.get_setting("db_name") or db_user if self.dbtype == "mysql": self._run_script( From b0fe49ae8390299d022c84b5c824057fc28f7c31 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Aug 2023 17:36:12 +0200 Subject: [PATCH 0235/1116] configpanels/forms : more edge cases with some questions not implementing some methods/attributes --- src/utils/configpanel.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index ee9019303..12024855a 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -160,11 +160,15 @@ class ConfigPanel: result[key] = {"ask": ask} if "current_value" in option: question_class = OPTIONS[option.get("type", OptionType.string)] - result[key]["value"] = question_class.humanize( - option["current_value"], option - ) + if hasattr(question_class, "humanize"): + result[key]["value"] = question_class.humanize( + option["current_value"], option + ) + else: + result[key]["value"] = option["current_value"] + # FIXME: semantics, technically here this is not about a prompt... - if question_class.hide_user_input_in_prompt: + if getattr(question_class, "hide_user_input_in_prompt", None): result[key][ "value" ] = "**************" # Prevent displaying password in `config get` From 0b0514374502bfa3093f71de7777a4a72598e43b Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 25 Aug 2023 03:23:33 +0200 Subject: [PATCH 0236/1116] [fix] Diagnosis: reverse DNS check should be case-insensitive #2235 --- src/diagnosers/24-mail.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index df14222a5..c7fe9d04b 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -43,7 +43,7 @@ class MyDiagnoser(Diagnoser): dependencies: List[str] = ["ip"] def run(self): - self.ehlo_domain = _get_maindomain() + self.ehlo_domain = _get_maindomain().lower() self.mail_domains = domain_list()["domains"] self.ipversions, self.ips = self.get_ips_checked() @@ -132,7 +132,7 @@ class MyDiagnoser(Diagnoser): summary=summary, details=[summary + "_details"], ) - elif r["helo"] != self.ehlo_domain: + elif r["helo"].lower() != self.ehlo_domain: yield dict( meta={"test": "mail_ehlo", "ipversion": ipversion}, data={"wrong_ehlo": r["helo"], "right_ehlo": self.ehlo_domain}, @@ -185,7 +185,7 @@ class MyDiagnoser(Diagnoser): rdns_domain = "" if len(value) > 0: rdns_domain = value[0][:-1] if value[0].endswith(".") else value[0] - if rdns_domain != self.ehlo_domain: + if rdns_domain.lower() != self.ehlo_domain: details = [ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details" ] + details @@ -194,7 +194,7 @@ class MyDiagnoser(Diagnoser): data={ "ip": ip, "ehlo_domain": self.ehlo_domain, - "rdns_domain": rdns_domain, + "rdns_domain": rdns_domain.lower(), }, status="ERROR", summary="diagnosis_mail_fcrdns_different_from_ehlo_domain", From fde05c0ac2af8c2009be9952f7819604f9d56b6a Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 25 Aug 2023 20:53:30 +0200 Subject: [PATCH 0237/1116] [enh] bind heritage in config panel --- helpers/config | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/helpers/config b/helpers/config index 77f118c5f..6c6285cf6 100644 --- a/helpers/config +++ b/helpers/config @@ -108,11 +108,11 @@ _ynh_app_config_apply_one() { else local bind_after="" local bind_key_="$(echo "$bind" | cut -d: -f1)" - bind_key_=${bind_key_:-$short_setting} if [[ "$bind_key_" == *">"* ]]; then bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" fi + bind_key_=${bind_key_:-$short_setting} 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" @@ -139,15 +139,31 @@ loaded_toml = toml.loads(file_content, _dict=OrderedDict) for panel_name, panel in loaded_toml.items(): if not isinstance(panel, dict): continue + bind_panel = panel.get('bind') for section_name, section in panel.items(): if not isinstance(section, dict): continue + bind_section = section.get('bind', bind_panel) for name, param in section.items(): if not isinstance(param, dict): continue + + bind = param.get('bind') + + if not bind: + if bind_section: + bind = bind_section + else: + bind = 'settings' if param.get('type', 'string') != 'file' else 'null' + elif bind[-1] == ":" and bind_section and ":" in bind_section: + regex, bind_file = bind_section.split(":") + if ">" in bind: + bind = bind + bind_file + else: + bind = regex + bind + bind_file print(';'.join([ name, param.get('type', 'string'), - param.get('bind', 'settings' if param.get('type', 'string') != 'file' else 'null') + bind ])) EOL ) From 79e620ef428629a65b9717092cf4109d158fb168 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 25 Aug 2023 21:12:01 +0200 Subject: [PATCH 0238/1116] [fix] Conflict type file and settings bind mode --- helpers/config | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helpers/config b/helpers/config index 6c6285cf6..a44582acf 100644 --- a/helpers/config +++ b/helpers/config @@ -153,13 +153,16 @@ for panel_name, panel in loaded_toml.items(): if bind_section: bind = bind_section else: - bind = 'settings' if param.get('type', 'string') != 'file' else 'null' + bind = 'settings' elif bind[-1] == ":" and bind_section and ":" in bind_section: regex, bind_file = bind_section.split(":") if ">" in bind: bind = bind + bind_file else: bind = regex + bind + bind_file + if bind == "settings" and param.get('type', 'string') == 'file': + bind = 'null' + print(';'.join([ name, param.get('type', 'string'), From dcafac1913364e5da690c5c01af8fe62b68bee55 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 25 Aug 2023 22:36:33 +0200 Subject: [PATCH 0239/1116] [fix] Combine bind section regex with bind panel file --- helpers/config | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/helpers/config b/helpers/config index a44582acf..5a41822cc 100644 --- a/helpers/config +++ b/helpers/config @@ -142,7 +142,16 @@ for panel_name, panel in loaded_toml.items(): bind_panel = panel.get('bind') for section_name, section in panel.items(): if not isinstance(section, dict): continue - bind_section = section.get('bind', bind_panel) + bind_section = section.get('bind') + if not bind_section: + bind_section = bind_panel + elif bind_section[-1] == ":" and bind_panel and ":" in bind_panel: + regex, bind_panel_file = bind_panel.split(":") + if ">" in bind_section: + bind_section = bind_section + bind_panel_file + else: + bind_section = regex + bind_section + bind_panel_file + for name, param in section.items(): if not isinstance(param, dict): continue From 8f0f85b7221a6f6577d1391916ade88a78506d17 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 29 Aug 2023 15:24:52 +0200 Subject: [PATCH 0240/1116] merge update_password with update --- share/actionsmap-portal.yml | 29 ++++++++------ src/portal.py | 78 ++++++++++++++++++++----------------- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/share/actionsmap-portal.yml b/share/actionsmap-portal.yml index 2673fc226..6b02a061d 100644 --- a/share/actionsmap-portal.yml +++ b/share/actionsmap-portal.yml @@ -25,8 +25,7 @@ portal: action_help: Allow user to update their infos (display name, mail aliases/forward, password, ...) api: PUT /update arguments: - -F: - full: --fullname + --fullname: help: The full name of the user. For example 'Camille Dupont' extra: pattern: &pattern_fullname @@ -48,18 +47,24 @@ portal: pattern: &pattern_email - !!str ^[\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ - "pattern_email" + --currentpassword: + help: Current password + nargs: "?" + --newpassword: + help: New password to set + nargs: "?" ### portal_update_password() - update_password: - action_help: Allow user to change their password - api: PUT /me/update_password - arguments: - -c: - full: --current - help: Current password - -p: - full: --password - help: New password to set + # update_password: + # action_help: Allow user to change their password + # api: PUT /me/update_password + # arguments: + # -c: + # full: --current + # help: Current password + # -p: + # full: --password + # help: New password to set ### portal_reset_password() reset_password: diff --git a/src/portal.py b/src/portal.py index fa2d1a30a..e9c2b3f48 100644 --- a/src/portal.py +++ b/src/portal.py @@ -20,17 +20,17 @@ """ from typing import Union -from moulinette.utils.log import getActionLogger +import ldap from moulinette.utils.filesystem import read_json - -from yunohost.authenticators.ldap_ynhuser import Authenticator as Auth -from yunohost.utils.ldap import LDAPInterface +from moulinette.utils.log import getActionLogger +from yunohost.authenticators.ldap_ynhuser import URI, USERDN, Authenticator as Auth +from yunohost.user import _hash_user_password from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.ldap import LDAPInterface from yunohost.utils.password import ( assert_password_is_compatible, assert_password_is_strong_enough, ) -from yunohost.user import _hash_user_password logger = getActionLogger("portal") @@ -40,12 +40,12 @@ ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] def _get_user_infos(user_attrs: list[str]): auth = Auth().get_session_cookie(decrypt_pwd=True) username = auth["user"] - ldap = LDAPInterface(username, auth["pwd"]) - result = ldap.search("ou=users", f"uid={username}", user_attrs) + ldap_interface = LDAPInterface(username, auth["pwd"]) + result = ldap_interface.search("ou=users", f"uid={username}", user_attrs) if not result: raise YunohostValidationError("user_unknown", user=username) - return username, result[0], ldap + return username, result[0], ldap_interface def portal_me(): @@ -53,7 +53,7 @@ def portal_me(): Get user informations """ - username, user, ldap = _get_user_infos( + username, user, _ = _get_user_infos( ["cn", "mail", "maildrop", "mailuserquota", "memberOf", "permission"] ) @@ -100,11 +100,13 @@ def portal_update( fullname: Union[str, None] = None, mailforward: Union[list[str], None] = None, mailalias: Union[list[str], None] = None, + currentpassword: Union[str, None] = None, + newpassword: Union[str, None] = None, ): from yunohost.domain import domain_list domains = domain_list()["domains"] - username, current_user, ldap = _get_user_infos( + username, current_user, ldap_interface = _get_user_infos( ["givenName", "sn", "cn", "mail", "maildrop", "memberOf"] ) new_attr_dict = {} @@ -128,23 +130,25 @@ def portal_update( for index, mail in enumerate(mailalias): if mail in current_user["mail"]: - if mail != current_user["mail"][0]: + if mail != current_user["mail"][0] and mail not in mails: mails.append(mail) continue # already in mails, skip validation - local_part, domain = mail.strip().split("@") + local_part, domain = mail.split("@") if local_part in ADMIN_ALIASES: raise YunohostValidationError( - "mail_unavailable", path="mailalias", index=index + "mail_unavailable", path=f"mailalias[{index}]" ) try: - ldap.validate_uniqueness({"mail": mail}) + ldap_interface.validate_uniqueness({"mail": mail}) except Exception as e: raise YunohostError("user_update_failed", user=username, error=e) if domain not in domains: - raise YunohostError("mail_domain_unknown", domain=domain) + raise YunohostValidationError( + "mail_domain_unknown", domain=domain, path=f"mailalias[{index}]" + ) mails.append(mail) @@ -157,8 +161,31 @@ def portal_update( if mail and mail.strip() and mail != current_user["maildrop"][0] ] + if newpassword: + # Check that current password is valid + try: + con = ldap.ldapobject.ReconnectLDAPObject(URI, retry_max=0) + con.simple_bind_s(USERDN.format(username=username), currentpassword) + except ldap.INVALID_CREDENTIALS: + raise YunohostValidationError("invalid_password", path="currentpassword") + finally: + # Free the connection, we don't really need it to keep it open as the point is only to check authentication... + if con: + con.unbind_s() + + # Ensure compatibility and sufficiently complex password + try: + assert_password_is_compatible(newpassword) + is_admin = "cn=admins,ou=groups,dc=yunohost,dc=org" in current_user["memberOf"] + assert_password_is_strong_enough("admin" if is_admin else "user", newpassword) + except YunohostValidationError as e: + raise YunohostValidationError(e.key, path="newpassword") + + Auth().delete_session_cookie() + new_attr_dict["userPassword"] = [_hash_user_password(newpassword)] + try: - ldap.update(f"uid={username},ou=users", new_attr_dict) + ldap_interface.update(f"uid={username},ou=users", new_attr_dict) except Exception as e: raise YunohostError("user_update_failed", user=username, error=e) @@ -170,22 +197,3 @@ def portal_update( "mailalias": new_attr_dict["mail"][1:], "mailforward": new_attr_dict["maildrop"][1:], } - - -def portal_update_password(current: str, password: str): - username, current_user, ldap = _get_user_infos(["userPassword", "memberOf"]) - is_admin = "cn=admins,ou=groups,dc=yunohost,dc=org" in current_user["memberOf"] - - # FIXME: Verify current password ? - - # Ensure compatibility and sufficiently complex password - assert_password_is_compatible(password) - assert_password_is_strong_enough("admin" if is_admin else "user", password) - - try: - ldap.update( - f"uid={username},ou=users", - {"userPassword": [_hash_user_password(password)]}, - ) - except Exception as e: - raise YunohostError("user_update_failed", user=username, error=e) From 65d25710725b06d281630644b80d8d01dfba1bde Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 31 Aug 2023 17:23:21 +0200 Subject: [PATCH 0241/1116] helpers: add new --group option for ynh_add_fpm_config to customize the Group parameter --- helpers/php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/helpers/php b/helpers/php index c9e5b1cb8..ee3d35cb1 100644 --- a/helpers/php +++ b/helpers/php @@ -70,8 +70,9 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} ynh_add_fpm_config() { local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. - local legacy_args=vufpd - local -A args_array=([v]=phpversion= [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service) + local legacy_args=vufpdg + local -A args_array=([v]=phpversion= [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service [g]=group=) + local group local phpversion local usage local footprint @@ -80,6 +81,7 @@ ynh_add_fpm_config() { # Manage arguments with getopts ynh_handle_getopts_args "$@" package=${package:-} + group=${group:-} # The default behaviour is to use the template. local autogenconf=false @@ -180,12 +182,13 @@ ynh_add_fpm_config() { # Define the values to use for the configuration of PHP. ynh_get_scalable_phpfpm --usage=$usage --footprint=$footprint + local phpfpm_group=$([[ -n "$group" ]] && echo "$group" || echo "$app") local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf" echo " [__APP__] user = __APP__ -group = __APP__ +group = __PHPFPM_GROUP__ chdir = __INSTALL_DIR__ From 51d8608b40e2e2acd76a0ce34ceca9dab0cf13d0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 31 Aug 2023 17:33:08 +0200 Subject: [PATCH 0242/1116] Update changelog for 11.2.4 --- debian/changelog | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/debian/changelog b/debian/changelog index 586f8387b..610109fcd 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +yunohost (11.2.4) stable; urgency=low + + - doc: Improve --help for 'yunohost app install' ([#1702](https://github.com/yunohost/yunohost/pull/1702)) + - helpers: add new --group option for ynh_add_fpm_config to customize the Group parameter (65d25710) + - apps: allow to use jinja {% if foobar %} blocks in their notifications/doc pages (57699289) + - apps: BACKUP_CORE_ONLY was not set for pre-upgrade safety backups, resulting in unecessarily large pre-upgrade backups (07daa687) + - apps: Use the existing db_name setting for database provising to ease v1->v2 transition with specific db_name ([#1704](https://github.com/yunohost/yunohost/pull/1704)) + - configpanels/forms: more edge cases with some questions not implementing some methods/attributes (b0fe49ae) + - diagnosis: reverse DNS check should be case-insensitive #2235 ([#1705](https://github.com/yunohost/yunohost/pull/1705)) + - i18n: Translations updated for Galician, Indonesian, Polish, Spanish, Turkish + + Thanks to all contributors <3 ! (Grzegorz Cichocki, José M, Kuba Bazan, ljf (zamentur), massyas, Neko Nekowazarashi, selfhoster1312, Suleyman Harmandar, taco, Tagada) + + -- Alexandre Aubin Thu, 31 Aug 2023 17:30:21 +0200 + yunohost (11.2.3) stable; urgency=low - apps: fix another case of no attribute 'value' due to config panels/questions refactoring (4fda8ed49) From 6f3b194944502460422d0f06c02a7fd225387f58 Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 2 Sep 2023 22:47:02 +0200 Subject: [PATCH 0243/1116] [fix] Avoid char conflict with bind properties --- helpers/config | 4 ++-- src/utils/configpanel.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/helpers/config b/helpers/config index 5a41822cc..fbf7febaa 100644 --- a/helpers/config +++ b/helpers/config @@ -172,7 +172,7 @@ for panel_name, panel in loaded_toml.items(): if bind == "settings" and param.get('type', 'string') == 'file': bind = 'null' - print(';'.join([ + print('|'.join([ name, param.get('type', 'string'), bind @@ -181,7 +181,7 @@ EOL ) for line in $lines; do # Split line into short_setting, type and bind - IFS=';' read short_setting type bind <<<"$line" + IFS='|' read short_setting type bind <<<"$line" binds[${short_setting}]="$bind" types[${short_setting}]="$type" file_hash[${short_setting}]="" diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 12024855a..cc93f888f 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -345,14 +345,14 @@ class ConfigPanel: "defaults": {"version": 1.0}, }, "panels": { - "properties": ["name", "services", "actions", "help"], + "properties": ["name", "services", "actions", "help", "bind"], "defaults": { "services": [], "actions": {"apply": {"en": "Apply"}}, }, }, "sections": { - "properties": ["name", "services", "optional", "help", "visible"], + "properties": ["name", "services", "optional", "help", "visible", "bind"], "defaults": { "name": "", "services": [], From ffa8eb38ed43cfee8c278d673c5ec72d009ed5e4 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Sep 2023 15:42:12 +0200 Subject: [PATCH 0244/1116] [enh] Add post_app_restore hook --- src/backup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backup.py b/src/backup.py index ce1e8ba2c..7dffce321 100644 --- a/src/backup.py +++ b/src/backup.py @@ -1554,6 +1554,12 @@ class RestoreManager: if not restore_failed: self.targets.set_result("apps", app_instance_name, "Success") operation_logger.success() + + # Call post_app_restore hook + env_dict = _make_environment_for_app_script( + app_instance_name + ) + hook_callback("post_app_restore", env=env_dict) else: self.targets.set_result("apps", app_instance_name, "Error") From 0645d18e677ad399d36d1ca23327097af2043504 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 4 Sep 2023 16:19:07 +0200 Subject: [PATCH 0245/1116] add host as session cookie info --- src/authenticators/ldap_ynhuser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index 2add68cab..331cf9e25 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -115,7 +115,7 @@ class Authenticator(BaseAuthenticator): def set_session_cookie(self, infos): - from bottle import response + from bottle import response, request assert isinstance(infos, dict) @@ -126,7 +126,8 @@ class Authenticator(BaseAuthenticator): # See https://pyjwt.readthedocs.io/en/latest/usage.html#registered-claim-names # for explanations regarding nbf, exp "nbf": int(datetime.datetime.now().timestamp()), - "exp": int(datetime.datetime.now().timestamp()) + (7 * 24 * 3600) # One week validity # FIXME : does it mean the session suddenly expires after a week ? Can we somehow auto-renew it at every usage or something ? + "exp": int(datetime.datetime.now().timestamp()) + (7 * 24 * 3600), # One week validity # FIXME : does it mean the session suddenly expires after a week ? Can we somehow auto-renew it at every usage or something ? + "host": request.get_header('host'), } new_infos.update(infos) From 5562b61db07de881f72405198156eecc80a0be7b Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 4 Sep 2023 16:20:29 +0200 Subject: [PATCH 0246/1116] add 'list_portal' AppOption modifier to add portal as a possible choice --- src/utils/configpanel.py | 1 + src/utils/form.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 42a030cbc..02454bd0b 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -382,6 +382,7 @@ class ConfigPanel: "filter", "readonly", "enabled", + "list_portal", # "confirm", # TODO: to ask confirmation before running an action ], "defaults": {}, diff --git a/src/utils/form.py b/src/utils/form.py index 1ca03373e..64155d8e2 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -914,6 +914,7 @@ class AppOption(BaseChoicesOption): super().__init__(question) self.filter = question.get("filter", None) + self.list_portal = question.get("list_portal", False) apps = app_list(full=True)["apps"] @@ -929,6 +930,8 @@ class AppOption(BaseChoicesOption): return app["label"] + domain_path_or_id self.choices = {"_none": "---"} + if self.list_portal: + self.choices["portal_public_apps"] = "Portal" self.choices.update({app["id"]: _app_display(app) for app in apps}) From a1a47e5221809751949f7bf17cf7c63148028164 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 4 Sep 2023 16:21:50 +0200 Subject: [PATCH 0247/1116] update config_domain.toml with portal options --- share/config_domain.toml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/share/config_domain.toml b/share/config_domain.toml index 82ef90c32..b6a0b51a7 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -4,11 +4,34 @@ i18n = "domain_config" [feature] name = "Features" + [feature.portal] + name = "Portal" + + [feature.portal.show_other_domains_apps] + type = "boolean" + default = 1 + + [feature.portal.portal_title] + type = "string" + default = "YunoHost" + + [feature.portal.portal_logo] + type = "string" + default = "" + + [feature.portal.portal_theme] + type = "select" + choices = ["system", "light", "dark", "cupcake", "bumblebee", "emerald", "corporate", "synthwave", "retro", "cyberpunk", "valentine", "halloween", "garden", "forest", "aqua", "lofi", "pastel", "fantasy", "wireframe", "black", "luxury", "dracula", "cmyk", "autumn", "business", "acid", "lemonade", "night", "coffee", "winter"] + default = "system" + + # FIXME link to GCU + [feature.app] [feature.app.default_app] type = "app" filter = "is_webapp" default = "_none" + list_portal = true [feature.mail] From 20d21b57e047863c421d5369cb710d33f7e53c0d Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 4 Sep 2023 16:24:01 +0200 Subject: [PATCH 0248/1116] wip: save portal configpanel options in separate file .portal.yml --- src/domain.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/domain.py b/src/domain.py index 5f0e76d28..9ddb67a76 100644 --- a/src/domain.py +++ b/src/domain.py @@ -737,6 +737,29 @@ class DomainConfigPanel(ConfigPanel): ): app_ssowatconf() + portal_options = [ + "default_app", + "show_other_domains_apps", + "portal_title", + "portal_logo", + "portal_theme", + ] + if any( + option in self.future_values + and self.future_values[option] != self.values[option] + for option in portal_options + ): + # Portal options are also saved in a `domain.portal.yml` file + # that can be read by the portal API. + # FIXME remove those from the config panel saved values? + portal_values = { + option: self.future_values[option] for option in portal_options + } + # FIXME config file should be readable by the portal entity + write_to_yaml( + f"{DOMAIN_SETTINGS_DIR}/{self.entity}.portal.yml", portal_values + ) + stuff_to_regen_conf = [] if ( "xmpp" in self.future_values From 2136db32b6d7da0d711bc441588d872ca49a2ee0 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 4 Sep 2023 16:27:06 +0200 Subject: [PATCH 0249/1116] return domain from _get_user_infos --- src/portal.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/portal.py b/src/portal.py index e9c2b3f48..cdb6fb9dd 100644 --- a/src/portal.py +++ b/src/portal.py @@ -18,7 +18,7 @@ along with this program; if not, see http://www.gnu.org/licenses """ -from typing import Union +from typing import Any, Union import ldap from moulinette.utils.filesystem import read_json @@ -37,7 +37,9 @@ logger = getActionLogger("portal") ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] -def _get_user_infos(user_attrs: list[str]): +def _get_user_infos( + user_attrs: list[str], +) -> tuple[str, str, dict[str, Any], LDAPInterface]: auth = Auth().get_session_cookie(decrypt_pwd=True) username = auth["user"] ldap_interface = LDAPInterface(username, auth["pwd"]) @@ -45,15 +47,14 @@ def _get_user_infos(user_attrs: list[str]): if not result: raise YunohostValidationError("user_unknown", user=username) - return username, result[0], ldap_interface + return username, auth["host"], result[0], ldap_interface def portal_me(): """ Get user informations """ - - username, user, _ = _get_user_infos( + username, domain, user, _ = _get_user_infos( ["cn", "mail", "maildrop", "mailuserquota", "memberOf", "permission"] ) @@ -106,7 +107,7 @@ def portal_update( from yunohost.domain import domain_list domains = domain_list()["domains"] - username, current_user, ldap_interface = _get_user_infos( + username, domain, current_user, ldap_interface = _get_user_infos( ["givenName", "sn", "cn", "mail", "maildrop", "memberOf"] ) new_attr_dict = {} @@ -176,8 +177,12 @@ def portal_update( # Ensure compatibility and sufficiently complex password try: assert_password_is_compatible(newpassword) - is_admin = "cn=admins,ou=groups,dc=yunohost,dc=org" in current_user["memberOf"] - assert_password_is_strong_enough("admin" if is_admin else "user", newpassword) + is_admin = ( + "cn=admins,ou=groups,dc=yunohost,dc=org" in current_user["memberOf"] + ) + assert_password_is_strong_enough( + "admin" if is_admin else "user", newpassword + ) except YunohostValidationError as e: raise YunohostValidationError(e.key, path="newpassword") From bfedf144b30e5f295bde724e2db0c69c1f85dbd6 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 4 Sep 2023 16:31:58 +0200 Subject: [PATCH 0250/1116] add settings getter + /public route to get settings and public apps --- src/portal.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/portal.py b/src/portal.py index cdb6fb9dd..61d0c301a 100644 --- a/src/portal.py +++ b/src/portal.py @@ -18,10 +18,11 @@ along with this program; if not, see http://www.gnu.org/licenses """ +from pathlib import Path from typing import Any, Union import ldap -from moulinette.utils.filesystem import read_json +from moulinette.utils.filesystem import read_json, read_yaml from moulinette.utils.log import getActionLogger from yunohost.authenticators.ldap_ynhuser import URI, USERDN, Authenticator as Auth from yunohost.user import _hash_user_password @@ -50,6 +51,56 @@ def _get_user_infos( return username, auth["host"], result[0], ldap_interface +def _get_portal_settings(domain: Union[str, None] = None): + from yunohost.domain import DOMAIN_SETTINGS_DIR + + if not domain: + from bottle import request + + domain = request.get_header("host") + + if Path(f"{DOMAIN_SETTINGS_DIR}/{domain}.portal.yml").exists(): + settings = read_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.portal.yml") + else: + settings = { + "public": False, + "portal_logo": "", + "portal_theme": "system", + "portal_title": "YunoHost", + "show_other_domains_apps": 1, + } + + settings["domain"] = domain + + return settings + + +def portal_public(): + settings = _get_portal_settings() + settings["apps"] = {} + settings["public"] = settings.pop("default_app") == "portal_public_apps" + + if settings["public"]: + ssowat_conf = read_json("/etc/ssowat/conf.json") + settings["apps"] = { + perm.replace(".main", ""): { + "label": infos["label"], + "url": infos["uris"][0], + } + for perm, infos in ssowat_conf["permissions"].items() + if infos["show_tile"] and infos["public"] + } + + if not settings["show_other_domains_apps"]: + settings["apps"] = { + name: data + for name, data in settings["apps"].items() + if settings["domain"] in data["url"] + } + + return settings + + def portal_me(): """ Get user informations @@ -76,6 +127,10 @@ def portal_me(): if perm in permissions and infos["show_tile"] and username in infos["users"] } + settings = _get_portal_settings(domain=domain) + if not settings["show_other_domains_apps"]: + apps = {name: data for name, data in apps.items() if domain in data["url"]} + result_dict = { "username": username, "fullname": user["cn"][0], From 3dfab89c1f4c94115110b17b9a98efdfb0590a55 Mon Sep 17 00:00:00 2001 From: Kayou Date: Thu, 7 Sep 2023 14:59:08 +0200 Subject: [PATCH 0251/1116] check and re-download a prefetched file that doesn't match the checksum --- helpers/utils | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/helpers/utils b/helpers/utils index 52d7c734f..4694dd724 100644 --- a/helpers/utils +++ b/helpers/utils @@ -244,9 +244,11 @@ ynh_setup_source() { if [ "$src_format" = "docker" ]; then src_platform="${src_platform:-"linux/$YNH_ARCH"}" - elif test -e "$local_src"; then - cp $local_src $src_filename else + if test -e "$local_src"; then + cp $local_src $src_filename + fi + [ -n "$src_url" ] || ynh_die "Couldn't parse SOURCE_URL from $src_file_path ?" # If the file was prefetched but somehow doesn't match the sum, rm and redownload it From 79e41a1e4b9deed01181e8788bb8bec4493e2a32 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Thu, 7 Sep 2023 16:22:16 +0200 Subject: [PATCH 0252/1116] app.py: fix typo in log statement --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 94485b176..627909b70 100644 --- a/src/app.py +++ b/src/app.py @@ -809,7 +809,7 @@ def app_upgrade( and not no_safety_backup ): logger.warning( - "Upgrade failed ... attempting to restore the satefy backup (Yunohost first need to remove the app for this) ..." + "Upgrade failed ... attempting to restore the safety backup (Yunohost first need to remove the app for this) ..." ) app_remove(app_instance_name, force_workdir=extracted_app_folder) From c641f099c5ae8162a91a3e020c16aa944a1df221 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 7 Sep 2023 17:57:08 +0200 Subject: [PATCH 0253/1116] add temp messy file handling for portal custom logo --- share/config_domain.toml | 3 +-- src/domain.py | 46 +++++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/share/config_domain.toml b/share/config_domain.toml index b6a0b51a7..27493b4e7 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -16,8 +16,7 @@ name = "Features" default = "YunoHost" [feature.portal.portal_logo] - type = "string" - default = "" + type = "file" [feature.portal.portal_theme] type = "select" diff --git a/src/domain.py b/src/domain.py index 9ddb67a76..a476112b2 100644 --- a/src/domain.py +++ b/src/domain.py @@ -728,15 +728,6 @@ class DomainConfigPanel(ConfigPanel): other_app=app_map(raw=True)[self.entity]["/"]["id"], ) - super()._apply() - - # Reload ssowat if default app changed - if ( - "default_app" in self.future_values - and self.future_values["default_app"] != self.values["default_app"] - ): - app_ssowatconf() - portal_options = [ "default_app", "show_other_domains_apps", @@ -755,11 +746,46 @@ class DomainConfigPanel(ConfigPanel): portal_values = { option: self.future_values[option] for option in portal_options } - # FIXME config file should be readable by the portal entity + if portal_values["portal_logo"].startswith("/tmp/ynh_filequestion_"): + # FIXME rework this whole mess + # currently only handling API sent images, need to adapt FileOption + # to handle file extensions and file saving since "bind" is only + # done in bash helpers which are not executed in domain config + if "portal_logo[name]" in self.args or self.values["portal_logo"]: + import mimetypes + import base64 + + if "portal_logo[name]" in self.args: + # FIXME choose where to save the file + filepath = os.path.join("/tmp", self.args["portal_logo[name]"]) + # move the temp file created by FileOption with proper name and extension + os.rename(self.new_values["portal_logo"], filepath) + mimetype = mimetypes.guess_type(filepath) + else: + # image has already been saved, do not overwrite it with the empty temp file created by the FileOption + filepath = self.values["portal_logo"] + mimetype = mimetypes.guess_type(filepath) + + # save the proper path to config panel settings + self.new_values["portal_logo"] = filepath + # save the base64 content with mimetype to portal settings + with open(filepath, "rb") as f: + portal_values["portal_logo"] = mimetype[0] + ":" + base64.b64encode(f.read()).decode("utf-8") + + # FIXME config file should be readable by non-root portal entity write_to_yaml( f"{DOMAIN_SETTINGS_DIR}/{self.entity}.portal.yml", portal_values ) + super()._apply() + + # Reload ssowat if default app changed + if ( + "default_app" in self.future_values + and self.future_values["default_app"] != self.values["default_app"] + ): + app_ssowatconf() + stuff_to_regen_conf = [] if ( "xmpp" in self.future_values From 920fe527f4ef1ca5ffef72bc247497ac09bf5404 Mon Sep 17 00:00:00 2001 From: orhtej2 <2871798+orhtej2@users.noreply.github.com> Date: Thu, 7 Sep 2023 22:14:06 +0200 Subject: [PATCH 0254/1116] Allow system users to send mails from IPv6 localhost and in no-IP contexts. --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 627909b70..df5eecae2 100644 --- a/src/app.py +++ b/src/app.py @@ -3261,7 +3261,7 @@ def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): if dovecot: hashed_password = _hash_user_password(settings["mail_pwd"]) dovecot_passwd.append( - f"{app}:{hashed_password}::::::allow_nets=127.0.0.1/24" + f"{app}:{hashed_password}::::::allow_nets=::1,127.0.0.1/24,local" ) if postfix: mail_user = settings.get("mail_user", app) From 8eb2e72282ea6bcb2aab3901ffc412d42b273e4a Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 8 Sep 2023 15:13:20 +0200 Subject: [PATCH 0255/1116] Update Fail2ban jail.conf file from https://sources.debian.org/src/fail2ban/1.0.2-2/config/jail.conf/ --- conf/fail2ban/jail.conf | 301 ++++++++++++++++++++++++++++------------ 1 file changed, 213 insertions(+), 88 deletions(-) diff --git a/conf/fail2ban/jail.conf b/conf/fail2ban/jail.conf index bd522c4ba..fe8db527d 100644 --- a/conf/fail2ban/jail.conf +++ b/conf/fail2ban/jail.conf @@ -18,7 +18,7 @@ # See man 5 jail.conf for details. # # [DEFAULT] -# bantime = 3600 +# bantime = 1h # # [sshd] # enabled = true @@ -44,10 +44,52 @@ before = paths-debian.conf # MISCELLANEOUS OPTIONS # -# "ignoreip" can be an IP address, a CIDR mask or a DNS host. Fail2ban will not -# ban a host which matches an address in this list. Several addresses can be -# defined using space (and/or comma) separator. -ignoreip = 127.0.0.1/8 +# "bantime.increment" allows to use database for searching of previously banned ip's to increase a +# default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32... +#bantime.increment = true + +# "bantime.rndtime" is the max number of seconds using for mixing with random time +# to prevent "clever" botnets calculate exact time IP can be unbanned again: +#bantime.rndtime = + +# "bantime.maxtime" is the max number of seconds using the ban time can reach (doesn't grow further) +#bantime.maxtime = + +# "bantime.factor" is a coefficient to calculate exponent growing of the formula or common multiplier, +# default value of factor is 1 and with default value of formula, the ban time +# grows by 1, 2, 4, 8, 16 ... +#bantime.factor = 1 + +# "bantime.formula" used by default to calculate next value of ban time, default value below, +# the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32... +#bantime.formula = ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor +# +# more aggressive example of formula has the same values only for factor "2.0 / 2.885385" : +#bantime.formula = ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor) + +# "bantime.multipliers" used to calculate next value of ban time instead of formula, corresponding +# previously ban count and given "bantime.factor" (for multipliers default is 1); +# following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count, +# always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours +#bantime.multipliers = 1 2 4 8 16 32 64 +# following example can be used for small initial ban time (bantime=60) - it grows more aggressive at begin, +# for bantime=60 the multipliers are minutes and equal: 1 min, 5 min, 30 min, 1 hour, 5 hour, 12 hour, 1 day, 2 day +#bantime.multipliers = 1 5 30 60 300 720 1440 2880 + +# "bantime.overalljails" (if true) specifies the search of IP in the database will be executed +# cross over all jails, if false (default), only current jail of the ban IP will be searched +#bantime.overalljails = false + +# -------------------- + +# "ignoreself" specifies whether the local resp. own IP addresses should be ignored +# (default is true). Fail2ban will not ban a host which matches such addresses. +#ignoreself = true + +# "ignoreip" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban +# will not ban a host which matches an address in this list. Several addresses +# can be defined using space (and/or comma) separator. +#ignoreip = 127.0.0.1/8 ::1 # External command that will take an tagged arguments to ignore, e.g. , # and return true if the IP is to be ignored. False otherwise. @@ -56,14 +98,17 @@ ignoreip = 127.0.0.1/8 ignorecommand = # "bantime" is the number of seconds that a host is banned. -bantime = 600 +bantime = 10m # A host is banned if it has generated "maxretry" during the last "findtime" # seconds. -findtime = 600 +findtime = 10m # "maxretry" is the number of failures before a host get banned. -maxretry = 10 +maxretry = 5 + +# "maxmatches" is the number of matches stored in ticket (resolvable via tag in actions). +maxmatches = %(maxretry)s # "backend" specifies the backend used to get files modification. # Available options are "pyinotify", "gamin", "polling", "systemd" and "auto". @@ -113,10 +158,13 @@ logencoding = auto enabled = false +# "mode" defines the mode of the filter (see corresponding filter implementation for more info). +mode = normal + # "filter" defines the filter to use by the jail. # By default jails have names matching their filter name # -filter = %(__name__)s +filter = %(__name__)s[mode=%(mode)s] # @@ -130,7 +178,7 @@ filter = %(__name__)s destemail = root@localhost # Sender email address used solely for some actions -sender = root@localhost +sender = root@ # E-mail action. Since 0.8.1 Fail2Ban uses sendmail MTA for the # mailing. Change mta configuration parameter to mail if you want to @@ -140,8 +188,8 @@ mta = sendmail # Default protocol protocol = tcp -# Specify chain where jumps would need to be added in iptables-* actions -chain = INPUT +# Specify chain where jumps would need to be added in ban-actions expecting parameter chain +chain = # Ports to be banned # Usually should be overridden in a particular jail @@ -161,51 +209,53 @@ banaction = iptables-multiport banaction_allports = iptables-allports # The simplest action to take: ban only -action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] +action_ = %(banaction)s[port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] # ban & send an e-mail with whois report to the destemail. -action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] - %(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"] +action_mw = %(action_)s + %(mta)s-whois[sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"] # ban & send an e-mail with whois report and relevant log lines # to the destemail. -action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] - %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] +action_mwl = %(action_)s + %(mta)s-whois-lines[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"] # See the IMPORTANT note in action.d/xarf-login-attack for when to use this action # # ban & send a xarf e-mail to abuse contact of IP address and include relevant log lines # to the destemail. -action_xarf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] - xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath=%(logpath)s, port="%(port)s"] +action_xarf = %(action_)s + xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath="%(logpath)s", port="%(port)s"] + +# ban & send a notification to one or more of the 50+ services supported by Apprise. +# See https://github.com/caronc/apprise/wiki for details on what is supported. +# +# You may optionally over-ride the default configuration line (containing the Apprise URLs) +# by using 'apprise[config="/alternate/path/to/apprise.cfg"]' otherwise +# /etc/fail2ban/apprise.conf is sourced for your supported notification configuration. +# action = %(action_)s +# apprise # ban IP on CloudFlare & send an e-mail with whois report and relevant log lines # to the destemail. action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"] - %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] + %(mta)s-whois-lines[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"] # Report block via blocklist.de fail2ban reporting service API # -# See the IMPORTANT note in action.d/blocklist_de.conf for when to -# use this action. Create a file jail.d/blocklist_de.local containing -# [Init] -# blocklist_de_apikey = {api key from registration] +# See the IMPORTANT note in action.d/blocklist_de.conf for when to use this action. +# Specify expected parameters in file action.d/blocklist_de.local or if the interpolation +# `action_blocklist_de` used for the action, set value of `blocklist_de_apikey` +# in your `jail.local` globally (section [DEFAULT]) or per specific jail section (resp. in +# corresponding jail.d/my-jail.local file). # -action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"] +action_blocklist_de = blocklist_de[email="%(sender)s", service="%(__name__)s", apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"] -# Report ban via badips.com, and use as blacklist +# Report ban via abuseipdb.com. # -# See BadIPsAction docstring in config/action.d/badips.py for -# documentation for this action. +# See action.d/abuseipdb.conf for usage example and details. # -# NOTE: This action relies on banaction being present on start and therefore -# should be last action defined for a jail. -# -action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"] -# -# Report ban via badips.com (uses action.d/badips.conf for reporting only) -# -action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"] +action_abuseipdb = abuseipdb # Choose default action. To change, just override value of 'action' with the # interpolation to the chosen action shortcut (e.g. action_mw, action_mwl, etc) in jail.local @@ -223,15 +273,10 @@ action = %(action_)s [sshd] -port = ssh -logpath = %(sshd_log)s -backend = %(sshd_backend)s - - -[sshd-ddos] -# This jail corresponds to the standard configuration in Fail2ban. -# The mail-whois action send a notification e-mail with a whois request -# in the body. +# To use more aggressive sshd modes set filter parameter "mode" in jail.local: +# normal (default), ddos, extra or aggressive (combines all). +# See "tests/files/logs/sshd" or "filter.d/sshd.conf" for usage example and details. +#mode = normal port = ssh logpath = %(sshd_log)s backend = %(sshd_backend)s @@ -265,7 +310,7 @@ logpath = %(apache_error_log)s # for email addresses. The mail outputs are buffered. port = http,https logpath = %(apache_access_log)s -bantime = 172800 +bantime = 48h maxretry = 1 @@ -301,7 +346,7 @@ maxretry = 2 port = http,https logpath = %(apache_access_log)s maxretry = 1 -ignorecommand = %(ignorecommands_dir)s/apache-fakegooglebot +ignorecommand = %(fail2ban_confpath)s/filter.d/ignorecommands/apache-fakegooglebot [apache-modsecurity] @@ -321,12 +366,15 @@ maxretry = 1 [openhab-auth] filter = openhab -action = iptables-allports[name=NoAuthFailures] +banaction = %(banaction_allports)s logpath = /opt/openhab/logs/request.log +# To use more aggressive http-auth modes set filter parameter "mode" in jail.local: +# normal (default), aggressive (combines all), auth or fallback +# See "tests/files/logs/nginx-http-auth" or "filter.d/nginx-http-auth.conf" for usage example and details. [nginx-http-auth] - +# mode = normal port = http,https logpath = %(nginx_error_log)s @@ -342,8 +390,10 @@ logpath = %(nginx_error_log)s port = http,https logpath = %(nginx_error_log)s -maxretry = 2 +[nginx-bad-request] +port = http,https +logpath = %(nginx_access_log)s # Ban attackers that try to use PHP's URL-fopen() functionality # through GET/POST variables. - Experimental, with more than a year @@ -377,6 +427,8 @@ logpath = %(lighttpd_error_log)s port = http,https logpath = %(roundcube_errors_log)s +# Use following line in your jail.local if roundcube logs to journal. +#backend = %(syslog_backend)s [openwebmail] @@ -426,11 +478,13 @@ backend = %(syslog_backend)s port = http,https logpath = /var/log/tomcat*/catalina.out +#logpath = /var/log/guacamole.log [monit] #Ban clients brute-forcing the monit gui login port = 2812 logpath = /var/log/monit + /var/log/monit.log [webmin-auth] @@ -513,27 +567,29 @@ logpath = %(vsftpd_log)s # ASSP SMTP Proxy Jail [assp] -port = smtp,submission +port = smtp,465,submission logpath = /root/path/to/assp/logs/maillog.txt [courier-smtp] -port = smtp,submission +port = smtp,465,submission logpath = %(syslog_mail)s backend = %(syslog_backend)s [postfix] - -port = smtp,submission -logpath = %(postfix_log)s -backend = %(postfix_backend)s +# To use another modes set filter parameter "mode" in jail.local: +mode = more +port = smtp,465,submission +logpath = %(postfix_log)s +backend = %(postfix_backend)s [postfix-rbl] -port = smtp,submission +filter = postfix[mode=rbl] +port = smtp,465,submission logpath = %(postfix_log)s backend = %(postfix_backend)s maxretry = 1 @@ -541,14 +597,17 @@ maxretry = 1 [sendmail-auth] -port = submission,smtp +port = submission,465,smtp logpath = %(syslog_mail)s backend = %(syslog_backend)s [sendmail-reject] - -port = smtp,submission +# To use more aggressive modes set filter parameter "mode" in jail.local: +# normal (default), extra or aggressive +# See "tests/files/logs/sendmail-reject" or "filter.d/sendmail-reject.conf" for usage example and details. +#mode = normal +port = smtp,465,submission logpath = %(syslog_mail)s backend = %(syslog_backend)s @@ -556,7 +615,7 @@ backend = %(syslog_backend)s [qmail-rbl] filter = qmail -port = smtp,submission +port = smtp,465,submission logpath = /service/qmail/log/main/current @@ -564,14 +623,14 @@ logpath = /service/qmail/log/main/current # but can be set by syslog_facility in the dovecot configuration. [dovecot] -port = pop3,pop3s,imap,imaps,submission,sieve +port = pop3,pop3s,imap,imaps,submission,465,sieve logpath = %(dovecot_log)s backend = %(dovecot_backend)s [sieve] -port = smtp,submission +port = smtp,465,submission logpath = %(dovecot_log)s backend = %(dovecot_backend)s @@ -583,20 +642,21 @@ logpath = %(solidpop3d_log)s [exim] - -port = smtp,submission +# see filter.d/exim.conf for further modes supported from filter: +#mode = normal +port = smtp,465,submission logpath = %(exim_main_log)s [exim-spam] -port = smtp,submission +port = smtp,465,submission logpath = %(exim_main_log)s [kerio] -port = imap,smtp,imaps +port = imap,smtp,imaps,465 logpath = /opt/kerio/mailserver/store/logs/security.log @@ -607,14 +667,15 @@ logpath = /opt/kerio/mailserver/store/logs/security.log [courier-auth] -port = smtp,submission,imaps,pop3,pop3s +port = smtp,465,submission,imap,imaps,pop3,pop3s logpath = %(syslog_mail)s backend = %(syslog_backend)s [postfix-sasl] -port = smtp,submission,imap,imaps,pop3,pop3s +filter = postfix[mode=auth] +port = smtp,465,submission,imap,imaps,pop3,pop3s # You might consider monitoring /var/log/mail.warn instead if you are # running postfix since it would provide the same log lines at the # "warn" level but overall at the smaller filesize. @@ -631,7 +692,7 @@ backend = %(syslog_backend)s [squirrelmail] -port = smtp,submission,imap,imap2,imaps,pop3,pop3s,http,https,socks +port = smtp,465,submission,imap,imap2,imaps,pop3,pop3s,http,https,socks logpath = /var/lib/squirrelmail/prefs/squirrelmail_access_log @@ -684,8 +745,8 @@ logpath = /var/log/named/security.log [nsd] port = 53 -action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] - %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] +action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, protocol="udp"] logpath = /var/log/nsd.log @@ -696,9 +757,8 @@ logpath = /var/log/nsd.log [asterisk] port = 5060,5061 -action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] - %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] - %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s"] +action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, protocol="udp"] logpath = /var/log/asterisk/messages maxretry = 10 @@ -706,16 +766,22 @@ maxretry = 10 [freeswitch] port = 5060,5061 -action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] - %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] - %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s"] +action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, protocol="udp"] logpath = /var/log/freeswitch.log maxretry = 10 +# enable adminlog; it will log to a file inside znc's directory by default. +[znc-adminlog] + +port = 6667 +logpath = /var/lib/znc/moddata/adminlog/znc.log + + # To log wrong MySQL access attempts add to /etc/my.cnf in [mysqld] or # equivalent section: -# log-warning = 2 +# log-warnings = 2 # # for syslog (daemon facility) # [mysqld_safe] @@ -731,6 +797,14 @@ logpath = %(mysql_log)s backend = %(mysql_backend)s +[mssql-auth] +# Default configuration for Microsoft SQL Server for Linux +# See the 'mssql-conf' manpage how to change logpath or port +logpath = /var/opt/mssql/log/errorlog +port = 1433 +filter = mssql-auth + + # Log wrong MongoDB auth (for details see filter 'filter.d/mongodb-auth.conf') [mongodb-auth] # change port when running with "--shardsvr" or "--configsvr" runtime operation @@ -749,8 +823,8 @@ logpath = /var/log/mongodb/mongodb.log logpath = /var/log/fail2ban.log banaction = %(banaction_allports)s -bantime = 604800 ; 1 week -findtime = 86400 ; 1 day +bantime = 1w +findtime = 1d # Generic filter for PAM. Has to be used with action which bans all @@ -786,11 +860,31 @@ logpath = /var/log/ejabberd/ejabberd.log [counter-strike] logpath = /opt/cstrike/logs/L[0-9]*.log -# Firewall: http://www.cstrike-planet.com/faq/6 tcpport = 27030,27031,27032,27033,27034,27035,27036,27037,27038,27039 udpport = 1200,27000,27001,27002,27003,27004,27005,27006,27007,27008,27009,27010,27011,27012,27013,27014,27015 -action = %(banaction)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] - %(banaction)s[name=%(__name__)s-udp, port="%(udpport)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] +action_ = %(default/action_)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, port="%(udpport)s", protocol="udp"] + +[softethervpn] +port = 500,4500 +protocol = udp +logpath = /usr/local/vpnserver/security_log/*/sec.log + +[gitlab] +port = http,https +logpath = /var/log/gitlab/gitlab-rails/application.log + +[grafana] +port = http,https +logpath = /var/log/grafana/grafana.log + +[bitwarden] +port = http,https +logpath = /home/*/bwdata/logs/identity/Identity/log.txt + +[centreon] +port = http,https +logpath = /var/log/centreon/login.log # consider low maxretry and a long bantime # nobody except your own Nagios server should ever probe nrpe @@ -824,7 +918,9 @@ filter = apache-pass[knocking_url="%(knocking_url)s"] logpath = %(apache_access_log)s blocktype = RETURN returntype = DROP -bantime = 3600 +action = %(action_)s[blocktype=%(blocktype)s, returntype=%(returntype)s, + actionstart_on_demand=false, actionrepair_on_unban=true] +bantime = 1h maxretry = 1 findtime = 1 @@ -832,8 +928,8 @@ findtime = 1 [murmur] # AKA mumble-server port = 64738 -action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol=tcp, chain="%(chain)s", actname=%(banaction)s-tcp] - %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol=udp, chain="%(chain)s", actname=%(banaction)s-udp] +action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, protocol="udp"] logpath = /var/log/mumble-server/mumble-server.log @@ -851,5 +947,34 @@ logpath = /var/log/haproxy.log [slapd] port = ldap,ldaps -filter = slapd logpath = /var/log/slapd.log + +[domino-smtp] +port = smtp,ssmtp +logpath = /home/domino01/data/IBM_TECHNICAL_SUPPORT/console.log + +[phpmyadmin-syslog] +port = http,https +logpath = %(syslog_authpriv)s +backend = %(syslog_backend)s + + +[zoneminder] +# Zoneminder HTTP/HTTPS web interface auth +# Logs auth failures to apache2 error log +port = http,https +logpath = %(apache_error_log)s + +[traefik-auth] +# to use 'traefik-auth' filter you have to configure your Traefik instance, +# see `filter.d/traefik-auth.conf` for details and service example. +port = http,https +logpath = /var/log/traefik/access.log + +[scanlogd] +logpath = %(syslog_local0)s +banaction = %(banaction_allports)s + +[monitorix] +port = 8080 +logpath = /var/log/monitorix-httpd From d0b65d56614a04c44ae917a4872485f39ced2132 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 8 Sep 2023 15:17:25 +0200 Subject: [PATCH 0256/1116] revert important variables in fail2ban jail.conf --- conf/fail2ban/jail.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/fail2ban/jail.conf b/conf/fail2ban/jail.conf index fe8db527d..2ffc71c28 100644 --- a/conf/fail2ban/jail.conf +++ b/conf/fail2ban/jail.conf @@ -178,7 +178,7 @@ filter = %(__name__)s[mode=%(mode)s] destemail = root@localhost # Sender email address used solely for some actions -sender = root@ +sender = root@localhost # E-mail action. Since 0.8.1 Fail2Ban uses sendmail MTA for the # mailing. Change mta configuration parameter to mail if you want to @@ -189,7 +189,7 @@ mta = sendmail protocol = tcp # Specify chain where jumps would need to be added in ban-actions expecting parameter chain -chain = +chain = INPUT # Ports to be banned # Usually should be overridden in a particular jail From 2bd3dd2bba93ec414b368d308d7684ca1ca3591b Mon Sep 17 00:00:00 2001 From: Kayou Date: Fri, 8 Sep 2023 22:31:08 +0200 Subject: [PATCH 0257/1116] set maxretry to 10 --- conf/fail2ban/jail.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/fail2ban/jail.conf b/conf/fail2ban/jail.conf index 2ffc71c28..8b6a2ec6d 100644 --- a/conf/fail2ban/jail.conf +++ b/conf/fail2ban/jail.conf @@ -105,7 +105,7 @@ bantime = 10m findtime = 10m # "maxretry" is the number of failures before a host get banned. -maxretry = 5 +maxretry = 10 # "maxmatches" is the number of matches stored in ticket (resolvable via tag in actions). maxmatches = %(maxretry)s From e77e9a0a9a804241159468b0024fbcf3e6801a40 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 8 Sep 2023 23:13:38 +0200 Subject: [PATCH 0258/1116] backup/restore tests from 11.2 --- src/tests/test_backuprestore.py | 52 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/tests/test_backuprestore.py b/src/tests/test_backuprestore.py index eb59d4fea..4a59e7574 100644 --- a/src/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -49,8 +49,8 @@ def setup_function(function): for m in function.__dict__.get("pytestmark", []) } - if "with_wordpress_archive_from_4p2" in markers: - add_archive_wordpress_from_4p2() + if "with_wordpress_archive_from_11p2" in markers: + add_archive_wordpress_from_11p2() assert len(backup_list()["archives"]) == 1 if "with_legacy_app_installed" in markers: @@ -72,8 +72,8 @@ def setup_function(function): ) assert app_is_installed("backup_recommended_app") - if "with_system_archive_from_4p2" in markers: - add_archive_system_from_4p2() + if "with_system_archive_from11p2" in markers: + add_archive_system_from_11p2() assert len(backup_list()["archives"]) == 1 if "with_permission_app_installed" in markers: @@ -148,7 +148,7 @@ def app_is_installed(app): def backup_test_dependencies_are_met(): # Dummy test apps (or backup archives) assert os.path.exists( - os.path.join(get_test_apps_dir(), "backup_wordpress_from_4p2") + os.path.join(get_test_apps_dir(), "backup_wordpress_from_11p2") ) assert os.path.exists(os.path.join(get_test_apps_dir(), "legacy_app_ynh")) assert os.path.exists( @@ -211,23 +211,23 @@ def install_app(app, path, additionnal_args=""): ) -def add_archive_wordpress_from_4p2(): +def add_archive_wordpress_from_11p2(): os.system("mkdir -p /home/yunohost.backup/archives") os.system( "cp " - + os.path.join(get_test_apps_dir(), "backup_wordpress_from_4p2/backup.tar") - + " /home/yunohost.backup/archives/backup_wordpress_from_4p2.tar" + + os.path.join(get_test_apps_dir(), "backup_wordpress_from_11p2/backup.tar") + + " /home/yunohost.backup/archives/backup_wordpress_from_11p2.tar" ) -def add_archive_system_from_4p2(): +def add_archive_system_from_11p2(): os.system("mkdir -p /home/yunohost.backup/archives") os.system( "cp " - + os.path.join(get_test_apps_dir(), "backup_system_from_4p2/backup.tar") - + " /home/yunohost.backup/archives/backup_system_from_4p2.tar" + + os.path.join(get_test_apps_dir(), "backup_system_from_11p2/backup.tar") + + " /home/yunohost.backup/archives/backup_system_from_11p2.tar" ) @@ -292,12 +292,12 @@ def test_backup_and_restore_all_sys(): # -# System restore from 3.8 # +# System restore from 11.2 # # -@pytest.mark.with_system_archive_from_4p2 -def test_restore_system_from_Ynh4p2(monkeypatch): +@pytest.mark.with_system_archive_from_11p2 +def test_restore_system_from_Ynh11p2(monkeypatch): name = random_ascii(8) # Backup current system with message("backup_created", name=name): @@ -305,7 +305,7 @@ def test_restore_system_from_Ynh4p2(monkeypatch): archives = backup_list()["archives"] assert len(archives) == 2 - # Restore system archive from 3.8 + # Restore system archive from 11.2 try: with message("restore_complete"): backup_restore( @@ -439,18 +439,17 @@ def test_backup_using_copy_method(): # App restore # # -# FIXME : switch to a backup from 11.x @pytest.mark.skip -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 @pytest.mark.with_custom_domain("yolo.test") -def test_restore_app_wordpress_from_Ynh4p2(): +def test_restore_app_wordpress_from_Ynh11p2(): with message("restore_complete"): backup_restore( system=None, name=backup_list()["archives"][0], apps=["wordpress"] ) -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_script_failure_handling(monkeypatch, mocker): def custom_hook_exec(name, *args, **kwargs): @@ -471,7 +470,7 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker): assert not _is_installed("wordpress") -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 def test_restore_app_not_enough_free_space(monkeypatch, mocker): def custom_free_space_in_directory(dirpath): return 0 @@ -490,7 +489,7 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker): assert not _is_installed("wordpress") -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 def test_restore_app_not_in_backup(mocker): assert not _is_installed("wordpress") assert not _is_installed("yoloswag") @@ -505,9 +504,8 @@ def test_restore_app_not_in_backup(mocker): assert not _is_installed("yoloswag") -# FIXME : switch to a backup from 11.x @pytest.mark.skip -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_already_installed(mocker): assert not _is_installed("wordpress") @@ -619,17 +617,17 @@ def test_restore_archive_with_no_json(mocker): backup_restore(name="badbackup", force=True) -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 def test_restore_archive_with_bad_archive(mocker): # Break the archive os.system( - "head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_4p2.tar > /home/yunohost.backup/archives/backup_wordpress_from_4p2_bad.tar" + "head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_11p2.tar > /home/yunohost.backup/archives/backup_wordpress_from_11p2_bad.tar" ) - assert "backup_wordpress_from_4p2_bad" in backup_list()["archives"] + assert "backup_wordpress_from_11p2_bad" in backup_list()["archives"] with raiseYunohostError(mocker, "backup_archive_corrupted"): - backup_restore(name="backup_wordpress_from_4p2_bad", force=True) + backup_restore(name="backup_wordpress_from_11p2_bad", force=True) clean_tmp_backup_directory() From aed8ecb64594f71c0340a172b7bb8fb6ca2d82ed Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 8 Sep 2023 23:47:57 +0200 Subject: [PATCH 0259/1116] do not skip tests from 11.2 --- src/tests/test_backuprestore.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tests/test_backuprestore.py b/src/tests/test_backuprestore.py index 4a59e7574..c0a55b65b 100644 --- a/src/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -439,7 +439,6 @@ def test_backup_using_copy_method(): # App restore # # -@pytest.mark.skip @pytest.mark.with_wordpress_archive_from_11p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_wordpress_from_Ynh11p2(): @@ -504,7 +503,6 @@ def test_restore_app_not_in_backup(mocker): assert not _is_installed("yoloswag") -@pytest.mark.skip @pytest.mark.with_wordpress_archive_from_11p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_already_installed(mocker): From 142fad4b7898686f970d9bbae704e7da64203ed5 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 8 Sep 2023 23:50:30 +0200 Subject: [PATCH 0260/1116] typo --- src/tests/test_backuprestore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_backuprestore.py b/src/tests/test_backuprestore.py index c0a55b65b..ad9a1c1f2 100644 --- a/src/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -72,7 +72,7 @@ def setup_function(function): ) assert app_is_installed("backup_recommended_app") - if "with_system_archive_from11p2" in markers: + if "with_system_archive_from_11p2" in markers: add_archive_system_from_11p2() assert len(backup_list()["archives"]) == 1 From df1f3149ea1d2182f212823c69e1ac07d4a3cf72 Mon Sep 17 00:00:00 2001 From: stanislas Date: Tue, 12 Sep 2023 00:37:47 +0200 Subject: [PATCH 0261/1116] The p value of a DMARC record can take the values none, quarantine or reject The validation is no more about the being similar to the expected config. Now wre check that the value of the p parameter of a DMARC record has the value none, quarantine or reject. No check for other parameters but it could be improved --- src/diagnosers/12-dnsrecords.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py index 196a2e1f9..19becb753 100644 --- a/src/diagnosers/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -215,6 +215,11 @@ class MyDiagnoser(Diagnoser): for part in current if not part.startswith("ip4:") and not part.startswith("ip6:") } + if "v=DMARC1" in r["value"]: + for param in current: + key, value = param.split("=") + if key == "p": + return value in ["none", "quarantine", "reject"] return expected == current elif r["type"] == "MX": # For MX, we want to ignore the priority From b54a71b0cf365042e3d6e6f364b64d5608b238e4 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 12 Sep 2023 09:02:04 +0300 Subject: [PATCH 0262/1116] Fix missleading example for ynh_setup_source There shouldn't be any trailing / for folders for ynh_setup_source --- helpers/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/utils b/helpers/utils index 4694dd724..8b28000d6 100644 --- a/helpers/utils +++ b/helpers/utils @@ -75,7 +75,7 @@ fi # 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 `main` (when the sources resource exists in manifest.toml) or (legacy) `app` otherwise -# | 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: -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' (no trailing `/` for folders) # | arg: -r, --full_replace= - Remove previous sources before installing new sources # # #### New 'sources' resources From cbb85f8c3b860422382c5ea5353b251127464442 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 24 Sep 2023 17:13:33 +0200 Subject: [PATCH 0263/1116] dyndns: handle too many requests in availability testing --- locales/en.json | 1 + src/dyndns.py | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index 60f58f639..05194c7f0 100644 --- a/locales/en.json +++ b/locales/en.json @@ -396,6 +396,7 @@ "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` 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_availability_too_many_requests": "YunoHost's free domain service received too many requests from you, wait 1 minute or so before trying again.", "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}.", "dyndns_ip_update_failed": "Could not update IP address to DynDNS", diff --git a/src/dyndns.py b/src/dyndns.py index a3afd655f..c9bf8afe6 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -63,19 +63,28 @@ def _dyndns_available(domain): Returns: True if the domain is available, False otherwise. """ + import requests # lazy loading this module for performance reasons + logger.debug(f"Checking if domain {domain} is available on {DYNDNS_PROVIDER} ...") try: - r = download_json( - f"https://{DYNDNS_PROVIDER}/test/{domain}", expected_status_code=None - ) + r = requests.get(f"https://{DYNDNS_PROVIDER}/test/{domain}", timeout=30) except MoulinetteError as e: logger.error(str(e)) raise YunohostError( "dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER ) - return r == f"Domain {domain} is available" + if r.status_code == 200: + return r == f"Domain {domain} is available" + elif r.status_code == 409: + return False + elif r.status_code == 429: + raise YunohostValidationError("dyndns_availability_too_many_requests") + else: + raise YunohostError( + "dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER + ) @is_unit_operation(exclude=["recovery_password"]) From 322fc3b7124d0cf5d274b4d822a511a896670588 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 24 Sep 2023 17:15:56 +0200 Subject: [PATCH 0264/1116] dyndns: try to unsubscribe with password before subscribing if domain is not available --- src/dyndns.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/dyndns.py b/src/dyndns.py index c9bf8afe6..c634f169d 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -103,14 +103,26 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): "dyndns_domain_not_provided", domain=domain, provider=DYNDNS_PROVIDER ) - # Verify if domain is available - if not _dyndns_available(domain): - raise YunohostValidationError("dyndns_unavailable", domain=domain) - # Check adding another dyndns domain is still allowed if not is_subscribing_allowed(): raise YunohostValidationError("domain_dyndns_already_subscribed") + # Verify if domain is available + if not _dyndns_available(domain): + # Prompt for a password if running in CLI and no password provided + if not recovery_password and Moulinette.interface.type == "cli": + logger.warning(m18n.n("ask_dyndns_recovery_password_explain_unavailable")) + recovery_password = Moulinette.prompt( + m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True + ) + + if recovery_password: + # Try to unsubscribe the domain so it can be subscribed again + # If successful, it will be resubscribed with the same recovery password + dyndns_unsubscribe(domain=domain, recovery_password=recovery_password) + else: + raise YunohostValidationError("dyndns_unavailable", domain=domain) + # Prompt for a password if running in CLI and no password provided if not recovery_password and Moulinette.interface.type == "cli": logger.warning(m18n.n("ask_dyndns_recovery_password_explain")) From 67e28567ff6d4865dc8c3a3c3a2de89885b0b3e5 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 24 Sep 2023 17:19:24 +0200 Subject: [PATCH 0265/1116] dyndns: switch to ValidationError for some service response --- locales/en.json | 2 ++ src/dyndns.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 05194c7f0..d9f42dede 100644 --- a/locales/en.json +++ b/locales/en.json @@ -91,6 +91,7 @@ "ask_new_path": "New path", "ask_password": "Password", "ask_dyndns_recovery_password_explain": "Please pick a recovery password for your DynDNS domain, in case you need to reset it later.", + "ask_dyndns_recovery_password_explain_unavailable": "This DynDNS domain is not available, please enter the recovery password if this your domain.", "ask_dyndns_recovery_password": "DynDNS recovery password", "ask_dyndns_recovery_password_explain_during_unsubscribe": "Please enter the recovery password for this DynDNS domain.", "ask_user_domain": "Domain to use for the user's email address and XMPP account", @@ -411,6 +412,7 @@ "dyndns_unsubscribed": "DynDNS domain unsubscribed", "dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials", "dyndns_unsubscribe_already_unsubscribed": "Domain is already unsubscribed", + "dyndns_unsubscribe_too_many_requests": "YunoHost's free domain service received too many requests from you, wait 1 hour or so before trying again.", "dyndns_set_recovery_password_denied": "Failed to set recovery password: invalid key", "dyndns_set_recovery_password_unknown_domain": "Failed to set recovery password: domain not registered", "dyndns_set_recovery_password_invalid_password": "Failed to set recovery password: password is not strong enough", diff --git a/src/dyndns.py b/src/dyndns.py index c634f169d..8efa5e6df 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -273,9 +273,11 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): # in /etc/yunohost/dyndns regen_conf(["yunohost"]) elif r.status_code == 403: - raise YunohostError("dyndns_unsubscribe_denied") + raise YunohostValidationError("dyndns_unsubscribe_denied") elif r.status_code == 409: - raise YunohostError("dyndns_unsubscribe_already_unsubscribed") + raise YunohostValidationError("dyndns_unsubscribe_already_unsubscribed") + elif r.status_code == 429: + raise YunohostValidationError("dyndns_unsubscribe_too_many_requests") else: raise YunohostError( "dyndns_unsubscribe_failed", From 65843bda6dd01ec679c809f8c501d12f06e8e6fd Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 24 Sep 2023 18:11:28 +0200 Subject: [PATCH 0266/1116] dyndns: add postinstall recovery password handling --- src/tools.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/tools.py b/src/tools.py index cd48f00ee..2b0b566e2 100644 --- a/src/tools.py +++ b/src/tools.py @@ -157,7 +157,7 @@ def tools_postinstall( overwrite_root_password=True, ): from yunohost.dyndns import _dyndns_available - from yunohost.utils.dns import is_yunohost_dyndns_domain + from yunohost.utils.dns import is_yunohost_dyndns_domain, dyndns_unsubscribe from yunohost.utils.password import ( assert_password_is_strong_enough, assert_password_is_compatible, @@ -218,7 +218,14 @@ def tools_postinstall( ) else: if not available: - raise YunohostValidationError("dyndns_unavailable", domain=domain) + if dyndns_recovery_password: + # Try to unsubscribe the domain so it can be subscribed again + # If successful, it will be resubscribed with the same recovery password + dyndns_unsubscribe( + domain=domain, recovery_password=dyndns_recovery_password + ) + else: + raise YunohostValidationError("dyndns_unavailable", domain=domain) if os.system("iptables -V >/dev/null 2>/dev/null") != 0: raise YunohostValidationError( From 97c2cdc593966b554ad792fd54d33af6be3d84dd Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 25 Sep 2023 13:55:01 +0200 Subject: [PATCH 0267/1116] domain: move domain files removal so dyndns API key still exists while trying to unsubscribe --- src/domain.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/domain.py b/src/domain.py index 8fc9799cd..40e8b50d0 100644 --- a/src/domain.py +++ b/src/domain.py @@ -342,6 +342,7 @@ def domain_remove( dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain ignore_dyndns -- If we just remove the DynDNS domain, without unsubscribing """ + import glob from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface @@ -427,15 +428,6 @@ def domain_remove( global domain_list_cache domain_list_cache = [] - stuff_to_delete = [ - f"/etc/yunohost/certs/{domain}", - f"/etc/yunohost/dyndns/K{domain}.+*", - f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", - ] - - for stuff in stuff_to_delete: - rm(stuff, force=True, recursive=True) - # Sometime we have weird issues with the regenconf where some files # appears as manually modified even though they weren't touched ... # There are a few ideas why this happens (like backup/restore nginx @@ -469,6 +461,11 @@ def domain_remove( domain=domain, recovery_password=dyndns_recovery_password ) + rm(f"/etc/yunohost/certs/{domain}", force=True, recursive=True) + for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*"): + rm(key_file, force=True) + rm(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", force=True) + logger.success(m18n.n("domain_deleted")) From fc68f769f9c2e3923838be99fe8701ca239cdd48 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 25 Sep 2023 14:37:18 +0200 Subject: [PATCH 0268/1116] domain: add recovery passoword in config panel --- src/dns.py | 3 +++ src/domain.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/dns.py b/src/dns.py index e25d0f3ec..fbb461463 100644 --- a/src/dns.py +++ b/src/dns.py @@ -545,6 +545,9 @@ def _get_registrar_config_section(domain): "value": "yunohost", } ) + registrar_infos["recovery_password"] = OrderedDict( + {"type": "password", "ask": m18n.n("ask_dyndns_recovery_password"), "default": ""} + ) return OrderedDict(registrar_infos) elif is_special_use_tld(dns_zone): registrar_infos["registrar"] = OrderedDict( diff --git a/src/domain.py b/src/domain.py index 40e8b50d0..b68c7dffc 100644 --- a/src/domain.py +++ b/src/domain.py @@ -317,6 +317,11 @@ def domain_add( pass raise e + if dyndns and dyndns_recovery_password: + domain_config_set( + domain, "dns.registrar.recovery_password", dyndns_recovery_password + ) + hook_callback("post_domain_add", args=[domain]) logger.success(m18n.n("domain_created")) @@ -704,6 +709,14 @@ class DomainConfigPanel(ConfigPanel): domain=self.entity, other_app=app_map(raw=True)[self.entity]["/"]["id"], ) + if ( + "recovery_password" in self.future_values + and self.future_values["recovery_password"] + != self.values["recovery_password"] + ): + domain_dyndns_set_recovery_password( + self.entity, self.future_values["recovery_password"] + ) super()._apply() From 9e87ea88df37bd7e924f5659c6ffe33ded8dbebf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 27 Sep 2023 19:30:55 +0200 Subject: [PATCH 0269/1116] portal-api: improve semantic for yunohost public portal stuff --- share/config_domain.toml | 2 +- src/portal.py | 20 ++++++++++---------- src/utils/configpanel.py | 2 +- src/utils/form.py | 7 ++++--- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/share/config_domain.toml b/share/config_domain.toml index 27493b4e7..1239b1fea 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -30,7 +30,7 @@ name = "Features" type = "app" filter = "is_webapp" default = "_none" - list_portal = true + add_yunohost_portal_to_choices = true [feature.mail] diff --git a/src/portal.py b/src/portal.py index 61d0c301a..cc6c03e4b 100644 --- a/src/portal.py +++ b/src/portal.py @@ -76,13 +76,13 @@ def _get_portal_settings(domain: Union[str, None] = None): def portal_public(): - settings = _get_portal_settings() - settings["apps"] = {} - settings["public"] = settings.pop("default_app") == "portal_public_apps" + portal_settings = _get_portal_settings() + portal_settings["apps"] = {} + portal_settings["public"] = portal_settings.pop("default_app") == "_yunohost_portal_with_public_apps" - if settings["public"]: + if portal_settings["public"]: ssowat_conf = read_json("/etc/ssowat/conf.json") - settings["apps"] = { + portal_settings["apps"] = { perm.replace(".main", ""): { "label": infos["label"], "url": infos["uris"][0], @@ -91,14 +91,14 @@ def portal_public(): if infos["show_tile"] and infos["public"] } - if not settings["show_other_domains_apps"]: - settings["apps"] = { + if not portal_settings["show_other_domains_apps"]: + portal_settings["apps"] = { name: data - for name, data in settings["apps"].items() - if settings["domain"] in data["url"] + for name, data in portal_settings["apps"].items() + if portal_settings["domain"] in data["url"] } - return settings + return portal_settings def portal_me(): diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 30a35e410..86dea2e7d 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -386,7 +386,7 @@ class ConfigPanel: "filter", "readonly", "enabled", - "list_portal", + "add_yunohost_portal_to_choices", # "confirm", # TODO: to ask confirmation before running an action ], "defaults": {}, diff --git a/src/utils/form.py b/src/utils/form.py index c7bc25305..f201f507b 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -914,7 +914,7 @@ class AppOption(BaseChoicesOption): super().__init__(question) self.filter = question.get("filter", None) - self.list_portal = question.get("list_portal", False) + self.add_yunohost_portal_to_choices = question.get("add_yunohost_portal_to_choices", False) apps = app_list(full=True)["apps"] @@ -930,8 +930,9 @@ class AppOption(BaseChoicesOption): return app["label"] + domain_path_or_id self.choices = {"_none": "---"} - if self.list_portal: - self.choices["portal_public_apps"] = "Portal" + if self.add_yunohost_portal_to_choices: + # FIXME: i18n + self.choices["_yunohost_portal_with_public_apps"] = "YunoHost's portal with public apps" self.choices.update({app["id"]: _app_display(app) for app in apps}) From e9802ce2dca0f91a175f121ebc31ff6f9a99c1f4 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 27 Sep 2023 19:31:50 +0200 Subject: [PATCH 0270/1116] domain dyndns: do not save recovery password --- src/domain.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/domain.py b/src/domain.py index b68c7dffc..154da54dc 100644 --- a/src/domain.py +++ b/src/domain.py @@ -317,11 +317,6 @@ def domain_add( pass raise e - if dyndns and dyndns_recovery_password: - domain_config_set( - domain, "dns.registrar.recovery_password", dyndns_recovery_password - ) - hook_callback("post_domain_add", args=[domain]) logger.success(m18n.n("domain_created")) @@ -710,13 +705,14 @@ class DomainConfigPanel(ConfigPanel): other_app=app_map(raw=True)[self.entity]["/"]["id"], ) if ( - "recovery_password" in self.future_values - and self.future_values["recovery_password"] - != self.values["recovery_password"] + "recovery_password" in self.new_values + and self.new_values["recovery_password"] ): domain_dyndns_set_recovery_password( - self.entity, self.future_values["recovery_password"] + self.entity, self.new_values["recovery_password"] ) + # Do not save password in yaml settings + del self.new_values["recovery_password"] super()._apply() From 2e86bae4efbeacff1026af9039f47090c65d504f Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 27 Sep 2023 19:58:52 +0200 Subject: [PATCH 0271/1116] domain: make sure of it --- src/domain.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/domain.py b/src/domain.py index 154da54dc..04d8370e4 100644 --- a/src/domain.py +++ b/src/domain.py @@ -712,7 +712,9 @@ class DomainConfigPanel(ConfigPanel): self.entity, self.new_values["recovery_password"] ) # Do not save password in yaml settings + del self.values["recovery_password"] del self.new_values["recovery_password"] + assert "recovery_password" not in self.future_values super()._apply() From a0dbf6a5b0ecf3965563b196f69f459b4db00c81 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 27 Sep 2023 20:40:50 +0200 Subject: [PATCH 0272/1116] portal: improve domain setting fetch + set show_other_domains_apps to false by default ? --- share/config_domain.toml | 2 +- src/portal.py | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/share/config_domain.toml b/share/config_domain.toml index 1239b1fea..6fc5fc50a 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -9,7 +9,7 @@ name = "Features" [feature.portal.show_other_domains_apps] type = "boolean" - default = 1 + default = false [feature.portal.portal_title] type = "string" diff --git a/src/portal.py b/src/portal.py index cc6c03e4b..c7a0eb68b 100644 --- a/src/portal.py +++ b/src/portal.py @@ -59,18 +59,19 @@ def _get_portal_settings(domain: Union[str, None] = None): domain = request.get_header("host") - if Path(f"{DOMAIN_SETTINGS_DIR}/{domain}.portal.yml").exists(): - settings = read_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.portal.yml") - else: - settings = { - "public": False, - "portal_logo": "", - "portal_theme": "system", - "portal_title": "YunoHost", - "show_other_domains_apps": 1, - } + assert domain and "/" not in domain - settings["domain"] = domain + settings = { + "public": False, + "portal_logo": "", + "portal_theme": "system", + "portal_title": "YunoHost", + "show_other_domains_apps": false, + "domain": domain, + } + + if Path(f"{DOMAIN_SETTINGS_DIR}/{domain}.portal.yml").exists(): + settings.update(read_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.portal.yml")) return settings From cd079459b98566d4fa2042ad1b30cde6a5a681cf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 27 Sep 2023 20:47:17 +0200 Subject: [PATCH 0273/1116] dyndns: fix import typo --- src/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools.py b/src/tools.py index 2b0b566e2..088400067 100644 --- a/src/tools.py +++ b/src/tools.py @@ -156,8 +156,8 @@ def tools_postinstall( force_diskspace=False, overwrite_root_password=True, ): - from yunohost.dyndns import _dyndns_available - from yunohost.utils.dns import is_yunohost_dyndns_domain, dyndns_unsubscribe + from yunohost.dyndns import _dyndns_available, dyndns_unsubscribe + from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.utils.password import ( assert_password_is_strong_enough, assert_password_is_compatible, From b61a16421b134d7899542a94c5da3f14ca7d639c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 27 Sep 2023 20:51:45 +0200 Subject: [PATCH 0274/1116] portal-api: fix cookie secret initialization --- hooks/conf_regen/01-yunohost | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 4d53997a5..4f934db76 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -57,6 +57,12 @@ do_init_regen() { chmod 700 /var/cache/yunohost getent passwd ynh-portal &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group ynh-portal + if [ ! -e /etc/yunohost/.ssowat_cookie_secret ]; then + # NB: we need this to be exactly 32 char long, because it is later used as a key for AES256 + dd if=/dev/urandom bs=1 count=1000 2>/dev/null | tr --complement --delete 'A-Za-z0-9' | head -c 32 > /etc/yunohost/.ssowat_cookie_secret + fi + chown ynh-portal:root /etc/yunohost/.ssowat_cookie_secret + chmod 400 /etc/yunohost/.ssowat_cookie_secret cp yunohost-api.service /etc/systemd/system/yunohost-api.service cp yunohost-portal-api.service /etc/systemd/system/yunohost-portal-api.service @@ -68,6 +74,8 @@ do_init_regen() { systemctl enable yunohost-api.service --quiet systemctl start yunohost-api.service + + systemctl enable yunohost-portal-api.service systemctl start yunohost-portal-api.service From 2e4f2e8e3a7df7f45fb228fefe1ab8b757cc86f3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 27 Sep 2023 20:57:16 +0200 Subject: [PATCH 0275/1116] quality: unused import --- src/dyndns.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dyndns.py b/src/dyndns.py index 8efa5e6df..78a6b0a90 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -27,7 +27,6 @@ from moulinette import Moulinette, m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file, rm, chown, chmod -from moulinette.utils.network import download_json from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.domain import _get_maindomain From f617b97d806c264d3999a236945805dae9d985ca Mon Sep 17 00:00:00 2001 From: Alexandre Aubin <4533074+alexAubin@users.noreply.github.com> Date: Wed, 27 Sep 2023 22:08:26 +0200 Subject: [PATCH 0276/1116] portal/ssowat: fix conf initialization --- hooks/conf_regen/01-yunohost | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 4f934db76..edf64012e 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -48,6 +48,9 @@ do_init_regen() { echo "{}" >'/etc/ssowat/conf.json.persistent' chmod 644 /etc/ssowat/conf.json.persistent chown root:root /etc/ssowat/conf.json.persistent + echo "{}" >'/etc/ssowat/conf.json' + chmod 644 /etc/ssowat/conf.json + chown root:root /etc/ssowat/conf.json # Empty service conf touch /etc/yunohost/services.yml From d3418479a20696f6a8ae78aecba2dc3c6dd01e88 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 27 Sep 2023 22:25:44 +0200 Subject: [PATCH 0277/1116] fix remaining log.getActionLogger --- src/diagnosers/00-basesystem.py | 4 ++-- src/diagnosers/10-ip.py | 4 ++-- src/diagnosers/12-dnsrecords.py | 4 ++-- src/diagnosers/24-mail.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/diagnosers/00-basesystem.py b/src/diagnosers/00-basesystem.py index 336271bd1..dec8c3c2c 100644 --- a/src/diagnosers/00-basesystem.py +++ b/src/diagnosers/00-basesystem.py @@ -19,9 +19,9 @@ import os import json import subprocess +import logging from typing import List -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 @@ -31,7 +31,7 @@ from yunohost.utils.system import ( system_arch, ) -logger = log.getActionLogger("yunohost.diagnosis") +logger = logging.getLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index 4f9cd9708..3d457991e 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -19,9 +19,9 @@ import re import os import random +import logging from typing import List -from moulinette.utils import log from moulinette.utils.network import download_text from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_file @@ -30,7 +30,7 @@ from yunohost.diagnosis import Diagnoser from yunohost.utils.network import get_network_interfaces from yunohost.settings import settings_get -logger = log.getActionLogger("yunohost.diagnosis") +logger = logging.getLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py index 19becb753..59a5e9857 100644 --- a/src/diagnosers/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -18,11 +18,11 @@ # import os import re +import logging from typing import List from datetime import datetime, timedelta from publicsuffix2 import PublicSuffixList -from moulinette.utils import log from moulinette.utils.process import check_output from yunohost.utils.dns import ( @@ -39,7 +39,7 @@ from yunohost.dns import ( _get_relative_name_for_dns_zone, ) -logger = log.getActionLogger("yunohost.diagnosis") +logger = logging.getLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index c7fe9d04b..88748661c 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -19,11 +19,11 @@ import os import dns.resolver import re +import logging from typing import List from subprocess import CalledProcessError -from moulinette.utils import log from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_yaml @@ -34,7 +34,7 @@ from yunohost.utils.dns import dig DEFAULT_DNS_BLACKLIST = "/usr/share/yunohost/dnsbl_list.yml" -logger = log.getActionLogger("yunohost.diagnosis") +logger = logging.getLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): From 127b6121d1692222f44ce48c8a48e6d30a3c996d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 27 Sep 2023 22:29:27 +0200 Subject: [PATCH 0278/1116] meh --- src/diagnosis.py | 1 - src/portal.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/diagnosis.py b/src/diagnosis.py index be3208b02..90709ad23 100644 --- a/src/diagnosis.py +++ b/src/diagnosis.py @@ -24,7 +24,6 @@ from importlib import import_module from logging import getLogger from moulinette import m18n, Moulinette -from moulinette.utils import log from moulinette.utils.filesystem import ( read_json, write_to_json, diff --git a/src/portal.py b/src/portal.py index c7a0eb68b..5b3af4f49 100644 --- a/src/portal.py +++ b/src/portal.py @@ -66,7 +66,7 @@ def _get_portal_settings(domain: Union[str, None] = None): "portal_logo": "", "portal_theme": "system", "portal_title": "YunoHost", - "show_other_domains_apps": false, + "show_other_domains_apps": False, "domain": domain, } From 82affd298452cbab360df4d93303c9817f146707 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 28 Sep 2023 14:19:00 +0200 Subject: [PATCH 0279/1116] dyndns: fix availability check, polish UX --- locales/en.json | 6 +++--- src/domain.py | 11 +++++++---- src/dyndns.py | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/locales/en.json b/locales/en.json index d9f42dede..dbf95caa6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -91,7 +91,7 @@ "ask_new_path": "New path", "ask_password": "Password", "ask_dyndns_recovery_password_explain": "Please pick a recovery password for your DynDNS domain, in case you need to reset it later.", - "ask_dyndns_recovery_password_explain_unavailable": "This DynDNS domain is not available, please enter the recovery password if this your domain.", + "ask_dyndns_recovery_password_explain_unavailable": "This DynDNS domain is aleady registered. If you are the person who originally registered this domain, you may enter the recovery password to reclaim this domain.", "ask_dyndns_recovery_password": "DynDNS recovery password", "ask_dyndns_recovery_password_explain_during_unsubscribe": "Please enter the recovery password for this DynDNS domain.", "ask_user_domain": "Domain to use for the user's email address and XMPP account", @@ -397,7 +397,7 @@ "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` 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_availability_too_many_requests": "YunoHost's free domain service received too many requests from you, wait 1 minute or so before trying again.", + "dyndns_availability_too_many_requests": "YunoHost's dyndns service received too many requests from you, wait 1 minute or so before trying again.", "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}.", "dyndns_ip_update_failed": "Could not update IP address to DynDNS", @@ -412,7 +412,7 @@ "dyndns_unsubscribed": "DynDNS domain unsubscribed", "dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials", "dyndns_unsubscribe_already_unsubscribed": "Domain is already unsubscribed", - "dyndns_unsubscribe_too_many_requests": "YunoHost's free domain service received too many requests from you, wait 1 hour or so before trying again.", + "dyndns_unsubscribe_too_many_requests": "YunoHost's dyndns service received too many requests from you, wait 1 hour or so before trying again.", "dyndns_set_recovery_password_denied": "Failed to set recovery password: invalid key", "dyndns_set_recovery_password_unknown_domain": "Failed to set recovery password: domain not registered", "dyndns_set_recovery_password_invalid_password": "Failed to set recovery password: password is not strong enough", diff --git a/src/domain.py b/src/domain.py index 04d8370e4..beedf43e0 100644 --- a/src/domain.py +++ b/src/domain.py @@ -456,10 +456,13 @@ def domain_remove( # If a password is provided, delete the DynDNS record if dyndns: - # Actually unsubscribe - domain_dyndns_unsubscribe( - domain=domain, recovery_password=dyndns_recovery_password - ) + try: + # Actually unsubscribe + domain_dyndns_unsubscribe( + domain=domain, recovery_password=dyndns_recovery_password + ) + except Exception as e: + logger.warning(str(e)) rm(f"/etc/yunohost/certs/{domain}", force=True, recursive=True) for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*"): diff --git a/src/dyndns.py b/src/dyndns.py index 78a6b0a90..70f44672d 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -75,7 +75,7 @@ def _dyndns_available(domain): ) if r.status_code == 200: - return r == f"Domain {domain} is available" + return r.text.strip('"') == f"Domain {domain} is available" elif r.status_code == 409: return False elif r.status_code == 429: @@ -112,7 +112,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): if not recovery_password and Moulinette.interface.type == "cli": logger.warning(m18n.n("ask_dyndns_recovery_password_explain_unavailable")) recovery_password = Moulinette.prompt( - m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True + m18n.n("ask_dyndns_recovery_password"), is_password=True ) if recovery_password: From 53ffe3c1c07078998d192fb2128fdc5a8fa6868c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 28 Sep 2023 14:25:36 +0200 Subject: [PATCH 0280/1116] dyndns: fix tests --- src/domain.py | 6 ++++-- src/tests/test_domains.py | 12 +++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/domain.py b/src/domain.py index beedf43e0..a503a129c 100644 --- a/src/domain.py +++ b/src/domain.py @@ -715,8 +715,10 @@ class DomainConfigPanel(ConfigPanel): self.entity, self.new_values["recovery_password"] ) # Do not save password in yaml settings - del self.values["recovery_password"] - del self.new_values["recovery_password"] + if "recovery_password" in self.values: + del self.values["recovery_password"] + if "recovery_password" in self.new_values: + del self.new_values["recovery_password"] assert "recovery_password" not in self.future_values super()._apply() diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 1bbbb7890..03141b4fe 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -75,12 +75,13 @@ def test_domain_add(): assert TEST_DOMAINS[2] in domain_list()["domains"] -def test_domain_add_subscribe(): +def test_domain_add_and_remove_dyndns(): time.sleep(35) # Dynette blocks requests that happen too frequently assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] - + domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] def test_domain_add_existing_domain(): with pytest.raises(MoulinetteError): @@ -94,13 +95,6 @@ def test_domain_remove(): assert TEST_DOMAINS[1] not in domain_list()["domains"] -def test_domain_remove_unsubscribe(): - time.sleep(35) # Dynette blocks requests that happen too frequently - assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] - domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) - assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] - - def test_main_domain(): current_main_domain = _get_maindomain() assert domain_main_domain()["current_main_domain"] == current_main_domain From 125af4670f74cf65890d4dc87d44749a9b546b32 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 28 Sep 2023 14:29:18 +0200 Subject: [PATCH 0281/1116] dyndns: typo --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index dbf95caa6..dddad3ef9 100644 --- a/locales/en.json +++ b/locales/en.json @@ -91,7 +91,7 @@ "ask_new_path": "New path", "ask_password": "Password", "ask_dyndns_recovery_password_explain": "Please pick a recovery password for your DynDNS domain, in case you need to reset it later.", - "ask_dyndns_recovery_password_explain_unavailable": "This DynDNS domain is aleady registered. If you are the person who originally registered this domain, you may enter the recovery password to reclaim this domain.", + "ask_dyndns_recovery_password_explain_unavailable": "This DynDNS domain is already registered. If you are the person who originally registered this domain, you may enter the recovery password to reclaim this domain.", "ask_dyndns_recovery_password": "DynDNS recovery password", "ask_dyndns_recovery_password_explain_during_unsubscribe": "Please enter the recovery password for this DynDNS domain.", "ask_user_domain": "Domain to use for the user's email address and XMPP account", From 0f109db6cadfa8bf29d45bfd76278197cf4a37e2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 28 Sep 2023 14:29:53 +0200 Subject: [PATCH 0282/1116] dyndns/domain_remove: be paranoid and keep the dyndns unsubscribe + cert/dyndns/setting cleanup before the regenconf --- src/domain.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/domain.py b/src/domain.py index a503a129c..79194df2d 100644 --- a/src/domain.py +++ b/src/domain.py @@ -428,6 +428,21 @@ def domain_remove( global domain_list_cache domain_list_cache = [] + # If a password is provided, delete the DynDNS record + if dyndns: + try: + # Actually unsubscribe + domain_dyndns_unsubscribe( + domain=domain, recovery_password=dyndns_recovery_password + ) + except Exception as e: + logger.warning(str(e)) + + rm(f"/etc/yunohost/certs/{domain}", force=True, recursive=True) + for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*"): + rm(key_file, force=True) + rm(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", force=True) + # Sometime we have weird issues with the regenconf where some files # appears as manually modified even though they weren't touched ... # There are a few ideas why this happens (like backup/restore nginx @@ -454,21 +469,6 @@ def domain_remove( hook_callback("post_domain_remove", args=[domain]) - # If a password is provided, delete the DynDNS record - if dyndns: - try: - # Actually unsubscribe - domain_dyndns_unsubscribe( - domain=domain, recovery_password=dyndns_recovery_password - ) - except Exception as e: - logger.warning(str(e)) - - rm(f"/etc/yunohost/certs/{domain}", force=True, recursive=True) - for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*"): - rm(key_file, force=True) - rm(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", force=True) - logger.success(m18n.n("domain_deleted")) From ca1e088f2948d53905d3477fbcccb77fc61ae8e3 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 28 Sep 2023 15:41:57 +0200 Subject: [PATCH 0283/1116] test:domains: add complex recovery password test --- src/tests/test_domains.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 03141b4fe..b968777d2 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -83,6 +83,32 @@ def test_domain_add_and_remove_dyndns(): domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + +def test_domain_dyndns_recovery(): + # time.sleep(35) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + # add domain without recovery password + domain_add(TEST_DYNDNS_DOMAIN) + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + # set the recovery password with config panel + domain_config_set(TEST_DYNDNS_DOMAIN, "dns.registrar.recovery_password", TEST_DYNDNS_PASSWORD) + # remove domain without unsubscribing + domain_remove(TEST_DYNDNS_DOMAIN, ignore_dyndns=True) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + # readding domain with bad password should fail + with pytest.raises(YunohostValidationError): + domain_add( + TEST_DYNDNS_DOMAIN, dyndns_recovery_password="wrong" + TEST_DYNDNS_PASSWORD + ) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + # readding domain with password should work + domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + # remove the dyndns domain + domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + + def test_domain_add_existing_domain(): with pytest.raises(MoulinetteError): assert TEST_DOMAINS[1] in domain_list()["domains"] From 4a7b2b2cbfdb57f50dc2376272942050a922462e Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 28 Sep 2023 15:54:57 +0200 Subject: [PATCH 0284/1116] domains: unique i18n key for dyndns too many requests --- locales/en.json | 3 +-- src/dyndns.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index dddad3ef9..e7deb9e95 100644 --- a/locales/en.json +++ b/locales/en.json @@ -397,7 +397,6 @@ "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` 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_availability_too_many_requests": "YunoHost's dyndns service received too many requests from you, wait 1 minute or so before trying again.", "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}.", "dyndns_ip_update_failed": "Could not update IP address to DynDNS", @@ -408,11 +407,11 @@ "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", "dyndns_subscribed": "DynDNS domain subscribed", "dyndns_subscribe_failed": "Could not subscribe DynDNS domain: {error}", + "dyndns_too_many_requests": "YunoHost's dyndns service received too many requests from you, wait 1 hour or so before trying again.", "dyndns_unsubscribe_failed": "Could not unsubscribe DynDNS domain: {error}", "dyndns_unsubscribed": "DynDNS domain unsubscribed", "dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials", "dyndns_unsubscribe_already_unsubscribed": "Domain is already unsubscribed", - "dyndns_unsubscribe_too_many_requests": "YunoHost's dyndns service received too many requests from you, wait 1 hour or so before trying again.", "dyndns_set_recovery_password_denied": "Failed to set recovery password: invalid key", "dyndns_set_recovery_password_unknown_domain": "Failed to set recovery password: domain not registered", "dyndns_set_recovery_password_invalid_password": "Failed to set recovery password: password is not strong enough", diff --git a/src/dyndns.py b/src/dyndns.py index 70f44672d..387f33930 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -79,7 +79,7 @@ def _dyndns_available(domain): elif r.status_code == 409: return False elif r.status_code == 429: - raise YunohostValidationError("dyndns_availability_too_many_requests") + raise YunohostValidationError("dyndns_too_many_requests") else: raise YunohostError( "dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER @@ -276,7 +276,7 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): elif r.status_code == 409: raise YunohostValidationError("dyndns_unsubscribe_already_unsubscribed") elif r.status_code == 429: - raise YunohostValidationError("dyndns_unsubscribe_too_many_requests") + raise YunohostValidationError("dyndns_too_many_requests") else: raise YunohostError( "dyndns_unsubscribe_failed", From c019f7f24ab80a9ab63d8f4bb8656dad24186f75 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 28 Sep 2023 16:48:31 +0200 Subject: [PATCH 0285/1116] test:domains: remove sleep --- src/tests/test_domains.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index b968777d2..bc2c31465 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -76,7 +76,7 @@ def test_domain_add(): def test_domain_add_and_remove_dyndns(): - time.sleep(35) # Dynette blocks requests that happen too frequently + # Devs: if you get `too_many_request` errors, ask the team to add your IP to the rate limit excempt assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] @@ -85,7 +85,7 @@ def test_domain_add_and_remove_dyndns(): def test_domain_dyndns_recovery(): - # time.sleep(35) + # Devs: if you get `too_many_request` errors, ask the team to add your IP to the rate limit excempt assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] # add domain without recovery password domain_add(TEST_DYNDNS_DOMAIN) From 253a042314ea5b4f24556c7019e054fdc59a190d Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 28 Sep 2023 17:23:51 +0200 Subject: [PATCH 0286/1116] test:domains: dyndns_recovery mock as api call to avoid cli prompts --- src/tests/test_domains.py | 45 ++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index bc2c31465..cc33a87d5 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -1,8 +1,10 @@ import pytest import os -import time import random +from mock import patch + +from moulinette import Moulinette from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError, YunohostValidationError @@ -87,25 +89,28 @@ def test_domain_add_and_remove_dyndns(): def test_domain_dyndns_recovery(): # Devs: if you get `too_many_request` errors, ask the team to add your IP to the rate limit excempt assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] - # add domain without recovery password - domain_add(TEST_DYNDNS_DOMAIN) - assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] - # set the recovery password with config panel - domain_config_set(TEST_DYNDNS_DOMAIN, "dns.registrar.recovery_password", TEST_DYNDNS_PASSWORD) - # remove domain without unsubscribing - domain_remove(TEST_DYNDNS_DOMAIN, ignore_dyndns=True) - assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] - # readding domain with bad password should fail - with pytest.raises(YunohostValidationError): - domain_add( - TEST_DYNDNS_DOMAIN, dyndns_recovery_password="wrong" + TEST_DYNDNS_PASSWORD - ) - assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] - # readding domain with password should work - domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) - assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] - # remove the dyndns domain - domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) + # mocked as API call to avoid CLI prompts + with patch.object(Moulinette.interface, "type", "api"): + # add domain without recovery password + domain_add(TEST_DYNDNS_DOMAIN) + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + # set the recovery password with config panel + domain_config_set(TEST_DYNDNS_DOMAIN, "dns.registrar.recovery_password", TEST_DYNDNS_PASSWORD) + # remove domain without unsubscribing + domain_remove(TEST_DYNDNS_DOMAIN, ignore_dyndns=True) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + # readding domain with bad password should fail + with pytest.raises(YunohostValidationError): + domain_add( + TEST_DYNDNS_DOMAIN, dyndns_recovery_password="wrong" + TEST_DYNDNS_PASSWORD + ) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + # readding domain with password should work + domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + # remove the dyndns domain + domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] From bb097fedca2db668cbc8cc490887048478c86362 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Fri, 29 Sep 2023 12:05:48 +0200 Subject: [PATCH 0287/1116] Add home.arpa as special TLD (#1718) * Add home.arpa as special TLD * Update dns.py --- src/utils/dns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/dns.py b/src/utils/dns.py index b3ca4b564..569dad466 100644 --- a/src/utils/dns.py +++ b/src/utils/dns.py @@ -21,7 +21,7 @@ from typing import List from moulinette.utils.filesystem import read_file -SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"] +SPECIAL_USE_TLDS = ["home.arpa", "local", "localhost", "onion", "test"] YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] From 814696e9c10425c115d28be804602f06ec862338 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 29 Sep 2023 14:34:01 +0200 Subject: [PATCH 0288/1116] portal: redirect to $host/yunohost/admin by default (cf recent commit in ssowat) --- src/domain.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/domain.py b/src/domain.py index 273fa9e6d..a5a68ca0f 100644 --- a/src/domain.py +++ b/src/domain.py @@ -117,6 +117,10 @@ def _get_domain_portal_dict(): out[domain] = f'{parent or domain}/yunohost/sso' + # By default, redirect to $host/yunohost/admin for domains not listed in the dict + # maybe in the future, we can allow to tweak this + out["default"] = "/yunohost/admin" + return dict(out) From a457f8dbcb754997cdcfb9205634839ee4a90394 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 29 Sep 2023 16:35:24 +0200 Subject: [PATCH 0289/1116] app: add "support_purge" to app info --- src/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app.py b/src/app.py index df5eecae2..78c61dfcd 100644 --- a/src/app.py +++ b/src/app.py @@ -237,6 +237,10 @@ def app_info(app, full=False, upgradable=False): ret["supports_config_panel"] = os.path.exists( os.path.join(setting_path, "config_panel.toml") ) + ret["supports_purge"] = ( + local_manifest["packaging_format"] >= 2 + and local_manifest["resources"].get("data_dir") is not None + ) ret["permissions"] = permissions ret["label"] = permissions.get(app + ".main", {}).get("label") From 385c131d0cf472f6e94d9bef9fa0d068eaa4c697 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 29 Sep 2023 16:53:18 +0200 Subject: [PATCH 0290/1116] regenconf: fix dummy warning --- hooks/conf_regen/01-yunohost | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index edf64012e..adccfa17c 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -217,7 +217,7 @@ do_post_regen() { find /etc/systemd/system/*.service -type f | xargs -r chown root:root find /etc/systemd/system/*.service -type f | xargs -r chmod 0644 - if ls -l /etc/php/*/fpm/pool.d/*.conf + if ls -l /etc/php/*/fpm/pool.d/*.conf 2>/dev/null then chown root:root /etc/php/*/fpm/pool.d/*.conf chmod 644 /etc/php/*/fpm/pool.d/*.conf From 58a539bf7d4edd87a9921d4381495a5bf441bf0c Mon Sep 17 00:00:00 2001 From: orhtej2 <2871798+orhtej2@users.noreply.github.com> Date: Mon, 2 Oct 2023 23:19:01 +0200 Subject: [PATCH 0291/1116] Allow `phpX.Y` as sole dependency that'll trigger usage of `$phpversion=X.Y` --- helpers/apt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/apt b/helpers/apt index a2f2d3de8..8ff9a3cd1 100644 --- a/helpers/apt +++ b/helpers/apt @@ -250,7 +250,7 @@ ynh_install_app_dependencies() { # Check for specific php dependencies which requires sury # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni" # The (?<=php) syntax corresponds to lookbehind ;) - local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>)' | sort -u) + local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>|)' | sort -u) if [[ -n "$specific_php_version" ]] then From a83fbc1bc96a2e9d0763cf916a3d728d5e384c5a Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Sat, 2 Sep 2023 20:37:20 +0000 Subject: [PATCH 0292/1116] Translated using Weblate (Basque) Currently translated at 95.0% (741 of 780 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/eu/ --- locales/eu.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/eu.json b/locales/eu.json index 0267b3366..85e7c36fc 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -766,5 +766,13 @@ "group_mailalias_add": "'{mail}' ePosta aliasa jarri zaio '{group}' taldeari", "group_mailalias_remove": "'{mail}' ePosta aliasa kendu zaio '{group}' taldeari", "group_user_remove": "'{user}' erabiltzailea '{group}' taldetik kenduko da", - "group_user_add": "'{user}' erabiltzailea '{group}' taldera gehituko da" + "group_user_add": "'{user}' erabiltzailea '{group}' taldera gehituko da", + "ask_dyndns_recovery_password_explain": "Aukeratu DynDNS domeinua berreskuratzeko pasahitza, etorkizunean berrezarri beharko bazenu.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Sartu DynDNS domeinurako berreskuraketa pasahitza.", + "dyndns_no_recovery_password": "Ez da berreskurapen pasahitzik zehaztu! Domeinuaren gaineko kontrola galduz gero, YunoHost taldeko administrariarekin jarri beharko zara harremanetan!", + "ask_dyndns_recovery_password": "DynDNS berreskuratzeko pasahitza", + "dyndns_subscribed": "DynDNS domeinua harpidetu da", + "dyndns_subscribe_failed": "Ezin izan da DynDNS domeinua harpidetu: {error}", + "dyndns_unsubscribe_failed": "Ezin izan da DynDNS domeinuaren harpidetza utzi: {error}", + "dyndns_unsubscribed": "DynDNS domeinuaren harpidetza utzi da" } From 179b60062191d333cf32225f309df9d45123569a Mon Sep 17 00:00:00 2001 From: ppr Date: Sat, 30 Sep 2023 11:07:30 +0000 Subject: [PATCH 0293/1116] Translated using Weblate (French) Currently translated at 98.3% (769 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 2cc0f612c..a6e41ba8e 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -781,6 +781,8 @@ "dyndns_set_recovery_password_unknown_domain": "Échec de la définition du mot de passe de récupération : le domaine n'est pas enregistré", "dyndns_set_recovery_password_invalid_password": "Échec de la définition du mot de passe de récupération : le mot de passe n'est pas assez fort/solide", "dyndns_set_recovery_password_failed": "Échec de la définition du mot de passe de récupération : {erreur}", - "dyndns_set_recovery_password_success": "Mot de passe de récupération défini/configuré !", - "log_dyndns_unsubscribe": "Se désabonner d'un sous-domaine YunoHost '{}'" + "dyndns_set_recovery_password_success": "Mot de passe de récupération activé/défini/configuré !", + "log_dyndns_unsubscribe": "Se désabonner d'un sous-domaine YunoHost '{}'", + "dyndns_too_many_requests": "Le service dyndns de YunoHost a reçu trop de requêtes/demandes de votre part, attendez environ 1 heure avant de réessayer.", + "ask_dyndns_recovery_password_explain_unavailable": "Ce domaine DynDNS est déjà enregistré. Si vous êtes la personne qui a enregistré ce domaine lors de sa création, vous pouvez entrer le mot de passe de récupération pour récupérer ce domaine." } From 6385b402f7665e4cc3a7308ace34a82cf57dd129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Tue, 3 Oct 2023 15:48:49 +0200 Subject: [PATCH 0294/1116] Add ynh_exec_stderr_on_error that only prints stderr when command fails --- helpers/logging | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/helpers/logging b/helpers/logging index ab5d564aa..8989140f6 100644 --- a/helpers/logging +++ b/helpers/logging @@ -186,6 +186,26 @@ ynh_exec_fully_quiet() { fi } +# Execute a command and redirect stderr in /dev/null. Print stderr on error. +# +# usage: ynh_exec_stderr_on_error your command and args +# | arg: command - command to execute +# +# Note that you should NOT quote the command but only prefix it with ynh_exec_stderr_on_error +# +# Requires YunoHost version 11.2 or higher. +ynh_exec_stderr_on_error() { + logfile="$(mktemp)" + rc=0 + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + "$@" 2> "$logfile" || rc="$?" + if (( rc != 0 )); then + ynh_exec_warn cat "$logfile" + ynh_secure_remove "$logfile" + return "$rc" + fi +} + # Remove any logs for all the following commands. # # usage: ynh_print_OFF @@ -248,7 +268,7 @@ ynh_script_progression() { # Re-disable xtrace, ynh_handle_getopts_args set it back set +o xtrace # set +x weight=${weight:-1} - + # Always activate time when running inside CI tests if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then time=${time:-1} From 2fc2acea51d0fa1d743634506b9e6a1bb09ebecb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 3 Oct 2023 19:58:22 +0200 Subject: [PATCH 0295/1116] portalapi: misc fixes related to logging, edgecases --- src/portal.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/portal.py b/src/portal.py index 5b3af4f49..e89d176fd 100644 --- a/src/portal.py +++ b/src/portal.py @@ -20,10 +20,10 @@ """ from pathlib import Path from typing import Any, Union - +import logging import ldap + from moulinette.utils.filesystem import read_json, read_yaml -from moulinette.utils.log import getActionLogger from yunohost.authenticators.ldap_ynhuser import URI, USERDN, Authenticator as Auth from yunohost.user import _hash_user_password from yunohost.utils.error import YunohostError, YunohostValidationError @@ -33,7 +33,7 @@ from yunohost.utils.password import ( assert_password_is_strong_enough, ) -logger = getActionLogger("portal") +logger = logging.getLogger("portal") ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] @@ -77,9 +77,10 @@ def _get_portal_settings(domain: Union[str, None] = None): def portal_public(): + portal_settings = _get_portal_settings() portal_settings["apps"] = {} - portal_settings["public"] = portal_settings.pop("default_app") == "_yunohost_portal_with_public_apps" + portal_settings["public"] = portal_settings.pop("default_app", None) == "_yunohost_portal_with_public_apps" if portal_settings["public"]: ssowat_conf = read_json("/etc/ssowat/conf.json") @@ -219,6 +220,9 @@ def portal_update( ] if newpassword: + + # FIXME: this ldap stuff should be handled in utils/ldap.py imho ? + # Check that current password is valid try: con = ldap.ldapobject.ReconnectLDAPObject(URI, retry_max=0) From 0548af0c25fe6e008c36e29a4e8054f78b8c2dad Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 5 Oct 2023 18:07:58 +0200 Subject: [PATCH 0296/1116] ci: add git status to debug what commit exactly is used during builds ... --- .gitlab/ci/build.gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index 610580dac..f1c137ad3 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -13,6 +13,8 @@ .build_script: &build_script - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" install devscripts --no-install-recommends - cd $YNH_BUILD_DIR/$PACKAGE + - git status || true + - git log -n 1 || true - VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null) - VERSION_NIGHTLY="${VERSION}+$(date +%Y%m%d%H%M)" - dch --package "${PACKAGE}" --force-bad-version -v "${VERSION_NIGHTLY}" -D "unstable" --force-distribution "Daily build." From fae3b676ea760f43999c7f13b3e201af2baff31d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 6 Oct 2023 17:06:53 +0200 Subject: [PATCH 0297/1116] Getting crazy about the ssowat/nginx stupid issue ... --- .gitlab/ci/install.gitlab-ci.yml | 4 ++++ .gitlab/ci/test.gitlab-ci.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.gitlab/ci/install.gitlab-ci.yml b/.gitlab/ci/install.gitlab-ci.yml index 65409c6eb..0f5571a57 100644 --- a/.gitlab/ci/install.gitlab-ci.yml +++ b/.gitlab/ci/install.gitlab-ci.yml @@ -17,7 +17,9 @@ upgrade: image: "after-install" script: - apt-get update -o Acquire::Retries=3 + - systemctl restart nginx || journalctl -u nginx -n 50 --no-pager --no-hostname - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb + - systemctl restart nginx || journalctl -u nginx -n 50 --no-pager --no-hostname install-postinstall: @@ -25,5 +27,7 @@ install-postinstall: image: "before-install" script: - apt-get update -o Acquire::Retries=3 + - systemctl restart nginx || journalctl -u nginx -n 50 --no-pager --no-hostname - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb + - systemctl restart nginx || journalctl -u nginx -n 50 --no-pager --no-hostname - 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 2ec84c114..f0a61c3c9 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -1,6 +1,8 @@ .install_debs: &install_debs - apt-get update -o Acquire::Retries=3 + - systemctl restart nginx || journalctl -u nginx -n 50 --no-pager --no-hostname - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb + - systemctl restart nginx || journalctl -u nginx -n 50 --no-pager --no-hostname .test-stage: stage: test From 80362269357aea416362bef57c2852985f45fad1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 7 Oct 2023 17:12:26 +0200 Subject: [PATCH 0298/1116] Typo --- src/app.py | 4 ++-- src/utils/legacy.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app.py b/src/app.py index e8317099d..eca528682 100644 --- a/src/app.py +++ b/src/app.py @@ -1657,10 +1657,10 @@ def app_ssowatconf(): redirected_regex.update(app_settings.get("redirected_regex", {})) from .utils.legacy import ( - translate_legacy_default_app_in_ssowant_conf_json_persistent, + translate_legacy_default_app_in_ssowat_conf_json_persistent, ) - translate_legacy_default_app_in_ssowant_conf_json_persistent() + translate_legacy_default_app_in_ssowat_conf_json_persistent() for domain in domains: default_app = domain_config_get(domain, "feature.app.default_app") diff --git a/src/utils/legacy.py b/src/utils/legacy.py index dfe8da250..af9c08b98 100644 --- a/src/utils/legacy.py +++ b/src/utils/legacy.py @@ -114,7 +114,7 @@ def legacy_permission_label(app, permission_type): ) -def translate_legacy_default_app_in_ssowant_conf_json_persistent(): +def translate_legacy_default_app_in_ssowat_conf_json_persistent(): from yunohost.app import app_list from yunohost.domain import domain_config_set From a0ce7c2d28ebba2df27bba2e7e5064c99533f310 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 7 Oct 2023 17:40:06 +0200 Subject: [PATCH 0299/1116] ssowatconf: drop unused redirected_regex mechanism + we don't need the label+show_tile info in ssowat conf anymore --- src/app.py | 28 ++------------------------ src/utils/legacy.py | 49 --------------------------------------------- 2 files changed, 2 insertions(+), 75 deletions(-) diff --git a/src/app.py b/src/app.py index eca528682..adcea56e4 100644 --- a/src/app.py +++ b/src/app.py @@ -1619,8 +1619,6 @@ def app_ssowatconf(): permissions = { "core_skipped": { "users": [], - "label": "Core permissions - skipped", - "show_tile": False, "auth_header": False, "public": True, "uris": [domain + "/yunohost/admin" for domain in domains] @@ -1635,11 +1633,6 @@ def app_ssowatconf(): } } - # FIXME : what's the reason we do this only for the maindomain ? x_X - redirected_regex = { - main_domain + r"/yunohost[\/]?$": "https://" + main_domain + "/yunohost/sso/" - } - redirected_urls = {} apps_using_remote_user_var_in_nginx = ( check_output( @@ -1649,19 +1642,8 @@ def app_ssowatconf(): .split("\n") ) - for app in _installed_apps(): - app_settings = read_yaml(APPS_SETTING_PATH + app + "/settings.yml") or {} - - # Redirected - redirected_urls.update(app_settings.get("redirected_urls", {})) - redirected_regex.update(app_settings.get("redirected_regex", {})) - - from .utils.legacy import ( - translate_legacy_default_app_in_ssowat_conf_json_persistent, - ) - - translate_legacy_default_app_in_ssowat_conf_json_persistent() - + # FIXME : this could be handled by nginx's regen conf to further simplify ssowat's code ... + redirected_urls = {} for domain in domains: default_app = domain_config_get(domain, "feature.app.default_app") if default_app != "_none" and _is_installed(default_app): @@ -1691,10 +1673,6 @@ def app_ssowatconf(): "use_remote_user_var_in_nginx_conf": app_id in apps_using_remote_user_var_in_nginx, "users": perm_info["corresponding_users"], - "label": perm_info["label"], - "show_tile": perm_info["show_tile"] - and perm_info["url"] - and (not perm_info["url"].startswith("re:")), "auth_header": perm_info["auth_header"], "public": "visitors" in perm_info["allowed"], "uris": uris, @@ -1703,9 +1681,7 @@ def app_ssowatconf(): conf_dict = { "cookie_secret_file": "/etc/yunohost/.ssowat_cookie_secret", "cookie_name": "yunohost.portal", - "theme": settings_get("misc.portal.portal_theme"), "redirected_urls": redirected_urls, - "redirected_regex": redirected_regex, "domain_portal_urls": _get_domain_portal_dict(), "permissions": permissions, } diff --git a/src/utils/legacy.py b/src/utils/legacy.py index af9c08b98..4f11d28d7 100644 --- a/src/utils/legacy.py +++ b/src/utils/legacy.py @@ -114,55 +114,6 @@ def legacy_permission_label(app, permission_type): ) -def translate_legacy_default_app_in_ssowat_conf_json_persistent(): - from yunohost.app import app_list - from yunohost.domain import domain_config_set - - persistent_file_name = "/etc/ssowat/conf.json.persistent" - if not os.path.exists(persistent_file_name): - return - - # Ugly hack because for some reason so many people have tabs in their conf.json.persistent ... - os.system(r"sed -i 's/\t/ /g' /etc/ssowat/conf.json.persistent") - - # Ugly hack to try not to misarably fail migration - persistent = read_yaml(persistent_file_name) - - if "redirected_urls" not in persistent: - return - - redirected_urls = persistent["redirected_urls"] - - if not any( - from_url.count("/") == 1 and from_url.endswith("/") - for from_url in redirected_urls - ): - return - - apps = app_list()["apps"] - - if not any(app.get("domain_path") in redirected_urls.values() for app in apps): - return - - for from_url, dest_url in redirected_urls.copy().items(): - # Not a root domain, skip - if from_url.count("/") != 1 or not from_url.endswith("/"): - continue - for app in apps: - if app.get("domain_path") != dest_url: - continue - domain_config_set(from_url.strip("/"), "feature.app.default_app", app["id"]) - del redirected_urls[from_url] - - persistent["redirected_urls"] = redirected_urls - - write_to_json(persistent_file_name, persistent, sort_keys=True, indent=4) - - logger.warning( - "YunoHost automatically translated some legacy redirections in /etc/ssowat/conf.json.persistent to match the new default application using domain configuration" - ) - - LEGACY_PHP_VERSION_REPLACEMENTS = [ ("/etc/php5", "/etc/php/8.2"), ("/etc/php/7.0", "/etc/php/8.2"), From f4d8ada3688917a5915486a8ce70d14d4cd74c37 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin <4533074+alexAubin@users.noreply.github.com> Date: Mon, 9 Oct 2023 23:12:44 +0200 Subject: [PATCH 0300/1116] Update locales/fr.json --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index a6e41ba8e..2e4d1602b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -781,7 +781,7 @@ "dyndns_set_recovery_password_unknown_domain": "Échec de la définition du mot de passe de récupération : le domaine n'est pas enregistré", "dyndns_set_recovery_password_invalid_password": "Échec de la définition du mot de passe de récupération : le mot de passe n'est pas assez fort/solide", "dyndns_set_recovery_password_failed": "Échec de la définition du mot de passe de récupération : {erreur}", - "dyndns_set_recovery_password_success": "Mot de passe de récupération activé/défini/configuré !", + "dyndns_set_recovery_password_success": "Mot de passe de récupération changé !", "log_dyndns_unsubscribe": "Se désabonner d'un sous-domaine YunoHost '{}'", "dyndns_too_many_requests": "Le service dyndns de YunoHost a reçu trop de requêtes/demandes de votre part, attendez environ 1 heure avant de réessayer.", "ask_dyndns_recovery_password_explain_unavailable": "Ce domaine DynDNS est déjà enregistré. Si vous êtes la personne qui a enregistré ce domaine lors de sa création, vous pouvez entrer le mot de passe de récupération pour récupérer ce domaine." From e8700bfe7b621a236dae3d68730cb4189ddda78f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 9 Oct 2023 23:15:54 +0200 Subject: [PATCH 0301/1116] debian: fix conflict with openssl that is too harsh, openssl version on bullseye is now 1.1.1w, bookworm has 3.x --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 0258eaac7..2fa1a1220 100644 --- a/debian/control +++ b/debian/control @@ -43,7 +43,7 @@ Conflicts: iptables-persistent , apache2 , bind9 , nginx-extras (>= 1.19) - , openssl (>= 1.1.1o-0) + , openssl (>= 3.0) , slapd (>= 2.4.58) , dovecot-core (>= 1:2.3.14) , redis-server (>= 5:6.1) From 6f1a00922a8fa866823ecd87dc6e6da2ab8e5005 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 10 Oct 2023 01:07:19 +0200 Subject: [PATCH 0302/1116] Update changelog for 11.2.5 --- debian/changelog | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/debian/changelog b/debian/changelog index 610109fcd..0effd40ba 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,21 @@ +yunohost (11.2.5) stable; urgency=low + + - debian: fix conflict with openssl that is too harsh, openssl version on bullseye is now 1.1.1w, bookworm has 3.x (e8700bfe7) + - dyndns: tweak dyndns subscribe/unsubscribe for dyndns recovery password integration in webadmin ([#1715](https://github.com/yunohost/yunohost/pull/1715)) + - helpers: ynh_setup_source: check and re-download a prefetched file that doesn't match the checksum (3dfab89c1) + - helpers: ynh_setup_source: fix misleading example ([#1714](https://github.com/yunohost/yunohost/pull/1714)) + - helpers: php/apt: allow `phpX.Y` as sole dependency for `$phpversion=X.Y` ([#1722](https://github.com/yunohost/yunohost/pull/1722)) + - apps: fix typo in log statement ([#1709](https://github.com/yunohost/yunohost/pull/1709)) + - apps: allow system users to send mails from IPv6 localhost. ([#1710](https://github.com/yunohost/yunohost/pull/1710)) + - apps: add "support_purge" to app info for webadmin integration ([#1719](https://github.com/yunohost/yunohost/pull/1719)) + - diagnosis: be more flexible regarding accepted values for DMARC DNS records ([#1713](https://github.com/yunohost/yunohost/pull/1713)) + - dns: add home.arpa as special TLD (#1718) (bb097fedc) + - i18n: Translations updated for Basque, French + + Thanks to all contributors <3 ! (axolotle, Florian, Kayou, orhtej2, Pierre de La Morinerie, ppr, stanislas, tituspijean, xabirequejo) + + -- Alexandre Aubin Mon, 09 Oct 2023 23:16:13 +0200 + yunohost (11.2.4) stable; urgency=low - doc: Improve --help for 'yunohost app install' ([#1702](https://github.com/yunohost/yunohost/pull/1702)) From f003565074f2349a1341c0a672c94888992635e0 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Tue, 10 Oct 2023 02:31:47 +0000 Subject: [PATCH 0303/1116] [CI] Format code with Black --- src/app.py | 6 ++++-- src/dns.py | 6 +++++- src/tests/test_domains.py | 7 +++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/app.py b/src/app.py index 78c61dfcd..12b3c4233 100644 --- a/src/app.py +++ b/src/app.py @@ -2255,11 +2255,11 @@ def _parse_app_doc_and_notifications(path): def _hydrate_app_template(template, data): - # Apply jinja for stuff like {% if .. %} blocks, # but only if there's indeed an if block (to try to reduce overhead or idk) if "{%" in template: from jinja2 import Template + template = Template(template).render(**data) stuff_to_replace = set(re.findall(r"__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__", template)) @@ -3187,7 +3187,9 @@ def _filter_and_hydrate_notifications(notifications, current_version=None, data= } # Filter out empty notifications (notifications may be empty because of if blocks) - return {name:content for name, content in out.items() if content and content.strip()} + return { + name: content for name, content in out.items() if content and content.strip() + } def _display_notifications(notifications, force=False): diff --git a/src/dns.py b/src/dns.py index fbb461463..1ec88a5c0 100644 --- a/src/dns.py +++ b/src/dns.py @@ -546,7 +546,11 @@ def _get_registrar_config_section(domain): } ) registrar_infos["recovery_password"] = OrderedDict( - {"type": "password", "ask": m18n.n("ask_dyndns_recovery_password"), "default": ""} + { + "type": "password", + "ask": m18n.n("ask_dyndns_recovery_password"), + "default": "", + } ) return OrderedDict(registrar_infos) elif is_special_use_tld(dns_zone): diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index cc33a87d5..13a9f63b8 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -95,14 +95,17 @@ def test_domain_dyndns_recovery(): domain_add(TEST_DYNDNS_DOMAIN) assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] # set the recovery password with config panel - domain_config_set(TEST_DYNDNS_DOMAIN, "dns.registrar.recovery_password", TEST_DYNDNS_PASSWORD) + domain_config_set( + TEST_DYNDNS_DOMAIN, "dns.registrar.recovery_password", TEST_DYNDNS_PASSWORD + ) # remove domain without unsubscribing domain_remove(TEST_DYNDNS_DOMAIN, ignore_dyndns=True) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] # readding domain with bad password should fail with pytest.raises(YunohostValidationError): domain_add( - TEST_DYNDNS_DOMAIN, dyndns_recovery_password="wrong" + TEST_DYNDNS_PASSWORD + TEST_DYNDNS_DOMAIN, + dyndns_recovery_password="wrong" + TEST_DYNDNS_PASSWORD, ) assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] # readding domain with password should work From 6a8693fa44dc4617015421f1364788b2001d6e3b Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Sun, 15 Oct 2023 05:10:23 +0000 Subject: [PATCH 0304/1116] Upgrade n to v --- helpers/vendor/n/n | 61 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/helpers/vendor/n/n b/helpers/vendor/n/n index 2a877c45b..eb276a2f5 100755 --- a/helpers/vendor/n/n +++ b/helpers/vendor/n/n @@ -61,7 +61,7 @@ function n_grep() { # Setup and state # -VERSION="v9.1.0" +VERSION="v9.2.0" N_PREFIX="${N_PREFIX-/usr/local}" N_PREFIX=${N_PREFIX%/} @@ -135,6 +135,7 @@ g_target_node= DOWNLOAD=false # set to opt-out of activate (install), and opt-in to download (run, exec) ARCH= SHOW_VERBOSE_LOG="true" +OFFLINE=false # ANSI escape codes # https://en.wikipedia.org/wiki/ANSI_escape_code @@ -393,6 +394,7 @@ Options: -q, --quiet Disable curl output. Disable log messages processing "auto" and "engine" labels. -d, --download Download if necessary, and don't make active -a, --arch Override system architecture + --offline Resolve target version against cached downloads instead of internet lookup --all ls-remote displays all matches instead of last 20 --insecure Turn off certificate checking for https requests (may be needed from behind a proxy server) --use-xz/--no-use-xz Override automatic detection of xz support and enable/disable use of xz compressed node downloads. @@ -784,6 +786,9 @@ install() { exit fi fi + if [[ "$OFFLINE" == "true" ]]; then + abort "version unavailable offline" + fi log installing "${g_mirror_folder_name}-v$version" @@ -1103,6 +1108,7 @@ function get_package_engine_version() { verbose_log "target" "${version}" else command -v npx &> /dev/null || abort "an active version of npx is required to use complex 'engine' ranges from package.json" + [[ "$OFFLINE" != "true" ]] || abort "offline: an internet connection is required for looking up complex 'engine' ranges from package.json" verbose_log "resolving" "${range}" local version_per_line="$(n lsr --all)" local versions_one_line=$(echo "${version_per_line}" | tr '\n' ' ') @@ -1199,6 +1205,8 @@ function get_latest_resolved_version() { # Just numbers, already resolved, no need to lookup first. simple_version="${simple_version#v}" g_target_node="${simple_version}" + elif [[ "$OFFLINE" == "true" ]]; then + g_target_node=$(display_local_versions "${version}") else # Complicated recognising exact version, KISS and lookup. g_target_node=$(N_MAX_REMOTE_MATCHES=1 display_remote_versions "$version") @@ -1232,6 +1240,56 @@ function display_match_limit(){ fi } +# +# Synopsis: display_local_versions version +# + +function display_local_versions() { + local version="$1" + local match='.' + verbose_log "offline" "matching cached versions" + + # Transform some labels before processing further. + if is_node_support_version "${version}"; then + version="$(display_latest_node_support_alias "${version}")" + match_count=1 + elif [[ "${version}" = "auto" ]]; then + # suppress stdout logging so lsr layout same as usual for scripting + get_auto_version || return 2 + version="${g_target_node}" + elif [[ "${version}" = "engine" ]]; then + # suppress stdout logging so lsr layout same as usual for scripting + get_engine_version || return 2 + version="${g_target_node}" + fi + + if [[ "${version}" = "latest" || "${version}" = "current" ]]; then + match='^node/.' + elif is_exact_numeric_version "${version}"; then + # Quote any dots in version so they are literal for expression + match="^node/${version//\./\.}" + elif is_numeric_version "${version}"; then + version="${version#v}" + # Quote any dots in version so they are literal for expression + match="${version//\./\.}" + # Avoid 1.2 matching 1.23 + match="^node/${match}[^0-9]" + # elif is_lts_codename "${version}"; then + # see if demand + elif is_download_folder "${version}"; then + match="^${version}/" + # elif is_download_version "${version}"; then + # see if demand + else + abort "invalid version '$1' for offline matching" + fi + + display_versions_paths \ + | n_grep -E "${match}" \ + | tail -n 1 \ + | sed 's|node/||' +} + # # Synopsis: display_remote_versions version # @@ -1577,6 +1635,7 @@ while [[ $# -ne 0 ]]; do -h|--help|help) display_help; exit ;; -q|--quiet) set_quiet ;; -d|--download) DOWNLOAD="true" ;; + --offline) OFFLINE="true" ;; --insecure) set_insecure ;; -p|--preserve) N_PRESERVE_NPM="true" N_PRESERVE_COREPACK="true" ;; --no-preserve) N_PRESERVE_NPM="" N_PRESERVE_COREPACK="" ;; From 075095b4d7d72c6abb8870a1914a5da436401930 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 15 Oct 2023 14:59:45 +0200 Subject: [PATCH 0305/1116] Display n version in auto-update PRs --- .github/workflows/n_updater.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/n_updater.yml b/.github/workflows/n_updater.yml index ce3e9c925..a1a835a78 100644 --- a/.github/workflows/n_updater.yml +++ b/.github/workflows/n_updater.yml @@ -20,9 +20,11 @@ jobs: # Setting up Git user git config --global user.name 'yunohost-bot' git config --global user.email 'yunohost-bot@users.noreply.github.com' - # Run the updater script + # Download n wget https://raw.githubusercontent.com/tj/n/master/bin/n --output-document=helpers/vendor/n/n + # Proceed only if there is a change [[ -z "$(git diff helpers/vendor/n/n)" ]] || echo "PROCEED=true" >> $GITHUB_ENV + echo "VERSION=$(sed -n 's/^VERSION=\"\(.*\)\"/\1/p' < n)" >> $GITHUB_ENV - name: Commit changes id: commit if: ${{ env.PROCEED == 'true' }} @@ -34,14 +36,14 @@ jobs: uses: peter-evans/create-pull-request@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - commit-message: Update n to version ${{ env.VERSION }} + commit-message: Update n to ${{ env.VERSION }} committer: 'yunohost-bot ' author: 'yunohost-bot ' signoff: false base: dev branch: ci-auto-update-n-v${{ env.VERSION }} delete-branch: true - title: 'Upgrade n to version ${{ env.VERSION }}' + title: 'Upgrade n to ${{ env.VERSION }}' body: | - Upgrade `n` to v${{ env.VERSION }} + Upgrade `n` to ${{ env.VERSION }} draft: false From 96e99459e6271c49de9b6f07ec8bbf4307aa24e4 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 15 Oct 2023 16:31:14 +0200 Subject: [PATCH 0306/1116] Support packages_from_raw_bash in extra packages --- src/utils/resources.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 69b260334..f206b9b96 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1086,7 +1086,6 @@ class AptDependenciesAppResource(AppResource): packages: List = [] packages_from_raw_bash: str = "" extras: Dict[str, Dict[str, Union[str, List]]] = {} - def __init__(self, properties: Dict[str, Any], *args, **kwargs): super().__init__(properties, *args, **kwargs) @@ -1106,13 +1105,25 @@ class AptDependenciesAppResource(AppResource): if isinstance(values.get("packages"), str): values["packages"] = [value.strip() for value in values["packages"].split(",")] # type: ignore + if isinstance(values.get("packages_from_raw_bash"), str): + out, err = self.check_output_bash_snippet(values.get("packages_from_raw_bash")) + if err: + logger.error( + "Error while running apt resource packages_from_raw_bash snippet for '" + str(key) + "' extras:" + ) + logger.error(err) + values["packages"] = [value.strip() for value in out.split("\n")] + if ( not isinstance(values.get("repo"), str) or not isinstance(values.get("key"), str) - or not isinstance(values.get("packages"), list) + or ( + not isinstance(values.get("packages"), list) + and not isinstance(values.get("packages_from_raw_bash"), str) + ) ): raise YunohostError( - "In apt resource in the manifest: 'extras' repo should have the keys 'repo', 'key' defined as strings and 'packages' defined as list", + "In apt resource in the manifest: 'extras' repo should have the keys 'repo', 'key' defined as strings, 'packages' defined as list or 'packages_from_raw_bash' defined as string", raw_msg=True, ) From a69b80972eaec9673dc848b7ef09a043e0730272 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 15 Oct 2023 16:46:24 +0200 Subject: [PATCH 0307/1116] Improve support for packages_from_raw_bash Co-authored-by: Alexandre Aubin <4533074+alexAubin@users.noreply.github.com> --- src/utils/resources.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index f206b9b96..18b1bdb4c 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1109,10 +1109,10 @@ class AptDependenciesAppResource(AppResource): out, err = self.check_output_bash_snippet(values.get("packages_from_raw_bash")) if err: logger.error( - "Error while running apt resource packages_from_raw_bash snippet for '" + str(key) + "' extras:" + f"Error while running apt resource packages_from_raw_bash snippet for '{key}' extras:" ) logger.error(err) - values["packages"] = [value.strip() for value in out.split("\n")] + values["packages"] = values.get("packages", []) + [value.strip() for value in out.split("\n")] if ( not isinstance(values.get("repo"), str) @@ -1126,6 +1126,9 @@ class AptDependenciesAppResource(AppResource): "In apt resource in the manifest: 'extras' repo should have the keys 'repo', 'key' defined as strings, 'packages' defined as list or 'packages_from_raw_bash' defined as string", raw_msg=True, ) + + # Drop 'extras' entries associated to no packages + self.extras = {key: value for key, values in self.extras.items() if values["packages"]} def provision_or_update(self, context: Dict = {}): script = " ".join(["ynh_install_app_dependencies", *self.packages]) From dc362dd636eaf912c788b2d468c015eb7bf035d1 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 15 Oct 2023 16:48:59 +0200 Subject: [PATCH 0308/1116] Revert packages_from_raw_bash test Co-authored-by: Alexandre Aubin --- src/utils/resources.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 18b1bdb4c..ba1a915e4 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1117,10 +1117,7 @@ class AptDependenciesAppResource(AppResource): if ( not isinstance(values.get("repo"), str) or not isinstance(values.get("key"), str) - or ( - not isinstance(values.get("packages"), list) - and not isinstance(values.get("packages_from_raw_bash"), str) - ) + or not isinstance(values.get("packages"), list) ): raise YunohostError( "In apt resource in the manifest: 'extras' repo should have the keys 'repo', 'key' defined as strings, 'packages' defined as list or 'packages_from_raw_bash' defined as string", From 23cdf91b0198ce8d4a034aea04f95406795b7c5f Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 15 Oct 2023 16:49:55 +0200 Subject: [PATCH 0309/1116] Restore deleted line --- src/utils/resources.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/resources.py b/src/utils/resources.py index ba1a915e4..87f6ae368 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1086,6 +1086,7 @@ class AptDependenciesAppResource(AppResource): packages: List = [] packages_from_raw_bash: str = "" extras: Dict[str, Dict[str, Union[str, List]]] = {} + def __init__(self, properties: Dict[str, Any], *args, **kwargs): super().__init__(properties, *args, **kwargs) From 662998a1ab229f1c3cd2cdb736149fb3589aa23d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin <4533074+alexAubin@users.noreply.github.com> Date: Mon, 16 Oct 2023 15:49:12 +0200 Subject: [PATCH 0310/1116] Update src/utils/resources.py --- src/utils/resources.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 87f6ae368..ba1a915e4 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1086,7 +1086,6 @@ class AptDependenciesAppResource(AppResource): packages: List = [] packages_from_raw_bash: str = "" extras: Dict[str, Dict[str, Union[str, List]]] = {} - def __init__(self, properties: Dict[str, Any], *args, **kwargs): super().__init__(properties, *args, **kwargs) From 089e0001c20681dc7e4c3b594e6cd4815d6c1048 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 17 Oct 2023 14:15:22 +0200 Subject: [PATCH 0311/1116] portal: retreive app permissions from ldap --- src/portal.py | 83 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/src/portal.py b/src/portal.py index e89d176fd..6cd4b7816 100644 --- a/src/portal.py +++ b/src/portal.py @@ -18,16 +18,16 @@ along with this program; if not, see http://www.gnu.org/licenses """ +import logging from pathlib import Path from typing import Any, Union -import logging -import ldap -from moulinette.utils.filesystem import read_json, read_yaml +import ldap +from moulinette.utils.filesystem import read_yaml from yunohost.authenticators.ldap_ynhuser import URI, USERDN, Authenticator as Auth from yunohost.user import _hash_user_password from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.utils.ldap import LDAPInterface +from yunohost.utils.ldap import LDAPInterface, _ldap_path_extract from yunohost.utils.password import ( assert_password_is_compatible, assert_password_is_strong_enough, @@ -51,6 +51,49 @@ def _get_user_infos( return username, auth["host"], result[0], ldap_interface +def _get_apps(username: Union[str, None] = None): + """Get public + user's authorized apps. + If `username` is not given, returns only public apps + (e.g. with `visitors` in group permissions) + """ + SYSTEM_PERMS = ("mail", "xmpp", "sftp", "ssh") + + ldap_interface = LDAPInterface("root") + permissions_infos = ldap_interface.search( + "ou=permission", + "(objectclass=permissionYnh)", + [ + "cn", + "groupPermission", + "inheritPermission", + "URL", + "label", + "showTile", + ], + ) + + apps = {} + + for perm in permissions_infos: + name = perm["cn"][0].replace(".main", "") + + if name in SYSTEM_PERMS or not perm.get("showTile", [False])[0]: + continue + + groups = [_ldap_path_extract(g, "cn") for g in perm["groupPermission"]] + users = [ + _ldap_path_extract(u, "uid") for u in perm.get("inheritPermission", []) + ] + + if username in users or "visitors" in groups: + apps[name] = { + "label": perm["label"][0], + "url": perm["URL"][0], + } + + return apps + + def _get_portal_settings(domain: Union[str, None] = None): from yunohost.domain import DOMAIN_SETTINGS_DIR @@ -80,18 +123,12 @@ def portal_public(): portal_settings = _get_portal_settings() portal_settings["apps"] = {} - portal_settings["public"] = portal_settings.pop("default_app", None) == "_yunohost_portal_with_public_apps" + portal_settings["public"] = ( + portal_settings.pop("default_app", None) == "_yunohost_portal_with_public_apps" + ) if portal_settings["public"]: - ssowat_conf = read_json("/etc/ssowat/conf.json") - portal_settings["apps"] = { - perm.replace(".main", ""): { - "label": infos["label"], - "url": infos["uris"][0], - } - for perm, infos in ssowat_conf["permissions"].items() - if infos["show_tile"] and infos["public"] - } + portal_settings["apps"] = _get_apps() if not portal_settings["show_other_domains_apps"]: portal_settings["apps"] = { @@ -111,23 +148,9 @@ def portal_me(): ["cn", "mail", "maildrop", "mailuserquota", "memberOf", "permission"] ) - groups = [ - g.replace("cn=", "").replace(",ou=groups,dc=yunohost,dc=org", "") - for g in user["memberOf"] - ] + groups = [_ldap_path_extract(g, "cn") for g in user["memberOf"]] groups = [g for g in groups if g not in [username, "all_users"]] - - permissions = [ - p.replace("cn=", "").replace(",ou=permission,dc=yunohost,dc=org", "") - for p in user["permission"] - ] - - ssowat_conf = read_json("/etc/ssowat/conf.json") - apps = { - perm.replace(".main", ""): {"label": infos["label"], "url": infos["uris"][0]} - for perm, infos in ssowat_conf["permissions"].items() - if perm in permissions and infos["show_tile"] and username in infos["users"] - } + apps = _get_apps(username) settings = _get_portal_settings(domain=domain) if not settings["show_other_domains_apps"]: From d65cca5ab165863e4598eb5dce88b4b8d0cf3d9e Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 17 Oct 2023 14:15:59 +0200 Subject: [PATCH 0312/1116] portal: fix decode error --- src/authenticators/ldap_ynhuser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py index 331cf9e25..8a2b10aa0 100644 --- a/src/authenticators/ldap_ynhuser.py +++ b/src/authenticators/ldap_ynhuser.py @@ -133,7 +133,7 @@ class Authenticator(BaseAuthenticator): response.set_cookie( "yunohost.portal", - jwt.encode(new_infos, session_secret, algorithm="HS256").decode(), + jwt.encode(new_infos, session_secret, algorithm="HS256"), secure=True, httponly=True, path="/", From c5771253636c9155097c67c2a697bc1065f4a52c Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 17 Oct 2023 14:18:21 +0200 Subject: [PATCH 0313/1116] portal: temp disable 'show_other_domains_apps' settings due to missing domain info in ldap --- share/config_domain.toml | 6 +++--- src/domain.py | 2 +- src/portal.py | 25 +++++++++++++++---------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/share/config_domain.toml b/share/config_domain.toml index 6fc5fc50a..d56a46b8e 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -7,9 +7,9 @@ name = "Features" [feature.portal] name = "Portal" - [feature.portal.show_other_domains_apps] - type = "boolean" - default = false + # [feature.portal.show_other_domains_apps] + # type = "boolean" + # default = false [feature.portal.portal_title] type = "string" diff --git a/src/domain.py b/src/domain.py index a5a68ca0f..98420b4be 100644 --- a/src/domain.py +++ b/src/domain.py @@ -735,7 +735,7 @@ class DomainConfigPanel(ConfigPanel): portal_options = [ "default_app", - "show_other_domains_apps", + # "show_other_domains_apps", "portal_title", "portal_logo", "portal_theme", diff --git a/src/portal.py b/src/portal.py index 6cd4b7816..9326bcc4b 100644 --- a/src/portal.py +++ b/src/portal.py @@ -109,7 +109,7 @@ def _get_portal_settings(domain: Union[str, None] = None): "portal_logo": "", "portal_theme": "system", "portal_title": "YunoHost", - "show_other_domains_apps": False, + # "show_other_domains_apps": False, "domain": domain, } @@ -130,12 +130,13 @@ def portal_public(): if portal_settings["public"]: portal_settings["apps"] = _get_apps() - if not portal_settings["show_other_domains_apps"]: - portal_settings["apps"] = { - name: data - for name, data in portal_settings["apps"].items() - if portal_settings["domain"] in data["url"] - } + # FIXME/TODO; See: filter apps that are available on specified domain + # if not portal_settings["show_other_domains_apps"]: + # portal_settings["apps"] = { + # name: data + # for name, data in portal_settings["apps"].items() + # if portal_settings["domain"] in data["url"] + # } return portal_settings @@ -152,9 +153,13 @@ def portal_me(): groups = [g for g in groups if g not in [username, "all_users"]] apps = _get_apps(username) - settings = _get_portal_settings(domain=domain) - if not settings["show_other_domains_apps"]: - apps = {name: data for name, data in apps.items() if domain in data["url"]} + # FIXME / TODO: filter apps that are available on specified domain + # settings = _get_portal_settings(domain=domain) + # if not settings["show_other_domains_apps"]: + # apps = {name: data for name, data in apps.items() if domain in data["url"]} + # App's `domain` info is not available in LDAP data, we need another config file + # that would be readable by the `ynh-portal` user. This conf file could be generated + # in `app_ssowatconf()` result_dict = { "username": username, From a2faa8add90d512e56c4dae999a813b53d513a5b Mon Sep 17 00:00:00 2001 From: Chris Vogel Date: Tue, 17 Oct 2023 14:49:32 +0200 Subject: [PATCH 0314/1116] add redis database configuration https://github.com/YunoHost/issues/issues/2266 without this rspamd does not use the redis database --- conf/rspamd/redis.conf | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 conf/rspamd/redis.conf diff --git a/conf/rspamd/redis.conf b/conf/rspamd/redis.conf new file mode 100644 index 000000000..03152581e --- /dev/null +++ b/conf/rspamd/redis.conf @@ -0,0 +1,2 @@ +# set redis server +servers = "127.0.0.1"; From 379b6922ad16cb744c9c3fba2138613fbf2e68d5 Mon Sep 17 00:00:00 2001 From: Chris Vogel Date: Tue, 17 Oct 2023 21:13:09 +0200 Subject: [PATCH 0315/1116] install file to redis configuration ouch... Thanks @Tagadda! --- hooks/conf_regen/31-rspamd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hooks/conf_regen/31-rspamd b/hooks/conf_regen/31-rspamd index 6807ce0cd..6333a755f 100755 --- a/hooks/conf_regen/31-rspamd +++ b/hooks/conf_regen/31-rspamd @@ -13,6 +13,8 @@ do_pre_regen() { "${pending_dir}/etc/rspamd/local.d/dkim_signing.conf" install -D -m 644 rspamd.sieve \ "${pending_dir}/etc/dovecot/global_script/rspamd.sieve" + install -D -m 644 redis.conf \ + "${pending_dir}/etc/rspamd/local.d/redis.conf" } do_post_regen() { From 827fbe337ddf964fad6e3b74052c3b479687c619 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 18 Oct 2023 18:21:03 +0200 Subject: [PATCH 0316/1116] conf_regen:yunohost: setup /etc/yunohost/portal --- hooks/conf_regen/01-yunohost | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index adccfa17c..256104a55 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -29,6 +29,11 @@ do_init_regen() { chown -R root:ssl-cert /etc/yunohost/certs chmod 750 /etc/yunohost/certs + # Portal folder + mkdir -p /etc/yunohost/portal + chmod 500 /etc/yunohost/portal + chown ynh-portal:ynh-portal /etc/yunohost/portal + # App folders mkdir -p /etc/yunohost/apps chmod 700 /etc/yunohost/apps @@ -243,6 +248,11 @@ do_post_regen() { [ ! -e "/home/$USER" ] || setfacl -m g:all_users:--- /home/$USER done + # Portal settings + mkdir -p /etc/yunohost/portal + [[ ! -e /etc/yunohost/portal ]] || (chown ynh-portal:ynh-portal /etc/yunohost/portal && chmod 500 /etc/yunohost/portal) + chmod g+s /etc/yunohost/portal + # Domain settings mkdir -p /etc/yunohost/domains @@ -267,7 +277,7 @@ do_post_regen() { systemctl restart ntp } fi - + [[ ! "$regen_conf_files" =~ "nftables.service.d/ynh-override.conf" ]] || systemctl daemon-reload [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || { systemctl daemon-reload From 8d366e67b04f411061136085ba9c8d0699103300 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 18 Oct 2023 18:29:55 +0200 Subject: [PATCH 0317/1116] app_ssowatconf: generate per domain portal config with available apps --- src/app.py | 49 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/src/app.py b/src/app.py index adcea56e4..754a920a0 100644 --- a/src/app.py +++ b/src/app.py @@ -29,6 +29,7 @@ import copy from typing import List, Tuple, Dict, Any, Iterator, Optional from packaging import version from logging import getLogger +from pathlib import Path from moulinette import Moulinette, m18n from moulinette.utils.process import run_commands, check_output @@ -36,7 +37,6 @@ from moulinette.utils.filesystem import ( read_file, read_json, read_toml, - read_yaml, write_to_file, write_to_json, cp, @@ -1606,12 +1606,16 @@ def app_ssowatconf(): """ - from yunohost.domain import domain_list, _get_maindomain, domain_config_get, _get_domain_portal_dict + from yunohost.domain import ( + domain_list, + domain_config_get, + _get_domain_portal_dict, + ) from yunohost.permission import user_permission_list - from yunohost.settings import settings_get + from yunohost.portal import PORTAL_SETTINGS_DIR - main_domain = _get_maindomain() domains = domain_list()["domains"] + portal_domains = domain_list(exclude_subdomains=True)["domains"] all_permissions = user_permission_list( full=True, ignore_system_perms=True, absolute_urls=True )["permissions"] @@ -1633,7 +1637,6 @@ def app_ssowatconf(): } } - apps_using_remote_user_var_in_nginx = ( check_output( "grep -nri '$remote_user' /etc/yunohost/apps/*/conf/*nginx*conf | awk -F/ '{print $5}' || true" @@ -1655,6 +1658,9 @@ def app_ssowatconf(): if domain + "/" != app_domain + app_path: redirected_urls[domain + "/"] = app_domain + app_path + # Will organize apps by portal domain + portal_domains_apps = {domain: {} for domain in portal_domains} + # New permission system for perm_name, perm_info in all_permissions.items(): uris = ( @@ -1678,6 +1684,23 @@ def app_ssowatconf(): "uris": uris, } + # Next: portal related + # No need to keep apps that aren't supposed to be displayed in portal + if not perm_info.get("show_tile", False): + continue + + app_domain = uris[0].split("/")[0] + # get "topest" domain + app_portal_domain = next( + domain for domain in portal_domains if domain in app_domain + ) + portal_domains_apps[app_portal_domain][app_id] = { + "label": perm_info["label"], + "users": perm_info["corresponding_users"], + "public": "visitors" in perm_info["allowed"], + "url": uris[0], + } + conf_dict = { "cookie_secret_file": "/etc/yunohost/.ssowat_cookie_secret", "cookie_name": "yunohost.portal", @@ -1688,6 +1711,22 @@ def app_ssowatconf(): write_to_json("/etc/ssowat/conf.json", conf_dict, sort_keys=True, indent=4) + # Generate a file per possible portal with available apps + for domain, apps in portal_domains_apps.items(): + portal_settings = {} + + portal_settings_path = Path(f"{PORTAL_SETTINGS_DIR}/{domain}.json") + if portal_settings_path.exists(): + portal_settings.update(read_json(str(portal_settings_path))) + + # Do no override anything else than "apps" since the file is shared + # with domain's config panel "portal" options + portal_settings["apps"] = apps + + write_to_json( + str(portal_settings_path), portal_settings, sort_keys=True, indent=4 + ) + logger.debug(m18n.n("ssowat_conf_generated")) From 9d21501648fc9cb6dbe71f419c95c8c9b9546b1f Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 18 Oct 2023 18:33:54 +0200 Subject: [PATCH 0318/1116] domain:config: update portal option saving --- src/domain.py | 35 ++++++++++++++++++++++++++++------- src/portal.py | 1 + 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/domain.py b/src/domain.py index 98420b4be..2a583db66 100644 --- a/src/domain.py +++ b/src/domain.py @@ -18,13 +18,21 @@ # import os import time +from pathlib import Path from typing import List, Optional from collections import OrderedDict from logging import getLogger from moulinette import m18n, Moulinette from moulinette.core import MoulinetteError -from moulinette.utils.filesystem import write_to_file, read_yaml, write_to_yaml, rm +from moulinette.utils.filesystem import ( + read_json, + read_yaml, + rm, + write_to_file, + write_to_json, + write_to_yaml, +) from yunohost.app import ( app_ssowatconf, @@ -735,16 +743,18 @@ class DomainConfigPanel(ConfigPanel): portal_options = [ "default_app", - # "show_other_domains_apps", + "show_other_domains_apps", "portal_title", "portal_logo", "portal_theme", ] if any( option in self.future_values - and self.future_values[option] != self.values[option] + and self.future_values[option] != self.values.get(option) for option in portal_options ): + from yunohost.portal import PORTAL_SETTINGS_DIR + # Portal options are also saved in a `domain.portal.yml` file # that can be read by the portal API. # FIXME remove those from the config panel saved values? @@ -775,11 +785,22 @@ class DomainConfigPanel(ConfigPanel): self.new_values["portal_logo"] = filepath # save the base64 content with mimetype to portal settings with open(filepath, "rb") as f: - portal_values["portal_logo"] = mimetype[0] + ":" + base64.b64encode(f.read()).decode("utf-8") + portal_values["portal_logo"] = ( + mimetype[0] + + ":" + + base64.b64encode(f.read()).decode("utf-8") + ) - # FIXME config file should be readable by non-root portal entity - write_to_yaml( - f"{DOMAIN_SETTINGS_DIR}/{self.entity}.portal.yml", portal_values + portal_settings_path = Path(f"{PORTAL_SETTINGS_DIR}/{self.entity}.json") + portal_settings = {"apps": {}} + + if portal_settings_path.exists(): + portal_settings.update(read_json(str(portal_settings_path))) + + # Merge settings since this config file is shared with `app_ssowatconf()` which populate the `apps` key. + portal_settings.update(portal_values) + write_to_json( + str(portal_settings_path), portal_settings, sort_keys=True, indent=4 ) super()._apply() diff --git a/src/portal.py b/src/portal.py index 9326bcc4b..da9564d3f 100644 --- a/src/portal.py +++ b/src/portal.py @@ -35,6 +35,7 @@ from yunohost.utils.password import ( logger = logging.getLogger("portal") +PORTAL_SETTINGS_DIR = "/etc/yunohost/portal" ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] From 2b5726f4a803035f821be068ec4594866f89e557 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 18 Oct 2023 18:36:07 +0200 Subject: [PATCH 0319/1116] portal: update settings reading from new config file --- src/portal.py | 124 +++++++++++++++++++------------------------------- 1 file changed, 47 insertions(+), 77 deletions(-) diff --git a/src/portal.py b/src/portal.py index da9564d3f..de852f5a6 100644 --- a/src/portal.py +++ b/src/portal.py @@ -23,7 +23,7 @@ from pathlib import Path from typing import Any, Union import ldap -from moulinette.utils.filesystem import read_yaml +from moulinette.utils.filesystem import read_json from yunohost.authenticators.ldap_ynhuser import URI, USERDN, Authenticator as Auth from yunohost.user import _hash_user_password from yunohost.utils.error import YunohostError, YunohostValidationError @@ -52,51 +52,13 @@ def _get_user_infos( return username, auth["host"], result[0], ldap_interface -def _get_apps(username: Union[str, None] = None): - """Get public + user's authorized apps. - If `username` is not given, returns only public apps - (e.g. with `visitors` in group permissions) +def _get_portal_settings( + domain: Union[str, None] = None, username: Union[str, None] = None +): + """ + Returns domain's portal settings which are a combo of domain's portal config panel options + and the list of apps availables on this domain computed by `app.app_ssowatconf()`. """ - SYSTEM_PERMS = ("mail", "xmpp", "sftp", "ssh") - - ldap_interface = LDAPInterface("root") - permissions_infos = ldap_interface.search( - "ou=permission", - "(objectclass=permissionYnh)", - [ - "cn", - "groupPermission", - "inheritPermission", - "URL", - "label", - "showTile", - ], - ) - - apps = {} - - for perm in permissions_infos: - name = perm["cn"][0].replace(".main", "") - - if name in SYSTEM_PERMS or not perm.get("showTile", [False])[0]: - continue - - groups = [_ldap_path_extract(g, "cn") for g in perm["groupPermission"]] - users = [ - _ldap_path_extract(u, "uid") for u in perm.get("inheritPermission", []) - ] - - if username in users or "visitors" in groups: - apps[name] = { - "label": perm["label"][0], - "url": perm["URL"][0], - } - - return apps - - -def _get_portal_settings(domain: Union[str, None] = None): - from yunohost.domain import DOMAIN_SETTINGS_DIR if not domain: from bottle import request @@ -105,41 +67,56 @@ def _get_portal_settings(domain: Union[str, None] = None): assert domain and "/" not in domain - settings = { + settings: dict[str, Any] = { + "apps": {}, "public": False, "portal_logo": "", "portal_theme": "system", "portal_title": "YunoHost", - # "show_other_domains_apps": False, + "show_other_domains_apps": False, "domain": domain, } - if Path(f"{DOMAIN_SETTINGS_DIR}/{domain}.portal.yml").exists(): - settings.update(read_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.portal.yml")) + portal_settings_path = Path(f"{PORTAL_SETTINGS_DIR}/{domain}.json") + + if portal_settings_path.exists(): + settings.update(read_json(str(portal_settings_path))) + # Portal may be public (no login required) + settings["public"] = ( + settings.pop("default_app", None) == "_yunohost_portal_with_public_apps" + ) + + # First clear apps since it may contains private apps + apps: dict[str, Any] = settings.pop("apps", {}) + settings["apps"] = {} + + if settings["show_other_domains_apps"]: + # Enhanced apps with all other domain's apps + import glob + + for path in glob.glob(f"{PORTAL_SETTINGS_DIR}/*.json"): + if path != str(portal_settings_path): + apps.update(read_json(path)["apps"]) + + if username: + # Add user allowed or public apps + settings["apps"] = { + name: app + for name, app in apps.items() + if username in app["users"] or app["public"] + } + elif settings["public"]: + # Add public apps (e.g. with "visitors" in group permission) + settings["apps"] = {name: app for name, app in apps.items() if app["public"]} return settings def portal_public(): - - portal_settings = _get_portal_settings() - portal_settings["apps"] = {} - portal_settings["public"] = ( - portal_settings.pop("default_app", None) == "_yunohost_portal_with_public_apps" - ) - - if portal_settings["public"]: - portal_settings["apps"] = _get_apps() - - # FIXME/TODO; See: filter apps that are available on specified domain - # if not portal_settings["show_other_domains_apps"]: - # portal_settings["apps"] = { - # name: data - # for name, data in portal_settings["apps"].items() - # if portal_settings["domain"] in data["url"] - # } - - return portal_settings + """Get public settings + If the portal is set as public, it will include the list of public apps + """ + return _get_portal_settings() def portal_me(): @@ -152,15 +129,8 @@ def portal_me(): groups = [_ldap_path_extract(g, "cn") for g in user["memberOf"]] groups = [g for g in groups if g not in [username, "all_users"]] - apps = _get_apps(username) - - # FIXME / TODO: filter apps that are available on specified domain - # settings = _get_portal_settings(domain=domain) - # if not settings["show_other_domains_apps"]: - # apps = {name: data for name, data in apps.items() if domain in data["url"]} - # App's `domain` info is not available in LDAP data, we need another config file - # that would be readable by the `ynh-portal` user. This conf file could be generated - # in `app_ssowatconf()` + # Get user allowed apps + apps = _get_portal_settings(domain, username)["apps"] result_dict = { "username": username, From e6e58ec2695fff37b617f67361ef218fb8d70c94 Mon Sep 17 00:00:00 2001 From: Chris Vogel Date: Wed, 18 Oct 2023 23:04:24 +0200 Subject: [PATCH 0320/1116] improve dovecots rspamd integration For rspamd being able to learn ham or spam from messages being moved into spam/junk folders or out of them dovecot needs to know how spam/junk folders and trash folders are named. The former rules narrowed the folders being recognized as spam/trash down to just 'Junk, SPAM, Trash' (case-senistive). Since users and admins can change the foldernames and write their own seive filters to use those folders I think it is a big improvement if more folders will be recognized. The change is supposed to accept some more commonly used folder names for spam and trash in a case-insensitive manner. --- conf/dovecot/dovecot.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index 152f4c01c..4f491d1df 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -119,8 +119,8 @@ plugin { antispam_debug_target = syslog antispam_verbose_debug = 0 antispam_backend = pipe - antispam_spam = Junk;SPAM - antispam_trash = Trash + antispam_spam_pattern_ignorecase = junk;spam + antispam_trash_pattern_ignorecase = trash;papierkorb;deleted messages antispam_pipe_program = /usr/bin/rspamc antispam_pipe_program_args = -h;localhost:11334;-P;q1 antispam_pipe_program_spam_arg = learn_spam From 6f085ad255451e4cf6cc7330f37636b584ad5cfb Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 19 Oct 2023 14:33:52 +0200 Subject: [PATCH 0321/1116] conf_regen:yunohost: repeat init portal setup in post hook --- hooks/conf_regen/01-yunohost | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 256104a55..3b810de30 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -248,10 +248,10 @@ do_post_regen() { [ ! -e "/home/$USER" ] || setfacl -m g:all_users:--- /home/$USER done - # Portal settings + # Portal folder mkdir -p /etc/yunohost/portal - [[ ! -e /etc/yunohost/portal ]] || (chown ynh-portal:ynh-portal /etc/yunohost/portal && chmod 500 /etc/yunohost/portal) - chmod g+s /etc/yunohost/portal + chmod 500 /etc/yunohost/portal + chown ynh-portal:ynh-portal /etc/yunohost/portal # Domain settings mkdir -p /etc/yunohost/domains From 163dd4d3594374401e552d56fb93bc8a01e1db7a Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 19 Oct 2023 18:28:29 +0200 Subject: [PATCH 0322/1116] domain:config: remove 'portal_logo' for now --- share/config_domain.toml | 10 +++++----- src/domain.py | 33 ++------------------------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/share/config_domain.toml b/share/config_domain.toml index d56a46b8e..e67407eaa 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -7,16 +7,16 @@ name = "Features" [feature.portal] name = "Portal" - # [feature.portal.show_other_domains_apps] - # type = "boolean" - # default = false + [feature.portal.show_other_domains_apps] + type = "boolean" + default = false [feature.portal.portal_title] type = "string" default = "YunoHost" - [feature.portal.portal_logo] - type = "file" + # [feature.portal.portal_logo] + # type = "file" [feature.portal.portal_theme] type = "select" diff --git a/src/domain.py b/src/domain.py index 2a583db66..831b6fdc2 100644 --- a/src/domain.py +++ b/src/domain.py @@ -745,12 +745,12 @@ class DomainConfigPanel(ConfigPanel): "default_app", "show_other_domains_apps", "portal_title", - "portal_logo", + # "portal_logo", "portal_theme", ] if any( option in self.future_values - and self.future_values[option] != self.values.get(option) + and self.new_values[option] != self.values.get(option) for option in portal_options ): from yunohost.portal import PORTAL_SETTINGS_DIR @@ -761,35 +761,6 @@ class DomainConfigPanel(ConfigPanel): portal_values = { option: self.future_values[option] for option in portal_options } - if portal_values["portal_logo"].startswith("/tmp/ynh_filequestion_"): - # FIXME rework this whole mess - # currently only handling API sent images, need to adapt FileOption - # to handle file extensions and file saving since "bind" is only - # done in bash helpers which are not executed in domain config - if "portal_logo[name]" in self.args or self.values["portal_logo"]: - import mimetypes - import base64 - - if "portal_logo[name]" in self.args: - # FIXME choose where to save the file - filepath = os.path.join("/tmp", self.args["portal_logo[name]"]) - # move the temp file created by FileOption with proper name and extension - os.rename(self.new_values["portal_logo"], filepath) - mimetype = mimetypes.guess_type(filepath) - else: - # image has already been saved, do not overwrite it with the empty temp file created by the FileOption - filepath = self.values["portal_logo"] - mimetype = mimetypes.guess_type(filepath) - - # save the proper path to config panel settings - self.new_values["portal_logo"] = filepath - # save the base64 content with mimetype to portal settings - with open(filepath, "rb") as f: - portal_values["portal_logo"] = ( - mimetype[0] - + ":" - + base64.b64encode(f.read()).decode("utf-8") - ) portal_settings_path = Path(f"{PORTAL_SETTINGS_DIR}/{self.entity}.json") portal_settings = {"apps": {}} From 5bd86808470f8a4e94c16c4100ecb3ef7dc56ec1 Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 19 Oct 2023 18:39:15 +0200 Subject: [PATCH 0323/1116] domain:config: restrict portal options to topest domains --- src/domain.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/domain.py b/src/domain.py index 831b6fdc2..2a897c625 100644 --- a/src/domain.py +++ b/src/domain.py @@ -673,6 +673,10 @@ class DomainConfigPanel(ConfigPanel): 1 if self.entity == _get_maindomain() else 0 ) + # Portal settings are only available on "topest" domains + if _get_parent_domain_of(self.entity, topest=True) is not None: + del toml["feature"]["portal"] + # Optimize wether or not to load the DNS section, # e.g. we don't want to trigger the whole _get_registary_config_section # when just getting the current value from the feature section @@ -748,7 +752,7 @@ class DomainConfigPanel(ConfigPanel): # "portal_logo", "portal_theme", ] - if any( + if _get_parent_domain_of(self.entity, topest=True) is None and any( option in self.future_values and self.new_values[option] != self.values.get(option) for option in portal_options From f5c56db10ea6e27fae15c959b4ee9c09c16fef6f Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 13 Apr 2023 20:11:03 +0200 Subject: [PATCH 0324/1116] form: use pydantic BaseModel in Options and add some validators --- src/utils/form.py | 470 +++++++++++++++++++++++++--------------------- 1 file changed, 261 insertions(+), 209 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index f201f507b..8b47be430 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -17,6 +17,7 @@ # along with this program. If not, see . # import ast +import datetime import operator as op import os import re @@ -24,9 +25,18 @@ import shutil import tempfile import urllib.parse from enum import Enum -from typing import Any, Callable, Dict, List, Literal, Mapping, Optional, Union +from typing import Any, Callable, Dict, List, Literal, Mapping, Union from logging import getLogger +from pydantic import ( + BaseModel, + root_validator, + validator, +) +from pydantic.color import Color +from pydantic.networks import EmailStr, HttpUrl +from pydantic.types import FilePath + from moulinette import Moulinette, m18n from moulinette.interfaces.cli import colorize from moulinette.utils.filesystem import read_file, write_to_file @@ -36,7 +46,6 @@ from yunohost.utils.i18n import _value_for_locale logger = getLogger("yunohost.form") -Context = dict[str, Any] # ╭───────────────────────────────────────────────────────╮ # │ ┌─╴╷ ╷╭─┐╷ │ @@ -240,27 +249,58 @@ FORBIDDEN_READONLY_TYPES = { } -class BaseOption: - def __init__( - self, - question: Dict[str, Any], - ): - self.id = question["id"] - self.type = question.get("type", OptionType.string) - self.visible = question.get("visible", True) +Context = dict[str, Any] +Translation = Union[dict[str, str], str] +JSExpression = str +Values = dict[str, Any] - self.readonly = question.get("readonly", False) - if self.readonly and self.type in FORBIDDEN_READONLY_TYPES: - # FIXME i18n - raise YunohostError( - "config_forbidden_readonly_type", - type=self.type, - id=self.id, + +class Pattern(BaseModel): + regexp: str + error: Translation = "error_pattern" # FIXME add generic i18n key + + +class BaseOption(BaseModel): + type: OptionType + id: str + ask: Union[Translation, None] + readonly: bool = False + visible: Union[JSExpression, bool] = True + bind: Union[str, None] = None + + class Config: + arbitrary_types_allowed = True + use_enum_values = True + validate_assignment = True + + @staticmethod + def schema_extra(schema: dict[str, Any], model: Type["BaseOption"]) -> None: + # FIXME Do proper doctstring for Options + del schema["description"] + schema["additionalProperties"] = False + + @validator("ask", always=True) + def parse_or_set_default_ask( + cls, value: Union[Translation, None], values: Values + ) -> Translation: + if value is None: + return {"en": values["id"]} + if isinstance(value, str): + return {"en": value} + return value + + @validator("readonly", pre=True) + def can_be_readonly(cls, value: bool, values: Values) -> bool: + forbidden_types = ("password", "app", "domain", "user", "file") + if value is True and values["type"] in forbidden_types: + raise ValueError( + m18n.n( + "config_forbidden_readonly_type", + type=values["type"], + id=values["id"], + ) ) - - self.ask = question.get("ask", self.id) - if not isinstance(self.ask, dict): - self.ask = {"en": self.ask} + return value def is_visible(self, context: Context) -> bool: if isinstance(self.visible, bool): @@ -268,7 +308,7 @@ class BaseOption: return evaluate_simple_js_expression(self.visible, context=context) - def _get_prompt_message(self) -> str: + def _get_prompt_message(self, value: None) -> str: return _value_for_locale(self.ask) @@ -278,9 +318,7 @@ class BaseOption: class BaseReadonlyOption(BaseOption): - def __init__(self, question): - super().__init__(question) - self.readonly = True + readonly: Literal[True] = True class DisplayTextOption(BaseReadonlyOption): @@ -291,38 +329,35 @@ class MarkdownOption(BaseReadonlyOption): type: Literal[OptionType.markdown] = OptionType.markdown +class State(str, Enum): + success = "success" + info = "info" + warning = "warning" + danger = "danger" + + class AlertOption(BaseReadonlyOption): type: Literal[OptionType.alert] = OptionType.alert + style: State = State.info + icon: Union[str, None] = None - def __init__(self, question): - super().__init__(question) - self.style = question.get("style", "info") - - def _get_prompt_message(self) -> str: - text = _value_for_locale(self.ask) - - if self.style in ["success", "info", "warning", "danger"]: - color = { - "success": "green", - "info": "cyan", - "warning": "yellow", - "danger": "red", - } - prompt = m18n.g(self.style) if self.style != "danger" else m18n.n("danger") - return colorize(prompt, color[self.style]) + f" {text}" - else: - return text + def _get_prompt_message(self, value: None) -> str: + colors = { + State.success: "green", + State.info: "cyan", + State.warning: "yellow", + State.danger: "red", + } + message = m18n.g(self.style) if self.style != State.danger else m18n.n("danger") + return f"{colorize(message, colors[self.style])} {_value_for_locale(self.ask)}" class ButtonOption(BaseReadonlyOption): type: Literal[OptionType.button] = OptionType.button - enabled = True - - def __init__(self, question): - super().__init__(question) - self.help = question.get("help") - self.style = question.get("style", "success") - self.enabled = question.get("enabled", True) + help: Union[Translation, None] = None + style: State = State.success + icon: Union[str, None] = None + enabled: Union[JSExpression, bool] = True def is_enabled(self, context: Context) -> bool: if isinstance(self.enabled, bool): @@ -337,16 +372,21 @@ class ButtonOption(BaseReadonlyOption): class BaseInputOption(BaseOption): - hide_user_input_in_prompt = False - pattern: Optional[Dict] = None + help: Union[Translation, None] = None + example: Union[str, None] = None + placeholder: Union[str, None] = None + redact: bool = False + optional: bool = False # FIXME keep required as default? + default: Any = None - def __init__(self, question: Dict[str, Any]): - super().__init__(question) - self.default = question.get("default", None) - self.optional = question.get("optional", False) - self.pattern = question.get("pattern", self.pattern) - self.help = question.get("help") - self.redact = question.get("redact", False) + @validator("default", pre=True) + def check_empty_default(value: Any) -> Any: + if value == "": + return None + return value + + # FIXME remove + def old__init__(self, question: Dict[str, Any]): # .current_value is the currently stored value self.current_value = question.get("current_value") # .value is the "proposed" value which we got from the user @@ -354,10 +394,6 @@ class BaseInputOption(BaseOption): # Use to return several values in case answer is in mutipart self.values: Dict[str, Any] = {} - # Empty value is parsed as empty string - if self.default == "": - self.default = None - @staticmethod def humanize(value, option={}): return str(value) @@ -368,12 +404,12 @@ class BaseInputOption(BaseOption): value = value.strip() return value - def _get_prompt_message(self) -> str: - message = super()._get_prompt_message() + def _get_prompt_message(self, value: Any) -> str: + message = super()._get_prompt_message(value) if self.readonly: message = colorize(message, "purple") - return f"{message} {self.humanize(self.current_value)}" + return f"{message} {self.humanize(value, self)}" return message @@ -418,7 +454,8 @@ class BaseInputOption(BaseOption): class BaseStringOption(BaseInputOption): - default_value = "" + default: Union[str, None] + pattern: Union[Pattern, None] = None class StringOption(BaseStringOption): @@ -429,27 +466,23 @@ class TextOption(BaseStringOption): type: Literal[OptionType.text] = OptionType.text +FORBIDDEN_PASSWORD_CHARS = r"{}" + + class PasswordOption(BaseInputOption): type: Literal[OptionType.password] = OptionType.password - hide_user_input_in_prompt = True - default_value = "" - forbidden_chars = "{}" - - def __init__(self, question): - super().__init__(question) - self.redact = True - if self.default is not None: - raise YunohostValidationError( - "app_argument_password_no_default", name=self.id - ) + example: Literal[None] = None + default: Literal[None] = None + redact: Literal[True] = True + _forbidden_chars: str = FORBIDDEN_PASSWORD_CHARS def _value_pre_validator(self): super()._value_pre_validator() if self.value not in [None, ""]: - if any(char in self.value for char in self.forbidden_chars): + if any(char in self.value for char in self._forbidden_chars): raise YunohostValidationError( - "pattern_password_app", forbidden_chars=self.forbidden_chars + "pattern_password_app", forbidden_chars=self._forbidden_chars ) # If it's an optional argument the value should be empty or strong enough @@ -458,26 +491,25 @@ class PasswordOption(BaseInputOption): assert_password_is_strong_enough("user", self.value) -class ColorOption(BaseStringOption): +class ColorOption(BaseInputOption): type: Literal[OptionType.color] = OptionType.color - pattern = { - "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", - "error": "config_validate_color", # i18n: config_validate_color - } + default: Union[str, None] + # pattern = { + # "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", + # "error": "config_validate_color", # i18n: config_validate_color + # } # ─ NUMERIC ─────────────────────────────────────────────── class NumberOption(BaseInputOption): + # `number` and `range` are exactly the same, but `range` does render as a slider in web-admin type: Literal[OptionType.number, OptionType.range] = OptionType.number - default_value = None - - def __init__(self, question): - super().__init__(question) - self.min = question.get("min", None) - self.max = question.get("max", None) - self.step = question.get("step", None) + default: Union[int, None] + min: Union[int, None] = None + max: Union[int, None] = None + step: Union[int, None] = None @staticmethod def normalize(value, option={}): @@ -493,7 +525,7 @@ class NumberOption(BaseInputOption): if value in [None, ""]: return None - option = option.__dict__ if isinstance(option, BaseOption) else option + option = option.dict() if isinstance(option, BaseOption) else option raise YunohostValidationError( "app_argument_invalid", name=option.get("id"), @@ -525,20 +557,15 @@ class NumberOption(BaseInputOption): class BooleanOption(BaseInputOption): type: Literal[OptionType.boolean] = OptionType.boolean - default_value = 0 - yes_answers = ["1", "yes", "y", "true", "t", "on"] - no_answers = ["0", "no", "n", "false", "f", "off"] - - def __init__(self, question): - super().__init__(question) - self.yes = question.get("yes", 1) - self.no = question.get("no", 0) - if self.default is None: - self.default = self.no + yes: Any = 1 + no: Any = 0 + default: Union[bool, int, str, None] = 0 + _yes_answers: set[str] = {"1", "yes", "y", "true", "t", "on"} + _no_answers: set[str] = {"0", "no", "n", "false", "f", "off"} @staticmethod def humanize(value, option={}): - option = option.__dict__ if isinstance(option, BaseOption) else option + option = option.dict() if isinstance(option, BaseOption) else option yes = option.get("yes", 1) no = option.get("no", 0) @@ -561,7 +588,7 @@ class BooleanOption(BaseInputOption): @staticmethod def normalize(value, option={}): - option = option.__dict__ if isinstance(option, BaseOption) else option + option = option.dict() if isinstance(option, BaseOption) else option if isinstance(value, str): value = value.strip() @@ -569,8 +596,8 @@ class BooleanOption(BaseInputOption): technical_yes = option.get("yes", 1) technical_no = option.get("no", 0) - no_answers = BooleanOption.no_answers - yes_answers = BooleanOption.yes_answers + no_answers = BooleanOption._no_answers + yes_answers = BooleanOption._yes_answers assert ( str(technical_yes).lower() not in no_answers @@ -579,8 +606,8 @@ class BooleanOption(BaseInputOption): str(technical_no).lower() not in yes_answers ), f"'no' value can't be in {yes_answers}" - no_answers += [str(technical_no).lower()] - yes_answers += [str(technical_yes).lower()] + no_answers.add(str(technical_no).lower()) + yes_answers.add(str(technical_yes).lower()) strvalue = str(value).lower() @@ -602,8 +629,8 @@ class BooleanOption(BaseInputOption): def get(self, key, default=None): return getattr(self, key, default) - def _get_prompt_message(self): - message = super()._get_prompt_message() + def _get_prompt_message(self, value: Union[bool, None]) -> str: + message = super()._get_prompt_message(value) if not self.readonly: message += " [yes | no]" @@ -614,12 +641,13 @@ class BooleanOption(BaseInputOption): # ─ TIME ────────────────────────────────────────────────── -class DateOption(BaseStringOption): +class DateOption(BaseInputOption): type: Literal[OptionType.date] = OptionType.date - pattern = { - "regexp": r"^\d{4}-\d\d-\d\d$", - "error": "config_validate_date", # i18n: config_validate_date - } + default: Union[str, None] + # pattern = { + # "regexp": r"^\d{4}-\d\d-\d\d$", + # "error": "config_validate_date", # i18n: config_validate_date + # } def _value_pre_validator(self): from datetime import datetime @@ -633,32 +661,34 @@ class DateOption(BaseStringOption): raise YunohostValidationError("config_validate_date") -class TimeOption(BaseStringOption): +class TimeOption(BaseInputOption): type: Literal[OptionType.time] = OptionType.time - pattern = { - "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", - "error": "config_validate_time", # i18n: config_validate_time - } + default: Union[str, int, None] + # pattern = { + # "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", + # "error": "config_validate_time", # i18n: config_validate_time + # } # ─ LOCATIONS ───────────────────────────────────────────── -class EmailOption(BaseStringOption): +class EmailOption(BaseInputOption): type: Literal[OptionType.email] = OptionType.email - pattern = { - "regexp": r"^.+@.+", - "error": "config_validate_email", # i18n: config_validate_email - } + default: Union[EmailStr, None] + # pattern = { + # "regexp": r"^.+@.+", + # "error": "config_validate_email", # i18n: config_validate_email + # } class WebPathOption(BaseInputOption): type: Literal[OptionType.path] = OptionType.path - default_value = "" + default: Union[str, None] @staticmethod def normalize(value, option={}): - option = option.__dict__ if isinstance(option, BaseOption) else option + option = option.dict() if isinstance(option, BaseOption) else option if not isinstance(value, str): raise YunohostValidationError( @@ -685,10 +715,11 @@ class WebPathOption(BaseInputOption): class URLOption(BaseStringOption): type: Literal[OptionType.url] = OptionType.url - pattern = { - "regexp": r"^https?://.*$", - "error": "config_validate_url", # i18n: config_validate_url - } + default: Union[str, None] + # pattern = { + # "regexp": r"^https?://.*$", + # "error": "config_validate_url", # i18n: config_validate_url + # } # ─ FILE ────────────────────────────────────────────────── @@ -696,16 +727,16 @@ class URLOption(BaseStringOption): class FileOption(BaseInputOption): type: Literal[OptionType.file] = OptionType.file - upload_dirs: List[str] = [] - - def __init__(self, question): - super().__init__(question) - self.accept = question.get("accept", "") + # `FilePath` for CLI (path must exists and must be a file) + # `bytes` for API (a base64 encoded file actually) + accept: Union[str, None] = "" # currently only used by the web-admin + default: Union[str, None] + _upload_dirs: set[str] = set() @classmethod def clean_upload_dirs(cls): # Delete files uploaded from API - for upload_dir in cls.upload_dirs: + for upload_dir in cls._upload_dirs: if os.path.exists(upload_dir): shutil.rmtree(upload_dir) @@ -738,7 +769,7 @@ class FileOption(BaseInputOption): upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_") _, file_path = tempfile.mkstemp(dir=upload_dir) - FileOption.upload_dirs += [upload_dir] + FileOption._upload_dirs.add(upload_dir) logger.debug(f"Saving file {self.id} for file question into {file_path}") @@ -760,26 +791,30 @@ class FileOption(BaseInputOption): # ─ CHOICES ─────────────────────────────────────────────── -class BaseChoicesOption(BaseInputOption): - def __init__( - self, - question: Dict[str, Any], - ): - super().__init__(question) - # Don't restrict choices if there's none specified - self.choices = question.get("choices", None) +ChoosableOptions = Literal[ + OptionType.string, + OptionType.color, + OptionType.number, + OptionType.date, + OptionType.time, + OptionType.email, + OptionType.path, + OptionType.url, +] - def _get_prompt_message(self) -> str: - message = super()._get_prompt_message() + +class BaseChoicesOption(BaseInputOption): + # FIXME probably forbid choices to be None? + choices: Union[dict[str, Any], list[Any], None] + + def _get_prompt_message(self, value: Any) -> str: + message = super()._get_prompt_message(value) if self.readonly: - message = message - choice = self.current_value + if isinstance(self.choices, dict) and value is not None: + value = self.choices[value] - if isinstance(self.choices, dict) and choice is not None: - choice = self.choices[choice] - - return f"{colorize(message, 'purple')} {choice}" + return f"{colorize(message, 'purple')} {value}" if self.choices: # Prevent displaying a shitload of choices @@ -789,17 +824,15 @@ class BaseChoicesOption(BaseInputOption): if isinstance(self.choices, dict) else self.choices ) - choices_to_display = choices[:20] + splitted_choices = choices[:20] remaining_choices = len(choices[20:]) if remaining_choices > 0: - choices_to_display += [ + splitted_choices += [ m18n.n("other_available_options", n=remaining_choices) ] - choices_to_display = " | ".join( - str(choice) for choice in choices_to_display - ) + choices_to_display = " | ".join(str(choice) for choice in splitted_choices) return f"{message} [{choices_to_display}]" @@ -821,12 +854,15 @@ class BaseChoicesOption(BaseInputOption): class SelectOption(BaseChoicesOption): type: Literal[OptionType.select] = OptionType.select - default_value = "" + choices: Union[dict[str, Any], list[Any]] + default: Union[str, None] class TagsOption(BaseChoicesOption): type: Literal[OptionType.tags] = OptionType.tags - default_value = "" + choices: Union[list[str], None] = None + pattern: Union[Pattern, None] = None + default: Union[str, list[str], None] @staticmethod def humanize(value, option={}): @@ -879,20 +915,24 @@ class TagsOption(BaseChoicesOption): class DomainOption(BaseChoicesOption): type: Literal[OptionType.domain] = OptionType.domain + choices: Union[dict[str, str], None] - def __init__(self, question): - from yunohost.domain import domain_list, _get_maindomain + @root_validator() + def inject_domains_choices_and_default(cls, values: Values) -> Values: + # TODO remove calls to resources in validators (pydantic V2 should adress this) + from yunohost.domain import domain_list - super().__init__(question) - - if self.default is None: - self.default = _get_maindomain() - - self.choices = { - domain: domain + " ★" if domain == self.default else domain - for domain in domain_list()["domains"] + data = domain_list() + values["choices"] = { + domain: domain + " ★" if domain == data["main"] else domain + for domain in data["domains"] } + if values["default"] is None: + values["default"] = data["main"] + + return values + @staticmethod def normalize(value, option={}): if value.startswith("https://"): @@ -908,87 +948,99 @@ class DomainOption(BaseChoicesOption): class AppOption(BaseChoicesOption): type: Literal[OptionType.app] = OptionType.app + choices: Union[dict[str, str], None] + add_yunohost_portal_to_choices: bool = False + filter: Union[str, None] = None - def __init__(self, question): + @root_validator() + def inject_apps_choices(cls, values: Values) -> Values: from yunohost.app import app_list - super().__init__(question) - self.filter = question.get("filter", None) - self.add_yunohost_portal_to_choices = question.get("add_yunohost_portal_to_choices", False) - apps = app_list(full=True)["apps"] - if self.filter: + if values.get("filter", None): apps = [ app for app in apps - if evaluate_simple_js_expression(self.filter, context=app) + if evaluate_simple_js_expression(values["filter"], context=app) ] + values["choices"] = {"_none": "---"} - def _app_display(app): - domain_path_or_id = f" ({app.get('domain_path', app['id'])})" - return app["label"] + domain_path_or_id + if values.get("add_yunohost_portal_to_choices", False): + values["choices"]["_yunohost_portal_with_public_apps"] = "YunoHost's portal with public apps" - self.choices = {"_none": "---"} - if self.add_yunohost_portal_to_choices: - # FIXME: i18n - self.choices["_yunohost_portal_with_public_apps"] = "YunoHost's portal with public apps" - self.choices.update({app["id"]: _app_display(app) for app in apps}) + values["choices"].update( + { + app["id"]: f"{app['label']} ({app.get('domain_path', app['id'])})" + for app in apps + } + ) + + return values class UserOption(BaseChoicesOption): type: Literal[OptionType.user] = OptionType.user + choices: Union[dict[str, str], None] - def __init__(self, question): - from yunohost.user import user_list, user_info + @root_validator() + def inject_users_choices_and_default(cls, values: dict[str, Any]) -> dict[str, Any]: from yunohost.domain import _get_maindomain + from yunohost.user import user_info, user_list - super().__init__(question) - - self.choices = { + values["choices"] = { username: f"{infos['fullname']} ({infos['mail']})" for username, infos in user_list()["users"].items() } - if not self.choices: + # FIXME keep this to test if any user, do not raise error if no admin? + if not values["choices"]: raise YunohostValidationError( "app_argument_invalid", - name=self.id, + name=values["id"], error="You should create a YunoHost user first.", ) - if self.default is None: + if values["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(): + for user in values["choices"].keys(): if root_mail in user_info(user).get("mail-aliases", []): - self.default = user + values["default"] = user break + return values + class GroupOption(BaseChoicesOption): type: Literal[OptionType.group] = OptionType.group + choices: Union[dict[str, str], None] - def __init__(self, question): + @root_validator() + def inject_groups_choices_and_default(cls, values: Values) -> Values: from yunohost.user import user_group_list - super().__init__(question) + groups = user_group_list(short=True, include_primary_groups=False)["groups"] - self.choices = list( - user_group_list(short=True, include_primary_groups=False)["groups"] - ) - - def _human_readable_group(g): + def _human_readable_group(groupname): # i18n: visitors # i18n: all_users # i18n: admins - return m18n.n(g) if g in ["visitors", "all_users", "admins"] else g + return ( + m18n.n(groupname) + if groupname in ["visitors", "all_users", "admins"] + else groupname + ) - self.choices = {g: _human_readable_group(g) for g in self.choices} + values["choices"] = { + groupname: _human_readable_group(groupname) for groupname in groups + } - if self.default is None: - self.default = "all_users" + if values["default"] is None: + values["default"] = "all_users" + + return values OPTIONS = { @@ -997,7 +1049,7 @@ OPTIONS = { OptionType.alert: AlertOption, OptionType.button: ButtonOption, OptionType.string: StringOption, - OptionType.text: StringOption, + OptionType.text: TextOption, OptionType.password: PasswordOption, OptionType.color: ColorOption, OptionType.number: NumberOption, From 89ae5e654de8104a0c2fe85b5582078c755c13d3 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 17 Apr 2023 15:06:53 +0200 Subject: [PATCH 0325/1116] form: update asking flow, separate form and options --- src/tests/test_questions.py | 32 ++-- src/utils/form.py | 297 +++++++++++++++++++++++++----------- 2 files changed, 225 insertions(+), 104 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index a695e834d..9eceedcaf 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -11,11 +11,11 @@ from typing import Any, Literal, Sequence, TypedDict, Union from _pytest.mark.structures import ParameterSet - from moulinette import Moulinette from yunohost import app, domain, user from yunohost.utils.form import ( OPTIONS, + FORBIDDEN_PASSWORD_CHARS, ask_questions_and_parse_answers, BaseChoicesOption, BaseInputOption, @@ -378,8 +378,8 @@ def _fill_or_prompt_one_option(raw_option, intake): options = {id_: raw_option} answers = {id_: intake} if intake is not None else {} - option = ask_questions_and_parse_answers(options, answers)[0] - return (option, option.value if isinstance(option, BaseInputOption) else None) + options, form = ask_questions_and_parse_answers(options, answers) + return (options[0], form[id_] if isinstance(options[0], BaseInputOption) else None) def _test_value_is_expected_output(value, expected_output): @@ -551,7 +551,7 @@ class TestDisplayText(BaseTest): ask_questions_and_parse_answers({_id: raw_option}, answers) else: with patch.object(sys, "stdout", new_callable=StringIO) as stdout: - options = ask_questions_and_parse_answers( + options, form = ask_questions_and_parse_answers( {_id: raw_option}, answers ) assert stdout.getvalue() == f"{options[0].ask['en']}\n" @@ -604,7 +604,7 @@ class TestAlert(TestDisplayText): ) else: with patch.object(sys, "stdout", new_callable=StringIO) as stdout: - options = ask_questions_and_parse_answers( + options, form = ask_questions_and_parse_answers( {"display_text_id": raw_option}, answers ) ask = options[0].ask["en"] @@ -1925,9 +1925,7 @@ def test_options_query_string(): "&fake_id=fake_value" ) - def _assert_correct_values(options, raw_options): - form = {option.id: option.value for option in options} - + def _assert_correct_values(options, form, raw_options): for k, v in results.items(): if k == "file_id": assert os.path.exists(form["file_id"]) and os.path.isfile( @@ -1943,24 +1941,24 @@ def test_options_query_string(): with patch_interface("api"), patch_file_api(file_content1) as b64content: with patch_query_string(b64content.decode("utf-8")) as query_string: - options = ask_questions_and_parse_answers(raw_options, query_string) - _assert_correct_values(options, raw_options) + options, form = ask_questions_and_parse_answers(raw_options, query_string) + _assert_correct_values(options, form, raw_options) with patch_interface("cli"), patch_file_cli(file_content1) as filepath: with patch_query_string(filepath) as query_string: - options = ask_questions_and_parse_answers(raw_options, query_string) - _assert_correct_values(options, raw_options) + options, form = ask_questions_and_parse_answers(raw_options, query_string) + _assert_correct_values(options, form, raw_options) def test_question_string_default_type(): questions = {"some_string": {}} answers = {"some_string": "some_value"} - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.id == "some_string" - assert out.type == "string" - assert out.value == "some_value" + options, form = ask_questions_and_parse_answers(questions, answers) + option = options[0] + assert option.id == "some_string" + assert option.type == "string" + assert form[option.id] == "some_value" def test_option_default_type_with_choices_is_select(): diff --git a/src/utils/form.py b/src/utils/form.py index 8b47be430..8a2a34f4b 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -25,17 +25,30 @@ import shutil import tempfile import urllib.parse from enum import Enum -from typing import Any, Callable, Dict, List, Literal, Mapping, Union from logging import getLogger +from typing import ( + Annotated, + Any, + Callable, + List, + Literal, + Mapping, + Type, + Union, + cast, +) from pydantic import ( BaseModel, + Extra, + ValidationError, + create_model, root_validator, validator, ) from pydantic.color import Color +from pydantic.fields import Field from pydantic.networks import EmailStr, HttpUrl -from pydantic.types import FilePath from moulinette import Moulinette, m18n from moulinette.interfaces.cli import colorize @@ -385,15 +398,6 @@ class BaseInputOption(BaseOption): return None return value - # FIXME remove - def old__init__(self, question: Dict[str, Any]): - # .current_value is the currently stored value - self.current_value = question.get("current_value") - # .value is the "proposed" value which we got from the user - self.value = question.get("value") - # Use to return several values in case answer is in mutipart - self.values: Dict[str, Any] = {} - @staticmethod def humanize(value, option={}): return str(value) @@ -650,8 +654,6 @@ class DateOption(BaseInputOption): # } def _value_pre_validator(self): - from datetime import datetime - super()._value_pre_validator() if self.value not in [None, ""]: @@ -1069,6 +1071,120 @@ OPTIONS = { OptionType.group: GroupOption, } +AnyOption = Union[ + DisplayTextOption, + MarkdownOption, + AlertOption, + ButtonOption, + StringOption, + TextOption, + PasswordOption, + ColorOption, + NumberOption, + BooleanOption, + DateOption, + TimeOption, + EmailOption, + WebPathOption, + URLOption, + FileOption, + SelectOption, + TagsOption, + DomainOption, + AppOption, + UserOption, + GroupOption, +] + + +# ╭───────────────────────────────────────────────────────╮ +# │ ┌─╴╭─╮┌─╮╭╮╮ │ +# │ ├─╴│ │├┬╯│││ │ +# │ ╵ ╰─╯╵ ╰╵╵╵ │ +# ╰───────────────────────────────────────────────────────╯ + + +class OptionsModel(BaseModel): + # Pydantic will match option types to their models class based on the "type" attribute + options: list[Annotated[AnyOption, Field(discriminator="type")]] + + @staticmethod + def options_dict_to_list(options: dict[str, Any], defaults: dict[str, Any] = {}): + return [ + option + | { + "id": id_, + "type": option.get("type", "string"), + } + for id_, option in options.items() + ] + + def __init__(self, **kwargs) -> None: + super().__init__(options=self.options_dict_to_list(kwargs)) + + +class FormModel(BaseModel): + """ + Base form on which dynamic forms are built upon Options. + """ + + class Config: + validate_assignment = True + extra = Extra.ignore + + def __getitem__(self, name: str): + # FIXME + # if a FormModel's required field is not instancied with a value, it is + # not available as an attr and therefor triggers an `AttributeError` + # Also since `BaseReadonlyOption`s do not end up in form, + # `form[AlertOption.id]` would also triggers an error + # For convinience in those 2 cases, we return `None` + if not hasattr(self, name): + # Return None to trigger a validation error instead for required fields + return None + + return getattr(self, name) + + def __setitem__(self, name: str, value: Any): + setattr(self, name, value) + + def get(self, attr: str, default: Any = None) -> Any: + try: + return getattr(self, attr) + except AttributeError: + return default + + +def build_form(options: list[AnyOption], name: str = "DynamicForm") -> Type[FormModel]: + """ + Returns a dynamic pydantic model class that can be used as a form. + Parsing/validation occurs at instanciation and assignements. + To avoid validation at instanciation, use `my_form.construct(**values)` + """ + options_as_fields: Any = {} + validators: dict[str, Any] = {} + + for option in options: + if not isinstance(option, BaseInputOption): + continue # filter out non input options + + options_as_fields[option.id] = option._as_dynamic_model_field() + + for step in ("pre", "post"): + validators[f"{option.id}_{step}_validator"] = validator( + option.id, allow_reuse=True, pre=step == "pre" + )(getattr(option, f"_value_{step}_validator")) + + return cast( + Type[FormModel], + create_model( + name, + __base__=FormModel, + __validators__=validators, + **options_as_fields, + ), + ) + def hydrate_option_type(raw_option: dict[str, Any]) -> dict[str, Any]: type_ = raw_option.get( @@ -1097,20 +1213,16 @@ Hooks = dict[str, Callable[[BaseInputOption], Any]] def prompt_or_validate_form( - raw_options: dict[str, Any], + options: list[AnyOption], + form: FormModel, prefilled_answers: dict[str, Any] = {}, context: Context = {}, hooks: Hooks = {}, -) -> list[BaseOption]: - options = [] +) -> FormModel: answers = {**prefilled_answers} + values = {} - for id_, raw_option in raw_options.items(): - raw_option["id"] = id_ - raw_option["value"] = answers.get(id_) - raw_option = hydrate_option_type(raw_option) - option = OPTIONS[raw_option["type"]](raw_option) - + for option in options: interactive = Moulinette.interface.type == "cli" and os.isatty(1) if isinstance(option, ButtonOption): @@ -1123,89 +1235,88 @@ def prompt_or_validate_form( help=_value_for_locale(option.help), ) - # FIXME not sure why we do not append Buttons to returned options - options.append(option) - if not option.is_visible(context): if isinstance(option, BaseInputOption): # FIXME There could be several use case if the question is not displayed: # - we doesn't want to give a specific value # - we want to keep the previous value # - we want the default value - option.value = context[option.id] = None + context[option.id] = form[option.id] = None continue - message = option._get_prompt_message() - - if option.readonly: - if interactive: - Moulinette.display(message) + # if we try to get a `BaseReadonlyOption` value, which doesn't exists in the form, + # we get `None` + value = form[option.id] + if isinstance(option, BaseReadonlyOption) or option.readonly: if isinstance(option, BaseInputOption): - option.value = context[option.id] = option.current_value + # FIXME normalized needed, form[option.id] should already be normalized + # only update the context with the value + context[option.id] = form[option.id] + + # FIXME here we could error out + if option.id in prefilled_answers: + logger.warning( + f"'{option.id}' is readonly, value '{prefilled_answers[option.id]}' is then ignored." + ) + + if interactive: + Moulinette.display(option._get_prompt_message(value)) continue - if isinstance(option, BaseInputOption): - for i in range(5): - if interactive and option.value is None: - prefill = "" - choices = ( - option.choices if isinstance(option, BaseChoicesOption) else [] - ) + for i in range(5): + if option.id in prefilled_answers: + value = prefilled_answers[option.id] + elif interactive: + value = option.humanize(value, option) + choices = ( + option.choices if isinstance(option, BaseChoicesOption) else [] + ) + value = Moulinette.prompt( + message=option._get_prompt_message(value), + is_password=isinstance(option, PasswordOption), + confirm=False, + prefill=value, + is_multiline=isinstance(option, TextOption), + autocomplete=choices, + help=_value_for_locale(option.help), + ) - if option.current_value is not None: - prefill = option.humanize(option.current_value, option) - elif option.default is not None: - prefill = option.humanize(option.default, option) + # Apply default value if none + if value is None or value == "" and option.default is not None: + value = option.default - option.value = Moulinette.prompt( - message=message, - is_password=isinstance(option, PasswordOption), - confirm=False, - prefill=prefill, - is_multiline=(option.type == "text"), - autocomplete=choices, - help=_value_for_locale(option.help), - ) + try: + # Normalize and validate + values[option.id] = form[option.id] = option.normalize(value, option) + except (ValidationError, YunohostValidationError) as e: + # If in interactive cli, re-ask the current question + if i < 4 and interactive: + logger.error(str(e)) + value = None + continue - # Apply default value - class_default = getattr(option, "default_value", None) - if option.value in [None, ""] and ( - option.default is not None or class_default is not None - ): - option.value = ( - class_default if option.default is None else option.default - ) + if isinstance(e, ValidationError): + error = "\n".join([err["msg"] for err in e.errors()]) + raise YunohostValidationError(error, raw_msg=True) - try: - # Normalize and validate - option.value = option.normalize(option.value, option) - option._value_pre_validator() - except YunohostValidationError as e: - # If in interactive cli, re-ask the current question - if i < 4 and interactive: - logger.error(str(e)) - option.value = None - continue + # Otherwise raise the ValidationError + raise e - # Otherwise raise the ValidationError - raise + break - break + # Search for post actions in hooks + post_hook = f"post_ask__{option.id}" + if post_hook in hooks: + values.update(hooks[post_hook](option)) + # FIXME reapply new values to form to validate it - option.value = option.values[option.id] = option._value_post_validator() + answers.update(values) + context.update(values) - # Search for post actions in hooks - post_hook = f"post_ask__{option.id}" - if post_hook in hooks: - option.values.update(hooks[post_hook](option)) - - answers.update(option.values) - context.update(option.values) - - return options + return form def ask_questions_and_parse_answers( @@ -1213,7 +1324,7 @@ def ask_questions_and_parse_answers( prefilled_answers: Union[str, Mapping[str, Any]] = {}, current_values: Mapping[str, Any] = {}, hooks: Hooks = {}, -) -> list[BaseOption]: +) -> tuple[list[AnyOption], FormModel]: """Parse arguments store in either manifest.json or actions.json or from a config panel against the user answers when they are present. @@ -1241,9 +1352,21 @@ def ask_questions_and_parse_answers( context = {**current_values, **answers} - return prompt_or_validate_form( - raw_options, prefilled_answers=answers, context=context, hooks=hooks + # Validate/parse the options attributes + try: + model = OptionsModel(**raw_options) + except ValidationError as e: + error = "\n".join([err["msg"] for err in e.errors()]) + # FIXME use YunohostError instead since it is not really a user mistake? + raise YunohostValidationError(error, raw_msg=True) + + # Build the form from those questions and instantiate it without + # parsing/validation (construct) since it may contains required questions. + form = build_form(model.options).construct() + form = prompt_or_validate_form( + model.options, form, prefilled_answers=answers, context=context, hooks=hooks ) + return (model.options, form) def hydrate_questions_with_choices(raw_questions: List) -> List: @@ -1251,7 +1374,7 @@ def hydrate_questions_with_choices(raw_questions: List) -> List: for raw_question in raw_questions: raw_question = hydrate_option_type(raw_question) - question = OPTIONS[raw_question["type"]](raw_question) + question = OPTIONS[raw_question["type"]](**raw_question) if isinstance(question, BaseChoicesOption) and question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default From 39437748111fc850003ccdb305f71876e49c2f6e Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 17 Apr 2023 15:23:05 +0200 Subject: [PATCH 0326/1116] form: add dynamic annotation getters --- src/utils/form.py | 161 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 140 insertions(+), 21 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index 8a2a34f4b..17b0a9432 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -27,6 +27,8 @@ import urllib.parse from enum import Enum from logging import getLogger from typing import ( + TYPE_CHECKING, + cast, Annotated, Any, Callable, @@ -35,7 +37,6 @@ from typing import ( Mapping, Type, Union, - cast, ) from pydantic import ( @@ -49,6 +50,7 @@ from pydantic import ( from pydantic.color import Color from pydantic.fields import Field from pydantic.networks import EmailStr, HttpUrl +from pydantic.types import constr from moulinette import Moulinette, m18n from moulinette.interfaces.cli import colorize @@ -57,6 +59,9 @@ from yunohost.log import OperationLogger from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.i18n import _value_for_locale +if TYPE_CHECKING: + from pydantic.fields import FieldInfo + logger = getLogger("yunohost.form") @@ -391,6 +396,7 @@ class BaseInputOption(BaseOption): redact: bool = False optional: bool = False # FIXME keep required as default? default: Any = None + _annotation = Any @validator("default", pre=True) def check_empty_default(value: Any) -> Any: @@ -408,6 +414,57 @@ class BaseInputOption(BaseOption): value = value.strip() return value + @property + def _dynamic_annotation(self) -> Any: + """ + Returns the expected type of an Option's value. + This may be dynamic based on constraints. + """ + return self._annotation + + def _get_field_attrs(self) -> dict[str, Any]: + """ + Returns attributes to build a `pydantic.Field`. + This may contains non `Field` attrs that will end up in `Field.extra`. + Those extra can be used as constraints in custom validators and ends up + in the JSON Schema. + """ + # TODO + # - help + # - placeholder + attrs: dict[str, Any] = { + "redact": self.redact, # extra + "none_as_empty_str": self._none_as_empty_str, + } + + if self.readonly: + attrs["allow_mutation"] = False + + if self.example: + attrs["examples"] = [self.example] + + if self.default is not None: + attrs["default"] = self.default + else: + attrs["default"] = ... if not self.optional else None + + return attrs + + def _as_dynamic_model_field(self) -> tuple[Any, "FieldInfo"]: + """ + Return a tuple of a type and a Field instance to be injected in a + custom form declaration. + """ + attrs = self._get_field_attrs() + anno = ( + self._dynamic_annotation + if not self.optional + else Union[self._dynamic_annotation, None] + ) + field = Field(default=attrs.pop("default", None), **attrs) + + return (anno, field) + def _get_prompt_message(self, value: Any) -> str: message = super()._get_prompt_message(value) @@ -460,6 +517,22 @@ class BaseInputOption(BaseOption): class BaseStringOption(BaseInputOption): default: Union[str, None] pattern: Union[Pattern, None] = None + _annotation = str + + @property + def _dynamic_annotation(self) -> Type[str]: + if self.pattern: + return constr(regex=self.pattern.regexp) + + return self._annotation + + def _get_field_attrs(self) -> dict[str, Any]: + attrs = super()._get_field_attrs() + + if self.pattern: + attrs["regex_error"] = self.pattern.error # extra + + return attrs class StringOption(BaseStringOption): @@ -478,8 +551,16 @@ class PasswordOption(BaseInputOption): example: Literal[None] = None default: Literal[None] = None redact: Literal[True] = True + _annotation = str _forbidden_chars: str = FORBIDDEN_PASSWORD_CHARS + def _get_field_attrs(self) -> dict[str, Any]: + attrs = super()._get_field_attrs() + + attrs["forbidden_chars"] = self._forbidden_chars # extra + + return attrs + def _value_pre_validator(self): super()._value_pre_validator() @@ -498,10 +579,7 @@ class PasswordOption(BaseInputOption): class ColorOption(BaseInputOption): type: Literal[OptionType.color] = OptionType.color default: Union[str, None] - # pattern = { - # "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", - # "error": "config_validate_color", # i18n: config_validate_color - # } + _annotation = Color # ─ NUMERIC ─────────────────────────────────────────────── @@ -514,6 +592,7 @@ class NumberOption(BaseInputOption): min: Union[int, None] = None max: Union[int, None] = None step: Union[int, None] = None + _annotation = int @staticmethod def normalize(value, option={}): @@ -536,6 +615,14 @@ class NumberOption(BaseInputOption): error=m18n.n("invalid_number"), ) + def _get_field_attrs(self) -> dict[str, Any]: + attrs = super()._get_field_attrs() + attrs["ge"] = self.min + attrs["le"] = self.max + attrs["step"] = self.step # extra + + return attrs + def _value_pre_validator(self): super()._value_pre_validator() if self.value in [None, ""]: @@ -564,6 +651,7 @@ class BooleanOption(BaseInputOption): yes: Any = 1 no: Any = 0 default: Union[bool, int, str, None] = 0 + _annotation = Union[bool, int, str] _yes_answers: set[str] = {"1", "yes", "y", "true", "t", "on"} _no_answers: set[str] = {"0", "no", "n", "false", "f", "off"} @@ -633,6 +721,14 @@ class BooleanOption(BaseInputOption): def get(self, key, default=None): return getattr(self, key, default) + def _get_field_attrs(self) -> dict[str, Any]: + attrs = super()._get_field_attrs() + attrs["parse"] = { # extra + True: self.yes, + False: self.no, + } + return attrs + def _get_prompt_message(self, value: Union[bool, None]) -> str: message = super()._get_prompt_message(value) @@ -648,10 +744,7 @@ class BooleanOption(BaseInputOption): class DateOption(BaseInputOption): type: Literal[OptionType.date] = OptionType.date default: Union[str, None] - # pattern = { - # "regexp": r"^\d{4}-\d\d-\d\d$", - # "error": "config_validate_date", # i18n: config_validate_date - # } + _annotation = datetime.date def _value_pre_validator(self): super()._value_pre_validator() @@ -666,10 +759,7 @@ class DateOption(BaseInputOption): class TimeOption(BaseInputOption): type: Literal[OptionType.time] = OptionType.time default: Union[str, int, None] - # pattern = { - # "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", - # "error": "config_validate_time", # i18n: config_validate_time - # } + _annotation = datetime.time # ─ LOCATIONS ───────────────────────────────────────────── @@ -678,15 +768,13 @@ class TimeOption(BaseInputOption): class EmailOption(BaseInputOption): type: Literal[OptionType.email] = OptionType.email default: Union[EmailStr, None] - # pattern = { - # "regexp": r"^.+@.+", - # "error": "config_validate_email", # i18n: config_validate_email - # } + _annotation = EmailStr class WebPathOption(BaseInputOption): type: Literal[OptionType.path] = OptionType.path default: Union[str, None] + _annotation = str @staticmethod def normalize(value, option={}): @@ -718,10 +806,7 @@ class WebPathOption(BaseInputOption): class URLOption(BaseStringOption): type: Literal[OptionType.url] = OptionType.url default: Union[str, None] - # pattern = { - # "regexp": r"^https?://.*$", - # "error": "config_validate_url", # i18n: config_validate_url - # } + _annotation = HttpUrl # ─ FILE ────────────────────────────────────────────────── @@ -733,6 +818,7 @@ class FileOption(BaseInputOption): # `bytes` for API (a base64 encoded file actually) accept: Union[str, None] = "" # currently only used by the web-admin default: Union[str, None] + _annotation = str # TODO could be Path at some point _upload_dirs: set[str] = set() @classmethod @@ -809,6 +895,17 @@ class BaseChoicesOption(BaseInputOption): # FIXME probably forbid choices to be None? choices: Union[dict[str, Any], list[Any], None] + @property + def _dynamic_annotation(self) -> Union[object, Type[str]]: + if self.choices is not None: + choices = ( + self.choices if isinstance(self.choices, list) else self.choices.keys() + ) + # FIXME in case of dict, try to parse keys with `item_type` (at least number) + return Literal[tuple(choices)] + + return self._annotation + def _get_prompt_message(self, value: Any) -> str: message = super()._get_prompt_message(value) @@ -858,6 +955,7 @@ class SelectOption(BaseChoicesOption): type: Literal[OptionType.select] = OptionType.select choices: Union[dict[str, Any], list[Any]] default: Union[str, None] + _annotation = str class TagsOption(BaseChoicesOption): @@ -865,6 +963,7 @@ class TagsOption(BaseChoicesOption): choices: Union[list[str], None] = None pattern: Union[Pattern, None] = None default: Union[str, list[str], None] + _annotation = str @staticmethod def humanize(value, option={}): @@ -880,6 +979,26 @@ class TagsOption(BaseChoicesOption): value = value.strip() return value + @property + def _dynamic_annotation(self): + # TODO use Literal when serialization is seperated from validation + # if self.choices is not None: + # return Literal[tuple(self.choices)] + + # Repeat pattern stuff since we can't call the bare class `_dynamic_annotation` prop without instantiating it + if self.pattern: + return constr(regex=self.pattern.regexp) + + return self._annotation + + def _get_field_attrs(self) -> dict[str, Any]: + attrs = super()._get_field_attrs() + + if self.choices: + attrs["choices"] = self.choices # extra + + return attrs + def _value_pre_validator(self): values = self.value if isinstance(values, str): From ec5da99a79512b0864b549ce916d8cd805d33c4b Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 17 Apr 2023 15:33:34 +0200 Subject: [PATCH 0327/1116] form: rework pre + post options validators --- src/utils/form.py | 277 ++++++++++++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 122 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index 17b0a9432..cbe64b499 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -60,7 +60,7 @@ from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.i18n import _value_for_locale if TYPE_CHECKING: - from pydantic.fields import FieldInfo + from pydantic.fields import ModelField, FieldInfo logger = getLogger("yunohost.form") @@ -397,6 +397,7 @@ class BaseInputOption(BaseOption): optional: bool = False # FIXME keep required as default? default: Any = None _annotation = Any + _none_as_empty_str: bool = True @validator("default", pre=True) def check_empty_default(value: Any) -> Any: @@ -405,7 +406,9 @@ class BaseInputOption(BaseOption): return value @staticmethod - def humanize(value, option={}): + def humanize(value: Any, option={}) -> str: + if value is None: + return "" return str(value) @staticmethod @@ -474,31 +477,30 @@ class BaseInputOption(BaseOption): return message - def _value_pre_validator(self): - if self.value in [None, ""] and not self.optional: - raise YunohostValidationError("app_argument_required", name=self.id) + @classmethod + def _value_pre_validator(cls, value: Any, field: "ModelField") -> Any: + if value == "": + return None - # we have an answer, do some post checks - if self.value not in [None, ""]: - if self.pattern and not re.match(self.pattern["regexp"], str(self.value)): - raise YunohostValidationError( - self.pattern["error"], - name=self.id, - value=self.value, - ) + return value - def _value_post_validator(self): - if not self.redact: - return self.value + @classmethod + def _value_post_validator(cls, value: Any, field: "ModelField") -> Any: + extras = field.field_info.extra + + if value is None and extras["none_as_empty_str"]: + value = "" + + if not extras.get("redact"): + return value # Tell the operation_logger to redact all password-type / secret args # Also redact the % escaped version of the password that might appear in # the 'args' section of metadata (relevant for password with non-alphanumeric char) data_to_redact = [] - if self.value and isinstance(self.value, str): - data_to_redact.append(self.value) - if self.current_value and isinstance(self.current_value, str): - data_to_redact.append(self.current_value) + if value and isinstance(value, str): + data_to_redact.append(value) + data_to_redact += [ urllib.parse.quote(data) for data in data_to_redact @@ -508,7 +510,7 @@ class BaseInputOption(BaseOption): for operation_logger in OperationLogger._instances: operation_logger.data_to_redact.extend(data_to_redact) - return self.value + return value # ─ STRINGS ─────────────────────────────────────────────── @@ -561,19 +563,25 @@ class PasswordOption(BaseInputOption): return attrs - def _value_pre_validator(self): - super()._value_pre_validator() + @classmethod + def _value_pre_validator( + cls, value: Union[str, None], field: "ModelField" + ) -> Union[str, None]: + value = super()._value_pre_validator(value, field) - if self.value not in [None, ""]: - if any(char in self.value for char in self._forbidden_chars): + if value is not None and value != "": + forbidden_chars: str = field.field_info.extra["forbidden_chars"] + if any(char in value for char in forbidden_chars): raise YunohostValidationError( - "pattern_password_app", forbidden_chars=self._forbidden_chars + "pattern_password_app", forbidden_chars=forbidden_chars ) # If it's an optional argument the value should be empty or strong enough from yunohost.utils.password import assert_password_is_strong_enough - assert_password_is_strong_enough("user", self.value) + assert_password_is_strong_enough("user", value) + + return value class ColorOption(BaseInputOption): @@ -581,6 +589,29 @@ class ColorOption(BaseInputOption): default: Union[str, None] _annotation = Color + @staticmethod + def humanize(value: Union[Color, str, None], option={}) -> str: + if isinstance(value, Color): + value.as_named(fallback=True) + + return super(ColorOption, ColorOption).humanize(value, option) + + @staticmethod + def normalize(value: Union[Color, str, None], option={}) -> str: + if isinstance(value, Color): + return value.as_hex() + + return super(ColorOption, ColorOption).normalize(value, option) + + @classmethod + def _value_post_validator( + cls, value: Union[Color, None], field: "ModelField" + ) -> Union[str, None]: + if isinstance(value, Color): + return value.as_hex() + + return super()._value_post_validator(value, field) + # ─ NUMERIC ─────────────────────────────────────────────── @@ -623,24 +654,16 @@ class NumberOption(BaseInputOption): return attrs - def _value_pre_validator(self): - super()._value_pre_validator() - if self.value in [None, ""]: - return + @classmethod + def _value_pre_validator( + cls, value: Union[int, None], field: "ModelField" + ) -> Union[int, None]: + value = super()._value_pre_validator(value, field) - if self.min is not None and int(self.value) < self.min: - raise YunohostValidationError( - "app_argument_invalid", - name=self.id, - error=m18n.n("invalid_number_min", min=self.min), - ) + if value is None: + return None - if self.max is not None and int(self.value) > self.max: - raise YunohostValidationError( - "app_argument_invalid", - name=self.id, - error=m18n.n("invalid_number_max", max=self.max), - ) + return value # ─ BOOLEAN ─────────────────────────────────────────────── @@ -654,6 +677,7 @@ class BooleanOption(BaseInputOption): _annotation = Union[bool, int, str] _yes_answers: set[str] = {"1", "yes", "y", "true", "t", "on"} _no_answers: set[str] = {"0", "no", "n", "false", "f", "off"} + _none_as_empty_str = False @staticmethod def humanize(value, option={}): @@ -737,6 +761,15 @@ class BooleanOption(BaseInputOption): return message + @classmethod + def _value_post_validator( + cls, value: Union[bool, None], field: "ModelField" + ) -> Any: + if isinstance(value, bool): + return field.field_info.extra["parse"][value] + + return super()._value_post_validator(value, field) + # ─ TIME ────────────────────────────────────────────────── @@ -746,14 +779,14 @@ class DateOption(BaseInputOption): default: Union[str, None] _annotation = datetime.date - def _value_pre_validator(self): - super()._value_pre_validator() + @classmethod + def _value_post_validator( + cls, value: Union[datetime.date, None], field: "ModelField" + ) -> Union[str, None]: + if isinstance(value, datetime.date): + return value.isoformat() - if self.value not in [None, ""]: - try: - datetime.strptime(self.value, "%Y-%m-%d") - except ValueError: - raise YunohostValidationError("config_validate_date") + return super()._value_post_validator(value, field) class TimeOption(BaseInputOption): @@ -761,6 +794,16 @@ class TimeOption(BaseInputOption): default: Union[str, int, None] _annotation = datetime.time + @classmethod + def _value_post_validator( + cls, value: Union[datetime.date, None], field: "ModelField" + ) -> Union[str, None]: + if isinstance(value, datetime.time): + # FIXME could use `value.isoformat()` to get `%H:%M:%S` + return value.strftime("%H:%M") + + return super()._value_post_validator(value, field) + # ─ LOCATIONS ───────────────────────────────────────────── @@ -780,6 +823,9 @@ class WebPathOption(BaseInputOption): def normalize(value, option={}): option = option.dict() if isinstance(option, BaseOption) else option + if value is None: + value = "" + if not isinstance(value, str): raise YunohostValidationError( "app_argument_invalid", @@ -828,52 +874,40 @@ class FileOption(BaseInputOption): if os.path.exists(upload_dir): shutil.rmtree(upload_dir) - def _value_pre_validator(self): - if self.value is None: - self.value = self.current_value - - super()._value_pre_validator() - - # Validation should have already failed if required - if self.value in [None, ""]: - return self.value - - if Moulinette.interface.type != "api": - if not os.path.exists(str(self.value)) or not os.path.isfile( - str(self.value) - ): - raise YunohostValidationError( - "app_argument_invalid", - name=self.id, - error=m18n.n("file_does_not_exist", path=str(self.value)), - ) - - def _value_post_validator(self): + @classmethod + def _value_post_validator(cls, value: Any, field: "ModelField") -> Any: from base64 import b64decode - if not self.value: + if not value: return "" + def is_file_path(s): + return ( + isinstance(s, str) + and s.startswith("/") + and os.path.exists(s) + and os.path.isfile(s) + ) + + file_exists = is_file_path(value) + if Moulinette.interface.type != "api" and not file_exists: + # FIXME error + raise YunohostValidationError("File doesn't exists", raw_msg=True) + elif file_exists: + content = read_file(str(value), file_mode="rb") + else: + content = b64decode(value) + upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_") _, file_path = tempfile.mkstemp(dir=upload_dir) FileOption._upload_dirs.add(upload_dir) - logger.debug(f"Saving file {self.id} for file question into {file_path}") - - def is_file_path(s): - return isinstance(s, str) and s.startswith("/") and os.path.exists(s) - - if Moulinette.interface.type != "api" or is_file_path(self.value): - content = read_file(str(self.value), file_mode="rb") - else: - content = b64decode(self.value) + logger.debug(f"Saving file {field.name} for file question into {file_path}") write_to_file(file_path, content, file_mode="wb") - self.value = file_path - - return self.value + return file_path # ─ CHOICES ─────────────────────────────────────────────── @@ -895,6 +929,13 @@ class BaseChoicesOption(BaseInputOption): # FIXME probably forbid choices to be None? choices: Union[dict[str, Any], list[Any], None] + @validator("choices", pre=True) + def parse_comalist_choices(value: Any) -> Union[dict[str, Any], list[Any], None]: + if isinstance(value, str): + values = [value.strip() for value in value.split(",")] + return [value for value in values if value] + return value + @property def _dynamic_annotation(self) -> Union[object, Type[str]]: if self.choices is not None: @@ -937,19 +978,6 @@ class BaseChoicesOption(BaseInputOption): return message - def _value_pre_validator(self): - super()._value_pre_validator() - - # we have an answer, do some post checks - if self.value not in [None, ""]: - if self.choices and self.value not in self.choices: - raise YunohostValidationError( - "app_argument_choice_invalid", - name=self.id, - value=self.value, - choices=", ".join(str(choice) for choice in self.choices), - ) - class SelectOption(BaseChoicesOption): type: Literal[OptionType.select] = OptionType.select @@ -969,6 +997,8 @@ class TagsOption(BaseChoicesOption): def humanize(value, option={}): if isinstance(value, list): return ",".join(str(v) for v in value) + if not value: + return "" return value @staticmethod @@ -976,7 +1006,9 @@ class TagsOption(BaseChoicesOption): if isinstance(value, list): return ",".join(str(v) for v in value) if isinstance(value, str): - value = value.strip() + value = value.strip().strip(",") + if value is None or value == "": + return "" return value @property @@ -999,36 +1031,37 @@ class TagsOption(BaseChoicesOption): return attrs - def _value_pre_validator(self): - values = self.value - if isinstance(values, str): - values = values.split(",") - elif values is None: - values = [] + @classmethod + def _value_pre_validator( + cls, value: Union[list, str, None], field: "ModelField" + ) -> Union[str, None]: + if value is None or value == "": + return None - if not isinstance(values, list): - if self.choices: - raise YunohostValidationError( - "app_argument_choice_invalid", - name=self.id, - value=self.value, - choices=", ".join(str(choice) for choice in self.choices), - ) + if not isinstance(value, (list, str, type(None))): raise YunohostValidationError( "app_argument_invalid", - name=self.id, - error=f"'{str(self.value)}' is not a list", + name=field.name, + error=f"'{str(value)}' is not a list", ) - for value in values: - self.value = value - super()._value_pre_validator() - self.value = values + if isinstance(value, str): + value = [v.strip() for v in value.split(",")] + value = [v for v in value if v] - def _value_post_validator(self): - if isinstance(self.value, list): - self.value = ",".join(self.value) - return super()._value_post_validator() + if isinstance(value, list): + choices = field.field_info.extra.get("choices") + if choices: + if not all(v in choices for v in value): + raise YunohostValidationError( + "app_argument_choice_invalid", + name=field.name, + value=value, + choices=", ".join(str(choice) for choice in choices), + ) + + return ",".join(str(v) for v in value) + return value # ─ ENTITIES ────────────────────────────────────────────── From c428ba616a3f2d63a22257560345429f0af29729 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 17 Apr 2023 15:37:56 +0200 Subject: [PATCH 0328/1116] test:options: update tests results to pydantic parsing --- src/tests/test_questions.py | 374 +++++++++++++++++++----------------- 1 file changed, 197 insertions(+), 177 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 9eceedcaf..1f8667adf 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -20,7 +20,6 @@ from yunohost.utils.form import ( BaseChoicesOption, BaseInputOption, BaseReadonlyOption, - PasswordOption, DomainOption, WebPathOption, BooleanOption, @@ -436,6 +435,10 @@ class BaseTest: @classmethod def _test_basic_attrs(self): raw_option = self.get_raw_option(optional=True) + + if raw_option["type"] == "select": + raw_option["choices"] = ["one"] + id_ = raw_option["id"] option, value = _fill_or_prompt_one_option(raw_option, None) @@ -481,6 +484,7 @@ class BaseTest: base_raw_option = prefill_data["raw_option"] prefill = prefill_data["prefill"] + # FIXME could patch prompt with prefill if we switch to "do not apply default if value is None|''" with patch_prompt("") as prompt: raw_option = self.get_raw_option( raw_option=base_raw_option, @@ -583,9 +587,7 @@ class TestAlert(TestDisplayText): (None, None, {"ask": "Some text\na new line"}), (None, None, {"ask": {"en": "Some text\na new line", "fr": "Un peu de texte\nune nouvelle ligne"}}), *[(None, None, {"ask": "question", "style": style}) for style in ("success", "info", "warning", "danger")], - *xpass(scenarios=[ - (None, None, {"ask": "question", "style": "nimp"}), - ], reason="Should fail, wrong style"), + (None, FAIL, {"ask": "question", "style": "nimp"}), ] # fmt: on @@ -643,11 +645,15 @@ class TestString(BaseTest): scenarios = [ *nones(None, "", output=""), # basic typed values - *unchanged(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), # FIXME should output as str? + (False, "False"), + (True, "True"), + (0, "0"), + (1, "1"), + (-1, "-1"), + (1337, "1337"), + (13.37, "13.37"), + *all_fails([], ["one"], {}), *unchanged("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", raw_option={"optional": True}), - *xpass(scenarios=[ - ([], []), - ], reason="Should fail"), # test strip ("value", "value"), ("value\n", "value"), @@ -660,7 +666,7 @@ class TestString(BaseTest): (" ##value \n \tvalue\n ", "##value \n \tvalue"), ], reason=r"should fail or without `\n`?"), # readonly - ("overwrite", "expected value", {"readonly": True, "current_value": "expected value"}), + ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), # FIXME do we want to fail instead? ] # fmt: on @@ -680,11 +686,15 @@ class TestText(BaseTest): scenarios = [ *nones(None, "", output=""), # basic typed values - *unchanged(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), # FIXME should fail or output as str? + (False, "False"), + (True, "True"), + (0, "0"), + (1, "1"), + (-1, "-1"), + (1337, "1337"), + (13.37, "13.37"), + *all_fails([], ["one"], {}), *unchanged("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", raw_option={"optional": True}), - *xpass(scenarios=[ - ([], []) - ], reason="Should fail"), ("value", "value"), ("value\n value", "value\n value"), # test no strip @@ -697,7 +707,7 @@ class TestText(BaseTest): (r" ##value \n \tvalue\n ", r"##value \n \tvalue\n"), ], reason="Should not be stripped"), # readonly - ("overwrite", "expected value", {"readonly": True, "current_value": "expected value"}), + ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), ] # fmt: on @@ -715,7 +725,7 @@ class TestPassword(BaseTest): } # fmt: off scenarios = [ - *all_fails(False, True, 0, 1, -1, 1337, 13.37, raw_option={"optional": True}, error=TypeError), # FIXME those fails with TypeError + *all_fails(False, True, 0, 1, -1, 1337, 13.37, raw_option={"optional": True}), *all_fails([], ["one"], {}, raw_option={"optional": True}, error=AttributeError), # FIXME those fails with AttributeError *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), *nones(None, "", output=""), @@ -729,9 +739,9 @@ class TestPassword(BaseTest): ], reason="Should output exactly the same"), ("s3cr3t!!", "s3cr3t!!"), ("secret", FAIL), - *[("supersecret" + char, FAIL) for char in PasswordOption.forbidden_chars], # FIXME maybe add ` \n` to the list? + *[("supersecret" + char, FAIL) for char in FORBIDDEN_PASSWORD_CHARS], # FIXME maybe add ` \n` to the list? # readonly - ("s3cr3t!!", YunohostError, {"readonly": True, "current_value": "isforbidden"}), # readonly is forbidden + ("s3cr3t!!", FAIL, {"readonly": True, "current_value": "isforbidden"}), # readonly is forbidden ] # fmt: on @@ -744,35 +754,31 @@ class TestPassword(BaseTest): class TestColor(BaseTest): raw_option = {"type": "color", "id": "color_id"} prefill = { - "raw_option": {"default": "#ff0000"}, - "prefill": "#ff0000", - # "intake": "#ff00ff", + "raw_option": {"default": "red"}, + "prefill": "red", } # fmt: off scenarios = [ *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), - *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "0", "1", "-1", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), *nones(None, "", output=""), # custom valid - ("#000000", "#000000"), + (" #fe1 ", "#fe1"), + ("#000000", "#000"), ("#000", "#000"), - ("#fe100", "#fe100"), - (" #fe100 ", "#fe100"), - ("#ABCDEF", "#ABCDEF"), + ("#ABCDEF", "#abcdef"), + ('1337', "#1337"), # rgba=(17, 51, 51, 0.47) + ("000000", "#000"), + ("#feaf", "#fea"), # `#feaf` is `#fea` with alpha at `f|100%` -> equivalent to `#fea` + # named + ("red", "#f00"), + ("yellow", "#ff0"), # custom fail - *xpass(scenarios=[ - ("#feaf", "#feaf"), - ], reason="Should fail; not a legal color value"), - ("000000", FAIL), ("#12", FAIL), ("#gggggg", FAIL), ("#01010101af", FAIL), - *xfail(scenarios=[ - ("red", "#ff0000"), - ("yellow", "#ffff00"), - ], reason="Should work with pydantic"), # readonly - ("#ffff00", "#fe100", {"readonly": True, "current_value": "#fe100"}), + ("#ffff00", "#000", {"readonly": True, "default": "#000"}), ] # fmt: on @@ -796,10 +802,8 @@ class TestNumber(BaseTest): *nones(None, "", output=None), *unchanged(0, 1, -1, 1337), - *xpass(scenarios=[(False, False)], reason="should fail or output as `0`"), - *xpass(scenarios=[(True, True)], reason="should fail or output as `1`"), - *all_as("0", 0, output=0), - *all_as("1", 1, output=1), + *all_as(False, "0", 0, output=0), # FIXME should `False` fail instead? + *all_as(True, "1", 1, output=1), # FIXME should `True` fail instead? *all_as("1337", 1337, output=1337), *xfail(scenarios=[ ("-1", -1) @@ -814,7 +818,7 @@ class TestNumber(BaseTest): (-10, -10, {"default": 10}), (-10, -10, {"default": 10, "optional": True}), # readonly - (1337, 10000, {"readonly": True, "current_value": 10000}), + (1337, 10000, {"readonly": True, "default": "10000"}), ] # fmt: on # FIXME should `step` be some kind of "multiple of"? @@ -839,14 +843,20 @@ class TestBoolean(BaseTest): *all_fails("none", "None"), # FIXME should output as `0` (default) like other none values when required? *all_as(None, "", output=0, raw_option={"optional": True}), # FIXME should output as `None`? *all_as("none", "None", output=None, raw_option={"optional": True}), - # FIXME even if default is explicity `None|""`, it ends up with class_default `0` - *all_as(None, "", output=0, raw_option={"default": None}), # FIXME this should fail, default is `None` - *all_as(None, "", output=0, raw_option={"optional": True, "default": None}), # FIXME even if default is explicity None, it ends up with class_default - *all_as(None, "", output=0, raw_option={"default": ""}), # FIXME this should fail, default is `""` - *all_as(None, "", output=0, raw_option={"optional": True, "default": ""}), # FIXME even if default is explicity None, it ends up with class_default - # With "none" behavior is ok - *all_fails(None, "", raw_option={"default": "none"}), - *all_as(None, "", output=None, raw_option={"optional": True, "default": "none"}), + { + "raw_options": [ + {"default": None}, + {"default": ""}, + {"default": "none"}, + {"default": "None"} + ], + "scenarios": [ + # All none values fails if default is overriden + *all_fails(None, "", "none", "None"), + # All none values ends up as None if default is overriden + *all_as(None, "", "none", "None", output=None, raw_option={"optional": True}), + ] + }, # Unhandled types should fail *all_fails(1337, "1337", "string", [], "[]", ",", "one,two"), *all_fails(1337, "1337", "string", [], "[]", ",", "one,two", {"optional": True}), @@ -879,7 +889,7 @@ class TestBoolean(BaseTest): "scenarios": all_fails("", "y", "n", error=AssertionError), }, # readonly - (1, 0, {"readonly": True, "current_value": 0}), + (1, 0, {"readonly": True, "default": 0}), ] @@ -896,8 +906,12 @@ class TestDate(BaseTest): } # fmt: off scenarios = [ - *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), - *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + # Those passes since False|True are parsed as 0|1 then int|float are considered a timestamp in seconds which ends up as default Unix date + *all_as(False, True, 0, 1, 1337, 13.37, "0", "1", "1337", "13.37", output="1970-01-01"), + # Those are negative one second timestamp ending up as Unix date - 1 sec (so day change) + *all_as(-1, "-1", output="1969-12-31"), + *all_fails([], ["one"], {}, raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), *nones(None, "", output=""), # custom valid ("2070-12-31", "2070-12-31"), @@ -906,18 +920,16 @@ class TestDate(BaseTest): ("2025-06-15T13:45:30", "2025-06-15"), ("2025-06-15 13:45:30", "2025-06-15") ], reason="iso date repr should be valid and extra data striped"), - *xfail(scenarios=[ - (1749938400, "2025-06-15"), - (1749938400.0, "2025-06-15"), - ("1749938400", "2025-06-15"), - ("1749938400.0", "2025-06-15"), - ], reason="timestamp could be an accepted value"), + (1749938400, "2025-06-14"), + (1749938400.0, "2025-06-14"), + ("1749938400", "2025-06-14"), + ("1749938400.0", "2025-06-14"), # custom invalid ("29-12-2070", FAIL), ("12-01-10", FAIL), ("2022-02-29", FAIL), # readonly - ("2070-12-31", "2024-02-29", {"readonly": True, "current_value": "2024-02-29"}), + ("2070-12-31", "2024-02-29", {"readonly": True, "default": "2024-02-29"}), ] # fmt: on @@ -935,22 +947,26 @@ class TestTime(BaseTest): } # fmt: off scenarios = [ - *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), - *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + # Those passes since False|True are parsed as 0|1 then int|float are considered a timestamp in seconds but we don't take seconds into account so -> 00:00 + *all_as(False, True, 0, 1, 13.37, "0", "1", "13.37", output="00:00"), + # 1337 seconds == 22 minutes + *all_as(1337, "1337", output="00:22"), + # Negative timestamp fails + *all_fails(-1, "-1", error=OverflowError), # FIXME should handle that as a validation error + # *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), *nones(None, "", output=""), # custom valid *unchanged("00:00", "08:00", "12:19", "20:59", "23:59"), - ("3:00", "3:00"), # FIXME should fail or output as `"03:00"`? - *xfail(scenarios=[ - ("22:35:05", "22:35"), - ("22:35:03.514", "22:35"), - ], reason="time as iso format could be valid"), + ("3:00", "03:00"), + ("23:1", "23:01"), + ("22:35:05", "22:35"), + ("22:35:03.514", "22:35"), # custom invalid ("24:00", FAIL), - ("23:1", FAIL), ("23:005", FAIL), # readonly - ("00:00", "08:00", {"readonly": True, "current_value": "08:00"}), + ("00:00", "08:00", {"readonly": True, "default": "08:00"}), ] # fmt: on @@ -973,72 +989,75 @@ class TestEmail(BaseTest): *nones(None, "", output=""), ("\n Abc@example.tld ", "Abc@example.tld"), + *xfail(scenarios=[("admin@ynh.local", "admin@ynh.local")], reason="Should this pass?"), # readonly - ("Abc@example.tld", "admin@ynh.local", {"readonly": True, "current_value": "admin@ynh.local"}), + ("Abc@example.tld", "admin@ynh.org", {"readonly": True, "default": "admin@ynh.org"}), # Next examples are from https://github.com/JoshData/python-email-validator/blob/main/tests/test_syntax.py # valid email values - ("Abc@example.tld", "Abc@example.tld"), - ("Abc.123@test-example.com", "Abc.123@test-example.com"), - ("user+mailbox/department=shipping@example.tld", "user+mailbox/department=shipping@example.tld"), - ("伊昭傑@郵件.商務", "伊昭傑@郵件.商務"), - ("राम@मोहन.ईन्फो", "राम@मोहन.ईन्फो"), - ("юзер@екзампл.ком", "юзер@екзампл.ком"), - ("θσερ@εχαμπλε.ψομ", "θσερ@εχαμπλε.ψομ"), - ("葉士豪@臺網中心.tw", "葉士豪@臺網中心.tw"), - ("jeff@臺網中心.tw", "jeff@臺網中心.tw"), - ("葉士豪@臺網中心.台灣", "葉士豪@臺網中心.台灣"), - ("jeff葉@臺網中心.tw", "jeff葉@臺網中心.tw"), - ("ñoñó@example.tld", "ñoñó@example.tld"), - ("甲斐黒川日本@example.tld", "甲斐黒川日本@example.tld"), - ("чебурашкаящик-с-апельсинами.рф@example.tld", "чебурашкаящик-с-апельсинами.рф@example.tld"), - ("उदाहरण.परीक्ष@domain.with.idn.tld", "उदाहरण.परीक्ष@domain.with.idn.tld"), - ("ιωάννης@εεττ.gr", "ιωάννης@εεττ.gr"), + *unchanged( + "Abc@example.tld", + "Abc.123@test-example.com", + "user+mailbox/department=shipping@example.tld", + "伊昭傑@郵件.商務", + "राम@मोहन.ईन्फो", + "юзер@екзампл.ком", + "θσερ@εχαμπλε.ψομ", + "葉士豪@臺網中心.tw", + "jeff@臺網中心.tw", + "葉士豪@臺網中心.台灣", + "jeff葉@臺網中心.tw", + "ñoñó@example.tld", + "甲斐黒川日本@example.tld", + "чебурашкаящик-с-апельсинами.рф@example.tld", + "उदाहरण.परीक्ष@domain.with.idn.tld", + "ιωάννης@εεττ.gr", + ), # invalid email (Hiding because our current regex is very permissive) - # ("my@localhost", FAIL), - # ("my@.leadingdot.com", FAIL), - # ("my@.leadingfwdot.com", FAIL), - # ("my@twodots..com", FAIL), - # ("my@twofwdots...com", FAIL), - # ("my@trailingdot.com.", FAIL), - # ("my@trailingfwdot.com.", FAIL), - # ("me@-leadingdash", FAIL), - # ("me@-leadingdashfw", FAIL), - # ("me@trailingdash-", FAIL), - # ("me@trailingdashfw-", FAIL), - # ("my@baddash.-.com", FAIL), - # ("my@baddash.-a.com", FAIL), - # ("my@baddash.b-.com", FAIL), - # ("my@baddashfw.-.com", FAIL), - # ("my@baddashfw.-a.com", FAIL), - # ("my@baddashfw.b-.com", FAIL), - # ("my@example.com\n", FAIL), - # ("my@example\n.com", FAIL), - # ("me@x!", FAIL), - # ("me@x ", FAIL), - # (".leadingdot@domain.com", FAIL), - # ("twodots..here@domain.com", FAIL), - # ("trailingdot.@domain.email", FAIL), - # ("me@⒈wouldbeinvalid.com", FAIL), - ("@example.com", FAIL), - # ("\nmy@example.com", FAIL), - ("m\ny@example.com", FAIL), - ("my\n@example.com", FAIL), - # ("11111111112222222222333333333344444444445555555555666666666677777@example.com", FAIL), - # ("111111111122222222223333333333444444444455555555556666666666777777@example.com", FAIL), - # ("me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444444444455555555556.com", FAIL), - # ("me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555566.com", FAIL), - # ("me@中1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555566.com", FAIL), - # ("my.long.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444.info", FAIL), - # ("my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333.info", FAIL), - # ("my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info", FAIL), - # ("my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444.info", FAIL), - # ("my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info", FAIL), - # ("me@bad-tld-1", FAIL), - # ("me@bad.tld-2", FAIL), - # ("me@xn--0.tld", FAIL), - # ("me@yy--0.tld", FAIL), - # ("me@yy--0.tld", FAIL), + *all_fails( + "my@localhost", + "my@.leadingdot.com", + "my@.leadingfwdot.com", + "my@twodots..com", + "my@twofwdots...com", + "my@trailingdot.com.", + "my@trailingfwdot.com.", + "me@-leadingdash", + "me@-leadingdashfw", + "me@trailingdash-", + "me@trailingdashfw-", + "my@baddash.-.com", + "my@baddash.-a.com", + "my@baddash.b-.com", + "my@baddashfw.-.com", + "my@baddashfw.-a.com", + "my@baddashfw.b-.com", + "my@example\n.com", + "me@x!", + "me@x ", + ".leadingdot@domain.com", + "twodots..here@domain.com", + "trailingdot.@domain.email", + "me@⒈wouldbeinvalid.com", + "@example.com", + "m\ny@example.com", + "my\n@example.com", + "11111111112222222222333333333344444444445555555555666666666677777@example.com", + "111111111122222222223333333333444444444455555555556666666666777777@example.com", + "me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444444444455555555556.com", + "me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555566.com", + "me@中1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555566.com", + "my.long.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444.info", + "my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333.info", + "my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info", + "my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444.info", + "my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info", + "me@bad-tld-1", + "me@bad.tld-2", + "me@xn--0.tld", + "me@yy--0.tld", + "me@yy--0.tld", + ) ] # fmt: on @@ -1087,7 +1106,7 @@ class TestWebPath(BaseTest): ("https://example.com/folder", "/https://example.com/folder") ], reason="Should fail or scheme+domain removed"), # readonly - ("/overwrite", "/value", {"readonly": True, "current_value": "/value"}), + ("/overwrite", "/value", {"readonly": True, "default": "/value"}), # FIXME should path have forbidden_chars? ] # fmt: on @@ -1111,21 +1130,17 @@ class TestUrl(BaseTest): *nones(None, "", output=""), ("http://some.org/folder/file.txt", "http://some.org/folder/file.txt"), + (' https://www.example.com \n', 'https://www.example.com'), # readonly - ("https://overwrite.org", "https://example.org", {"readonly": True, "current_value": "https://example.org"}), + ("https://overwrite.org", "https://example.org", {"readonly": True, "default": "https://example.org"}), # rest is taken from https://github.com/pydantic/pydantic/blob/main/tests/test_networks.py # valid *unchanged( # Those are valid but not sure how they will output with pydantic 'http://example.org', - 'http://test', - 'http://localhost', 'https://example.org/whatever/next/', 'https://example.org', - 'http://localhost', - 'http://localhost/', - 'http://localhost:8000', - 'http://localhost:8000/', + 'https://foo_bar.example.com/', 'http://example.co.jp', 'http://www.example.com/a%C2%B1b', @@ -1149,29 +1164,31 @@ class TestUrl(BaseTest): 'http://twitter.com/@handle/', 'http://11.11.11.11.example.com/action', 'http://abc.11.11.11.11.example.com/action', - 'http://example#', - 'http://example/#', - 'http://example/#fragment', - 'http://example/?#', 'http://example.org/path#', 'http://example.org/path#fragment', 'http://example.org/path?query#', 'http://example.org/path?query#fragment', + 'https://foo_bar.example.com/', + 'https://exam_ple.com/', + 'HTTP://EXAMPLE.ORG', + 'https://example.org', + 'https://example.org?a=1&b=2', + 'https://example.org#a=3;b=3', + 'https://example.xn--p1ai', + 'https://example.xn--vermgensberatung-pwb', + 'https://example.xn--zfr164b', ), - # Pydantic default parsing add a final `/` - ('https://foo_bar.example.com/', 'https://foo_bar.example.com/'), - ('https://exam_ple.com/', 'https://exam_ple.com/'), *xfail(scenarios=[ - (' https://www.example.com \n', 'https://www.example.com/'), - ('HTTP://EXAMPLE.ORG', 'http://example.org/'), - ('https://example.org', 'https://example.org/'), - ('https://example.org?a=1&b=2', 'https://example.org/?a=1&b=2'), - ('https://example.org#a=3;b=3', 'https://example.org/#a=3;b=3'), - ('https://example.xn--p1ai', 'https://example.xn--p1ai/'), - ('https://example.xn--vermgensberatung-pwb', 'https://example.xn--vermgensberatung-pwb/'), - ('https://example.xn--zfr164b', 'https://example.xn--zfr164b/'), - ], reason="pydantic default behavior would append a final `/`"), - + ('http://test', 'http://test'), + ('http://localhost', 'http://localhost'), + ('http://localhost/', 'http://localhost/'), + ('http://localhost:8000', 'http://localhost:8000'), + ('http://localhost:8000/', 'http://localhost:8000/'), + ('http://example#', 'http://example#'), + ('http://example/#', 'http://example/#'), + ('http://example/#fragment', 'http://example/#fragment'), + ('http://example/?#', 'http://example/?#'), + ], reason="Should this be valid?"), # invalid *all_fails( 'ftp://example.com/', @@ -1182,15 +1199,13 @@ class TestUrl(BaseTest): "/", "+http://example.com/", "ht*tp://example.com/", + "http:///", + "http://??", + "https://example.org more", + "http://2001:db8::ff00:42:8329", + "http://[192.168.1.1]:8329", + "http://example.com:99999", ), - *xpass(scenarios=[ - ("http:///", "http:///"), - ("http://??", "http://??"), - ("https://example.org more", "https://example.org more"), - ("http://2001:db8::ff00:42:8329", "http://2001:db8::ff00:42:8329"), - ("http://[192.168.1.1]:8329", "http://[192.168.1.1]:8329"), - ("http://example.com:99999", "http://example.com:99999"), - ], reason="Should fail"), ] # fmt: on @@ -1361,7 +1376,6 @@ class TestSelect(BaseTest): # [-1, 0, 1] "raw_options": [ {"choices": [-1, 0, 1, 10]}, - {"choices": {-1: "verbose -one", 0: "verbose zero", 1: "verbose one", 10: "verbose ten"}}, ], "scenarios": [ *nones(None, "", output=""), @@ -1375,6 +1389,18 @@ class TestSelect(BaseTest): *all_fails("100", 100), ] }, + { + "raw_options": [ + {"choices": {-1: "verbose -one", 0: "verbose zero", 1: "verbose one", 10: "verbose ten"}}, + {"choices": {"-1": "verbose -one", "0": "verbose zero", "1": "verbose one", "10": "verbose ten"}}, + ], + "scenarios": [ + *nones(None, "", output=""), + *all_fails(-1, 0, 1, 10), # Should pass? converted to str? + *unchanged("-1", "0", "1", "10"), + *all_fails("100", 100), + ] + }, # [True, False, None] *unchanged(True, False, raw_option={"choices": [True, False, None]}), # FIXME we should probably forbid None in choices (None, FAIL, {"choices": [True, False, None]}), @@ -1402,7 +1428,7 @@ class TestSelect(BaseTest): ] }, # readonly - ("one", "two", {"readonly": True, "choices": ["one", "two"], "current_value": "two"}), + ("one", "two", {"readonly": True, "choices": ["one", "two"], "default": "two"}), ] # fmt: on @@ -1411,7 +1437,7 @@ class TestSelect(BaseTest): # │ TAGS │ # ╰───────────────────────────────────────────────────────╯ - +# [], ["one"], {} class TestTags(BaseTest): raw_option = {"type": "tags", "id": "tags_id"} prefill = { @@ -1420,12 +1446,7 @@ class TestTags(BaseTest): } # fmt: off scenarios = [ - *nones(None, [], "", output=""), - # FIXME `","` could be considered a none value which kinda already is since it fail when required - (",", FAIL), - *xpass(scenarios=[ - (",", ",", {"optional": True}) - ], reason="Should output as `''`? ie: None"), + *nones(None, [], "", ",", output=""), { "raw_options": [ {}, @@ -1450,7 +1471,7 @@ class TestTags(BaseTest): *all_fails(*([t] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), *all_fails(*([str(t)] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), # readonly - ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "current_value": "one,two"}), + ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "default": "one,two"}), ] # fmt: on @@ -1566,8 +1587,7 @@ class TestApp(BaseTest): ], "scenarios": [ # FIXME there are currently 3 different nones (`None`, `""` and `_none`), choose one? - *nones(None, output=None), # FIXME Should return chosen none? - *nones("", output=""), # FIXME Should return chosen none? + *nones(None, "", output=""), # FIXME Should return chosen none? *xpass(scenarios=[ ("_none", "_none"), ("_none", "_none", {"default": "_none"}), @@ -1590,7 +1610,7 @@ class TestApp(BaseTest): (installed_webapp["id"], installed_webapp["id"], {"filter": "is_webapp"}), (installed_webapp["id"], FAIL, {"filter": "is_webapp == false"}), (installed_webapp["id"], FAIL, {"filter": "id != 'my_webapp'"}), - (None, None, {"filter": "id == 'fake_app'", "optional": True}), + (None, "", {"filter": "id == 'fake_app'", "optional": True}), ] }, { @@ -1800,7 +1820,7 @@ class TestGroup(BaseTest): ("", "custom_group", {"default": "custom_group"}), ], reason="Should throw 'default must be in (None, 'all_users', 'visitors', 'admins')"), # readonly - ("admins", YunohostError, {"readonly": True}), # readonly is forbidden + ("admins", "all_users", {"readonly": True}), # readonly is forbidden (default is "all_users") ] }, ] @@ -1880,12 +1900,12 @@ def test_options_query_string(): "string_id": "string", "text_id": "text\ntext", "password_id": "sUpRSCRT", - "color_id": "#ffff00", + "color_id": "#ff0", "number_id": 10, "boolean_id": 1, "date_id": "2030-03-06", "time_id": "20:55", - "email_id": "coucou@ynh.local", + "email_id": "coucou@ynh.org", "path_id": "/ynh-dev", "url_id": "https://yunohost.org", "file_id": file_content1, @@ -1908,7 +1928,7 @@ def test_options_query_string(): "&boolean_id=y" "&date_id=2030-03-06" "&time_id=20:55" - "&email_id=coucou@ynh.local" + "&email_id=coucou@ynh.org" "&path_id=ynh-dev/" "&url_id=https://yunohost.org" f"&file_id={file_repr}" From 3ff6e6ed96c91e9ae94821f54f9939811edb930e Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 17 Apr 2023 16:20:52 +0200 Subject: [PATCH 0329/1116] app: update app_install --- src/app.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/app.py b/src/app.py index 754a920a0..1a2e80442 100644 --- a/src/app.py +++ b/src/app.py @@ -1098,13 +1098,9 @@ def app_install( app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) # Retrieve arguments list for install script - raw_questions = manifest["install"] - questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) - args = { - question.id: question.value - for question in questions - if not question.readonly and question.value is not None - } + raw_options = manifest["install"] + options, form = ask_questions_and_parse_answers(raw_options, prefilled_answers=args) + args = form.dict(exclude_none=True) # Validate domain / path availability for webapps # (ideally this should be handled by the resource system for manifest v >= 2 @@ -1141,15 +1137,15 @@ def app_install( "current_revision": manifest.get("remote", {}).get("revision", "?"), } - # If packaging_format v2+, save all install questions as settings + # If packaging_format v2+, save all install options as settings if packaging_format >= 2: - for question in questions: + for option in options: # Except user-provider passwords # ... which we need to reinject later in the env_dict - if question.type == "password": + if option.type == "password": continue - app_settings[question.id] = question.value + app_settings[option.id] = form[option.id] _set_app_settings(app_instance_name, app_settings) @@ -1202,23 +1198,23 @@ def app_install( app_instance_name, args=args, workdir=extracted_app_folder, action="install" ) - # If packaging_format v2+, save all install questions as settings + # If packaging_format v2+, save all install options as settings if packaging_format >= 2: - for question in questions: + for option in options: # Reinject user-provider passwords which are not in the app settings # (cf a few line before) - if question.type == "password": - env_dict[question.id] = question.value + if option.type == "password": + env_dict[option.id] = form[option.id] # We want to hav the env_dict in the log ... but not password values env_dict_for_logging = env_dict.copy() - for question in questions: - # Or should it be more generally question.redact ? - if question.type == "password": - if f"YNH_APP_ARG_{question.id.upper()}" in env_dict_for_logging: - del env_dict_for_logging[f"YNH_APP_ARG_{question.id.upper()}"] - if question.id in env_dict_for_logging: - del env_dict_for_logging[question.id] + for option in options: + # Or should it be more generally option.redact ? + if option.type == "password": + if f"YNH_APP_ARG_{option.id.upper()}" in env_dict_for_logging: + del env_dict_for_logging[f"YNH_APP_ARG_{option.id.upper()}"] + if option.id in env_dict_for_logging: + del env_dict_for_logging[option.id] operation_logger.extra.update({"env": env_dict_for_logging}) From 582b1ed311e5feb0823a2742a1f8b58c64a2165d Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 17 Apr 2023 19:51:16 +0200 Subject: [PATCH 0330/1116] form: add translating method --- src/tests/test_questions.py | 10 ++++----- src/utils/form.py | 41 +++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 1f8667adf..99ab9f156 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -447,7 +447,7 @@ class BaseTest: assert isinstance(option, OPTIONS[raw_option["type"]]) assert option.type == raw_option["type"] assert option.id == id_ - assert option.ask == {"en": id_} + assert option.ask == id_ assert option.readonly is (True if is_special_readonly_option else False) assert option.visible is True # assert option.bind is None @@ -493,7 +493,7 @@ class BaseTest: ) option, value = _fill_or_prompt_one_option(raw_option, None) - expected_message = option.ask["en"] + expected_message = option.ask choices = [] if isinstance(option, BaseChoicesOption): @@ -510,7 +510,7 @@ class BaseTest: prefill=prefill, is_multiline=option.type == "text", autocomplete=choices, - help=option.help["en"], + help=option.help, ) def test_scenarios(self, intake, expected_output, raw_option, data): @@ -558,7 +558,7 @@ class TestDisplayText(BaseTest): options, form = ask_questions_and_parse_answers( {_id: raw_option}, answers ) - assert stdout.getvalue() == f"{options[0].ask['en']}\n" + assert stdout.getvalue() == f"{options[0].ask}\n" # ╭───────────────────────────────────────────────────────╮ @@ -609,7 +609,7 @@ class TestAlert(TestDisplayText): options, form = ask_questions_and_parse_answers( {"display_text_id": raw_option}, answers ) - ask = options[0].ask["en"] + ask = options[0].ask if style in colors: color = colors[style] title = style.title() + (":" if style != "success" else "!") diff --git a/src/utils/form.py b/src/utils/form.py index cbe64b499..71954ee2b 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -297,15 +297,6 @@ class BaseOption(BaseModel): del schema["description"] schema["additionalProperties"] = False - @validator("ask", always=True) - def parse_or_set_default_ask( - cls, value: Union[Translation, None], values: Values - ) -> Translation: - if value is None: - return {"en": values["id"]} - if isinstance(value, str): - return {"en": value} - return value @validator("readonly", pre=True) def can_be_readonly(cls, value: bool, values: Values) -> bool: @@ -327,7 +318,9 @@ class BaseOption(BaseModel): return evaluate_simple_js_expression(self.visible, context=context) def _get_prompt_message(self, value: None) -> str: - return _value_for_locale(self.ask) + # force type to str + # `OptionsModel.translate_options()` should have been called before calling this method + return cast(str, self.ask) # ╭───────────────────────────────────────────────────────╮ @@ -367,7 +360,7 @@ class AlertOption(BaseReadonlyOption): State.danger: "red", } message = m18n.g(self.style) if self.style != State.danger else m18n.n("danger") - return f"{colorize(message, colors[self.style])} {_value_for_locale(self.ask)}" + return f"{colorize(message, colors[self.style])} {self.ask}" class ButtonOption(BaseReadonlyOption): @@ -624,6 +617,7 @@ class NumberOption(BaseInputOption): max: Union[int, None] = None step: Union[int, None] = None _annotation = int + _none_as_empty_str = False @staticmethod def normalize(value, option={}): @@ -1274,6 +1268,26 @@ class OptionsModel(BaseModel): def __init__(self, **kwargs) -> None: super().__init__(options=self.options_dict_to_list(kwargs)) + def translate_options(self, i18n_key: Union[str, None] = None): + """ + Mutate in place translatable attributes of options to their translations + """ + for option in self.options: + for key in ("ask", "help"): + if not hasattr(option, key): + continue + + value = getattr(option, key) + if value: + setattr(option, key, _value_for_locale(value)) + elif key == "ask" and m18n.key_exists(f"{i18n_key}_{option.id}"): + setattr(option, key, m18n.n(f"{i18n_key}_{option.id}")) + elif key == "help" and m18n.key_exists(f"{i18n_key}_{option.id}_help"): + setattr(option, key, m18n.n(f"{i18n_key}_{option.id}_help")) + elif key == "ask": + # FIXME warn? + option.ask = option.id + class FormModel(BaseModel): """ @@ -1384,7 +1398,7 @@ def prompt_or_validate_form( raise YunohostValidationError( "config_action_disabled", action=option.id, - help=_value_for_locale(option.help), + help=option.help, ) if not option.is_visible(context): @@ -1433,7 +1447,7 @@ def prompt_or_validate_form( prefill=value, is_multiline=isinstance(option, TextOption), autocomplete=choices, - help=_value_for_locale(option.help), + help=option.help, ) # Apply default value if none @@ -1512,6 +1526,7 @@ def ask_questions_and_parse_answers( # FIXME use YunohostError instead since it is not really a user mistake? raise YunohostValidationError(error, raw_msg=True) + model.translate_options() # Build the form from those questions and instantiate it without # parsing/validation (construct) since it may contains required questions. form = build_form(model.options).construct() From a574855a039b0ef39cd459690e0ac62c31ae7cda Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 17 Apr 2023 20:01:31 +0200 Subject: [PATCH 0331/1116] form: fix forbidden readonly type --- src/tests/test_questions.py | 10 +++++----- src/utils/form.py | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 99ab9f156..959f2c8b7 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -741,7 +741,7 @@ class TestPassword(BaseTest): ("secret", FAIL), *[("supersecret" + char, FAIL) for char in FORBIDDEN_PASSWORD_CHARS], # FIXME maybe add ` \n` to the list? # readonly - ("s3cr3t!!", FAIL, {"readonly": True, "current_value": "isforbidden"}), # readonly is forbidden + ("s3cr3t!!", FAIL, {"readonly": True}), # readonly is forbidden ] # fmt: on @@ -1519,7 +1519,7 @@ class TestDomain(BaseTest): ("doesnt_exist.pouet", FAIL, {}), ("fake.com", FAIL, {"choices": ["fake.com"]}), # readonly - (domains1[0], YunohostError, {"readonly": True}), # readonly is forbidden + (domains1[0], FAIL, {"readonly": True}), # readonly is forbidden ] }, { @@ -1619,7 +1619,7 @@ class TestApp(BaseTest): (installed_non_webapp["id"], installed_non_webapp["id"]), (installed_non_webapp["id"], FAIL, {"filter": "is_webapp"}), # readonly - (installed_non_webapp["id"], YunohostError, {"readonly": True}), # readonly is forbidden + (installed_non_webapp["id"], FAIL, {"readonly": True}), # readonly is forbidden ] }, ] @@ -1736,7 +1736,7 @@ class TestUser(BaseTest): ("", regular_username, {"default": regular_username}) ], reason="Should throw 'no default allowed'"), # readonly - (admin_username, YunohostError, {"readonly": True}), # readonly is forbidden + (admin_username, FAIL, {"readonly": True}), # readonly is forbidden ] }, ] @@ -1820,7 +1820,7 @@ class TestGroup(BaseTest): ("", "custom_group", {"default": "custom_group"}), ], reason="Should throw 'default must be in (None, 'all_users', 'visitors', 'admins')"), # readonly - ("admins", "all_users", {"readonly": True}), # readonly is forbidden (default is "all_users") + ("admins", FAIL, {"readonly": True}), # readonly is forbidden ] }, ] diff --git a/src/utils/form.py b/src/utils/form.py index 71954ee2b..6246a91e5 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -300,8 +300,7 @@ class BaseOption(BaseModel): @validator("readonly", pre=True) def can_be_readonly(cls, value: bool, values: Values) -> bool: - forbidden_types = ("password", "app", "domain", "user", "file") - if value is True and values["type"] in forbidden_types: + if value is True and values["type"] in FORBIDDEN_READONLY_TYPES: raise ValueError( m18n.n( "config_forbidden_readonly_type", From 774b11cbbeffcc2480b146f8557143876d992509 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 17 Apr 2023 20:02:02 +0200 Subject: [PATCH 0332/1116] form: add legacy "name" attr --- src/utils/form.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/form.py b/src/utils/form.py index 6246a91e5..7e009c5b5 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -285,6 +285,7 @@ class BaseOption(BaseModel): readonly: bool = False visible: Union[JSExpression, bool] = True bind: Union[str, None] = None + name: Union[str, None] = None # LEGACY (replaced by `id`) class Config: arbitrary_types_allowed = True @@ -297,6 +298,12 @@ class BaseOption(BaseModel): del schema["description"] schema["additionalProperties"] = False + # FIXME Legacy, is `name` still needed? + @validator("name", pre=True, always=True) + def apply_legacy_name(cls, value: Union[str, None], values: Values) -> str: + if value is None: + return values["id"] + return value @validator("readonly", pre=True) def can_be_readonly(cls, value: bool, values: Values) -> bool: From bec34b92b065f964b88be37eef7c79bafa4598fc Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 17 Apr 2023 20:03:35 +0200 Subject: [PATCH 0333/1116] form: add reserved "id" validator --- src/utils/configpanel.py | 25 ------------------------- src/utils/form.py | 27 ++++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 86dea2e7d..53cd4b9c8 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -455,31 +455,6 @@ class ConfigPanel: "config_unknown_filter_key", filter_key=self.filter_key ) - # List forbidden keywords from helpers and sections toml (to avoid conflict) - forbidden_keywords = [ - "old", - "app", - "changed", - "file_hash", - "binds", - "types", - "formats", - "getter", - "setter", - "short_setting", - "type", - "bind", - "nothing_changed", - "changes_validated", - "result", - "max_progression", - ] - forbidden_keywords += format_description["sections"] - - for _, _, option in self._iterate(): - if option["id"] in forbidden_keywords: - raise YunohostError("config_forbidden_keyword", keyword=option["id"]) - return self.config def _get_default_values(self): diff --git a/src/utils/form.py b/src/utils/form.py index 7e009c5b5..6c14bcdf0 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -265,7 +265,26 @@ FORBIDDEN_READONLY_TYPES = { OptionType.user, OptionType.group, } - +FORBIDDEN_KEYWORDS = { + "old", + "app", + "changed", + "file_hash", + "binds", + "types", + "formats", + "getter", + "setter", + "short_setting", + "type", + "bind", + "nothing_changed", + "changes_validated", + "result", + "max_progression", + "properties", + "defaults", +} Context = dict[str, Any] Translation = Union[dict[str, str], str] @@ -298,6 +317,12 @@ class BaseOption(BaseModel): del schema["description"] schema["additionalProperties"] = False + @validator("id", pre=True) + def check_id_is_not_forbidden(cls, value: str) -> str: + if value in FORBIDDEN_KEYWORDS: + raise ValueError(m18n.n("config_forbidden_keyword", keyword=value)) + return value + # FIXME Legacy, is `name` still needed? @validator("name", pre=True, always=True) def apply_legacy_name(cls, value: Union[str, None], values: Values) -> str: From 564a66de2fab6b29b6d4457ea5e5b9084fdfbb0c Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 17 Apr 2023 20:08:52 +0200 Subject: [PATCH 0334/1116] configpanel: add config panel models --- src/utils/configpanel.py | 192 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 190 insertions(+), 2 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 53cd4b9c8..bf441798c 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -21,8 +21,10 @@ import os import re import urllib.parse from collections import OrderedDict -from typing import Union from logging import getLogger +from typing import TYPE_CHECKING, Any, Literal, Sequence, Type, Union + +from pydantic import BaseModel, Extra, validator from moulinette import Moulinette, m18n from moulinette.interfaces.cli import colorize @@ -30,20 +32,206 @@ from moulinette.utils.filesystem import mkdir, read_toml, read_yaml, write_to_ya from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.form import ( OPTIONS, - BaseChoicesOption, BaseInputOption, BaseOption, + BaseReadonlyOption, FileOption, + OptionsModel, OptionType, + Translation, ask_questions_and_parse_answers, evaluate_simple_js_expression, ) from yunohost.utils.i18n import _value_for_locale +if TYPE_CHECKING: + from pydantic.fields import ModelField + logger = getLogger("yunohost.configpanel") + + +# ╭───────────────────────────────────────────────────────╮ +# │ ╭╮╮╭─╮┌─╮┌─╴╷ ╭─╴ │ +# │ ││││ ││ │├─╴│ ╰─╮ │ +# │ ╵╵╵╰─╯└─╯╰─╴╰─╴╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + CONFIG_PANEL_VERSION_SUPPORTED = 1.0 +class ContainerModel(BaseModel): + id: str + name: Union[Translation, None] = None + services: list[str] = [] + help: Union[Translation, None] = None + + def translate(self, i18n_key: Union[str, None] = None): + """ + Translate `ask` and `name` attributes of panels and section. + This is in place mutation. + """ + + for key in ("help", "name"): + value = getattr(self, key) + if value: + setattr(self, key, _value_for_locale(value)) + elif key == "help" and m18n.key_exists(f"{i18n_key}_{self.id}_help"): + setattr(self, key, m18n.n(f"{i18n_key}_{self.id}_help")) + + +class SectionModel(ContainerModel, OptionsModel): + visible: Union[bool, str] = True + optional: bool = True + + # Don't forget to pass arguments to super init + def __init__( + self, + id: str, + name: Union[Translation, None] = None, + services: list[str] = [], + help: Union[Translation, None] = None, + visible: Union[bool, str] = True, + **kwargs, + ) -> None: + options = self.options_dict_to_list(kwargs, defaults={"optional": True}) + + ContainerModel.__init__( + self, + id=id, + name=name, + services=services, + help=help, + visible=visible, + options=options, + ) + + @property + def is_action_section(self): + return any([option.type is OptionType.button for option in self.options]) + + def is_visible(self, context: dict[str, Any]): + if isinstance(self.visible, bool): + return self.visible + + return evaluate_simple_js_expression(self.visible, context=context) + + def translate(self, i18n_key: Union[str, None] = None): + """ + Call to `Container`'s `translate` for self translation + + Call to `OptionsContainer`'s `translate_options` for options translation + """ + super().translate(i18n_key) + self.translate_options(i18n_key) + + +class PanelModel(ContainerModel): + # FIXME what to do with `actions? + actions: dict[str, Translation] = {"apply": {"en": "Apply"}} + sections: list[SectionModel] + + class Config: + extra = Extra.allow + + # Don't forget to pass arguments to super init + def __init__( + self, + id: str, + name: Union[Translation, None] = None, + services: list[str] = [], + help: Union[Translation, None] = None, + **kwargs, + ) -> None: + sections = [data | {"id": name} for name, data in kwargs.items()] + super().__init__( + id=id, name=name, services=services, help=help, sections=sections + ) + + def translate(self, i18n_key: Union[str, None] = None): + """ + Recursivly mutate translatable attributes to their translation + """ + super().translate(i18n_key) + + for section in self.sections: + section.translate(i18n_key) + + +class ConfigPanelModel(BaseModel): + version: float = CONFIG_PANEL_VERSION_SUPPORTED + i18n: Union[str, None] = None + panels: list[PanelModel] + + class Config: + arbitrary_types_allowed = True + extra = Extra.allow + + # Don't forget to pass arguments to super init + def __init__( + self, + version: float, + i18n: Union[str, None] = None, + **kwargs, + ) -> None: + panels = [data | {"id": name} for name, data in kwargs.items()] + super().__init__(version=version, i18n=i18n, panels=panels) + + @property + def sections(self): + """Convinient prop to iter on all sections""" + for panel in self.panels: + for section in panel.sections: + yield section + + @property + def options(self): + """Convinient prop to iter on all options""" + for section in self.sections: + for option in section.options: + yield option + + + def iter_children( + self, + trigger: list[Literal["panel", "section", "option", "action"]] = ["option"], + ): + for panel in self.panels: + if "panel" in trigger: + yield (panel, None, None) + for section in panel.sections: + if "section" in trigger: + yield (panel, section, None) + if "action" in trigger: + for option in section.options: + if option.type is OptionType.button: + yield (panel, section, option) + if "option" in trigger: + for option in section.options: + yield (panel, section, option) + + def translate(self): + """ + Recursivly mutate translatable attributes to their translation + """ + for panel in self.panels: + panel.translate(self.i18n) + + @validator("version", always=True) + def check_version(cls, value, field: "ModelField"): + if value < CONFIG_PANEL_VERSION_SUPPORTED: + raise ValueError( + f"Config panels version '{value}' are no longer supported." + ) + + return value + + +# ╭───────────────────────────────────────────────────────╮ +# │ ╭─╴╭─╮╭╮╷┌─╴╶┬╴╭─╮ ╶┬╴╭╮╮┌─╮╷ │ +# │ │ │ ││││├─╴ │ │╶╮ │ │││├─╯│ │ +# │ ╰─╴╰─╯╵╰╯╵ ╶┴╴╰─╯ ╶┴╴╵╵╵╵ ╰─╴ │ +# ╰───────────────────────────────────────────────────────╯ + + class ConfigPanel: entity_type = "config" save_path_tpl: Union[str, None] = None From 02948ad49c909f3d8eaed9bf3fdde0aada5bf11a Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 01:26:41 +0200 Subject: [PATCH 0335/1116] config: rework config+settings getter methods --- src/app.py | 10 +- src/dns.py | 10 +- src/domain.py | 59 ++++--- src/settings.py | 26 ++-- src/tests/test_dns.py | 6 +- src/utils/configpanel.py | 327 +++++++++++++-------------------------- src/utils/form.py | 4 +- 7 files changed, 166 insertions(+), 276 deletions(-) diff --git a/src/app.py b/src/app.py index 1a2e80442..ffcd1ecc3 100644 --- a/src/app.py +++ b/src/app.py @@ -26,7 +26,8 @@ import re import subprocess import tempfile import copy -from typing import List, Tuple, Dict, Any, Iterator, Optional +from collections import OrderedDict +from typing import TYPE_CHECKING, List, Tuple, Dict, Any, Iterator, Optional from packaging import version from logging import getLogger from pathlib import Path @@ -71,6 +72,9 @@ from yunohost.app_catalog import ( # noqa APPS_CATALOG_LOGOS, ) +if TYPE_CHECKING: + from yunohost.utils.configpanel import ConfigPanelModel, RawSettings + logger = getLogger("yunohost.app") APPS_SETTING_PATH = "/etc/yunohost/apps/" @@ -1802,8 +1806,8 @@ class AppConfigPanel(ConfigPanel): env = {key: str(value) for key, value in self.new_values.items()} self._call_config_script(action, env=env) - def _get_raw_settings(self): - self.values = self._call_config_script("show") + def _get_raw_settings(self, config: "ConfigPanelModel") -> "RawSettings": + return self._call_config_script("show") def _apply(self): env = {key: str(value) for key, value in self.new_values.items()} diff --git a/src/dns.py b/src/dns.py index 9a081e228..07ff2fb21 100644 --- a/src/dns.py +++ b/src/dns.py @@ -528,7 +528,7 @@ def _get_registrar_config_section(domain): parent_domain=parent_domain, parent_domain_link=parent_domain_link, ), - "value": "parent_domain", + "default": "parent_domain", } ) return OrderedDict(registrar_infos) @@ -541,7 +541,7 @@ def _get_registrar_config_section(domain): "type": "alert", "style": "success", "ask": m18n.n("domain_dns_registrar_yunohost"), - "value": "yunohost", + "default": "yunohost", } ) return OrderedDict(registrar_infos) @@ -551,7 +551,7 @@ def _get_registrar_config_section(domain): "type": "alert", "style": "info", "ask": m18n.n("domain_dns_conf_special_use_tld"), - "value": None, + "default": None, } ) @@ -563,7 +563,7 @@ def _get_registrar_config_section(domain): "type": "alert", "style": "warning", "ask": m18n.n("domain_dns_registrar_not_supported"), - "value": None, + "default": None, } ) else: @@ -572,7 +572,7 @@ def _get_registrar_config_section(domain): "type": "alert", "style": "info", "ask": m18n.n("domain_dns_registrar_supported", registrar=registrar), - "value": registrar, + "default": registrar, } ) diff --git a/src/domain.py b/src/domain.py index 2a897c625..a796e0142 100644 --- a/src/domain.py +++ b/src/domain.py @@ -19,7 +19,7 @@ import os import time from pathlib import Path -from typing import List, Optional +from typing import TYPE_CHECKING, List, Optional from collections import OrderedDict from logging import getLogger @@ -47,6 +47,9 @@ from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.log import is_unit_operation +if TYPE_CHECKING: + from yunohost.utils.configpanel import RawConfig + logger = getLogger("yunohost.domain") DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" @@ -666,10 +669,14 @@ class DomainConfigPanel(ConfigPanel): return result - def _get_raw_config(self): - toml = super()._get_raw_config() + def _get_raw_config(self) -> "RawConfig": + # TODO add mechanism to share some settings with other domains on the same zone + raw_config = super()._get_raw_config() - toml["feature"]["xmpp"]["xmpp"]["default"] = ( + any_filter = all(self.filter_key) + panel_id, section_id, option_id = self.filter_key + + raw_config["feature"]["xmpp"]["xmpp"]["default"] = ( 1 if self.entity == _get_maindomain() else 0 ) @@ -680,55 +687,43 @@ class DomainConfigPanel(ConfigPanel): # Optimize wether or not to load the DNS section, # e.g. we don't want to trigger the whole _get_registary_config_section # when just getting the current value from the feature section - filter_key = self.filter_key.split(".") if self.filter_key != "" else [] - if not filter_key or filter_key[0] == "dns": + if not any_filter or panel_id == "dns": from yunohost.dns import _get_registrar_config_section - toml["dns"]["registrar"] = _get_registrar_config_section(self.entity) - - # FIXME: Ugly hack to save the registar id/value and reinject it in _get_raw_settings ... - self.registar_id = toml["dns"]["registrar"]["registrar"]["value"] - del toml["dns"]["registrar"]["registrar"]["value"] + raw_config["dns"]["registrar"] = _get_registrar_config_section(self.entity) # Cert stuff - if not filter_key or filter_key[0] == "cert": + if not any_filter or panel_id == "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"] + raw_config["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( + raw_config["cert"]["cert"]["cert_summary"]["ask"] = m18n.n( f"domain_config_cert_summary_{status['summary']}" ) - # FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ... - self.cert_status = status + for option_id, status_key in [ + ("cert_validity", "validity"), + ("cert_issuer", "CA_type"), + ("acme_eligible", "ACME_eligible"), + # FIXME not sure why "summary" was injected in settings values + # ("summary", "summary") + ]: + raw_config["cert"]["cert"][option_id]["default"] = status[status_key] - return toml + # Other specific strings used in config panels + # i18n: domain_config_cert_renew_help - def _get_raw_settings(self): - # TODO add mechanism to share some settings with other domains on the same zone - super()._get_raw_settings() - - # FIXME: Ugly hack to save the registar id/value and reinject it in _get_raw_settings ... - filter_key = self.filter_key.split(".") if self.filter_key != "" else [] - if not filter_key or filter_key[0] == "dns": - self.values["registrar"] = self.registar_id - - # FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ... - 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"] + return raw_config def _apply(self): if ( diff --git a/src/settings.py b/src/settings.py index e2f34bda9..f3340e8e9 100644 --- a/src/settings.py +++ b/src/settings.py @@ -19,6 +19,7 @@ import os import subprocess from logging import getLogger +from typing import TYPE_CHECKING from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError @@ -29,6 +30,9 @@ from yunohost.firewall import firewall_reload from yunohost.log import is_unit_operation from yunohost.utils.legacy import translate_legacy_settings_to_configpanel_settings +if TYPE_CHECKING: + from yunohost.utils.configpanel import ConfigPanelModel, RawConfig, RawSettings + logger = getLogger("yunohost.settings") SETTINGS_PATH = "/etc/yunohost/settings.yml" @@ -180,8 +184,8 @@ class SettingsConfigPanel(ConfigPanel): logger.success(m18n.n("global_settings_reset_success")) operation_logger.success() - def _get_raw_config(self): - toml = super()._get_raw_config() + def _get_raw_config(self) -> "RawConfig": + raw_config = super()._get_raw_config() # Dynamic choice list for portal themes THEMEDIR = "/usr/share/ssowat/portal/assets/themes/" @@ -189,28 +193,30 @@ class SettingsConfigPanel(ConfigPanel): 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 + raw_config["misc"]["portal"]["portal_theme"]["choices"] = themes - return toml + return raw_config - def _get_raw_settings(self): - super()._get_raw_settings() + def _get_raw_settings(self, config: "ConfigPanelModel") -> "RawSettings": + raw_settings = super()._get_raw_settings(config) # 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"] = "" + raw_settings["root_password"] = "" + raw_settings["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( + raw_settings["passwordless_sudo"] = "!authenticate" in ldap.search( "ou=sudo", "cn=admins", ["sudoOption"] )[0].get("sudoOption", []) except Exception: - self.values["passwordless_sudo"] = False + raw_settings["passwordless_sudo"] = False + + return raw_settings def _apply(self): root_password = self.new_values.pop("root_password", None) diff --git a/src/tests/test_dns.py b/src/tests/test_dns.py index e896d9c9f..744e3e789 100644 --- a/src/tests/test_dns.py +++ b/src/tests/test_dns.py @@ -49,19 +49,19 @@ def test_registrar_list_integrity(): def test_magic_guess_registrar_weird_domain(): - assert _get_registrar_config_section("yolo.tld")["registrar"]["value"] is None + assert _get_registrar_config_section("yolo.tld")["registrar"]["default"] is None def test_magic_guess_registrar_ovh(): assert ( - _get_registrar_config_section("yolo.yunohost.org")["registrar"]["value"] + _get_registrar_config_section("yolo.yunohost.org")["registrar"]["default"] == "ovh" ) def test_magic_guess_registrar_yunodyndns(): assert ( - _get_registrar_config_section("yolo.nohost.me")["registrar"]["value"] + _get_registrar_config_section("yolo.nohost.me")["registrar"]["default"] == "yunohost" ) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index bf441798c..e113d007b 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -31,15 +31,16 @@ from moulinette.interfaces.cli import colorize from moulinette.utils.filesystem import mkdir, read_toml, read_yaml, write_to_yaml from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.form import ( - OPTIONS, BaseInputOption, BaseOption, BaseReadonlyOption, FileOption, + FormModel, OptionsModel, OptionType, Translation, ask_questions_and_parse_answers, + build_form, evaluate_simple_js_expression, ) from yunohost.utils.i18n import _value_for_locale @@ -93,7 +94,7 @@ class SectionModel(ContainerModel, OptionsModel): visible: Union[bool, str] = True, **kwargs, ) -> None: - options = self.options_dict_to_list(kwargs, defaults={"optional": True}) + options = self.options_dict_to_list(kwargs, optional=True) ContainerModel.__init__( self, @@ -231,12 +232,33 @@ class ConfigPanelModel(BaseModel): # │ ╰─╴╰─╯╵╰╯╵ ╶┴╴╰─╯ ╶┴╴╵╵╵╵ ╰─╴ │ # ╰───────────────────────────────────────────────────────╯ +if TYPE_CHECKING: + FilterKey = Sequence[Union[str, None]] + RawConfig = OrderedDict[str, Any] + RawSettings = dict[str, Any] + + +def parse_filter_key(key: Union[str, None] = None) -> "FilterKey": + if key and key.count(".") > 2: + raise YunohostError( + f"The filter key {key} has too many sub-levels, the max is 3.", + raw_msg=True, + ) + + if not key: + return (None, None, None) + keys = key.split(".") + return tuple(keys[i] if len(keys) > i else None for i in range(3)) + class ConfigPanel: entity_type = "config" save_path_tpl: Union[str, None] = None config_path_tpl = "/usr/share/yunohost/config_{entity_type}.toml" save_mode = "full" + filter_key: "FilterKey" = (None, None, None) + config: Union[ConfigPanelModel, None] = None + form: Union[FormModel, None] = None @classmethod def list(cls): @@ -265,9 +287,6 @@ class ConfigPanel: self.save_path = save_path if not save_path and self.save_path_tpl: self.save_path = self.save_path_tpl.format(entity=entity) - self.config = {} - self.values = {} - self.new_values = {} if ( self.save_path @@ -501,215 +520,103 @@ class ConfigPanel: logger.success(f"Action {action_id} successful") operation_logger.success() - def _get_raw_config(self): + def _get_raw_config(self) -> "RawConfig": + if not os.path.exists(self.config_path): + raise YunohostValidationError("config_no_panel") + return read_toml(self.config_path) - def _get_config_panel(self): - # Split filter_key - filter_key = self.filter_key.split(".") if self.filter_key != "" else [] - if len(filter_key) > 3: - raise YunohostError( - f"The filter key {filter_key} has too many sub-levels, the max is 3.", - raw_msg=True, + def _get_raw_settings(self, config: ConfigPanelModel) -> "RawSettings": + if not self.save_path or not os.path.exists(self.save_path): + raise YunohostValidationError("config_no_settings") + + return read_yaml(self.save_path) + + def _get_partial_raw_config(self) -> "RawConfig": + def filter_keys( + data: "RawConfig", + key: str, + model: Union[Type[ConfigPanelModel], Type[PanelModel], Type[SectionModel]], + ) -> "RawConfig": + # filter in keys defined in model, filter out panels/sections/options that aren't `key` + return OrderedDict( + {k: v for k, v in data.items() if k in model.__fields__ or k == key} ) - if not os.path.exists(self.config_path): - logger.debug(f"Config panel {self.config_path} doesn't exists") - return None + raw_config = self._get_raw_config() - toml_config_panel = self._get_raw_config() + panel_id, section_id, option_id = self.filter_key + if panel_id: + raw_config = filter_keys(raw_config, panel_id, ConfigPanelModel) - # Check TOML config panel is in a supported version - if float(toml_config_panel["version"]) < CONFIG_PANEL_VERSION_SUPPORTED: - logger.error( - f"Config panels version {toml_config_panel['version']} are not supported" - ) - return None + if section_id: + raw_config[panel_id] = filter_keys( + raw_config[panel_id], section_id, PanelModel + ) - # Transform toml format into internal format - format_description = { - "root": { - "properties": ["version", "i18n"], - "defaults": {"version": 1.0}, - }, - "panels": { - "properties": ["name", "services", "actions", "help"], - "defaults": { - "services": [], - "actions": {"apply": {"en": "Apply"}}, - }, - }, - "sections": { - "properties": ["name", "services", "optional", "help", "visible"], - "defaults": { - "name": "", - "services": [], - "optional": True, - "is_action_section": False, - }, - }, - "options": { - "properties": [ - "ask", - "type", - "bind", - "help", - "example", - "default", - "style", - "icon", - "placeholder", - "visible", - "optional", - "choices", - "yes", - "no", - "pattern", - "limit", - "min", - "max", - "step", - "accept", - "redact", - "filter", - "readonly", - "enabled", - "add_yunohost_portal_to_choices", - # "confirm", # TODO: to ask confirmation before running an action - ], - "defaults": {}, - }, - } - - def _build_internal_config_panel(raw_infos, level): - """Convert TOML in internal format ('full' mode used by webadmin) - Here are some properties of 1.0 config panel in toml: - - node properties and node children are mixed, - - text are in english only - - some properties have default values - This function detects all children nodes and put them in a list - """ - - defaults = format_description[level]["defaults"] - properties = format_description[level]["properties"] - - # Start building the ouput (merging the raw infos + defaults) - out = {key: raw_infos.get(key, value) for key, value in defaults.items()} - - # Now fill the sublevels (+ apply filter_key) - i = list(format_description).index(level) - sublevel = list(format_description)[i + 1] if level != "options" else None - search_key = filter_key[i] if len(filter_key) > i else False - - for key, value in raw_infos.items(): - # Key/value are a child node - if ( - isinstance(value, OrderedDict) - and key not in properties - and sublevel - ): - # We exclude all nodes not referenced by the filter_key - if search_key and key != search_key: - continue - subnode = _build_internal_config_panel(value, sublevel) - subnode["id"] = key - if level == "root": - subnode.setdefault("name", {"en": key.capitalize()}) - 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.get("type") == OptionType.button: - out["is_action_section"] = True - out.setdefault(sublevel, []).append(subnode) - # Key/value are a property - else: - if key not in properties: - logger.warning(f"Unknown key '{key}' found in config panel") - # Todo search all i18n keys - out[key] = ( - value - if key not in ["ask", "help", "name"] or isinstance(value, dict) - else {"en": value} + if option_id: + raw_config[panel_id][section_id] = filter_keys( + raw_config[panel_id][section_id], option_id, SectionModel ) - return out - self.config = _build_internal_config_panel(toml_config_panel, "root") + return raw_config + + def _get_partial_raw_settings_and_mutate_config( + self, config: ConfigPanelModel + ) -> tuple[ConfigPanelModel, "RawSettings"]: + raw_settings = self._get_raw_settings(config) + values = {} + + for _, section, option in config.iter_children(): + value = data = raw_settings.get(option.id, getattr(option, "default", None)) + + if isinstance(data, dict): + # Settings data if gathered from bash "ynh_app_config_show" + # may be a custom getter that returns a dict with `value` or `current_value` + # and other attributes meant to override those of the option. + + if "value" in data: + value = data.pop("value") + + # 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 + # FIXME do we still need the `current_value`? + if "current_value" in data: + value = data.pop("current_value") + + # Mutate other possible option attributes + for k, v in data.items(): + setattr(option, k, v) + + if isinstance(option, BaseInputOption): # or option.bind == "null": + values[option.id] = value + + return (config, values) + + def _get_config_panel( + self, prevalidate: bool = False + ) -> tuple[ConfigPanelModel, FormModel]: + raw_config = self._get_partial_raw_config() + config = ConfigPanelModel(**raw_config) + config, raw_settings = self._get_partial_raw_settings_and_mutate_config(config) + config.translate() + Settings = build_form(config.options) + settings = ( + Settings(**raw_settings) + if prevalidate + else Settings.construct(**raw_settings) + ) try: - self.config["panels"][0]["sections"][0]["options"][0] + config.panels[0].sections[0].options[0] except (KeyError, IndexError): raise YunohostValidationError( "config_unknown_filter_key", filter_key=self.filter_key ) - return self.config - - def _get_default_values(self): - return { - option["id"]: option["default"] - for _, _, option in self._iterate() - if "default" in option - } - - def _get_raw_settings(self): - """ - Retrieve entries in YAML file - And set default values if needed - """ - - # Inject defaults if needed (using the magic .update() ;)) - self.values = self._get_default_values() - - # Retrieve entries in the YAML - if os.path.exists(self.save_path) and os.path.isfile(self.save_path): - self.values.update(read_yaml(self.save_path) or {}) - - def _hydrate(self): - # Hydrating config panel with current value - for _, section, option in self._iterate(): - if option["id"] not in self.values: - allowed_empty_types = { - OptionType.alert, - OptionType.display_text, - OptionType.markdown, - OptionType.file, - OptionType.button, - } - - if section["is_action_section"] and option.get("default") is not None: - self.values[option["id"]] = option["default"] - elif ( - option["type"] in allowed_empty_types - or option.get("bind") == "null" - ): - continue - else: - raise YunohostError( - f"Config panel question '{option['id']}' should be initialized with a value during install or upgrade.", - raw_msg=True, - ) - value = self.values[option["id"]] - - # Allow to use value instead of current_value in app config script. - # e.g. apps may write `echo 'value: "foobar"'` in the config file (which is more intuitive that `echo 'current_value: "foobar"'` - # 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 + return (config, settings) def _ask(self, action=None): logger.debug("Ask unanswered question and prevalidate data") @@ -781,19 +688,6 @@ class ConfigPanel: } ) - @property - def future_values(self): - return {**self.values, **self.new_values} - - def __getattr__(self, name): - if "new_values" in self.__dict__ and name in self.new_values: - return self.new_values[name] - - if "values" in self.__dict__ and name in self.values: - return self.values[name] - - return self.__dict__[name] - def _parse_pre_answered(self, args, value, args_file): args = urllib.parse.parse_qs(args or "", keep_blank_values=True) self.args = {key: ",".join(value_) for key, value_ in args.items()} @@ -836,14 +730,3 @@ class ConfigPanel: if hasattr(self, "entity"): service = service.replace("__APP__", self.entity) service_reload_or_restart(service) - - def _iterate(self, trigger=["option"]): - for panel in self.config.get("panels", []): - if "panel" in trigger: - yield (panel, None, panel) - for section in panel.get("sections", []): - if "section" in trigger: - yield (panel, section, section) - if "option" in trigger: - for option in section.get("options", []): - yield (panel, section, option) diff --git a/src/utils/form.py b/src/utils/form.py index 6c14bcdf0..9ca0393d0 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1286,12 +1286,14 @@ class OptionsModel(BaseModel): options: list[Annotated[AnyOption, Field(discriminator="type")]] @staticmethod - def options_dict_to_list(options: dict[str, Any], defaults: dict[str, Any] = {}): + def options_dict_to_list(options: dict[str, Any], optional: bool = False): return [ option | { "id": id_, "type": option.get("type", "string"), + # ConfigPanel options needs to be set as optional by default + "optional": option.get("optional", optional) } for id_, option in options.items() ] From 80dbd6dac46cbe2848a063758f29f3b369417529 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 15:25:40 +0200 Subject: [PATCH 0336/1116] form: rework entities validators to avoid multiple calls to them --- src/utils/form.py | 90 +++++++++++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index 9ca0393d0..8ed83393e 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -44,7 +44,6 @@ from pydantic import ( Extra, ValidationError, create_model, - root_validator, validator, ) from pydantic.color import Color @@ -324,7 +323,7 @@ class BaseOption(BaseModel): return value # FIXME Legacy, is `name` still needed? - @validator("name", pre=True, always=True) + @validator("name") def apply_legacy_name(cls, value: Union[str, None], values: Values) -> str: if value is None: return values["id"] @@ -1096,21 +1095,30 @@ class DomainOption(BaseChoicesOption): type: Literal[OptionType.domain] = OptionType.domain choices: Union[dict[str, str], None] - @root_validator() - def inject_domains_choices_and_default(cls, values: Values) -> Values: + @validator("choices", pre=True, always=True) + def inject_domains_choices( + cls, value: Union[dict[str, str], None], values: Values + ) -> dict[str, str]: # TODO remove calls to resources in validators (pydantic V2 should adress this) from yunohost.domain import domain_list data = domain_list() - values["choices"] = { + return { domain: domain + " ★" if domain == data["main"] else domain for domain in data["domains"] } - if values["default"] is None: - values["default"] = data["main"] + @validator("default") + def inject_default( + cls, value: Union[str, None], values: Values + ) -> Union[str, None]: + # TODO remove calls to resources in validators (pydantic V2 should adress this) + from yunohost.domain import _get_maindomain - return values + if value is None: + return _get_maindomain() + + return value @staticmethod def normalize(value, option={}): @@ -1131,8 +1139,11 @@ class AppOption(BaseChoicesOption): add_yunohost_portal_to_choices: bool = False filter: Union[str, None] = None - @root_validator() - def inject_apps_choices(cls, values: Values) -> Values: + @validator("choices", pre=True, always=True) + def inject_apps_choices( + cls, value: Union[dict[str, str], None], values: Values + ) -> dict[str, str]: + # TODO remove calls to resources in validators (pydantic V2 should adress this) from yunohost.app import app_list apps = app_list(full=True)["apps"] @@ -1143,61 +1154,77 @@ class AppOption(BaseChoicesOption): for app in apps if evaluate_simple_js_expression(values["filter"], context=app) ] - values["choices"] = {"_none": "---"} + + value = {"_none": "---"} if values.get("add_yunohost_portal_to_choices", False): - values["choices"]["_yunohost_portal_with_public_apps"] = "YunoHost's portal with public apps" + value["_yunohost_portal_with_public_apps"] = "YunoHost's portal with public apps" - values["choices"].update( + value.update( { app["id"]: f"{app['label']} ({app.get('domain_path', app['id'])})" for app in apps } ) - return values + return value class UserOption(BaseChoicesOption): type: Literal[OptionType.user] = OptionType.user choices: Union[dict[str, str], None] - @root_validator() - def inject_users_choices_and_default(cls, values: dict[str, Any]) -> dict[str, Any]: - from yunohost.domain import _get_maindomain - from yunohost.user import user_info, user_list + @validator("choices", pre=True, always=True) + def inject_users_choices( + cls, value: Union[dict[str, str], None], values: Values + ) -> dict[str, str]: + # TODO remove calls to resources in validators (pydantic V2 should adress this) + from yunohost.user import user_list - values["choices"] = { + value = { username: f"{infos['fullname']} ({infos['mail']})" for username, infos in user_list()["users"].items() } # FIXME keep this to test if any user, do not raise error if no admin? - if not values["choices"]: + if not value: raise YunohostValidationError( "app_argument_invalid", name=values["id"], error="You should create a YunoHost user first.", ) - if values["default"] is None: + return value + + @validator("default") + def inject_default( + cls, value: Union[str, None], values: Values + ) -> Union[str, None]: + # TODO remove calls to resources in validators (pydantic V2 should adress this) + from yunohost.domain import _get_maindomain + from yunohost.user import user_info + + if value 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 values["choices"].keys(): if root_mail in user_info(user).get("mail-aliases", []): - values["default"] = user - break + return user - return values + return value class GroupOption(BaseChoicesOption): type: Literal[OptionType.group] = OptionType.group choices: Union[dict[str, str], None] + default: Union[Literal["visitors", "all_users", "admins"], None] = "all_users" - @root_validator() - def inject_groups_choices_and_default(cls, values: Values) -> Values: + @validator("choices", pre=True, always=True) + def inject_groups_choices( + cls, value: Union[dict[str, str], None], values: Values + ) -> dict[str, str]: + # TODO remove calls to resources in validators (pydantic V2 should adress this) from yunohost.user import user_group_list groups = user_group_list(short=True, include_primary_groups=False)["groups"] @@ -1212,14 +1239,7 @@ class GroupOption(BaseChoicesOption): else groupname ) - values["choices"] = { - groupname: _human_readable_group(groupname) for groupname in groups - } - - if values["default"] is None: - values["default"] = "all_users" - - return values + return {groupname: _human_readable_group(groupname) for groupname in groups} OPTIONS = { @@ -1293,7 +1313,7 @@ class OptionsModel(BaseModel): "id": id_, "type": option.get("type", "string"), # ConfigPanel options needs to be set as optional by default - "optional": option.get("optional", optional) + "optional": option.get("optional", optional), } for id_, option in options.items() ] From a92e22b6534e22e4ce95e50b4312f44953e10713 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 16:11:14 +0200 Subject: [PATCH 0337/1116] config: rework get method --- src/domain.py | 19 ++----- src/settings.py | 21 ++++---- src/utils/configpanel.py | 108 ++++++++++++++++++--------------------- 3 files changed, 62 insertions(+), 86 deletions(-) diff --git a/src/domain.py b/src/domain.py index a796e0142..6d57a0f10 100644 --- a/src/domain.py +++ b/src/domain.py @@ -652,22 +652,9 @@ class DomainConfigPanel(ConfigPanel): save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml" save_mode = "diff" - def get(self, key="", mode="classic"): - result = super().get(key=key, mode=mode) - - if mode == "full": - for panel, section, option in self._iterate(): - # This injects: - # i18n: domain_config_cert_renew_help - # i18n: domain_config_default_app_help - # i18n: domain_config_xmpp_help - if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"): - option["help"] = m18n.n( - self.config["i18n"] + "_" + option["id"] + "_help" - ) - return self.config - - return result + # i18n: domain_config_cert_renew_help + # i18n: domain_config_default_app_help + # i18n: domain_config_xmpp_help def _get_raw_config(self) -> "RawConfig": # TODO add mechanism to share some settings with other domains on the same zone diff --git a/src/settings.py b/src/settings.py index f3340e8e9..e66195802 100644 --- a/src/settings.py +++ b/src/settings.py @@ -19,7 +19,7 @@ import os import subprocess from logging import getLogger -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Union from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError @@ -31,7 +31,12 @@ from yunohost.log import is_unit_operation from yunohost.utils.legacy import translate_legacy_settings_to_configpanel_settings if TYPE_CHECKING: - from yunohost.utils.configpanel import ConfigPanelModel, RawConfig, RawSettings + from yunohost.utils.configpanel import ( + ConfigPanelGetMode, + ConfigPanelModel, + RawConfig, + RawSettings, + ) logger = getLogger("yunohost.settings") @@ -129,17 +134,11 @@ class SettingsConfigPanel(ConfigPanel): def __init__(self, config_path=None, save_path=None, creation=False): super().__init__("settings") - def get(self, key="", mode="classic"): + def get( + self, key: Union[str, None] = None, mode: "ConfigPanelGetMode" = "classic" + ) -> Any: 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" - ) - 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") diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index e113d007b..e39d5c91c 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -31,6 +31,7 @@ from moulinette.interfaces.cli import colorize from moulinette.utils.filesystem import mkdir, read_toml, read_yaml, write_to_yaml from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.form import ( + AnyOption, BaseInputOption, BaseOption, BaseReadonlyOption, @@ -190,6 +191,13 @@ class ConfigPanelModel(BaseModel): for option in section.options: yield option + def get_option(self, option_id) -> Union[AnyOption, None]: + for option in self.options: + if option.id == option_id: + return option + # FIXME raise error? + return None + def iter_children( self, @@ -236,6 +244,7 @@ if TYPE_CHECKING: FilterKey = Sequence[Union[str, None]] RawConfig = OrderedDict[str, Any] RawSettings = dict[str, Any] + ConfigPanelGetMode = Literal["classic", "full", "export"] def parse_filter_key(key: Union[str, None] = None) -> "FilterKey": @@ -310,78 +319,59 @@ class ConfigPanel: and re.match("^(validate|post_ask)__", func) } - def get(self, key="", mode="classic"): - self.filter_key = key or "" + def get( + self, key: Union[str, None] = None, mode: "ConfigPanelGetMode" = "classic" + ) -> Any: + self.filter_key = parse_filter_key(key) + self.config, self.form = self._get_config_panel(prevalidate=False) - # Read config panel toml - self._get_config_panel() - - if not self.config: - raise YunohostValidationError("config_no_panel") - - # Read or get values and hydrate the config - self._get_raw_settings() - self._hydrate() + panel_id, section_id, option_id = self.filter_key # 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] - value = self.values.get(option, None) + if option_id and mode == "classic": + option = self.config.get_option(option_id) - option_type = None - for _, _, option_ in self._iterate(): - if option_["id"] == option: - option_type = OPTIONS[option_["type"]] - break + if option is None: + # FIXME i18n + raise YunohostValidationError( + f"Couldn't find any option with id {option_id}" + ) - return option_type.normalize(value) if option_type else value + if isinstance(option, BaseReadonlyOption): + return None + + return self.form[option_id] # Format result in 'classic' or 'export' mode + self.config.translate() 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 + result = OrderedDict() + for panel in self.config.panels: + for section in panel.sections: + 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") - continue + for option in section.options: + if mode == "export": + if isinstance(option, BaseInputOption): + result[option.id] = self.form[option.id] + continue - ask = None - if "ask" in option: - ask = _value_for_locale(option["ask"]) - elif "i18n" in self.config: - ask = m18n.n(self.config["i18n"] + "_" + option["id"]) + if mode == "classic": + key = f"{panel.id}.{section.id}.{option.id}" + result[key] = {"ask": option.ask} - if mode == "full": - option["ask"] = ask - question_class = OPTIONS[option.get("type", OptionType.string)] - # FIXME : maybe other properties should be taken from the question, not just choices ?. - if issubclass(question_class, BaseChoicesOption): - option["choices"] = question_class(option).choices - if issubclass(question_class, BaseInputOption): - option["default"] = question_class(option).default - option["pattern"] = question_class(option).pattern - else: - result[key] = {"ask": ask} - if "current_value" in option: - question_class = OPTIONS[option.get("type", OptionType.string)] - if hasattr(question_class, "humanize"): - result[key]["value"] = question_class.humanize( - option["current_value"], option - ) - else: - result[key]["value"] = option["current_value"] - - # FIXME: semantics, technically here this is not about a prompt... - if getattr(question_class, "hide_user_input_in_prompt", None): - result[key][ - "value" - ] = "**************" # Prevent displaying password in `config get` + if isinstance(option, BaseInputOption): + result[key]["value"] = option.humanize( + self.form[option.id], option + ) + if option.type is OptionType.password: + result[key][ + "value" + ] = "**************" # Prevent displaying password in `config get` if mode == "full": - return self.config + return self.config.dict(exclude_none=True) else: return result From dbaea019fe1945fe8e126d51c00fcc019eeca1c2 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 16:54:54 +0200 Subject: [PATCH 0338/1116] form+config: replace _parse_pre_answered method with generic function --- src/utils/configpanel.py | 19 ++++++------------- src/utils/form.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index e39d5c91c..1f240a105 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -43,6 +43,7 @@ from yunohost.utils.form import ( ask_questions_and_parse_answers, build_form, evaluate_simple_js_expression, + parse_prefilled_values, ) from yunohost.utils.i18n import _value_for_locale @@ -397,7 +398,10 @@ class ConfigPanel: # Import and parse pre-answered options logger.debug("Import and parse pre-answered options") - self._parse_pre_answered(args, value, args_file) + if option_id and value is not None: + self.args = {option_id: value} + else: + self.args = parse_prefilled_values(args, value, args_file) # Read or get values and hydrate the config self._get_raw_settings() @@ -468,7 +472,7 @@ class ConfigPanel: # Import and parse pre-answered options logger.debug("Import and parse pre-answered options") - self._parse_pre_answered(args, None, args_file) + self.args = parse_prefilled_values(args, args_file) # Read or get values and hydrate the config self._get_raw_settings() @@ -678,17 +682,6 @@ class ConfigPanel: } ) - def _parse_pre_answered(self, args, value, args_file): - args = urllib.parse.parse_qs(args or "", keep_blank_values=True) - self.args = {key: ",".join(value_) for key, value_ in args.items()} - - if args_file: - # Import YAML / JSON file but keep --args values - self.args = {**read_yaml(args_file), **self.args} - - if value is not None: - self.args = {self.filter_key.split(".")[-1]: value} - def _apply(self): logger.info("Saving the new configuration...") dir_path = os.path.dirname(os.path.realpath(self.save_path)) diff --git a/src/utils/form.py b/src/utils/form.py index 8ed83393e..7a97259b9 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -53,7 +53,7 @@ from pydantic.types import constr from moulinette import Moulinette, m18n from moulinette.interfaces.cli import colorize -from moulinette.utils.filesystem import read_file, write_to_file +from moulinette.utils.filesystem import read_file, read_yaml, write_to_file from yunohost.log import OperationLogger from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.i18n import _value_for_locale @@ -1431,6 +1431,31 @@ def hydrate_option_type(raw_option: dict[str, Any]) -> dict[str, Any]: Hooks = dict[str, Callable[[BaseInputOption], Any]] +def parse_prefilled_values( + args: Union[str, None] = None, + args_file: Union[str, None] = None, + method: Literal["parse_qs", "parse_qsl"] = "parse_qs", +) -> dict[str, Any]: + """ + Retrieve form values from yaml file or query string. + """ + values: Values = {} + if args_file: + # Import YAML / JSON file + values |= read_yaml(args_file) + if args: + # FIXME See `ask_questions_and_parse_answers` + parsed = getattr(urllib.parse, method)(args, keep_blank_values=True) + if isinstance(parsed, dict): # parse_qs + # FIXME could do the following to get a list directly? + # k: None if not len(v) else (v if len(v) > 1 else v[0]) + values |= {k: ",".join(v) for k, v in parsed.items()} + else: + values |= dict(parsed) + + return values + + def prompt_or_validate_form( options: list[AnyOption], form: FormModel, @@ -1561,9 +1586,7 @@ def ask_questions_and_parse_answers( # whereas parse.qs return list of values (which is useful for tags, etc) # For now, let's not migrate this piece of code to parse_qs # Because Aleks believes some bits of the app CI rely on overriding values (e.g. foo=foo&...&foo=bar) - answers = dict( - urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True) - ) + answers = parse_prefilled_values(prefilled_answers, method="parse_qsl") elif isinstance(prefilled_answers, Mapping): answers = {**prefilled_answers} else: From f1038de56d0289d770317109d770bf5144129703 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 17:51:21 +0200 Subject: [PATCH 0339/1116] form: fix entities validators order for filter and apply the right default --- src/tests/test_questions.py | 11 +---------- src/utils/form.py | 28 ++++++++++++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 959f2c8b7..387d5c0f9 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -1816,9 +1816,7 @@ class TestGroup(BaseTest): "scenarios": [ ("custom_group", "custom_group"), *all_as("", None, output="visitors", raw_option={"default": "visitors"}), - *xpass(scenarios=[ - ("", "custom_group", {"default": "custom_group"}), - ], reason="Should throw 'default must be in (None, 'all_users', 'visitors', 'admins')"), + ("", FAIL, {"default": "custom_group"}), # Not allowed to set a default which is not a default group # readonly ("admins", FAIL, {"readonly": True}), # readonly is forbidden ] @@ -1837,13 +1835,6 @@ class TestGroup(BaseTest): "prefill": "admins", } ) - # FIXME This should fail, not allowed to set a default which is not a default group - super().test_options_prompted_with_ask_help( - prefill_data={ - "raw_option": {"default": "custom_group"}, - "prefill": "custom_group", - } - ) def test_scenarios(self, intake, expected_output, raw_option, data): with patch_groups(**data): diff --git a/src/utils/form.py b/src/utils/form.py index 7a97259b9..7098692a4 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -951,6 +951,7 @@ ChoosableOptions = Literal[ class BaseChoicesOption(BaseInputOption): # FIXME probably forbid choices to be None? + filter: Union[JSExpression, None] = None # filter before choices choices: Union[dict[str, Any], list[Any], None] @validator("choices", pre=True) @@ -1093,6 +1094,7 @@ class TagsOption(BaseChoicesOption): class DomainOption(BaseChoicesOption): type: Literal[OptionType.domain] = OptionType.domain + filter: Literal[None] = None choices: Union[dict[str, str], None] @validator("choices", pre=True, always=True) @@ -1108,17 +1110,14 @@ class DomainOption(BaseChoicesOption): for domain in data["domains"] } - @validator("default") + @validator("default", pre=True, always=True) def inject_default( cls, value: Union[str, None], values: Values ) -> Union[str, None]: # TODO remove calls to resources in validators (pydantic V2 should adress this) from yunohost.domain import _get_maindomain - if value is None: - return _get_maindomain() - - return value + return _get_maindomain() @staticmethod def normalize(value, option={}): @@ -1135,9 +1134,9 @@ class DomainOption(BaseChoicesOption): class AppOption(BaseChoicesOption): type: Literal[OptionType.app] = OptionType.app - choices: Union[dict[str, str], None] + filter: Union[JSExpression, None] = None add_yunohost_portal_to_choices: bool = False - filter: Union[str, None] = None + choices: Union[dict[str, str], None] @validator("choices", pre=True, always=True) def inject_apps_choices( @@ -1172,6 +1171,7 @@ class AppOption(BaseChoicesOption): class UserOption(BaseChoicesOption): type: Literal[OptionType.user] = OptionType.user + filter: Literal[None] = None choices: Union[dict[str, str], None] @validator("choices", pre=True, always=True) @@ -1196,19 +1196,19 @@ class UserOption(BaseChoicesOption): return value - @validator("default") + @validator("default", pre=True, always=True) def inject_default( cls, value: Union[str, None], values: Values ) -> Union[str, None]: # TODO remove calls to resources in validators (pydantic V2 should adress this) from yunohost.domain import _get_maindomain - from yunohost.user import user_info + from yunohost.user import user_list, user_info if value 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 values["choices"].keys(): + for user in user_list()["users"].keys(): if root_mail in user_info(user).get("mail-aliases", []): return user @@ -1217,6 +1217,7 @@ class UserOption(BaseChoicesOption): class GroupOption(BaseChoicesOption): type: Literal[OptionType.group] = OptionType.group + filter: Literal[None] = None choices: Union[dict[str, str], None] default: Union[Literal["visitors", "all_users", "admins"], None] = "all_users" @@ -1241,6 +1242,13 @@ class GroupOption(BaseChoicesOption): return {groupname: _human_readable_group(groupname) for groupname in groups} + @validator("default", pre=True, always=True) + def inject_default(cls, value: Union[str, None], values: Values) -> str: + # FIXME do we really want to default to something all the time? + if value is None: + return "all_users" + return value + OPTIONS = { OptionType.display_text: DisplayTextOption, From 2c35dcbb2498f2227282a10a8d8eac6da06f82e6 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 20:04:20 +0200 Subject: [PATCH 0340/1116] configpanel: update _reload_services --- src/utils/configpanel.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 1f240a105..22ea5b3b8 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -199,6 +199,17 @@ class ConfigPanelModel(BaseModel): # FIXME raise error? return None + @property + def services(self) -> list[str]: + services = set() + for panel in self.panels: + services |= set(panel.services) + for section in panel.sections: + services |= set(section.services) + + services_ = list(services) + services_.sort(key="nginx".__eq__) + return services_ def iter_children( self, @@ -701,12 +712,8 @@ class ConfigPanel: def _reload_services(self): from yunohost.service import service_reload_or_restart - services_to_reload = set() - for panel, section, obj in self._iterate(["panel", "section", "option"]): - services_to_reload |= set(obj.get("services", [])) + services_to_reload = self.config.services - services_to_reload = list(services_to_reload) - services_to_reload.sort(key="nginx".__eq__) if services_to_reload: logger.info("Reloading services...") for service in services_to_reload: From 7a60703ef58b17de4e1cde9e7861fa4e50298e6e Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 20:13:01 +0200 Subject: [PATCH 0341/1116] configpanel: update _ask --- src/utils/configpanel.py | 95 +++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 22ea5b3b8..22318e5e0 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -19,7 +19,6 @@ import glob import os import re -import urllib.parse from collections import OrderedDict from logging import getLogger from typing import TYPE_CHECKING, Any, Literal, Sequence, Type, Union @@ -36,19 +35,19 @@ from yunohost.utils.form import ( BaseOption, BaseReadonlyOption, FileOption, - FormModel, OptionsModel, OptionType, Translation, - ask_questions_and_parse_answers, build_form, evaluate_simple_js_expression, parse_prefilled_values, + prompt_or_validate_form, ) from yunohost.utils.i18n import _value_for_locale if TYPE_CHECKING: from pydantic.fields import ModelField + from yunohost.utils.form import FormModel, Hooks logger = getLogger("yunohost.configpanel") @@ -279,7 +278,8 @@ class ConfigPanel: save_mode = "full" filter_key: "FilterKey" = (None, None, None) config: Union[ConfigPanelModel, None] = None - form: Union[FormModel, None] = None + form: Union["FormModel", None] = None + hooks: "Hooks" = {} @classmethod def list(cls): @@ -602,7 +602,7 @@ class ConfigPanel: def _get_config_panel( self, prevalidate: bool = False - ) -> tuple[ConfigPanelModel, FormModel]: + ) -> tuple[ConfigPanelModel, "FormModel"]: raw_config = self._get_partial_raw_config() config = ConfigPanelModel(**raw_config) config, raw_settings = self._get_partial_raw_settings_and_mutate_config(config) @@ -623,58 +623,62 @@ class ConfigPanel: return (config, settings) - def _ask(self, action=None): + def ask( + self, + config: ConfigPanelModel, + settings: "FormModel", + prefilled_answers: dict[str, Any] = {}, + action_id: Union[str, None] = None, + hooks: "Hooks" = {}, + ) -> "FormModel": + # FIXME could be turned into a staticmethod 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" - ) + interactive = Moulinette.interface.type == "cli" and os.isatty(1) - def display_header(message): - """CLI panel/section header display""" - if Moulinette.interface.type == "cli" and self.filter_key.count(".") < 2: - Moulinette.display(colorize(message, "purple")) + if interactive: + config.translate() - for panel, section, obj in self._iterate(["panel", "section"]): - if ( - section - and section.get("visible") - and not evaluate_simple_js_expression( - section["visible"], context=self.future_values + for panel in config.panels: + if interactive: + Moulinette.display( + colorize(f"\n{'='*40}\n>>>> {panel.name}\n{'='*40}", "purple") ) - ): - continue - # Ugly hack to skip action section ... except when when explicitly running actions - if not action: - if section and section["is_action_section"]: + # A section or option may only evaluate its conditions (`visible` + # and `enabled`) with its panel's local context that is built + # prompt after prompt. + # That means that a condition can only reference options of its + # own panel and options that are previously defined. + context: dict[str, Any] = {} + + for section in panel.sections: + if ( + action_id is None and section.is_action_section + ) or not section.is_visible(context): + # FIXME useless? + Moulinette.display("Skipping section '{panel.id}.{section.id}'…") 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: + if interactive and section.name: + Moulinette.display(colorize(f"\n# {section.name}", "purple")) + # filter action section options in case of multiple buttons - section["options"] = [ + options = [ option - for option in section["options"] - if option.get("type", OptionType.string) != OptionType.button - or option["id"] == action + for option in section.options + if option.type is not OptionType.button or option.id == action_id ] - if panel == obj: - continue + settings = prompt_or_validate_form( + options, + settings, + prefilled_answers=prefilled_answers, + context=context, + hooks=hooks, + ) +<<<<<<< HEAD # Check and ask unanswered questions prefilled_answers = self.args.copy() prefilled_answers.update(self.new_values) @@ -692,6 +696,9 @@ class ConfigPanel: if not question.readonly and question.value is not None } ) +======= + return settings +>>>>>>> be777b928 (configpanel: update _ask) def _apply(self): logger.info("Saving the new configuration...") From 5f9ea5831391908eeace58c3f496796c69351bd5 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 20:15:25 +0200 Subject: [PATCH 0342/1116] configpanel: update _apply --- src/app.py | 14 +++++++-- src/domain.py | 66 +++++++++++++++++----------------------- src/settings.py | 42 +++++++++++++------------ src/utils/configpanel.py | 47 ++++++++++++---------------- 4 files changed, 81 insertions(+), 88 deletions(-) diff --git a/src/app.py b/src/app.py index ffcd1ecc3..08e222579 100644 --- a/src/app.py +++ b/src/app.py @@ -27,7 +27,7 @@ import subprocess import tempfile import copy from collections import OrderedDict -from typing import TYPE_CHECKING, List, Tuple, Dict, Any, Iterator, Optional +from typing import TYPE_CHECKING, List, Tuple, Dict, Any, Iterator, Optional, Union from packaging import version from logging import getLogger from pathlib import Path @@ -73,7 +73,10 @@ from yunohost.app_catalog import ( # noqa ) if TYPE_CHECKING: + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + from yunohost.utils.configpanel import ConfigPanelModel, RawSettings + from yunohost.utils.form import FormModel logger = getLogger("yunohost.app") @@ -1809,8 +1812,13 @@ class AppConfigPanel(ConfigPanel): def _get_raw_settings(self, config: "ConfigPanelModel") -> "RawSettings": return self._call_config_script("show") - def _apply(self): - env = {key: str(value) for key, value in self.new_values.items()} + def _apply( + self, + form: "FormModel", + previous_settings: dict[str, Any], + exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, + ): + env = {key: str(value) for key, value in form.dict().items()} return_content = self._call_config_script("apply", env=env) # If the script returned validation error diff --git a/src/domain.py b/src/domain.py index 6d57a0f10..f0531e624 100644 --- a/src/domain.py +++ b/src/domain.py @@ -19,7 +19,7 @@ import os import time from pathlib import Path -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Any, List, Optional, Union from collections import OrderedDict from logging import getLogger @@ -48,7 +48,10 @@ from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.log import is_unit_operation if TYPE_CHECKING: + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + from yunohost.utils.configpanel import RawConfig + from yunohost.utils.form import FormModel logger = getLogger("yunohost.domain") @@ -669,7 +672,7 @@ class DomainConfigPanel(ConfigPanel): # Portal settings are only available on "topest" domains if _get_parent_domain_of(self.entity, topest=True) is not None: - del toml["feature"]["portal"] + del raw_config["feature"]["portal"] # Optimize wether or not to load the DNS section, # e.g. we don't want to trigger the whole _get_registary_config_section @@ -712,17 +715,23 @@ class DomainConfigPanel(ConfigPanel): return raw_config - def _apply(self): - if ( - "default_app" in self.future_values - and self.future_values["default_app"] != self.values["default_app"] - ): + def _apply( + self, + form: "FormModel", + previous_settings: dict[str, Any], + exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, + ): + next_settings = { + k: v for k, v in form.dict().items() if previous_settings.get(k) != v + } + + if "default_app" in next_settings: from yunohost.app import app_ssowatconf, app_map if "/" in app_map(raw=True).get(self.entity, {}): raise YunohostValidationError( "app_make_default_location_already_used", - app=self.future_values["default_app"], + app=next_settings["default_app"], domain=self.entity, other_app=app_map(raw=True)[self.entity]["/"]["id"], ) @@ -735,8 +744,7 @@ class DomainConfigPanel(ConfigPanel): "portal_theme", ] if _get_parent_domain_of(self.entity, topest=True) is None and any( - option in self.future_values - and self.new_values[option] != self.values.get(option) + option in next_settings for option in portal_options ): from yunohost.portal import PORTAL_SETTINGS_DIR @@ -744,9 +752,8 @@ class DomainConfigPanel(ConfigPanel): # Portal options are also saved in a `domain.portal.yml` file # that can be read by the portal API. # FIXME remove those from the config panel saved values? - portal_values = { - option: self.future_values[option] for option in portal_options - } + + portal_values = form.dict(include=portal_options) portal_settings_path = Path(f"{PORTAL_SETTINGS_DIR}/{self.entity}.json") portal_settings = {"apps": {}} @@ -760,38 +767,21 @@ class DomainConfigPanel(ConfigPanel): str(portal_settings_path), portal_settings, sort_keys=True, indent=4 ) - super()._apply() + super()._apply(form, previous_settings) # Reload ssowat if default app changed - if ( - "default_app" in self.future_values - and self.future_values["default_app"] != self.values["default_app"] - ): + if "default_app" in next_settings: 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") + stuff_to_regen_conf = set() + if "xmpp" in next_settings: + stuff_to_regen_conf.update({"nginx", "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 "mail_in" in next_settings or "mail_out" in next_settings: + stuff_to_regen_conf.update({"nginx", "postfix", "dovecot", "rspamd"}) if stuff_to_regen_conf: - regen_conf(names=stuff_to_regen_conf) + regen_conf(names=list(stuff_to_regen_conf)) def domain_action_run(domain, action, args=None): diff --git a/src/settings.py b/src/settings.py index e66195802..6a05217dc 100644 --- a/src/settings.py +++ b/src/settings.py @@ -31,12 +31,15 @@ from yunohost.log import is_unit_operation from yunohost.utils.legacy import translate_legacy_settings_to_configpanel_settings if TYPE_CHECKING: + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + from yunohost.utils.configpanel import ( ConfigPanelGetMode, ConfigPanelModel, RawConfig, RawSettings, ) + from yunohost.utils.form import FormModel logger = getLogger("yunohost.settings") @@ -217,19 +220,15 @@ class SettingsConfigPanel(ConfigPanel): return raw_settings - def _apply(self): - 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) - - 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 - } - - assert all(v not in self.future_values for v in self.virtual_settings) + def _apply( + self, + form: "FormModel", + previous_settings: dict[str, Any], + exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, + ): + root_password = form.get("root_password", None) + root_password_confirm = form.get("root_password_confirm", None) + passwordless_sudo = form.get("passwordless_sudo", None) if root_password and root_password.strip(): if root_password != root_password_confirm: @@ -248,15 +247,20 @@ class SettingsConfigPanel(ConfigPanel): {"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 + # First save settings except virtual + default ones + super()._apply(form, previous_settings, exclude=self.virtual_settings) + next_settings = { + k: v + for k, v in form.dict(exclude=self.virtual_settings).items() + if previous_settings.get(k) != v } - for setting_name, value in settings.items(): + + for setting_name, value in next_settings.items(): try: + # FIXME not sure to understand why we need the previous value if + # updated_settings has already been filtered trigger_post_change_hook( - setting_name, self.values.get(setting_name), value + setting_name, previous_settings.get(setting_name), value ) except Exception as e: logger.error(f"Post-change hook for setting failed : {e}") diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 22318e5e0..56b0584e3 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -47,6 +47,8 @@ from yunohost.utils.i18n import _value_for_locale if TYPE_CHECKING: from pydantic.fields import ModelField + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + from yunohost.utils.form import FormModel, Hooks logger = getLogger("yunohost.configpanel") @@ -678,43 +680,32 @@ class ConfigPanel: hooks=hooks, ) -<<<<<<< HEAD - # Check and ask unanswered questions - prefilled_answers = self.args.copy() - prefilled_answers.update(self.new_values) - - questions = ask_questions_and_parse_answers( - {question["id"]: question for question in section["options"]}, - prefilled_answers=prefilled_answers, - current_values=self.values, - hooks=self.hooks, - ) - self.new_values.update( - { - question.id: question.value - for question in questions - if not question.readonly and question.value is not None - } - ) -======= return settings ->>>>>>> be777b928 (configpanel: update _ask) - def _apply(self): + def _apply( + self, + form: "FormModel", + previous_settings: dict[str, Any], + exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, + ) -> dict[str, Any]: + """ + Save settings in yaml file. + If `save_mode` is `"diff"` (which is the default), only values that are + different from their default value will be saved. + """ logger.info("Saving the new configuration...") + dir_path = os.path.dirname(os.path.realpath(self.save_path)) if not os.path.exists(dir_path): mkdir(dir_path, mode=0o700) - values_to_save = self.future_values - if self.save_mode == "diff": - defaults = self._get_default_values() - values_to_save = { - k: v for k, v in values_to_save.items() if defaults.get(k) != v - } + exclude_defaults = self.save_mode == "diff" + settings = form.dict(exclude_defaults=exclude_defaults, exclude=exclude) # type: ignore # Save the settings to the .yaml file - write_to_yaml(self.save_path, values_to_save) + write_to_yaml(self.save_path, settings) + + return settings def _reload_services(self): from yunohost.service import service_reload_or_restart From 6b3691ce534df73c98479e4eba3387024cca8000 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 21:04:05 +0200 Subject: [PATCH 0343/1116] configpanel: update set --- src/settings.py | 2 +- src/utils/configpanel.py | 49 ++++++++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/settings.py b/src/settings.py index 6a05217dc..0d0a5406b 100644 --- a/src/settings.py +++ b/src/settings.py @@ -132,7 +132,7 @@ class SettingsConfigPanel(ConfigPanel): entity_type = "global" save_path_tpl = SETTINGS_PATH save_mode = "diff" - virtual_settings = ["root_password", "root_password_confirm", "passwordless_sudo"] + virtual_settings = {"root_password", "root_password_confirm", "passwordless_sudo"} def __init__(self, config_path=None, save_path=None, creation=False): super().__init__("settings") diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 56b0584e3..20a421925 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -50,6 +50,7 @@ if TYPE_CHECKING: from pydantic.typing import AbstractSetIntStr, MappingIntStrAny from yunohost.utils.form import FormModel, Hooks + from yunohost.log import OperationLogger logger = getLogger("yunohost.configpanel") @@ -390,15 +391,15 @@ class ConfigPanel: return result def set( - self, key=None, value=None, args=None, args_file=None, operation_logger=None + self, + key: Union[str, None] = None, + value: Any = None, + args: Union[str, None] = None, + args_file: Union[str, None] = None, + operation_logger: Union["OperationLogger", None] = None, ): - self.filter_key = key or "" - - # Read config panel toml - self._get_config_panel() - - if not self.config: - raise YunohostValidationError("config_no_panel") + self.filter_key = parse_filter_key(key) + panel_id, section_id, option_id = self.filter_key if (args is not None or args_file is not None) and value is not None: raise YunohostValidationError( @@ -406,27 +407,35 @@ class ConfigPanel: raw_msg=True, ) - if self.filter_key.count(".") != 2 and value is not None: + if not option_id and value is not None: raise YunohostValidationError("config_cant_set_value_on_section") # Import and parse pre-answered options logger.debug("Import and parse pre-answered options") if option_id and value is not None: - self.args = {option_id: value} + prefilled_answers = {option_id: value} else: - self.args = parse_prefilled_values(args, value, args_file) + prefilled_answers = parse_prefilled_values(args, args_file) - # Read or get values and hydrate the config - self._get_raw_settings() - self._hydrate() - BaseOption.operation_logger = operation_logger - self._ask() + self.config, self.form = self._get_config_panel() + # FIXME find a better way to exclude previous settings + previous_settings = self.form.dict() + + # FIXME Not sure if this is need (redact call to operation logger does it on all the instances) + # BaseOption.operation_logger = operation_logger + + self.form = self._ask( + self.config, + self.form, + prefilled_answers=prefilled_answers, + hooks=self.hooks, + ) if operation_logger: operation_logger.start() try: - self._apply() + self._apply(self.form, previous_settings) except YunohostError: raise # Script got manually interrupted ... @@ -452,7 +461,9 @@ class ConfigPanel: self._reload_services() logger.success("Config updated as expected") - operation_logger.success() + + if operation_logger: + operation_logger.success() def list_actions(self): actions = {} @@ -625,7 +636,7 @@ class ConfigPanel: return (config, settings) - def ask( + def _ask( self, config: ConfigPanelModel, settings: "FormModel", From 15c827908f917ab57e61b303d4af5e36a1a9d5ce Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 21:05:25 +0200 Subject: [PATCH 0344/1116] configpanel: update run_action --- src/app.py | 11 ++++---- src/utils/configpanel.py | 59 +++++++++++++++++++++++++--------------- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/app.py b/src/app.py index 08e222579..970a66fb2 100644 --- a/src/app.py +++ b/src/app.py @@ -46,10 +46,11 @@ from moulinette.utils.filesystem import ( chmod, ) -from yunohost.utils.configpanel import ConfigPanel, ask_questions_and_parse_answers +from yunohost.utils.configpanel import ConfigPanel from yunohost.utils.form import ( DomainOption, WebPathOption, + ask_questions_and_parse_answers, hydrate_questions_with_choices, ) from yunohost.utils.i18n import _value_for_locale @@ -1805,10 +1806,6 @@ class AppConfigPanel(ConfigPanel): save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml") config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.toml") - 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 _get_raw_settings(self, config: "ConfigPanelModel") -> "RawSettings": return self._call_config_script("show") @@ -1832,6 +1829,10 @@ class AppConfigPanel(ConfigPanel): error=message, ) + def _run_action(self, form: "FormModel", action_id: str): + env = {key: str(value) for key, value in form.dict().items()} + self._call_config_script(action_id, env=env) + def _call_config_script(self, action, env=None): from yunohost.hook import hook_exec diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 20a421925..eb244aa08 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -479,30 +479,41 @@ class ConfigPanel: return actions - def run_action(self, action=None, args=None, args_file=None, operation_logger=None): + def run_action( + self, + key: Union[str, None] = None, + args: Union[str, None] = None, + args_file: Union[str, None] = None, + operation_logger: Union["OperationLogger", None] = None, + ): # # FIXME : this stuff looks a lot like set() ... # + panel_id, section_id, action_id = parse_filter_key(key) + # since an action may require some options from its section, + # remove the action_id from the filter + self.filter_key = (panel_id, section_id, None) - self.filter_key = ".".join(action.split(".")[:2]) - action_id = action.split(".")[2] - - # Read config panel toml - self._get_config_panel() + self.config, self.form = 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) + if not action_id or not self.config.get_option(action_id): + raise YunohostValidationError(f"No action named {action_id}", raw_msg=True) # Import and parse pre-answered options logger.debug("Import and parse pre-answered options") - self.args = parse_prefilled_values(args, args_file) + prefilled_answers = parse_prefilled_values(args, args_file) - # Read or get values and hydrate the config - self._get_raw_settings() - self._hydrate() - BaseOption.operation_logger = operation_logger - self._ask(action=action_id) + self.form = self._ask( + self.config, + self.form, + prefilled_answers=prefilled_answers, + action_id=action_id, + hooks=self.hooks, + ) + + # FIXME Not sure if this is need (redact call to operation logger does it on all the instances) + # BaseOption.operation_logger = operation_logger # FIXME: here, we could want to check constrains on # the action's visibility / requirements wrt to the answer to questions ... @@ -511,21 +522,21 @@ class ConfigPanel: operation_logger.start() try: - self._run_action(action_id) + self._run_action(self.form, 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)) + logger.error(m18n.n("config_action_failed", action=key, 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)) + logger.error(m18n.n("config_action_failed", action=key, error=error)) raise finally: # Delete files uploaded from API @@ -536,7 +547,9 @@ class ConfigPanel: # FIXME: i18n logger.success(f"Action {action_id} successful") - operation_logger.success() + + if operation_logger: + operation_logger.success() def _get_raw_config(self) -> "RawConfig": if not os.path.exists(self.config_path): @@ -648,12 +661,13 @@ class ConfigPanel: logger.debug("Ask unanswered question and prevalidate data") interactive = Moulinette.interface.type == "cli" and os.isatty(1) + verbose = action_id is None or len(list(config.options)) > 1 if interactive: config.translate() for panel in config.panels: - if interactive: + if interactive and verbose: Moulinette.display( colorize(f"\n{'='*40}\n>>>> {panel.name}\n{'='*40}", "purple") ) @@ -669,11 +683,9 @@ class ConfigPanel: if ( action_id is None and section.is_action_section ) or not section.is_visible(context): - # FIXME useless? - Moulinette.display("Skipping section '{panel.id}.{section.id}'…") continue - if interactive and section.name: + if interactive and verbose and section.name: Moulinette.display(colorize(f"\n# {section.name}", "purple")) # filter action section options in case of multiple buttons @@ -718,6 +730,9 @@ class ConfigPanel: return settings + def _run_action(self, form: "FormModel", action_id: str): + raise NotImplementedError() + def _reload_services(self): from yunohost.service import service_reload_or_restart From 25ccbd5f78e25cc7a75128ae7f0e2fcc8f8f74fe Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 18 Apr 2023 22:00:58 +0200 Subject: [PATCH 0345/1116] configpanel: quickly update list_actions --- src/utils/configpanel.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index eb244aa08..96bf910b0 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -470,12 +470,12 @@ class ConfigPanel: # 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"] == OptionType.button: - key = f"{panel['id']}.{section['id']}.{option['id']}" - actions[key] = _value_for_locale(option["ask"]) + self.config, self.form = self._get_config_panel() + + for panel, section, option in self.config.iter_children(): + if option.type == OptionType.button: + key = f"{panel.id}.{section.id}.{option.id}" + actions[key] = _value_for_locale(option.ask) return actions From f087a6b967d434c32517822bd1b8741c62547798 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 14:34:43 +0200 Subject: [PATCH 0346/1116] config: normalize get option value --- src/utils/configpanel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 96bf910b0..4e1e0e8ce 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -355,7 +355,7 @@ class ConfigPanel: if isinstance(option, BaseReadonlyOption): return None - return self.form[option_id] + return option.normalize(self.form[option_id], option) # Format result in 'classic' or 'export' mode self.config.translate() From 73b795be1d192329936a052787ec67178dc81ddd Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 14:37:49 +0200 Subject: [PATCH 0347/1116] config: readd value has been initialized + change test removed value from settings since boolean has a native default value --- src/tests/test_app_config.py | 4 ++-- src/utils/configpanel.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/tests/test_app_config.py b/src/tests/test_app_config.py index 4a74cbc0d..24abdc5dc 100644 --- a/src/tests/test_app_config.py +++ b/src/tests/test_app_config.py @@ -125,9 +125,9 @@ def test_app_config_get_nonexistentstuff(config_app): with pytest.raises(YunohostValidationError): app_config_get(config_app, "main.components.nonexistent") - app_setting(config_app, "boolean", delete=True) + app_setting(config_app, "number", delete=True) with pytest.raises(YunohostError): - app_config_get(config_app, "main.components.boolean") + app_config_get(config_app, "main.components.number") def test_app_config_regular_setting(config_app): diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 4e1e0e8ce..eb9455a80 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -601,6 +601,17 @@ class ConfigPanel: for _, section, option in config.iter_children(): value = data = raw_settings.get(option.id, getattr(option, "default", None)) + if isinstance(option, BaseInputOption) and option.id not in raw_settings: + if option.default is not None: + value = option.default + elif option.type is OptionType.file or option.bind == "null": + continue + else: + raise YunohostError( + f"Config panel question '{option.id}' should be initialized with a value during install or upgrade.", + raw_msg=True, + ) + if isinstance(data, dict): # Settings data if gathered from bash "ynh_app_config_show" # may be a custom getter that returns a dict with `value` or `current_value` From 98d3b4ffc8d7bfed4eea3c147a4da68a2f80dd68 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 15:05:16 +0200 Subject: [PATCH 0348/1116] form: rework context/values/hooks in prompt_or_validate_form --- src/utils/form.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index 7098692a4..1496f445b 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1471,9 +1471,6 @@ def prompt_or_validate_form( context: Context = {}, hooks: Hooks = {}, ) -> FormModel: - answers = {**prefilled_answers} - values = {} - for option in options: interactive = Moulinette.interface.type == "cli" and os.isatty(1) @@ -1505,7 +1502,7 @@ def prompt_or_validate_form( if isinstance(option, BaseInputOption): # FIXME normalized needed, form[option.id] should already be normalized # only update the context with the value - context[option.id] = form[option.id] + context[option.id] = option.normalize(form[option.id]) # FIXME here we could error out if option.id in prefilled_answers: @@ -1542,7 +1539,8 @@ def prompt_or_validate_form( try: # Normalize and validate - values[option.id] = form[option.id] = option.normalize(value, option) + form[option.id] = option.normalize(value, option) + context[option.id] = form[option.id] except (ValidationError, YunohostValidationError) as e: # If in interactive cli, re-ask the current question if i < 4 and interactive: @@ -1562,11 +1560,13 @@ def prompt_or_validate_form( # Search for post actions in hooks post_hook = f"post_ask__{option.id}" if post_hook in hooks: - values.update(hooks[post_hook](option)) - # FIXME reapply new values to form to validate it - - answers.update(values) - context.update(values) + # Hooks looks like they can return multiple values, validate those + values = hooks[post_hook](option) + for option_id, value in values.items(): + option = next(opt for opt in options if option.id == option_id) + if option and isinstance(option, BaseInputOption): + form[option.id] = option.normalize(value, option) + context[option.id] = form[option.id] return form From 98ec5448f2aba6e8b51c5720b5f80fe600cf51ab Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 15:07:34 +0200 Subject: [PATCH 0349/1116] form: cli retries as variable to be patched in tests --- src/tests/test_questions.py | 8 ++++++++ src/utils/form.py | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 387d5c0f9..6aca55e1a 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -26,6 +26,7 @@ from yunohost.utils.form import ( FileOption, evaluate_simple_js_expression, ) +from yunohost.utils import form from yunohost.utils.error import YunohostError, YunohostValidationError @@ -94,6 +95,12 @@ def patch_with_tty(): yield +@pytest.fixture +def patch_cli_retries(): + with patch.object(form, "MAX_RETRIES", 0): + yield + + # ╭───────────────────────────────────────────────────────╮ # │ ╭─╴╭─╴┌─╴╭╮╷╭─┐┌─╮╶┬╴╭─╮╭─╴ │ # │ ╰─╮│ ├─╴│││├─┤├┬╯ │ │ │╰─╮ │ @@ -405,6 +412,7 @@ def _test_intake_may_fail(raw_option, intake, expected_output): _test_intake(raw_option, intake, expected_output) +@pytest.mark.usefixtures("patch_cli_retries") # To avoid chain error logging class BaseTest: raw_option: dict[str, Any] = {} prefill: dict[Literal["raw_option", "prefill", "intake"], Any] diff --git a/src/utils/form.py b/src/utils/form.py index 1496f445b..bf50d93a4 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1464,6 +1464,9 @@ def parse_prefilled_values( return values +MAX_RETRIES = 4 + + def prompt_or_validate_form( options: list[AnyOption], form: FormModel, @@ -1543,7 +1546,7 @@ def prompt_or_validate_form( context[option.id] = form[option.id] except (ValidationError, YunohostValidationError) as e: # If in interactive cli, re-ask the current question - if i < 4 and interactive: + if i < MAX_RETRIES and interactive: logger.error(str(e)) value = None continue From b45515049dfee9af447a4244ba653b86cb1225fb Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 17:26:46 +0200 Subject: [PATCH 0350/1116] config: fix wrong diff logic on settings apply --- src/utils/configpanel.py | 31 ++++++++++++++++++++++++++++--- src/utils/form.py | 2 +- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index eb9455a80..b48046ca3 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -282,6 +282,7 @@ class ConfigPanel: filter_key: "FilterKey" = (None, None, None) config: Union[ConfigPanelModel, None] = None form: Union["FormModel", None] = None + raw_settings: "RawSettings" = {} hooks: "Hooks" = {} @classmethod @@ -596,6 +597,8 @@ class ConfigPanel: self, config: ConfigPanelModel ) -> tuple[ConfigPanelModel, "RawSettings"]: raw_settings = self._get_raw_settings(config) + # Save `raw_settings` for diff at `_apply` + self.raw_settings = raw_settings values = {} for _, section, option in config.iter_children(): @@ -734,12 +737,34 @@ class ConfigPanel: mkdir(dir_path, mode=0o700) exclude_defaults = self.save_mode == "diff" - settings = form.dict(exclude_defaults=exclude_defaults, exclude=exclude) # type: ignore + # get settings keys filtered by filter_key + partial_settings_keys = form.__fields__.keys() + # get filtered settings + partial_settings = form.dict(exclude_defaults=exclude_defaults, exclude=exclude) # type: ignore + # get previous settings that we will updated with new settings + current_settings = self.raw_settings.copy() + + if exclude: + current_settings = { + key: value + for key, value in current_settings.items() + if key not in exclude + } + + for key in partial_settings_keys: + if ( + exclude_defaults + and key not in partial_settings + and key in current_settings + ): + del current_settings[key] + elif key in partial_settings: + current_settings[key] = partial_settings[key] # Save the settings to the .yaml file - write_to_yaml(self.save_path, settings) + write_to_yaml(self.save_path, current_settings) - return settings + return current_settings def _run_action(self, form: "FormModel", action_id: str): raise NotImplementedError() diff --git a/src/utils/form.py b/src/utils/form.py index bf50d93a4..d8ff4b8c7 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1493,7 +1493,7 @@ def prompt_or_validate_form( # - we doesn't want to give a specific value # - we want to keep the previous value # - we want the default value - context[option.id] = form[option.id] = None + context[option.id] = None continue From 54cc23c90cc87d853cfac9e355efc8891e9bda07 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 17:46:55 +0200 Subject: [PATCH 0351/1116] config: update SettingsConfigPanel.reset --- src/settings.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/settings.py b/src/settings.py index 0d0a5406b..5f645e3dc 100644 --- a/src/settings.py +++ b/src/settings.py @@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Any, Union from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.utils.configpanel import ConfigPanel +from yunohost.utils.configpanel import ConfigPanel, parse_filter_key from yunohost.utils.form import BaseOption from yunohost.regenconf import regen_conf from yunohost.firewall import firewall_reload @@ -31,6 +31,8 @@ from yunohost.log import is_unit_operation from yunohost.utils.legacy import translate_legacy_settings_to_configpanel_settings if TYPE_CHECKING: + from yunohost.log import OperationLogger + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny from yunohost.utils.configpanel import ( @@ -148,25 +150,26 @@ class SettingsConfigPanel(ConfigPanel): return result - def reset(self, key="", operation_logger=None): - self.filter_key = key + def reset(self, key: Union[str, None] = None, operation_logger: Union["OperationLogger", None] = None,): + self.filter_key = parse_filter_key(key) # Read config panel toml - self._get_config_panel() + self.config, self.form = self._get_config_panel(prevalidate=True) - if not self.config: - raise YunohostValidationError("config_no_panel") + # FIXME find a better way to exclude previous settings + previous_settings = self.form.dict() - # Replace all values with default values - self.values = self._get_default_values() + for option in self.config.options: + if not option.readonly and (option.optional or option.default not in {None, ""}): + self.form[option.id] = option.normalize(option.default, option) - BaseOption.operation_logger = operation_logger + # FIXME Not sure if this is need (redact call to operation logger does it on all the instances) + # BaseOption.operation_logger = operation_logger if operation_logger: operation_logger.start() - try: - self._apply() + self._apply(self.form, previous_settings) except YunohostError: raise # Script got manually interrupted ... @@ -184,7 +187,9 @@ class SettingsConfigPanel(ConfigPanel): raise logger.success(m18n.n("global_settings_reset_success")) - operation_logger.success() + + if operation_logger: + operation_logger.success() def _get_raw_config(self) -> "RawConfig": raw_config = super()._get_raw_config() From 37b4eb956da2afe63e1c8bc4480d4fa4654fc7ca Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 18:52:59 +0200 Subject: [PATCH 0352/1116] typing: add missing type + misc typing fixes --- src/app.py | 13 ++++++---- src/domain.py | 2 +- src/log.py | 2 +- src/settings.py | 17 ++++++++++---- src/utils/configpanel.py | 51 ++++++++++++++++++++-------------------- src/utils/form.py | 46 +++++++++++++++++++----------------- 6 files changed, 72 insertions(+), 59 deletions(-) diff --git a/src/app.py b/src/app.py index 970a66fb2..0514066c9 100644 --- a/src/app.py +++ b/src/app.py @@ -1814,26 +1814,29 @@ class AppConfigPanel(ConfigPanel): form: "FormModel", previous_settings: dict[str, Any], exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, - ): + ) -> None: env = {key: str(value) for key, value in form.dict().items()} return_content = self._call_config_script("apply", env=env) # If the script returned validation error # raise a ValidationError exception using # the first key - if return_content: - for key, message in return_content.get("validation_errors").items(): + errors = return_content.get("validation_errors") + if errors: + for key, message in errors.items(): raise YunohostValidationError( "app_argument_invalid", name=key, error=message, ) - def _run_action(self, form: "FormModel", action_id: str): + def _run_action(self, form: "FormModel", action_id: str) -> None: env = {key: str(value) for key, value in form.dict().items()} self._call_config_script(action_id, env=env) - def _call_config_script(self, action, env=None): + def _call_config_script( + self, action: str, env: Union[dict[str, Any], None] = None + ) -> dict[str, Any]: from yunohost.hook import hook_exec if env is None: diff --git a/src/domain.py b/src/domain.py index f0531e624..892220a68 100644 --- a/src/domain.py +++ b/src/domain.py @@ -720,7 +720,7 @@ class DomainConfigPanel(ConfigPanel): form: "FormModel", previous_settings: dict[str, Any], exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, - ): + ) -> None: next_settings = { k: v for k, v in form.dict().items() if previous_settings.get(k) != v } diff --git a/src/log.py b/src/log.py index 13683d8ef..5a72411d8 100644 --- a/src/log.py +++ b/src/log.py @@ -469,7 +469,7 @@ class OperationLogger: This class record logs and metadata like context or start time/end time. """ - _instances: List[object] = [] + _instances: List["OperationLogger"] = [] def __init__(self, operation, related_to=None, **kwargs): # TODO add a way to not save password on app installation diff --git a/src/settings.py b/src/settings.py index 5f645e3dc..f70f9df61 100644 --- a/src/settings.py +++ b/src/settings.py @@ -136,7 +136,7 @@ class SettingsConfigPanel(ConfigPanel): save_mode = "diff" virtual_settings = {"root_password", "root_password_confirm", "passwordless_sudo"} - def __init__(self, config_path=None, save_path=None, creation=False): + def __init__(self, config_path=None, save_path=None, creation=False) -> None: super().__init__("settings") def get( @@ -150,7 +150,11 @@ class SettingsConfigPanel(ConfigPanel): return result - def reset(self, key: Union[str, None] = None, operation_logger: Union["OperationLogger", None] = None,): + def reset( + self, + key: Union[str, None] = None, + operation_logger: Union["OperationLogger", None] = None, + ) -> None: self.filter_key = parse_filter_key(key) # Read config panel toml @@ -160,8 +164,11 @@ class SettingsConfigPanel(ConfigPanel): previous_settings = self.form.dict() for option in self.config.options: - if not option.readonly and (option.optional or option.default not in {None, ""}): - self.form[option.id] = option.normalize(option.default, option) + if not option.readonly and ( + option.optional or option.default not in {None, ""} + ): + # FIXME Mypy complains about option.default not being a valid type for normalize but this should be ok + self.form[option.id] = option.normalize(option.default, option) # type: ignore # FIXME Not sure if this is need (redact call to operation logger does it on all the instances) # BaseOption.operation_logger = operation_logger @@ -230,7 +237,7 @@ class SettingsConfigPanel(ConfigPanel): form: "FormModel", previous_settings: dict[str, Any], exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, - ): + ) -> None: root_password = form.get("root_password", None) root_password_confirm = form.get("root_password_confirm", None) passwordless_sudo = form.get("passwordless_sudo", None) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index b48046ca3..b23df6ddd 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -21,7 +21,7 @@ import os import re from collections import OrderedDict from logging import getLogger -from typing import TYPE_CHECKING, Any, Literal, Sequence, Type, Union +from typing import TYPE_CHECKING, Any, Iterator, Literal, Sequence, Type, Union from pydantic import BaseModel, Extra, validator @@ -32,7 +32,6 @@ from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.form import ( AnyOption, BaseInputOption, - BaseOption, BaseReadonlyOption, FileOption, OptionsModel, @@ -70,7 +69,7 @@ class ContainerModel(BaseModel): services: list[str] = [] help: Union[Translation, None] = None - def translate(self, i18n_key: Union[str, None] = None): + def translate(self, i18n_key: Union[str, None] = None) -> None: """ Translate `ask` and `name` attributes of panels and section. This is in place mutation. @@ -111,16 +110,16 @@ class SectionModel(ContainerModel, OptionsModel): ) @property - def is_action_section(self): + def is_action_section(self) -> bool: return any([option.type is OptionType.button for option in self.options]) - def is_visible(self, context: dict[str, Any]): + def is_visible(self, context: dict[str, Any]) -> bool: if isinstance(self.visible, bool): return self.visible return evaluate_simple_js_expression(self.visible, context=context) - def translate(self, i18n_key: Union[str, None] = None): + def translate(self, i18n_key: Union[str, None] = None) -> None: """ Call to `Container`'s `translate` for self translation + Call to `OptionsContainer`'s `translate_options` for options translation @@ -151,7 +150,7 @@ class PanelModel(ContainerModel): id=id, name=name, services=services, help=help, sections=sections ) - def translate(self, i18n_key: Union[str, None] = None): + def translate(self, i18n_key: Union[str, None] = None) -> None: """ Recursivly mutate translatable attributes to their translation """ @@ -181,14 +180,14 @@ class ConfigPanelModel(BaseModel): super().__init__(version=version, i18n=i18n, panels=panels) @property - def sections(self): + def sections(self) -> Iterator[SectionModel]: """Convinient prop to iter on all sections""" for panel in self.panels: for section in panel.sections: yield section @property - def options(self): + def options(self) -> Iterator[AnyOption]: """Convinient prop to iter on all options""" for section in self.sections: for option in section.options: @@ -231,7 +230,7 @@ class ConfigPanelModel(BaseModel): for option in section.options: yield (panel, section, option) - def translate(self): + def translate(self) -> None: """ Recursivly mutate translatable attributes to their translation """ @@ -239,7 +238,7 @@ class ConfigPanelModel(BaseModel): panel.translate(self.i18n) @validator("version", always=True) - def check_version(cls, value, field: "ModelField"): + def check_version(cls, value: float, field: "ModelField") -> float: if value < CONFIG_PANEL_VERSION_SUPPORTED: raise ValueError( f"Config panels version '{value}' are no longer supported." @@ -302,7 +301,9 @@ class ConfigPanel: entities = [] return entities - def __init__(self, entity, config_path=None, save_path=None, creation=False): + def __init__( + self, entity, config_path=None, save_path=None, creation=False + ) -> None: self.entity = entity self.config_path = config_path if not config_path: @@ -350,7 +351,7 @@ class ConfigPanel: if option is None: # FIXME i18n raise YunohostValidationError( - f"Couldn't find any option with id {option_id}" + f"Couldn't find any option with id {option_id}", raw_msg=True ) if isinstance(option, BaseReadonlyOption): @@ -398,7 +399,7 @@ class ConfigPanel: args: Union[str, None] = None, args_file: Union[str, None] = None, operation_logger: Union["OperationLogger", None] = None, - ): + ) -> None: self.filter_key = parse_filter_key(key) panel_id, section_id, option_id = self.filter_key @@ -466,7 +467,7 @@ class ConfigPanel: if operation_logger: operation_logger.success() - def list_actions(self): + def list_actions(self) -> dict[str, str]: actions = {} # FIXME : meh, loading the entire config panel is again going to cause @@ -486,7 +487,7 @@ class ConfigPanel: args: Union[str, None] = None, args_file: Union[str, None] = None, operation_logger: Union["OperationLogger", None] = None, - ): + ) -> None: # # FIXME : this stuff looks a lot like set() ... # @@ -666,7 +667,7 @@ class ConfigPanel: def _ask( self, config: ConfigPanelModel, - settings: "FormModel", + form: "FormModel", prefilled_answers: dict[str, Any] = {}, action_id: Union[str, None] = None, hooks: "Hooks" = {}, @@ -709,22 +710,22 @@ class ConfigPanel: if option.type is not OptionType.button or option.id == action_id ] - settings = prompt_or_validate_form( + form = prompt_or_validate_form( options, - settings, + form, prefilled_answers=prefilled_answers, context=context, hooks=hooks, ) - return settings + return form def _apply( self, form: "FormModel", previous_settings: dict[str, Any], exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, - ) -> dict[str, Any]: + ) -> None: """ Save settings in yaml file. If `save_mode` is `"diff"` (which is the default), only values that are @@ -764,15 +765,13 @@ class ConfigPanel: # Save the settings to the .yaml file write_to_yaml(self.save_path, current_settings) - return current_settings - - def _run_action(self, form: "FormModel", action_id: str): + def _run_action(self, form: "FormModel", action_id: str) -> None: raise NotImplementedError() - def _reload_services(self): + def _reload_services(self) -> None: from yunohost.service import service_reload_or_restart - services_to_reload = self.config.services + services_to_reload = self.config.services if self.config else [] if services_to_reload: logger.info("Reloading services...") diff --git a/src/utils/form.py b/src/utils/form.py index d8ff4b8c7..4d62b0a29 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -32,7 +32,7 @@ from typing import ( Annotated, Any, Callable, - List, + Iterable, Literal, Mapping, Type, @@ -207,7 +207,7 @@ def js_to_python(expr): return py_expr -def evaluate_simple_js_expression(expr, context={}): +def evaluate_simple_js_expression(expr: str, context: dict[str, Any] = {}) -> bool: if not expr.strip(): return False node = ast.parse(js_to_python(expr), mode="eval").body @@ -650,7 +650,7 @@ class NumberOption(BaseInputOption): _none_as_empty_str = False @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> Union[int, None]: if isinstance(value, int): return value @@ -704,7 +704,7 @@ class BooleanOption(BaseInputOption): _none_as_empty_str = False @staticmethod - def humanize(value, option={}): + def humanize(value, option={}) -> str: option = option.dict() if isinstance(option, BaseOption) else option yes = option.get("yes", 1) @@ -727,7 +727,7 @@ class BooleanOption(BaseInputOption): ) @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> Any: option = option.dict() if isinstance(option, BaseOption) else option if isinstance(value, str): @@ -844,7 +844,7 @@ class WebPathOption(BaseInputOption): _annotation = str @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> str: option = option.dict() if isinstance(option, BaseOption) else option if value is None: @@ -892,14 +892,14 @@ class FileOption(BaseInputOption): _upload_dirs: set[str] = set() @classmethod - def clean_upload_dirs(cls): + def clean_upload_dirs(cls) -> None: # Delete files uploaded from API for upload_dir in cls._upload_dirs: if os.path.exists(upload_dir): shutil.rmtree(upload_dir) @classmethod - def _value_post_validator(cls, value: Any, field: "ModelField") -> Any: + def _value_post_validator(cls, value: Any, field: "ModelField") -> str: from base64 import b64decode if not value: @@ -967,7 +967,6 @@ class BaseChoicesOption(BaseInputOption): choices = ( self.choices if isinstance(self.choices, list) else self.choices.keys() ) - # FIXME in case of dict, try to parse keys with `item_type` (at least number) return Literal[tuple(choices)] return self._annotation @@ -1006,6 +1005,7 @@ class BaseChoicesOption(BaseInputOption): class SelectOption(BaseChoicesOption): type: Literal[OptionType.select] = OptionType.select + filter: Literal[None] = None choices: Union[dict[str, Any], list[Any]] default: Union[str, None] _annotation = str @@ -1013,13 +1013,14 @@ class SelectOption(BaseChoicesOption): class TagsOption(BaseChoicesOption): type: Literal[OptionType.tags] = OptionType.tags + filter: Literal[None] = None choices: Union[list[str], None] = None pattern: Union[Pattern, None] = None default: Union[str, list[str], None] _annotation = str @staticmethod - def humanize(value, option={}): + def humanize(value, option={}) -> str: if isinstance(value, list): return ",".join(str(v) for v in value) if not value: @@ -1027,7 +1028,7 @@ class TagsOption(BaseChoicesOption): return value @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> str: if isinstance(value, list): return ",".join(str(v) for v in value) if isinstance(value, str): @@ -1037,7 +1038,7 @@ class TagsOption(BaseChoicesOption): return value @property - def _dynamic_annotation(self): + def _dynamic_annotation(self) -> Type[str]: # TODO use Literal when serialization is seperated from validation # if self.choices is not None: # return Literal[tuple(self.choices)] @@ -1120,7 +1121,7 @@ class DomainOption(BaseChoicesOption): return _get_maindomain() @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> str: if value.startswith("https://"): value = value[len("https://") :] elif value.startswith("http://"): @@ -1314,7 +1315,9 @@ class OptionsModel(BaseModel): options: list[Annotated[AnyOption, Field(discriminator="type")]] @staticmethod - def options_dict_to_list(options: dict[str, Any], optional: bool = False): + def options_dict_to_list( + options: dict[str, Any], optional: bool = False + ) -> list[dict[str, Any]]: return [ option | { @@ -1329,7 +1332,7 @@ class OptionsModel(BaseModel): def __init__(self, **kwargs) -> None: super().__init__(options=self.options_dict_to_list(kwargs)) - def translate_options(self, i18n_key: Union[str, None] = None): + def translate_options(self, i18n_key: Union[str, None] = None) -> None: """ Mutate in place translatable attributes of options to their translations """ @@ -1359,7 +1362,7 @@ class FormModel(BaseModel): validate_assignment = True extra = Extra.ignore - def __getitem__(self, name: str): + def __getitem__(self, name: str) -> Any: # FIXME # if a FormModel's required field is not instancied with a value, it is # not available as an attr and therefor triggers an `AttributeError` @@ -1372,7 +1375,7 @@ class FormModel(BaseModel): return getattr(self, name) - def __setitem__(self, name: str, value: Any): + def __setitem__(self, name: str, value: Any) -> None: setattr(self, name, value) def get(self, attr: str, default: Any = None) -> Any: @@ -1382,7 +1385,9 @@ class FormModel(BaseModel): return default -def build_form(options: list[AnyOption], name: str = "DynamicForm") -> Type[FormModel]: +def build_form( + options: Iterable[AnyOption], name: str = "DynamicForm" +) -> Type[FormModel]: """ Returns a dynamic pydantic model class that can be used as a form. Parsing/validation occurs at instanciation and assignements. @@ -1468,7 +1473,7 @@ MAX_RETRIES = 4 def prompt_or_validate_form( - options: list[AnyOption], + options: Iterable[AnyOption], form: FormModel, prefilled_answers: dict[str, Any] = {}, context: Context = {}, @@ -1503,7 +1508,6 @@ def prompt_or_validate_form( if isinstance(option, BaseReadonlyOption) or option.readonly: if isinstance(option, BaseInputOption): - # FIXME normalized needed, form[option.id] should already be normalized # only update the context with the value context[option.id] = option.normalize(form[option.id]) @@ -1623,7 +1627,7 @@ def ask_questions_and_parse_answers( return (model.options, form) -def hydrate_questions_with_choices(raw_questions: List) -> List: +def hydrate_questions_with_choices(raw_questions: list[dict[str, Any]]) -> list[dict[str, Any]]: out = [] for raw_question in raw_questions: From 2a28e289adcd0224ecd4856314c2ae75b2135b7c Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 19:58:14 +0200 Subject: [PATCH 0353/1116] form: rework 'hydrate_questions...' with a new 'parse_raw_options' that parse and validate options --- src/app.py | 5 ++--- src/utils/form.py | 50 +++++++++++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/app.py b/src/app.py index 0514066c9..8ab683d81 100644 --- a/src/app.py +++ b/src/app.py @@ -51,7 +51,7 @@ from yunohost.utils.form import ( DomainOption, WebPathOption, ask_questions_and_parse_answers, - hydrate_questions_with_choices, + parse_raw_options, ) from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError @@ -963,8 +963,7 @@ def app_upgrade( 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) + manifest["install"] = parse_raw_options(manifest.get("install", {}), serialize=True) # Add a base64 image to be displayed in web-admin if with_screenshot and Moulinette.interface.type == "api": diff --git a/src/utils/form.py b/src/utils/form.py index 4d62b0a29..dce4b94c8 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -29,6 +29,7 @@ from logging import getLogger from typing import ( TYPE_CHECKING, cast, + overload, Annotated, Any, Callable, @@ -1609,6 +1610,33 @@ def ask_questions_and_parse_answers( context = {**current_values, **answers} + model_options = parse_raw_options(raw_options, serialize=False) + # Build the form from those questions and instantiate it without + # parsing/validation (construct) since it may contains required questions. + form = build_form(model_options).construct() + form = prompt_or_validate_form( + model_options, form, prefilled_answers=answers, context=context, hooks=hooks + ) + return (model_options, form) + + +@overload +def parse_raw_options( + raw_options: dict[str, Any], serialize: Literal[True] +) -> list[dict[str, Any]]: + ... + + +@overload +def parse_raw_options( + raw_options: dict[str, Any], serialize: Literal[False] = False +) -> list[AnyOption]: + ... + + +def parse_raw_options( + raw_options: dict[str, Any], serialize: bool = False +) -> Union[list[dict[str, Any]], list[AnyOption]]: # Validate/parse the options attributes try: model = OptionsModel(**raw_options) @@ -1618,24 +1646,8 @@ def ask_questions_and_parse_answers( raise YunohostValidationError(error, raw_msg=True) model.translate_options() - # Build the form from those questions and instantiate it without - # parsing/validation (construct) since it may contains required questions. - form = build_form(model.options).construct() - form = prompt_or_validate_form( - model.options, form, prefilled_answers=answers, context=context, hooks=hooks - ) - return (model.options, form) + if serialize: + return model.dict()["options"] -def hydrate_questions_with_choices(raw_questions: list[dict[str, Any]]) -> list[dict[str, Any]]: - out = [] - - for raw_question in raw_questions: - raw_question = hydrate_option_type(raw_question) - question = OPTIONS[raw_question["type"]](**raw_question) - if isinstance(question, BaseChoicesOption) and question.choices: - raw_question["choices"] = question.choices - raw_question["default"] = question.default - out.append(raw_question) - - return out + return model.options From 6bef4b1e0e9a856ec4dfb026d704addedfb13dc6 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 20:00:02 +0200 Subject: [PATCH 0354/1116] app: remove call of 'domain_config_get' to avoid infinite recursion --- src/app.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app.py b/src/app.py index 8ab683d81..b3e34221d 100644 --- a/src/app.py +++ b/src/app.py @@ -130,7 +130,6 @@ def app_info(app, full=False, upgradable=False): Get info for a specific app """ from yunohost.permission import user_permission_list - from yunohost.domain import domain_config_get _assert_is_installed(app) @@ -229,9 +228,7 @@ def app_info(app, full=False, upgradable=False): ret["is_webapp"] = "domain" in settings and "path" in settings if ret["is_webapp"]: - ret["is_default"] = ( - domain_config_get(settings["domain"], "feature.app.default_app") == app - ) + ret["is_default"] = settings.get("default_app", "_none") ret["supports_change_url"] = os.path.exists( os.path.join(setting_path, "scripts", "change_url") From b778aaf780ff5deb708bd2624547b4fc739a3102 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 19 Apr 2023 20:01:18 +0200 Subject: [PATCH 0355/1116] form: remove ChoosableOptions for now --- src/utils/form.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index dce4b94c8..81bd10ba8 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -938,18 +938,6 @@ class FileOption(BaseInputOption): # ─ CHOICES ─────────────────────────────────────────────── -ChoosableOptions = Literal[ - OptionType.string, - OptionType.color, - OptionType.number, - OptionType.date, - OptionType.time, - OptionType.email, - OptionType.path, - OptionType.url, -] - - class BaseChoicesOption(BaseInputOption): # FIXME probably forbid choices to be None? filter: Union[JSExpression, None] = None # filter before choices From bd9bf29a88670efc495680137125f49a01c6ddbe Mon Sep 17 00:00:00 2001 From: axolotle Date: Thu, 20 Apr 2023 15:48:51 +0200 Subject: [PATCH 0356/1116] debian: add python3-pydantic + python3-email-validator dependencies --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 9f156ddea..70a780af7 100644 --- a/debian/control +++ b/debian/control @@ -16,7 +16,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , python3-toml, python3-packaging, python3-publicsuffix2 , python3-ldap, python3-zeroconf (>= 0.47), python3-lexicon, , python3-cryptography, python3-jwt - , python-is-python3 + , python-is-python3, python3-pydantic, python3-email-validator , nginx, nginx-extras (>=1.22) , apt, apt-transport-https, apt-utils, dirmngr , openssh-server, iptables, fail2ban, bind9-dnsutils From fccb291d7885c92a9e26a34867a715e9b9ef6045 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 21 Apr 2023 22:05:49 +0200 Subject: [PATCH 0357/1116] form: readd `pattern` to `path` --- src/utils/form.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index 81bd10ba8..76328f113 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -396,6 +396,7 @@ class AlertOption(BaseReadonlyOption): class ButtonOption(BaseReadonlyOption): type: Literal[OptionType.button] = OptionType.button + bind: Literal["null"] = "null" help: Union[Translation, None] = None style: State = State.success icon: Union[str, None] = None @@ -839,10 +840,8 @@ class EmailOption(BaseInputOption): _annotation = EmailStr -class WebPathOption(BaseInputOption): +class WebPathOption(BaseStringOption): type: Literal[OptionType.path] = OptionType.path - default: Union[str, None] - _annotation = str @staticmethod def normalize(value, option={}) -> str: @@ -876,7 +875,6 @@ class WebPathOption(BaseInputOption): class URLOption(BaseStringOption): type: Literal[OptionType.url] = OptionType.url - default: Union[str, None] _annotation = HttpUrl From 48f882ecd314ba5cb9849dc55372a8ebc074834b Mon Sep 17 00:00:00 2001 From: axolotle Date: Sat, 22 Apr 2023 18:47:05 +0200 Subject: [PATCH 0358/1116] form+configpanel: reflect Section `optional` value to all its Options --- src/utils/configpanel.py | 3 ++- src/utils/form.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index b23df6ddd..55dd07787 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -95,9 +95,10 @@ class SectionModel(ContainerModel, OptionsModel): services: list[str] = [], help: Union[Translation, None] = None, visible: Union[bool, str] = True, + optional: bool = True, **kwargs, ) -> None: - options = self.options_dict_to_list(kwargs, optional=True) + options = self.options_dict_to_list(kwargs, optional=optional) ContainerModel.__init__( self, diff --git a/src/utils/form.py b/src/utils/form.py index 76328f113..bc21c309a 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1308,9 +1308,8 @@ class OptionsModel(BaseModel): return [ option | { - "id": id_, + "id": option.get("id", id_), "type": option.get("type", "string"), - # ConfigPanel options needs to be set as optional by default "optional": option.get("optional", optional), } for id_, option in options.items() From d370cb0b241695d9d1dd6cf6e0c2482ac3f66648 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 24 Apr 2023 15:09:44 +0200 Subject: [PATCH 0359/1116] configpanel: add `value` in options dict for config get --full --- src/utils/configpanel.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 55dd07787..776577d3e 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -363,7 +363,20 @@ class ConfigPanel: # Format result in 'classic' or 'export' mode self.config.translate() logger.debug(f"Formating result in '{mode}' mode") + + if mode == "full": + result = self.config.dict(exclude_none=True) + + for panel in result["panels"]: + for section in panel["sections"]: + for opt in section["options"]: + instance = self.config.get_option(opt["id"]) + if isinstance(instance, BaseInputOption): + opt["value"] = instance.normalize(self.form[opt["id"]], instance) + return result + result = OrderedDict() + for panel in self.config.panels: for section in panel.sections: if section.is_action_section and mode != "full": @@ -388,10 +401,7 @@ class ConfigPanel: "value" ] = "**************" # Prevent displaying password in `config get` - if mode == "full": - return self.config.dict(exclude_none=True) - else: - return result + return result def set( self, From 51d302bf180bf6f8fbc3cbf16494147f2a7f2b7d Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 24 Apr 2023 15:10:27 +0200 Subject: [PATCH 0360/1116] configpanel: `is_action_section` as attr --- src/utils/configpanel.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 776577d3e..756981b63 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -86,6 +86,7 @@ class ContainerModel(BaseModel): class SectionModel(ContainerModel, OptionsModel): visible: Union[bool, str] = True optional: bool = True + is_action_section: bool = False # Don't forget to pass arguments to super init def __init__( @@ -99,7 +100,7 @@ class SectionModel(ContainerModel, OptionsModel): **kwargs, ) -> None: options = self.options_dict_to_list(kwargs, optional=optional) - + is_action_section = any([option["type"] == OptionType.button for option in options]) ContainerModel.__init__( self, id=id, @@ -108,12 +109,9 @@ class SectionModel(ContainerModel, OptionsModel): help=help, visible=visible, options=options, + is_action_section=is_action_section, ) - @property - def is_action_section(self) -> bool: - return any([option.type is OptionType.button for option in self.options]) - def is_visible(self, context: dict[str, Any]) -> bool: if isinstance(self.visible, bool): return self.visible From 2f4c88ec55b85fbe813470d0881896331a30551e Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 24 Apr 2023 15:12:54 +0200 Subject: [PATCH 0361/1116] form: parse pydantic error in logging --- src/utils/form.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index bc21c309a..36e816110 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1537,7 +1537,11 @@ def prompt_or_validate_form( except (ValidationError, YunohostValidationError) as e: # If in interactive cli, re-ask the current question if i < MAX_RETRIES and interactive: - logger.error(str(e)) + logger.error( + "\n".join([err["msg"] for err in e.errors()]) + if isinstance(e, ValidationError) + else str(e) + ) value = None continue @@ -1627,8 +1631,7 @@ def parse_raw_options( model = OptionsModel(**raw_options) except ValidationError as e: error = "\n".join([err["msg"] for err in e.errors()]) - # FIXME use YunohostError instead since it is not really a user mistake? - raise YunohostValidationError(error, raw_msg=True) + raise YunohostError(error, raw_msg=True) model.translate_options() From 6953a8bf15202856c25c9114f3491dabe9b8f5ea Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 24 Apr 2023 15:22:10 +0200 Subject: [PATCH 0362/1116] configpanel: quick fix option typing --- src/utils/configpanel.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 756981b63..a602266da 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -21,7 +21,7 @@ import os import re from collections import OrderedDict from logging import getLogger -from typing import TYPE_CHECKING, Any, Iterator, Literal, Sequence, Type, Union +from typing import TYPE_CHECKING, Any, Iterator, Literal, Sequence, Type, Union, cast from pydantic import BaseModel, Extra, validator @@ -100,7 +100,9 @@ class SectionModel(ContainerModel, OptionsModel): **kwargs, ) -> None: options = self.options_dict_to_list(kwargs, optional=optional) - is_action_section = any([option["type"] == OptionType.button for option in options]) + is_action_section = any( + [option["type"] == OptionType.button for option in options] + ) ContainerModel.__init__( self, id=id, @@ -370,7 +372,9 @@ class ConfigPanel: for opt in section["options"]: instance = self.config.get_option(opt["id"]) if isinstance(instance, BaseInputOption): - opt["value"] = instance.normalize(self.form[opt["id"]], instance) + opt["value"] = instance.normalize( + self.form[opt["id"]], instance + ) return result result = OrderedDict() @@ -381,6 +385,9 @@ class ConfigPanel: continue for option in section.options: + # FIXME not sure why option resolves as possibly `None` + option = cast(AnyOption, option) + if mode == "export": if isinstance(option, BaseInputOption): result[option.id] = self.form[option.id] From ef860ee6eebb8d64aaad2707dd06363755183eed Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 28 Apr 2023 16:11:38 +0200 Subject: [PATCH 0363/1116] form: default type to "select" if choices in option --- src/utils/form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/form.py b/src/utils/form.py index 36e816110..396419f3e 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1309,7 +1309,7 @@ class OptionsModel(BaseModel): option | { "id": option.get("id", id_), - "type": option.get("type", "string"), + "type": option.get("type", "select" if "choices" in option else "string"), "optional": option.get("optional", optional), } for id_, option in options.items() From 3f417bb9b32a45be1e8dd48c8ee806cc8c0cdde9 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 28 Apr 2023 17:23:02 +0200 Subject: [PATCH 0364/1116] tests: update error instance in tests to YunohostError for packaging errors --- src/tests/test_questions.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 6aca55e1a..8f8e701e9 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -595,7 +595,7 @@ class TestAlert(TestDisplayText): (None, None, {"ask": "Some text\na new line"}), (None, None, {"ask": {"en": "Some text\na new line", "fr": "Un peu de texte\nune nouvelle ligne"}}), *[(None, None, {"ask": "question", "style": style}) for style in ("success", "info", "warning", "danger")], - (None, FAIL, {"ask": "question", "style": "nimp"}), + (None, YunohostError, {"ask": "question", "style": "nimp"}), ] # fmt: on @@ -737,7 +737,7 @@ class TestPassword(BaseTest): *all_fails([], ["one"], {}, raw_option={"optional": True}, error=AttributeError), # FIXME those fails with AttributeError *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), *nones(None, "", output=""), - ("s3cr3t!!", FAIL, {"default": "SUPAs3cr3t!!"}), # default is forbidden + ("s3cr3t!!", YunohostError, {"default": "SUPAs3cr3t!!"}), # default is forbidden *xpass(scenarios=[ ("s3cr3t!!", "s3cr3t!!", {"example": "SUPAs3cr3t!!"}), # example is forbidden ], reason="Should fail; example is forbidden"), @@ -749,7 +749,7 @@ class TestPassword(BaseTest): ("secret", FAIL), *[("supersecret" + char, FAIL) for char in FORBIDDEN_PASSWORD_CHARS], # FIXME maybe add ` \n` to the list? # readonly - ("s3cr3t!!", FAIL, {"readonly": True}), # readonly is forbidden + ("s3cr3t!!", YunohostError, {"readonly": True}), # readonly is forbidden ] # fmt: on @@ -1474,10 +1474,10 @@ class TestTags(BaseTest): # basic types (not in a list) should fail *all_fails(True, False, -1, 0, 1, 1337, 13.37, {}), # Mixed choices should fail - ([False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}], FAIL, {"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), - ("False,True,-1,0,1,1337,13.37,[],['one'],{}", FAIL, {"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), - *all_fails(*([t] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), - *all_fails(*([str(t)] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), + ([False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}], YunohostError, {"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), + ("False,True,-1,0,1,1337,13.37,[],['one'],{}", YunohostError, {"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), + *all_fails(*([t] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}, error=YunohostError), + *all_fails(*([str(t)] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}, error=YunohostError), # readonly ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "default": "one,two"}), ] @@ -1527,7 +1527,7 @@ class TestDomain(BaseTest): ("doesnt_exist.pouet", FAIL, {}), ("fake.com", FAIL, {"choices": ["fake.com"]}), # readonly - (domains1[0], FAIL, {"readonly": True}), # readonly is forbidden + (domains1[0], YunohostError, {"readonly": True}), # readonly is forbidden ] }, { @@ -1627,7 +1627,7 @@ class TestApp(BaseTest): (installed_non_webapp["id"], installed_non_webapp["id"]), (installed_non_webapp["id"], FAIL, {"filter": "is_webapp"}), # readonly - (installed_non_webapp["id"], FAIL, {"readonly": True}), # readonly is forbidden + (installed_non_webapp["id"], YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] @@ -1744,7 +1744,7 @@ class TestUser(BaseTest): ("", regular_username, {"default": regular_username}) ], reason="Should throw 'no default allowed'"), # readonly - (admin_username, FAIL, {"readonly": True}), # readonly is forbidden + (admin_username, YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] @@ -1824,9 +1824,9 @@ class TestGroup(BaseTest): "scenarios": [ ("custom_group", "custom_group"), *all_as("", None, output="visitors", raw_option={"default": "visitors"}), - ("", FAIL, {"default": "custom_group"}), # Not allowed to set a default which is not a default group + ("", YunohostError, {"default": "custom_group"}), # Not allowed to set a default which is not a default group # readonly - ("admins", FAIL, {"readonly": True}), # readonly is forbidden + ("admins", YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] From 3a5d353c4b60770b0ed5ddaaa1a9caa85e66895d Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 28 Apr 2023 17:24:11 +0200 Subject: [PATCH 0365/1116] form: force option type to 'select' if there's 'choices' --- src/tests/test_questions.py | 4 ++-- src/utils/form.py | 26 ++++++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 8f8e701e9..fbbf757c9 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -1989,10 +1989,10 @@ def test_option_default_type_with_choices_is_select(): } answers = {"some_choices": "a", "some_legacy": "a"} - options = ask_questions_and_parse_answers(questions, answers) + options, form = ask_questions_and_parse_answers(questions, answers) for option in options: assert option.type == "select" - assert option.value == "a" + assert form[option.id] == "a" @pytest.mark.skip # we should do something with this example diff --git a/src/utils/form.py b/src/utils/form.py index 396419f3e..c586467fc 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1305,15 +1305,25 @@ class OptionsModel(BaseModel): def options_dict_to_list( options: dict[str, Any], optional: bool = False ) -> list[dict[str, Any]]: - return [ - option - | { - "id": option.get("id", id_), - "type": option.get("type", "select" if "choices" in option else "string"), - "optional": option.get("optional", optional), + options_list = [] + + for id_, data in options.items(): + option = data | { + "id": data.get("id", id_), + "type": data.get("type", OptionType.select if "choices" in data else OptionType.string), + "optional": data.get("optional", optional), } - for id_, option in options.items() - ] + + # LEGACY (`choices` in option `string` used to be valid) + if "choices" in option and option["type"] == OptionType.string: + logger.warning( + f"Packagers: option {id_} has 'choices' but has type 'string', use 'select' instead to remove this warning." + ) + option["type"] = OptionType.select + + options_list.append(option) + + return options_list def __init__(self, **kwargs) -> None: super().__init__(options=self.options_dict_to_list(kwargs)) From 3cae07970e913f04a249b3e930b9693a08aebaa9 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 30 Apr 2023 17:29:28 +0200 Subject: [PATCH 0366/1116] form: remove no longer used hydrate_option_type method --- src/utils/form.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index c586467fc..07be55312 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1310,7 +1310,10 @@ class OptionsModel(BaseModel): for id_, data in options.items(): option = data | { "id": data.get("id", id_), - "type": data.get("type", OptionType.select if "choices" in data else OptionType.string), + "type": data.get( + "type", + OptionType.select if "choices" in data else OptionType.string, + ), "optional": data.get("optional", optional), } @@ -1414,22 +1417,6 @@ def build_form( ) -def hydrate_option_type(raw_option: dict[str, Any]) -> dict[str, Any]: - type_ = raw_option.get( - "type", OptionType.select if "choices" in raw_option else OptionType.string - ) - # LEGACY (`choices` in option `string` used to be valid) - if "choices" in raw_option and type_ == OptionType.string: - logger.warning( - f"Packagers: option {raw_option['id']} has 'choices' but has type 'string', use 'select' instead to remove this warning." - ) - type_ = OptionType.select - - raw_option["type"] = type_ - - return raw_option - - # ╭───────────────────────────────────────────────────────╮ # │ ╷ ╷╶┬╴╶┬╴╷ ╭─╴ │ # │ │ │ │ │ │ ╰─╮ │ From 66cb855c0ca0f484e62db4087bac8b802af0b0bc Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 22 Oct 2023 17:47:43 +0200 Subject: [PATCH 0367/1116] domain: type fix --- src/domain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain.py b/src/domain.py index 892220a68..7ac8a50cb 100644 --- a/src/domain.py +++ b/src/domain.py @@ -753,10 +753,10 @@ class DomainConfigPanel(ConfigPanel): # that can be read by the portal API. # FIXME remove those from the config panel saved values? - portal_values = form.dict(include=portal_options) + portal_values = form.dict(include=set(portal_options)) portal_settings_path = Path(f"{PORTAL_SETTINGS_DIR}/{self.entity}.json") - portal_settings = {"apps": {}} + portal_settings: dict[str, Any] = {"apps": {}} if portal_settings_path.exists(): portal_settings.update(read_json(str(portal_settings_path))) From e7b43c763c28af767fef4b7e0acd97df9fb9059a Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 22 Oct 2023 17:49:08 +0200 Subject: [PATCH 0368/1116] configpanel: do not raise error if no settings file --- src/utils/configpanel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index a602266da..d79b8a80a 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -577,7 +577,8 @@ class ConfigPanel: def _get_raw_settings(self, config: ConfigPanelModel) -> "RawSettings": if not self.save_path or not os.path.exists(self.save_path): - raise YunohostValidationError("config_no_settings") + return {} + # raise YunohostValidationError("config_no_settings") return read_yaml(self.save_path) From 3a31984e3c2c193fc3ab5b2b27acbabd8e50d6db Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 22 Oct 2023 17:51:04 +0200 Subject: [PATCH 0369/1116] configpanel: allow other ConfigPanels to have no settings defined --- src/app.py | 1 + src/utils/configpanel.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index b3e34221d..21219b2ce 100644 --- a/src/app.py +++ b/src/app.py @@ -1801,6 +1801,7 @@ class AppConfigPanel(ConfigPanel): entity_type = "app" save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml") config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.toml") + settings_must_be_defined: bool = True def _get_raw_settings(self, config: "ConfigPanelModel") -> "RawSettings": return self._call_config_script("show") diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index d79b8a80a..325f6579d 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -279,6 +279,7 @@ class ConfigPanel: save_path_tpl: Union[str, None] = None config_path_tpl = "/usr/share/yunohost/config_{entity_type}.toml" save_mode = "full" + settings_must_be_defined: bool = False filter_key: "FilterKey" = (None, None, None) config: Union[ConfigPanelModel, None] = None form: Union["FormModel", None] = None @@ -627,7 +628,7 @@ class ConfigPanel: value = option.default elif option.type is OptionType.file or option.bind == "null": continue - else: + elif self.settings_must_be_defined: raise YunohostError( f"Config panel question '{option.id}' should be initialized with a value during install or upgrade.", raw_msg=True, From 9134515604782cbde14a08745c2da393a5880958 Mon Sep 17 00:00:00 2001 From: axolotle Date: Sun, 22 Oct 2023 17:53:50 +0200 Subject: [PATCH 0370/1116] domain:config: make 'registrar' info a frozen input since an alert has no value --- src/dns.py | 88 +++++++++++++++++++++++------------------------------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/src/dns.py b/src/dns.py index 07ff2fb21..fc4b26a75 100644 --- a/src/dns.py +++ b/src/dns.py @@ -502,11 +502,26 @@ def _get_relative_name_for_dns_zone(domain, base_dns_zone): def _get_registrar_config_section(domain): from lexicon.providers.auto import _relevant_provider_for_domain - registrar_infos = { - "name": m18n.n( - "registrar_infos" - ), # This is meant to name the config panel section, for proper display in the webadmin - } + registrar_infos = OrderedDict( + { + "name": m18n.n( + "registrar_infos" + ), # This is meant to name the config panel section, for proper display in the webadmin + "registrar": OrderedDict( + { + "readonly": True, + "visible": False, + "default": None, + } + ), + "infos": OrderedDict( + { + "type": "alert", + "style": "info", + } + ), + } + ) dns_zone = _get_dns_zone_for_domain(domain) @@ -519,61 +534,34 @@ def _get_registrar_config_section(domain): else: parent_domain_link = parent_domain - registrar_infos["registrar"] = OrderedDict( - { - "type": "alert", - "style": "info", - "ask": m18n.n( - "domain_dns_registrar_managed_in_parent_domain", - parent_domain=parent_domain, - parent_domain_link=parent_domain_link, - ), - "default": "parent_domain", - } + registrar_infos["registrar"]["default"] = "parent_domain" + registrar_infos["infos"]["ask"] = m18n.n( + "domain_dns_registrar_managed_in_parent_domain", + parent_domain=parent_domain, + parent_domain_link=parent_domain_link, ) - return OrderedDict(registrar_infos) + return registrar_infos # TODO big project, integrate yunohost's dynette as a registrar-like provider # TODO big project, integrate other dyndns providers such as netlib.re, or cf the list of dyndns providers supported by cloudron... if is_yunohost_dyndns_domain(dns_zone): - registrar_infos["registrar"] = OrderedDict( - { - "type": "alert", - "style": "success", - "ask": m18n.n("domain_dns_registrar_yunohost"), - "default": "yunohost", - } - ) - return OrderedDict(registrar_infos) + registrar_infos["registrar"]["default"] = "yunohost" + registrar_infos["infos"]["style"] = "success" + registrar_infos["infos"]["ask"] = m18n.n("domain_dns_registrar_yunohost") + + return registrar_infos elif is_special_use_tld(dns_zone): - registrar_infos["registrar"] = OrderedDict( - { - "type": "alert", - "style": "info", - "ask": m18n.n("domain_dns_conf_special_use_tld"), - "default": None, - } - ) + registrar_infos["infos"]["ask"] = m18n.n("domain_dns_conf_special_use_tld") try: registrar = _relevant_provider_for_domain(dns_zone)[0] except ValueError: - registrar_infos["registrar"] = OrderedDict( - { - "type": "alert", - "style": "warning", - "ask": m18n.n("domain_dns_registrar_not_supported"), - "default": None, - } - ) + registrar_infos["registrar"]["default"] = None + registrar_infos["infos"]["ask"] = m18n.n("domain_dns_registrar_not_supported") else: - registrar_infos["registrar"] = OrderedDict( - { - "type": "alert", - "style": "info", - "ask": m18n.n("domain_dns_registrar_supported", registrar=registrar), - "default": registrar, - } + registrar_infos["registrar"]["default"] = registrar + registrar_infos["infos"]["ask"] = m18n.n( + "domain_dns_registrar_supported", registrar=registrar ) TESTED_REGISTRARS = ["ovh", "gandi"] @@ -601,7 +589,7 @@ def _get_registrar_config_section(domain): infos["optional"] = infos.get("optional", "False") registrar_infos.update(registrar_credentials) - return OrderedDict(registrar_infos) + return registrar_infos def _get_registar_settings(domain): From 74fd75ea33e8ddf246a8c83e3035d6f11b733089 Mon Sep 17 00:00:00 2001 From: Sebastian Gumprich Date: Sun, 22 Oct 2023 18:43:26 +0200 Subject: [PATCH 0371/1116] fix typos --- src/utils/resources.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 69b260334..1720b4890 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -61,10 +61,10 @@ class AppResourceManager: try: if todo == "deprovision": # FIXME : i18n, better info strings - logger.info(f"Deprovisionning {name}...") + logger.info(f"Deprovisioning {name}...") old.deprovision(context=context) elif todo == "provision": - logger.info(f"Provisionning {name}...") + logger.info(f"Provisioning {name}...") new.provision_or_update(context=context) elif todo == "update": logger.info(f"Updating {name}...") @@ -90,14 +90,14 @@ class AppResourceManager: # (NB. here we want to undo the todo) if todo == "deprovision": # FIXME : i18n, better info strings - logger.info(f"Reprovisionning {name}...") + logger.info(f"Reprovisioning {name}...") old.provision_or_update(context=context) elif todo == "provision": - logger.info(f"Deprovisionning {name}...") + logger.info(f"Deprovisioning {name}...") new.deprovision(context=context) elif todo == "update": logger.info(f"Reverting {name}...") - old.provision_or_update(context=context) + old.sion_or_update(context=context) except (KeyboardInterrupt, Exception) as e: if isinstance(e, KeyboardInterrupt): logger.error(m18n.n("operation_interrupted")) From 54bc79ed587fc55c467e03e349d9097f56e1118d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin <4533074+alexAubin@users.noreply.github.com> Date: Sun, 22 Oct 2023 18:53:57 +0200 Subject: [PATCH 0372/1116] Apply suggestions from code review --- src/utils/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 1720b4890..3f9719c39 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -97,7 +97,7 @@ class AppResourceManager: new.deprovision(context=context) elif todo == "update": logger.info(f"Reverting {name}...") - old.sion_or_update(context=context) + old.provision_or_update(context=context) except (KeyboardInterrupt, Exception) as e: if isinstance(e, KeyboardInterrupt): logger.error(m18n.n("operation_interrupted")) From 16146913b0207853880a5972256dc4c107f25074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Tue, 10 Oct 2023 04:26:14 +0000 Subject: [PATCH 0373/1116] Translated using Weblate (Galician) Currently translated at 100.0% (782 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/gl.json b/locales/gl.json index 93f73b528..acbb50fe4 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -782,5 +782,7 @@ "dyndns_set_recovery_password_invalid_password": "Fallo ao establecer contrasinal de recuperación: o contrasinal non é suficientemente forte", "dyndns_set_recovery_password_failed": "Fallo ao establecer o contrasinal de recuperación: {error}", "dyndns_set_recovery_password_success": "Estableceuse o contrasinal de recuperación!", - "log_dyndns_unsubscribe": "Retirar subscrición para o subdominio YunoHost '{}'" + "log_dyndns_unsubscribe": "Retirar subscrición para o subdominio YunoHost '{}'", + "ask_dyndns_recovery_password_explain_unavailable": "Este dominio DynDNS xa está rexistrado. Se es a persoa que o rexistrou orixinalmente, podes escribir o código de recuperación para reclamar o dominio.", + "dyndns_too_many_requests": "O servicio dyndns de YunoHost recibeu demasiadas peticións do teu sistema, agarda 1 hora e volve intentalo." } From 5d77843cd9d9807321b21d112be48ccac8d638e5 Mon Sep 17 00:00:00 2001 From: Jose Riha Date: Wed, 11 Oct 2023 18:00:04 +0000 Subject: [PATCH 0374/1116] Translated using Weblate (Slovak) Currently translated at 32.0% (251 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/sk/ --- locales/sk.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/locales/sk.json b/locales/sk.json index bead46713..a6a4f3bb1 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -218,8 +218,8 @@ "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_connected_ipv4": "Server je pripojený k internetu prostredníctvom IPv4!", + "diagnosis_ip_connected_ipv6": "Server 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}", @@ -259,5 +259,7 @@ "app_change_url_script_failed": "Vo skripte na zmenu URL adresy sa vyskytla chyba", "app_not_enough_disk": "Táto aplikácia vyžaduje {required} voľného miesta.", "app_not_enough_ram": "Táto aplikácia vyžaduje {required} pamäte na inštaláciu/aktualizáciu, ale k dispozícii je momentálne iba {current}.", - "apps_failed_to_upgrade": "Nasledovné aplikácie nebolo možné aktualizovať: {apps}" + "apps_failed_to_upgrade": "Nasledovné aplikácie nebolo možné aktualizovať: {apps}", + "global_settings_setting_security_experimental_enabled": "Experimentálne bezpečnostné funkcie", + "global_settings_setting_security_experimental_enabled_help": "Povoliť experimentálne bezpečnostné funkcie (nezapínajte túto možnosť, ak neviete, čo môže spôsobiť!)" } From ae70eebc1317cadb4d84231e0a0d878cc8402d34 Mon Sep 17 00:00:00 2001 From: Jose Riha Date: Wed, 11 Oct 2023 18:34:57 +0000 Subject: [PATCH 0375/1116] Translated using Weblate (Slovak) Currently translated at 32.4% (254 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/sk/ --- locales/sk.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/locales/sk.json b/locales/sk.json index a6a4f3bb1..5e057bf55 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -114,7 +114,7 @@ "certmanager_attempt_to_replace_valid_cert": "Chystáte sa prepísať správny a platný certifikát pre doménu {domain}! (Použite --force na vynútenie)", "certmanager_cannot_read_cert": "Počas otvárania aktuálneho certifikátu pre doménu {domain} došlo k neznámej chybe (súbor: {file}), príčina: {reason}", "certmanager_cert_install_success": "Pre doménu '{domain}' bol práve nainštalovaný certifikát od Let's Encrypt", - "certmanager_cert_install_success_selfsigned": "Pre doménu '{domain}' bol práve nainštalovaný vlastnoručne podpísany (self-signed) certifikát", + "certmanager_cert_install_success_selfsigned": "Pre doménu '{domain}' bol práve nainštalovaný vlastnoručne podpísaný (self-signed) certifikát", "certmanager_cert_renew_success": "Certifikát od Let's Encrypt pre doménu '{domain}' bol úspešne obnovený", "certmanager_cert_signing_failed": "Nepodarilo sa podpísať nový certifikát", "certmanager_domain_cert_not_selfsigned": "Certifikát pre doménu {domain} nie je vlastnoručne podpísaný (self-signed). Naozaj ho chcete nahradiť? (Použite '--force', ak to chcete urobiť.)", @@ -261,5 +261,8 @@ "app_not_enough_ram": "Táto aplikácia vyžaduje {required} pamäte na inštaláciu/aktualizáciu, ale k dispozícii je momentálne iba {current}.", "apps_failed_to_upgrade": "Nasledovné aplikácie nebolo možné aktualizovať: {apps}", "global_settings_setting_security_experimental_enabled": "Experimentálne bezpečnostné funkcie", - "global_settings_setting_security_experimental_enabled_help": "Povoliť experimentálne bezpečnostné funkcie (nezapínajte túto možnosť, ak neviete, čo môže spôsobiť!)" + "global_settings_setting_security_experimental_enabled_help": "Povoliť experimentálne bezpečnostné funkcie (nezapínajte túto možnosť, ak neviete, čo môže spôsobiť!)", + "service_description_rspamd": "Filtruje spam a iné funkcie týkajúce sa e-mailu", + "log_letsencrypt_cert_renew": "Obnoviť '{}' certifikát Let's Encrypt", + "domain_config_cert_summary_selfsigned": "UPOZORNENIE: Aktuálny certifikát je vlastnoručne podpísaný. Prehliadače budú návštevníkom zobrazovať strašidelné varovanie!" } From 00d7f1a20850cacc1cf088e135d943029579a43b Mon Sep 17 00:00:00 2001 From: "Jorge-vitrubio.net" Date: Fri, 13 Oct 2023 11:49:22 +0000 Subject: [PATCH 0376/1116] Translated using Weblate (Catalan) Currently translated at 62.9% (492 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ca/ --- locales/ca.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/ca.json b/locales/ca.json index 821e5c3eb..ea9b74015 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -546,7 +546,7 @@ "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_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). Visita https://infosec.mozilla.org/guidelines/openssh (anglés) per mes informació.", "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 +} From 9c33d9e74f4de80a84becfa9574e64c075353b47 Mon Sep 17 00:00:00 2001 From: cristian amoyao Date: Fri, 13 Oct 2023 11:59:23 +0000 Subject: [PATCH 0377/1116] Translated using Weblate (Spanish) Currently translated at 100.0% (782 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/es/ --- locales/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index 5003c305c..e889ba385 100644 --- a/locales/es.json +++ b/locales/es.json @@ -737,7 +737,7 @@ "migration_0024_rebuild_python_venv_disclaimer_base": "Tras la actualización a Debian Bullseye, algunas aplicaciones Python necesitan ser parcialmente reconstruidas para ser convertidas a la nueva versión de Python distribuida en Debian (en términos técnicos: lo que se llama el 'virtualenv' necesita ser recreado). Mientras tanto, esas aplicaciones Python pueden no funcionar. YunoHost puede intentar reconstruir el virtualenv para algunas de ellas, como se detalla a continuación. Para otras aplicaciones, o si el intento de reconstrucción falla, necesitarás forzar manualmente una actualización para esas aplicaciones.", "migration_description_0024_rebuild_python_venv": "Reparar la aplicación Python tras la migración a bullseye", "global_settings_setting_smtp_relay_enabled": "Activar el relé SMTP", - "domain_config_acme_eligible": "Elegibilidad ACME", + "domain_config_acme_eligible": "Requisitos ACME", "global_settings_setting_ssh_password_authentication": "Autenticación por contraseña", "domain_config_cert_issuer": "Autoridad de certificación", "invalid_shell": "Shell inválido: {shell}", From 75e6afaf5c6ba317180d037db9301bfc547dae88 Mon Sep 17 00:00:00 2001 From: "Jorge-vitrubio.net" Date: Fri, 13 Oct 2023 11:38:21 +0000 Subject: [PATCH 0378/1116] Translated using Weblate (Spanish) Currently translated at 100.0% (782 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/es/ --- locales/es.json | 60 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/locales/es.json b/locales/es.json index e889ba385..a1a89ca8c 100644 --- a/locales/es.json +++ b/locales/es.json @@ -27,10 +27,10 @@ "backup_archive_name_unknown": "Copia de seguridad local desconocida '{name}'", "backup_archive_open_failed": "No se pudo abrir el archivo de respaldo", "backup_cleaning_failed": "No se pudo limpiar la carpeta de respaldo temporal", - "backup_created": "Se ha creado la copia de seguridad", + "backup_created": "Copia de seguridad creada: {name}", "backup_creation_failed": "No se pudo crear el archivo de respaldo", "backup_delete_error": "No se pudo eliminar «{path}»", - "backup_deleted": "Eliminada la copia de seguridad", + "backup_deleted": "Copia de seguridad eliminada: {name}", "backup_hook_unknown": "El gancho «{hook}» de la copia de seguridad es desconocido", "backup_nothings_done": "Nada que guardar", "backup_output_directory_forbidden": "Elija un directorio de salida diferente. Las copias de seguridad no se pueden crear en /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o /home/yunohost.backup/archives subcarpetas", @@ -75,7 +75,7 @@ "main_domain_change_failed": "No se pudo cambiar el dominio principal", "main_domain_changed": "El dominio principal ha cambiado", "not_enough_disk_space": "No hay espacio libre suficiente en «{path}»", - "pattern_backup_archive_name": "Debe ser un nombre de archivo válido con un máximo de 30 caracteres, solo se admiten caracteres alfanuméricos y los caracteres -_. (guiones y punto)", + "pattern_backup_archive_name": "Debe ser un nombre de archivo válido con un máximo de 30 caracteres, solo se admiten caracteres alfanuméricos y los caracteres -_. (guiones y punto)", "pattern_domain": "El nombre de dominio debe ser válido (por ejemplo mi-dominio.org)", "pattern_email": "Debe ser una dirección de correo electrónico válida, sin el símbolo '+' (ej. alguien@ejemplo.com)", "pattern_firstname": "Debe ser un nombre válido (al menos 3 caracteres)", @@ -310,7 +310,7 @@ "group_deleted": "Eliminado el grupo «{group}»", "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).", + "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 como contraseña) y/o una variación de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).", "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.", @@ -342,7 +342,7 @@ "app_action_broke_system": "Esta acción parece que ha roto estos servicios importantes: {services}", "operation_interrupted": "¿La operación fue interrumpida manualmente?", "apps_already_up_to_date": "Todas las aplicaciones están ya actualizadas", - "dyndns_provider_unreachable": "No se puede conectar con el proveedor de Dyndns {provider}: o su YunoHost no está correctamente conectado a Internet o el servidor dynette está caído.", + "dyndns_provider_unreachable": "No se puede conectar con el proveedor de Dyndns {provider}: o su YunoHost no está correctamente conectado a Internet o el servidor dynette está caído.", "group_already_exist": "El grupo {group} ya existe", "group_already_exist_on_system": "El grupo {group} ya existe en los grupos del sistema", "group_cannot_be_deleted": "El grupo {group} no se puede eliminar manualmente.", @@ -569,7 +569,7 @@ "domain_config_auth_consumer_key": "Llave de consumidor", "domain_config_default_app": "App predeterminada", "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_push_failed_to_authenticate": "No se pudo autenticar en la API del registrador para el dominio '{domain}'. ¿Es probable 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 {action} registrar {type}/{name}: {error}", "domain_config_mail_in": "Correos entrantes", @@ -654,7 +654,7 @@ "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_compatibility_help": "Compromiso entre compatibilidad y seguridad para el servidor SSH. Afecta al cifrado (y otros aspectos relacionados con la seguridad). Visite https://infosec.mozilla.org/guidelines/openssh (inglés) para más información.", "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.", @@ -681,15 +681,15 @@ "domain_cannot_add_muc_upload": "No puedes añadir dominios que empiecen por 'muc.'. Este tipo de nombre está reservado para la función de chat XMPP multi-usuarios integrada en YunoHost.", "domain_config_cert_renew_help": "El certificado se renovará automáticamente durante los últimos 15 días de validez. Si lo desea, puede renovarlo manualmente. (No recomendado).", "domain_config_cert_summary_expired": "CRÍTICO: ¡El certificado actual no es válido! ¡HTTPS no funcionará en absoluto!", - "domain_config_cert_summary_letsencrypt": "¡Muy bien! Estás utilizando un certificado Let's Encrypt válido.", + "domain_config_cert_summary_letsencrypt": "¡Muy bien! ¡Estás utilizando un certificado Let's Encrypt válido!", "global_settings_setting_postfix_compatibility": "Compatibilidad con Postfix", "global_settings_setting_root_password_confirm": "Nueva contraseña de root (confirmar)", "global_settings_setting_webadmin_allowlist_enabled": "Activar la lista de IPs permitidas para Webadmin", "migration_0024_rebuild_python_venv_broken_app": "Omitiendo {app} porque virtualenv no puede ser reconstruido fácilmente para esta app. En su lugar, deberías arreglar la situación forzando la actualización de esta app usando `yunohost app upgrade --force {app}`.", "migration_0024_rebuild_python_venv_in_progress": "Ahora intentando reconstruir el virtualenv de Python para `{app}`", - "confirm_app_insufficient_ram": "¡PELIGRO! Esta aplicación requiere {required} de RAM para ser instalada/actualizada, pero solo hay {current} disponible actualmente. Incluso si esta aplicación pudiera ejecutarse, su proceso de instalación/actualización requiere una gran cantidad de RAM, por lo que tu servidor puede congelarse y fallar miserablemente. Si estás dispuesto a asumir ese riesgo de todos modos, escribe '{answers}'", + "confirm_app_insufficient_ram": "¡PELIGRO! Esta aplicación requiere {required} de RAM para ser instalada/actualizada, pero solo hay {current} disponible actualmente. Incluso si esta aplicación pudiera ejecutarse, su proceso de instalación/actualización requiere una gran cantidad de RAM, por lo que tu servidor puede congelarse y fallar miserablemente. Si estás dispuesto a asumir ese riesgo de todos modos, teclea '{answers}'", "confirm_notifications_read": "ADVERTENCIA: Deberías revisar las notificaciones de la aplicación antes de continuar, puede haber información importante que debes conocer. [{answers}]", - "domain_config_cert_summary_selfsigned": "ADVERTENCIA: El certificado actual es autofirmado. ¡Los navegadores mostrarán una espeluznante advertencia a los nuevos visitantes!.", + "domain_config_cert_summary_selfsigned": "ADVERTENCIA: El certificado actual es autofirmado. ¡Los navegadores mostrarán una espeluznante advertencia a los nuevos visitantes!", "global_settings_setting_backup_compress_tar_archives": "Comprimir las copias de seguridad", "global_settings_setting_root_access_explain": "En sistemas Linux, 'root' es el administrador absoluto. En el contexto de YunoHost, el acceso directo 'root' SSH está deshabilitado por defecto - excepto desde la red local del servidor. Los miembros del grupo 'admins' pueden usar el comando sudo para actuar como root desde la linea de comandos. Sin embargo, puede ser útil tener una contraseña de root (robusta) para depurar el sistema si por alguna razón los administradores regulares ya no pueden iniciar sesión.", "migration_0021_not_buster2": "¡La distribución Debian actual no es Buster! Si ya ha ejecutado la migración Buster->Bullseye, entonces este error es sintomático del hecho de que el procedimiento de migración no fue 100% exitoso (de lo contrario YunoHost lo habría marcado como completado). Se recomienda investigar lo sucedido con el equipo de soporte, que necesitará el registro **completo** de la `migración, que se puede encontrar en Herramientas > Registros en el webadmin.", @@ -703,7 +703,7 @@ "global_settings_setting_security_experimental_enabled": "Funciones de seguridad experimentales", "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs no puede reconstruirse automáticamente para esas aplicaciones. Necesitas forzar una actualización para ellas, lo que puede hacerse desde la línea de comandos con: `yunohost app upgrade --force APP`: {ignored_apps}", "migration_0024_rebuild_python_venv_failed": "Error al reconstruir el virtualenv de Python para {app}. La aplicación puede no funcionar mientras esto no se resuelva. Deberías arreglar la situación forzando la actualización de esta app usando `yunohost app upgrade --force {app}`.", - "app_arch_not_supported": "Esta aplicación sólo puede instalarse en arquitecturas {required} pero la arquitectura de su servidor es {current}", + "app_arch_not_supported": "Esta aplicación solo se puede instalar en arquitecturas {', '.join(required)} pero la arquitectura de tu servidor es {current}", "app_resource_failed": "Falló la asignación, desasignación o actualización de recursos para {app}: {error}", "app_not_enough_disk": "Esta aplicación requiere {required} espacio libre.", "app_not_enough_ram": "Esta aplicación requiere {required} de RAM para ser instalada/actualizada, pero solo hay {current} disponible actualmente.", @@ -724,9 +724,9 @@ "pattern_fullname": "Debe ser un nombre completo válido (al menos 3 caracteres)", "password_confirmation_not_the_same": "La contraseña y su confirmación no coinciden", "password_too_long": "Elija una contraseña de menos de 127 caracteres", - "diagnosis_using_yunohost_testing": "apt (el gestor de paquetes del sistema) está actualmente configurado para instalar cualquier actualización de 'testing' para el núcleo de YunoHost.", + "diagnosis_using_yunohost_testing": "apt (el gestor de paquetes del sistema) está configurado actualmente para instalar cualquier actualización 'testing' para el núcleo de YunoHost.", "diagnosis_using_yunohost_testing_details": "Esto probablemente esté bien si sabes lo que estás haciendo, ¡pero presta atención a las notas de la versión antes de instalar actualizaciones de YunoHost! Si quieres deshabilitar las actualizaciones de prueba, debes eliminar la palabra clave testing de /etc/apt/sources.list.d/yunohost.list.", - "global_settings_setting_passwordless_sudo": "Permitir a los administradores utilizar 'sudo' sin tener que volver a escribir sus contraseñas.", + "global_settings_setting_passwordless_sudo": "Permitir a los administradores utilizar 'sudo' sin tener que volver a escribir sus contraseñas", "group_update_aliases": "Actualizando alias para el grupo '{group}'", "group_no_change": "Nada que cambiar para el grupo '{group}'", "global_settings_setting_portal_theme": "Tema del portal", @@ -752,5 +752,37 @@ "app_corrupt_source": "YunoHost ha podido descargar el recurso '{source_id}' ({url}) para {app}, pero no coincide con la suma de comprobación esperada. Esto puede significar que ocurrió un fallo de red en tu servidor, o que el recurso ha sido modificado por el responsable de la aplicación (¿o un actor malicioso?) y los responsables de empaquetar esta aplicación para YunoHost necesitan investigar y actualizar el manifesto de la aplicación para reflejar estos cambios. \n Suma de control sha256 esperada: {expected_sha256}\n Suma de control sha256 descargada: {computed_sha256}\n Tamaño del archivo descargado: {size}", "app_change_url_failed": "No es possible cambiar la URL para {app}: {error}", "app_change_url_require_full_domain": "{app} no se puede mover a esta nueva URL porque requiere un dominio completo (es decir, con una ruta = /)", - "app_change_url_script_failed": "Se ha producido un error en el script de modificación de la url" + "app_change_url_script_failed": "Se ha producido un error en el script de modificación de la url", + "group_mailalias_add": "El alias de '{mail}' será añadido al del grupo '{ group} '", + "group_user_add": "La persona usuaria '{ user} ' será añadida al grupo '{ group} '", + "dyndns_no_recovery_password": "¡No especificó la password de recuperación! ¡En caso de perder control de este dominio, necesitará contactar con una persona administradora del equipo de YunoHost!", + "dyndns_too_many_requests": "El servicio DynDNS de YunoHost recibió demasiadas peticiones de su parte, por favor espere 1 hora antes de intentarlo de nuevo.", + "dyndns_set_recovery_password_unknown_domain": "Falló al establecer la password de recuperación: dominio no registrado", + "global_settings_setting_dns_exposure": "Versión IP del DNS establecida en la configuración y diagnósticos", + "group_mailalias_remove": "El alias de '{mail}' será eliminado del grupo '{ group} '", + "group_user_remove": "La persona usuaria '{ user} ' será eliminada al grupo '{ group} '", + "app_failed_to_upgrade_but_continue": "La aplicación {failed_app} no pudo actualizarse, continúe con las siguientes actualizaciones como solicitado. Ejecuta 'yunohost log show {operation_logger_name}' para visualizar el log de fallos", + "app_not_upgraded_broken_system": "La aplicacion '{failed_app}' falló en la actualización he hizo que el sistema pasase a un estado de fallo, como consecuencia las siguientes actualizaciones de aplicaciones han sido canceladas: {apps}", + "app_not_upgraded_broken_system_continue": "La aplicacion '{failed_app}' falló en la actualización he hizo que el sistema pasase a un estado de fallo (así que --continue-on-failure se ignoró), como consecuencia las siguientes actualizaciones de aplicaciones han sido canceladas: {apps}", + "apps_failed_to_upgrade": "Estas actualizaciones de aplicaciones fallaron: {apps}", + "apps_failed_to_upgrade_line": "\n * {app_id} (para ver el correspondiente log utilice 'yunohost log show {operation_logger_name}')", + "diagnosis_ip_no_ipv6_tip_important": "La IPv6 normalmente debería ser automáticamente configurada por su proveedor de sistemas si estuviese disponible. Si no fuese saí, quizás deba configurar algunos parámetros manualmente tal y como lo explica la documentación: https://yunohost.org/#/ipv6.", + "ask_dyndns_recovery_password_explain": "Porfavor obtenga una password de recuperación para su dominio DynDNS, por si la necesita más adelante.", + "ask_dyndns_recovery_password": "Password de recuperación de DynDNS", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Por favor introduzca la password de recuperación de este dominio DynDNS.", + "log_dyndns_unsubscribe": "Desuscribir del subdominio YunoHost '{}'", + "ask_dyndns_recovery_password_explain_unavailable": "Este domino DynDNS ya está registrado. Si usted es la persona que originalmente registro este dominio, puede introducir la password de recuperación de este dominio.", + "domain_config_default_app_help": "Las personas será automáticamente redirigidas a esta aplicación cuando visiten este dominio. Si no especifica una aplicación, estas serán redirigidas al formulario del portal de usuarias.", + "domain_config_xmpp_help": "NB: algunas opciones de XMPP necesitarán que actualice sus registros DNS y que regenere sus certificados Let's Encrypt para que sean habilitadas", + "dyndns_subscribed": "Dominio DynDNS suscrito", + "dyndns_subscribe_failed": "No pudo suscribirse al dominio DynDNS: {error}", + "dyndns_unsubscribe_failed": "No pudo desuscribirse del dominio DynDNS: {error}", + "dyndns_unsubscribed": "Dominio DynDNS desuscrito", + "dyndns_unsubscribe_denied": "Falló la desuscripción del dominio: credenciales incorrectas", + "dyndns_unsubscribe_already_unsubscribed": "El dominio está ya desuscrito", + "dyndns_set_recovery_password_denied": "Falló al establecer la password de recuperación: llave invalida", + "dyndns_set_recovery_password_invalid_password": "Falló al establecer la password de recuperación: la password no es suficientemente fuerte", + "dyndns_set_recovery_password_failed": "Falló al establecer la password de recuperación: {error}", + "dyndns_set_recovery_password_success": "¡Password de recuperación establecida!", + "global_settings_setting_dns_exposure_help": "NB: Esto afecta únicamente a la configuración recomentada de DNS y en las pruebas de diagnóstico. No afecta a la configuración del sistema." } From 4f4dfdb7f7ef5d248138326b7b78e6c636a86f7e Mon Sep 17 00:00:00 2001 From: "Jorge-vitrubio.net" Date: Fri, 13 Oct 2023 11:43:53 +0000 Subject: [PATCH 0379/1116] Translated using Weblate (Italian) Currently translated at 74.5% (583 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/it.json b/locales/it.json index 21fb52367..4ef8ab5f2 100644 --- a/locales/it.json +++ b/locales/it.json @@ -561,7 +561,7 @@ "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.", "domain_dns_registrar_not_supported": "YunoHost non è riuscito a riconoscere quale registrar sta gestendo questo dominio. Dovresti configurare i tuoi record DNS manualmente, seguendo la documentazione.", "domain_dns_registrar_experimental": "Per ora, il collegamento con le API di **{registrar}** non è stata opportunamente testata e revisionata dalla comunità di YunoHost. Questa funzionalità è **altamente sperimentale**, fai attenzione!", - "domain_dns_push_failed_to_authenticate": "L’autenticazione sulle API del registrar per il dominio '{domain}' è fallita. Probabilmente le credenziali non sono corrette. (Error: {error})", + "domain_dns_push_failed_to_authenticate": "L’autenticazione sulle API del registrar per il dominio '{domain}' è fallita. Probabilmente le credenziali non sono giuste. (Error: {error})", "domain_dns_push_failed_to_list": "Il reperimento dei record attuali usando le API del registrar è fallito: {error}", "domain_dns_push_already_up_to_date": "I record sono aggiornati, nulla da fare.", "domain_dns_pushing": "Sincronizzando i record DNS…", @@ -631,7 +631,7 @@ "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_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server SSH. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza). Segue https://infosec.mozilla.org/guidelines/openssh (inglesse) per averne piú informazione.", "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.", @@ -639,4 +639,4 @@ "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", "app_change_url_failed": "Non è possibile cambiare l'URL per {app}:{error}" -} \ No newline at end of file +} From b719563c7ee08efcc9b6c09fdc483d5fe291900b Mon Sep 17 00:00:00 2001 From: cristian amoyao Date: Fri, 13 Oct 2023 12:01:41 +0000 Subject: [PATCH 0380/1116] Translated using Weblate (Spanish) Currently translated at 100.0% (782 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/es/ --- locales/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index a1a89ca8c..892658fc9 100644 --- a/locales/es.json +++ b/locales/es.json @@ -738,7 +738,7 @@ "migration_description_0024_rebuild_python_venv": "Reparar la aplicación Python tras la migración a bullseye", "global_settings_setting_smtp_relay_enabled": "Activar el relé SMTP", "domain_config_acme_eligible": "Requisitos ACME", - "global_settings_setting_ssh_password_authentication": "Autenticación por contraseña", + "global_settings_setting_ssh_password_authentication": "Autenticación de contraseña", "domain_config_cert_issuer": "Autoridad de certificación", "invalid_shell": "Shell inválido: {shell}", "log_settings_reset": "Restablecer ajuste", From 1d1cbedf3a6de8c9abadc0bff5e14f763eec7f1f Mon Sep 17 00:00:00 2001 From: "Jorge-vitrubio.net" Date: Fri, 13 Oct 2023 12:04:40 +0000 Subject: [PATCH 0381/1116] Translated using Weblate (Italian) Currently translated at 74.6% (584 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/it.json b/locales/it.json index 4ef8ab5f2..98512df40 100644 --- a/locales/it.json +++ b/locales/it.json @@ -182,7 +182,7 @@ "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.", - "app_not_upgraded": "Impossibile aggiornare le applicazioni '{failed_app}' e di conseguenza l'aggiornamento delle seguenti applicazione è stato cancellato: {apps}", + "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}...", "app_start_backup": "Raccogliendo file da salvare nel backup per '{app}'...", @@ -361,7 +361,7 @@ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS invero corrente: {rdns_domain}
Valore atteso: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Il DNS inverso non è correttamente configurato su IPv{ipversion}. Alcune email potrebbero non essere spedite o segnalate come SPAM.", "diagnosis_mail_fcrdns_nok_alternatives_6": "Alcuni provider non permettono di configurare un DNS inverso (o non è configurato bene...). Se il tuo DNS inverso è correttamente configurato per IPv4, puoi provare a disabilitare l'utilizzo di IPv6 durante l'invio mail eseguendo yunohost settings set smtp.allow_ipv6 -v off. NB: se esegui il comando non sarà più possibile inviare o ricevere email da i pochi IPv6-only server mail esistenti.", - "yunohost_postinstall_end_tip": "La post-installazione è completata! Per rifinire il tuo setup, considera di:\n\t- aggiungere il primo utente nella sezione 'Utenti' del webadmin (o eseguendo da terminale 'yunohost user create ');\n\t- eseguire una diagnosi alla ricerca di problemi nella sezione 'Diagnosi' del webadmin (o eseguendo da terminale 'yunohost diagnosis run');\n\t- leggere 'Finalizing your setup' e 'Getting to know YunoHost' nella documentazione admin: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "La post-installazione è completata! Per rifinire il tuo setup, considera di:\n\t- eseguire una diagnosi per la ricerca di problemi nella sezione 'Diagnosi' del webadmin (o eseguendo da terminale 'yunohost diagnosis run');\n\t- leggere 'Finalizing your setup' e 'Getting to know YunoHost' nella documentazione admin: https://yunohost.org/admindoc.", "user_already_exists": "L'utente '{user}' esiste già", "update_apt_cache_warning": "Qualcosa è andato storto mentre eseguivo l'aggiornamento della cache APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}", "update_apt_cache_failed": "Impossibile aggiornare la cache di APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}", From 9c6be3ffd631cbe048eb9469baf24ad880fa3932 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Sat, 21 Oct 2023 16:06:42 +0000 Subject: [PATCH 0382/1116] Translated using Weblate (Basque) Currently translated at 99.1% (775 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/eu/ --- locales/eu.json | 48 +++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/locales/eu.json b/locales/eu.json index 85e7c36fc..c2a461445 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -11,7 +11,7 @@ "backup_deleted": "Babeskopia ezabatu da: {name}", "app_argument_required": "'{name}' argumentua ezinbestekoa da", "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.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "'{domain}' domeinurako DNS balioak ez datoz bat zerbitzariaren IParekin. Egiaztatu 'DNS balioak' (oinarrizkoa) atala diagnostikoen gunean. '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", @@ -83,7 +83,7 @@ "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.", "config_validate_color": "RGB hamaseitar kolore bat izan behar da", - "diagnosis_cant_run_because_of_dep": "Ezinezkoa da diagnosia abiaraztea {category} atalerako {dep}(r)i lotutako arazo garrantzitsuak / garrantzitsuek dirau(t)en artean.", + "diagnosis_cant_run_because_of_dep": "Ezinezkoa da diagnostikoa abiaraztea {category} atalerako {dep}(r)i lotutako arazo garrantzitsuak / garrantzitsuek dirau(t)en artean.", "diagnosis_dns_missing_record": "Proposatutako DNS konfigurazioaren arabera, ondorengo informazioa gehitu beharko zenuke DNS erregistroan:
Mota: {type}
Izena: {name}
Balioa: {value}", "diagnosis_http_nginx_conf_not_up_to_date": "Domeinu honen nginx ezarpenak eskuz moldatu direla dirudi eta YunoHostek ezin du egiaztatu HTTP bidez eskuragarri dagoenik.", "ask_new_admin_password": "Administrazio-pasahitz berria", @@ -116,7 +116,7 @@ "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": "(Katxea oraindik baliogarria da {category} ataleko diagnosirako. Ez da berrabiaraziko!)", + "diagnosis_cache_still_valid": "(Katxea oraindik baliogarria da {category} ataleko diagnostikorako. 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!", @@ -138,7 +138,7 @@ "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_display_tip": "Aurkitu diren arazoak ikusteko joan administrazio-atariko Diagnostikoak gunera, edo exekutatu 'yunohost diagnosis show --issues --human-readable' komandoak nahiago badituzu.", "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.", @@ -216,7 +216,7 @@ "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. Egiaztatu 'Weba' atala diagnosien gunean informazio gehiagorako. (Zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", + "certmanager_domain_http_not_working": "Ez dirudi {domain} domeinua HTTP bidez ikusgai dagoenik. Egiaztatu 'Weba' atala diagnostikoen 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})", @@ -333,7 +333,7 @@ "log_tools_migrations_migrate_forward": "Exekutatu migrazioak", "log_tools_postinstall": "Abiarazi YunoHost zerbitzariaren instalazio ondorengo prozesua", "diagnosis_mail_fcrdns_nok_alternatives_4": "Operadore batzuek ez dute alderantzizko DNSa konfiguratzen uzten (edo funtzioa ez dabil…). Hau dela-eta arazoak badituzu, irtenbide batzuk eduki ditzakezu:
- Operadore batzuek relay posta zerbitzari bat eskaini dezakete, baina kasu horretan zure posta elektronikoa zelatatu dezakete.
- Pribatutasuna bermatzeko *IP publikoa* duen VPN bat erabiltzea izan daiteke irtenbidea. Ikus https://yunohost.org/#/vpn_advantage
- Edo operadore desberdin batera aldatu", - "domain_dns_registrar_supported": "YunoHostek automatikoki antzeman du domeinu hau **{registrar}** erregistro-enpresak kudeatzen duela. Nahi baduzu YunoHostek automatikoki konfiguratu ditzake DNS ezarpenak, API egiaztagiri zuzenak zehazten badituzu. API egiaztagiriak non lortzeko dokumentazioa orri honetan duzu: https://yunohost.org/registar_api_{registrar}. (Baduzu DNS erregistroak eskuz konfiguratzeko aukera ere, gidalerro hauetan ageri den bezala: https://yunohost.org/dns)", + "domain_dns_registrar_supported": "YunoHostek automatikoki antzeman du domeinu hau **{registrar}** erregistro-enpresak kudeatzen duela. Nahi baduzu YunoHostek automatikoki konfiguratu ditzake DNS ezarpenak, API egiaztagiri zuzenak zehazten badituzu. API egiaztagiriak non lortzeko dokumentazioa orri honetan daukazu: https://yunohost.org/registar_api_{registrar}. (Baduzu DNS erregistroak eskuz konfiguratzeko aukera ere, gidalerro hauetan ageri den bezala: https://yunohost.org/dns)", "domain_dns_push_failed_to_list": "Ezinezkoa izan da APIa erabiliz oraingo erregistroak antzematea: {error}", "domain_dns_push_already_up_to_date": "Ezarpenak egunean daude, ez dago zereginik.", "domain_config_mail_out": "Bidalitako mezuak", @@ -583,7 +583,7 @@ "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- 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_postinstall_end_tip": "Instalazio ondorengo prozesua amaitu da! Sistemaren konfigurazioa bukatzeko:\n- erabili 'Diagnostikoak' gunea 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}", @@ -693,16 +693,16 @@ "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_reset_success": "Berrezarri ezarpen orokorrak", "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", + "global_settings_setting_smtp_relay_host": "SMTP errele-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_acme_eligible_explain": "Ez dirudi domeinu hau Let's Encrypt ziurtagirirako prest dagoenik. Egiaztatu DNS ezarpenak eta zerbitzariaren HTTP irisgarritasuna. Diagnostikoen guneko '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", @@ -719,7 +719,7 @@ "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_smtp_relay_enabled": "Gaitu SMTP errelea", "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", @@ -738,7 +738,7 @@ "app_not_enough_ram": "Aplikazio honek {required} RAM behar ditu instalatu edo bertsio-berritzeko, baina {current} bakarrik daude erabilgarri une honetan.", "domain_cannot_add_muc_upload": "Ezin duzu 'muc.'-ekin hasten den domeinurik gehitu. Mota honetako izenak YunoHosten integratuta dagoen XMPP taldeko txatek erabil ditzaten gordeta daude.", "confirm_app_insufficient_ram": "KONTUZ! Aplikazio honek {required} RAM behar ditu instalatu edo bertsio-berritzeko baina unean soilik {current} daude erabilgarri. Aplikazioa ibiliko balitz ere, instalazioak edo bertsio-berritzeak RAM kopuru handia behar du eta zure zerbitzariak erantzuteari utzi eta huts egin lezake. Hala ere arriskatu nahi baduzu idatzi '{answers}'", - "confirm_notifications_read": "ADI: ikuskatu aplikazioaren jakinarazpenak jarraitu baino lehen, baliteke jakin beharreko zerbait esatea. [{answers}]", + "confirm_notifications_read": "ADI: Aztertu aplikazioaren jakinarazpenak jarraitu baino lehen, baliteke jakin beharreko zerbait esatea. [{answers}]", "app_arch_not_supported": "Aplikazio hau {required} arkitekturan instala daiteke bakarrik, baina zure zerbitzariaren arkitektura {current} da", "app_resource_failed": "Huts egin du {app} aplikaziorako baliabideen eguneraketak / prestaketak / askapenak: {error}", "app_not_enough_disk": "Aplikazio honek {required} espazio libre behar ditu.", @@ -754,10 +754,10 @@ "diagnosis_ip_no_ipv6_tip_important": "IPv6 automatikoki ezarri ohi du sistemak edo hornitzaileak erabilgarri baldin badago. Bestela eskuz ezarri beharko dituzu aukera batzuk ondorengo dokumentazioan azaldu bezala: https://yunohost.org/#/ipv6.", "pattern_fullname": "Baliozko izen oso bat izan behar da (gutxienez hiru karaktere)", "app_change_url_failed": "Ezin izan da {app} aplikazioaren URLa aldatu: {error}", - "app_change_url_require_full_domain": "Ezin da {app} aplikazioa URL berri honetara aldatu domeinu oso bat behar duelako (i.e. with path = /)", + "app_change_url_require_full_domain": "Ezin da {app} aplikazioa URL berri honetara aldatu domeinu oso bat behar duelako (hots, / bide-izena duena)", "app_change_url_script_failed": "Errorea gertatu da URLa aldatzeko aginduaren barnean", "app_corrupt_source": "YunoHostek deskargatu du {app} aplikaziorako '{source_id}' ({url}) baliabidea baina ez dator bat espero zen 'checksum'arekin. Agian zerbitzariak interneteko konexioa galdu du tarte batez, EDO baliabidea nolabait moldatua izan da arduradunaren aldetik (edo partehartzaile maltzur batetik?) eta YunoHosten arduradunek egoera aztertu eta aplikazioaren manifestua eguneratu behar dute aldaketa hau kontuan hartzeko.\n Espero zen sha256 checksuma: {expected_sha256}\n Deskargatutakoaren sha256 checksuma: {computed_sha256}\n Deskargatutako fitxategiaren tamaina: {size}", - "app_failed_to_upgrade_but_continue": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du, jarraitu hurrengo bertsio-berritzeetara eskatu bezala. Exekutatu 'yunohost log show {operation_logger_name}' errorearen erregistroa ikusteko", + "app_failed_to_upgrade_but_continue": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du, jarraitu hurrengo bertsio-berritzeekin, eskatu bezala. Exekutatu 'yunohost log show {operation_logger_name}' errorearen erregistroa ikusteko", "app_not_upgraded_broken_system": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du eta sistema hondatu du, beraz, ondorengo aplikazioen bertsio-berritzeak ezeztatu dira: {apps}", "app_not_upgraded_broken_system_continue": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du eta sistema hondatu du (beraz, --continue-on-failure aukerari muzin egin zaio) eta ondorengo aplikazioen bertsio-berritzeak ezeztatu dira: {apps}", "app_failed_to_download_asset": "{app} aplikaziorako '{source_id}' ({url}) baliabidea deskargatzeak huts egin du: {out}", @@ -767,12 +767,22 @@ "group_mailalias_remove": "'{mail}' ePosta aliasa kendu zaio '{group}' taldeari", "group_user_remove": "'{user}' erabiltzailea '{group}' taldetik kenduko da", "group_user_add": "'{user}' erabiltzailea '{group}' taldera gehituko da", - "ask_dyndns_recovery_password_explain": "Aukeratu DynDNS domeinua berreskuratzeko pasahitza, etorkizunean berrezarri beharko bazenu.", - "ask_dyndns_recovery_password_explain_during_unsubscribe": "Sartu DynDNS domeinurako berreskuraketa pasahitza.", - "dyndns_no_recovery_password": "Ez da berreskurapen pasahitzik zehaztu! Domeinuaren gaineko kontrola galduz gero, YunoHost taldeko administrariarekin jarri beharko zara harremanetan!", - "ask_dyndns_recovery_password": "DynDNS berreskuratzeko pasahitza", + "ask_dyndns_recovery_password_explain": "Aukeratu DynDNS domeinurako berreskuratze-pasahitza, etorkizunean berrezarri beharko bazenu.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Sartu DynDNS domeinuaren berreskuratze-pasahitza.", + "dyndns_no_recovery_password": "Ez da berreskuratze-pasahitzik zehaztu! Domeinuaren gaineko kontrola galduz gero, YunoHost taldeko administrariarekin jarri beharko zara harremanetan!", + "ask_dyndns_recovery_password": "DynDNS berreskuratze-pasahitza", "dyndns_subscribed": "DynDNS domeinua harpidetu da", "dyndns_subscribe_failed": "Ezin izan da DynDNS domeinua harpidetu: {error}", "dyndns_unsubscribe_failed": "Ezin izan da DynDNS domeinuaren harpidetza utzi: {error}", - "dyndns_unsubscribed": "DynDNS domeinuaren harpidetza utzi da" + "dyndns_unsubscribed": "DynDNS domeinuaren harpidetza utzi da", + "log_dyndns_unsubscribe": "Utzi '{}' YunoHost azpidomeinuaren harpidetza", + "ask_dyndns_recovery_password_explain_unavailable": "DynDNS domeinu hau erregistratuta dago lehendik ere. Domeinua zeuk erregistratu bazenuen, sartu berreskuratze-pasahitza domeinua berreskuratzeko.", + "dyndns_too_many_requests": "YunoHosten dyndns zerbitzuak zuk egindako eskaera gehiegi jaso ditu, itxaron ordubete inguru berriro saiatu baino lehen.", + "dyndns_unsubscribe_denied": "Ezin izan da domeinuaren harpidetza utzi: datu okerrak", + "dyndns_unsubscribe_already_unsubscribed": "Domeinuaren harpidetza utzita dago lehendik ere", + "dyndns_set_recovery_password_denied": "Berreskuratze-pasahitza ezartzeak huts egin du: gako okerra", + "dyndns_set_recovery_password_unknown_domain": "Berreskuratze-pasahitza ezartzeak huts egin du: domeinua ez dago erregistratuta", + "dyndns_set_recovery_password_invalid_password": "Berreskuratze-pasahitza ezartzeak huts egin du: pasahitza ez da nahikoa sendoa", + "dyndns_set_recovery_password_failed": "Berreskuratze-pasahitza ezartzeak huts egin du: {error}", + "dyndns_set_recovery_password_success": "Berreskuratze-pasahitza ezarri da!" } From 238c1ac4d30b1e37ed53c7515252582c5dcf1ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= Date: Sun, 22 Oct 2023 12:49:29 +0000 Subject: [PATCH 0383/1116] Translated using Weblate (French) Currently translated at 98.4% (770 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 2e4d1602b..d439516de 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -718,7 +718,7 @@ "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_summary_selfsigned": "AVERTISSEMENT : Le certificat actuel est auto-signé. Les navigateurs afficheront un avertissement alarmiste 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", From 022fed7787f7103f13d54fb878b650546d173381 Mon Sep 17 00:00:00 2001 From: Jose Riha Date: Sun, 22 Oct 2023 14:21:04 +0000 Subject: [PATCH 0384/1116] Translated using Weblate (Slovak) Currently translated at 32.4% (254 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/sk/ --- locales/sk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/sk.json b/locales/sk.json index 5e057bf55..34332c981 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -30,7 +30,7 @@ "app_manifest_install_ask_domain": "Vyberte doménu, kam bude táto aplikácia nainštalovaná", "app_manifest_install_ask_is_public": "Má byť táto aplikácia viditeľná pre anonymných návštevníkov?", "app_manifest_install_ask_password": "Vyberte heslo pre správu tejto aplikácie", - "app_manifest_install_ask_path": "Vyberte cestu adresy URL (po názve domény), kde bude táto aplikácia nainštalovaná", + "app_manifest_install_ask_path": "Vyberte cestu adresy URL (po názve domény), kam bude táto aplikácia nainštalovaná", "app_not_correctly_installed": "Zdá sa, že {app} nie je správne nainštalovaná", "app_not_properly_removed": "{app} nebola správne odstránená", "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.", From 1221fd1458a2cd8a2d9249f2ee7c38c3466cb923 Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 21 Apr 2023 22:15:34 +0200 Subject: [PATCH 0385/1116] doc:options: add documentation and generator for configpanel/manifest options --- doc/generate_options_doc.py | 365 +++++++++++++++++++++++++++ src/utils/form.py | 486 ++++++++++++++++++++++++++++++++++++ 2 files changed, 851 insertions(+) create mode 100644 doc/generate_options_doc.py diff --git a/doc/generate_options_doc.py b/doc/generate_options_doc.py new file mode 100644 index 000000000..fc6078370 --- /dev/null +++ b/doc/generate_options_doc.py @@ -0,0 +1,365 @@ +import ast +import datetime +import subprocess + +version = open("../debian/changelog").readlines()[0].split()[1].strip("()") +today = datetime.datetime.now().strftime("%d/%m/%Y") + + +def get_current_commit(): + p = subprocess.Popen( + "git rev-parse --verify HEAD", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + stdout, stderr = p.communicate() + + current_commit = stdout.strip().decode("utf-8") + return current_commit + + +current_commit = get_current_commit() + + +print( + f"""--- +title: Options +template: docs +taxonomy: + category: docs +routes: + default: '/packaging_apps_options' +--- + +# Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_options_doc.py) on {today} (YunoHost version {version}) + +# Options + +Options are fields declaration that renders as form items in the web-admin and prompts in cli. +They are used in app manifests to declare the before installation form and in config panels. + +## Glossary + +You may encounter some named types which are used for simplicity. + +- `Translation`: a translated property + - used for properties: `ask`, `help` and `Pattern.error` + - a `dict` with locales as keys and translations as values: + ```toml + ask.en = "The text in english" + ask.fr = "Le texte en français" + ``` + It is not currently possible for translators to translate those string in weblate. + - a single `str` for a single english default string + ```toml + help = "The text in english" + ``` +- `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`: + - used for properties: `visible` and `enabled` + - operators availables: `==`, `!=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%`, `match()` + - [examples available in the advanced section](#advanced-use-cases) +- `Binding`: bind a value to a file/property/variable/getter/setter/validator + - save the value in `settings.yaml` when not defined (`None`) + - nothing at all with `"null"` + - a custom getter/setter/validator with `"null"` + a function starting with `get__`, `set__`, `validate__` in `scripts/config` + - a variable/property in a file with `:__FINALPATH__/my_file.php` + - a whole file with `__FINALPATH__/my_file.php` + - [examples available in the advanced section](#bind) +- `Pattern`: a `dict` with a regex to match the value against and an error message + ```toml + pattern.regexp = "^[A-F]\d\d$" + pattern.error = "Provide a room like F12: one uppercase and 2 numbers" + # or with translated error + pattern.error.en = "Provide a room like F12: one uppercase and 2 numbers" + pattern.error.fr = "Entrez un numéro de salle comme F12: une lettre majuscule et deux chiffres." + ``` + +""" +) + + +fname = "../src/utils/form.py" +content = open(fname).read() + +# NB: This magic is because we want to be able to run this script outside of a YunoHost context, +# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports... +tree = ast.parse(content) + +OptionClasses = [ + c + for c in tree.body + if isinstance(c, ast.ClassDef) and c.name.endswith("Option") +] + +OptionDocString = {} + +for c in OptionClasses: + if not isinstance(c.body[0], ast.Expr): + continue + option_type = None + + if c.name in {"BaseOption", "BaseInputOption"}: + option_type = c.name + elif c.body[1].target.id == "type": + option_type = c.body[1].value.attr + + docstring = ast.get_docstring(c) + if docstring: + if "##### Properties" not in docstring: + docstring += """ +##### Properties + +- [common properties](#common-option-properties) + """ + OptionDocString[option_type] = docstring + +for option_type, doc in OptionDocString.items(): + print("") + if option_type == "BaseOption": + print("## Common Option properties") + elif option_type == "BaseInputOption": + print("## Common Inputs properties") + elif option_type == "display_text": + print("----------------") + print("## Readonly Options") + print(f"### Option `{option_type}`") + elif option_type == "string": + print("----------------") + print("## Input Options") + print(f"### Option `{option_type}`") + else: + print(f"### Option `{option_type}`") + print("") + print(doc) + print("") + +print( + """ +---------------- + +## Advanced use cases + +### `visible` & `enabled` expression evaluation + +Sometimes we may want to conditionaly display a message or prompt for a value, for this we have the `visible` prop. +And we may want to allow a user to trigger an action only if some condition are met, for this we have the `enabled` prop. + +Expressions are evaluated against a context containing previous values of the current section's options. This quite limited current design exists because on the web-admin or on the CLI we cannot guarantee that a value will be present in the form if the user queried only a single panel/section/option. +In the case of an action, the user will be shown or asked for each of the options of the section in which the button is present. + +The expression has to be written in javascript (this has been designed for the web-admin first and is converted to python on the fly on the cli). + +Available operators are: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`. + +##### Examples + +```toml +# simple "my_option_id" is thruthy/falsy +visible = "my_option_id" +visible = "!my_option_id" +# misc +visible = "my_value >= 10" +visible = "-(my_value + 1) < 0" +visible = "!!my_value || my_other_value" +``` +For a more complete set of examples, [check the tests at the end of the file](https://github.com/YunoHost/yunohost/blob/dev/src/tests/test_questions.py). + +##### match() + +For more complex evaluation we can use regex matching. + +```toml +[my_string] +default = "Lorem ipsum dolor et si qua met!" + +[my_boolean] +type = "boolean" +visible = "my_string && match(my_string, '^Lorem [ia]psumE?')" +``` + +Match the content of a file. + +```toml +[my_file] +type = "file" +accept = ".txt" +bind = "/etc/random/lorem.txt" + +[my_boolean] +type = "boolean" +visible = "my_file && match(my_file, '^Lorem [ia]psumE?')" +``` + +with a file with content like: +```txt +Lorem ipsum dolor et si qua met! +``` + + +### `bind` + +Config panels only + +`bind` allows us to alter the generic behavior of option's values which is: get from and set in `settings.yml`. + +We can: +- alter the source the value comes from with getters. +- alter the destination with setters +- parse/validate the value before destination with validators + +##### Getters + +Define an option's custom getter in a bash script `script/config`. +It has to be named after an option's id prepended by `get__`. + +To display a custom alert message for example. We setup a base option in `config_panel.toml`. + +```toml +[panel.section.alert] +type = "alert" +# bind to "null" to inform there's something in `scripts/config` +bind = "null" +# `ask` & `style` will be injected by a custom getter +``` + +Then add a custom getter that output yaml, every properties defined here will override any property previously declared. + +```bash +get__alert() { + if [ "$whatever" ]; then + cat << EOF +style: success +ask: Your VPN is running :) +EOF + else + cat << EOF +style: danger +ask: Your VPN is down +EOF + fi +} +``` + +Or to inject a custom value: + +```toml +[panel.section.my_hidden_value] +type = "number" +bind = "null" +# option will act as an hidden variable that can be used in context evaluation +# (ie: `visible` or `enabled`) +readonly = true +visible = false +# `default` injected by a custom getter +``` + +```bash +get__my_hidden_value() { + if [ "$whatever" ]; then + # if only a value is needed + echo "10" + else + # or if we need to override some other props + # (use `default` or `value` to inject the value) + cat << EOF +ask: Here's a number +visible: true +default: 0 +EOF + fi +} +``` + +##### Setters + +Define an option's custom setter in a bash script `script/config`. +It has to be named after an option's id prepended by `set__`. + +```toml +[panel.section.my_value] +type = "string" +bind = "null" +ask = "gimme complex string" +``` + +```bash +set__my_value() { + if [ -n "$my_value" ]; then + # split the string into multiple elements or idk + fi + # To save the value or modified value as a setting: + ynh_app_setting_set --app=$app --key=my_value --value="$my_value" +} +``` + +##### Validators + +Define an option's custom validator in a bash script `script/config`. +It has to be named after an option's id prepended by `validate__`. + +Validators allows us to return custom error messages depending on the value. + +```toml +[panel.section.my_value] +type = "string" +bind = "null" +ask = "Gimme a long string" +default = "too short" +``` + +```bash +validate__my_value() { + if [[ "${#my_value}" -lt 12 ]]; then echo 'Too short!'; fi +} +``` + +##### Actions + +Define an option's action in a bash script `script/config`. +It has to be named after a `button`'s id prepended by `run__`. + +```toml +[panel.section.my_action] +type = "button" +# no need to set `bind` to "null" it is its hard default +ask = "Run action" +``` + +```bash +run__my_action() { + ynh_print_info "Running 'my_action'..." +} +``` + +A more advanced example could look like: + +```toml +[panel.my_action_section] +name = "Action section" + [panel.my_action_section.my_repo] + type = "url" + bind = "null" # value will not be saved as a setting + ask = "gimme a repo link" + + [panel.my_action_section.my_repo_name] + type = "string" + bind = "null" # value will not be saved as a setting + ask = "gimme a custom folder name" + + [panel.my_action_section.my_action] + type = "button" + ask = "Clone the repo" + # enabled the button only if the above values is defined + enabled = "my_repo && my_repo_name" +``` + +```bash +run__my_action() { + ynh_print_info "Cloning '$my_repo'..." + cd /tmp + git clone "$my_repo" "$my_repo_name" +} +``` +""" +) diff --git a/src/utils/form.py b/src/utils/form.py index 07be55312..f030523d6 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -298,6 +298,76 @@ class Pattern(BaseModel): class BaseOption(BaseModel): + """ + ##### Examples + + ```toml + [my_option_id] + type = "string" + # ask as `str` + ask = "The text in english" + # ask as `dict` + ask.en = "The text in english" + ask.fr = "Le texte en français" + # advanced props + visible = "my_other_option_id != 'success'" + readonly = true + # much advanced: config panel only? + bind = "null" + ``` + + ##### Properties + + - `type`: + - readonly types: + - [`display_text`](#option-display_text) + - [`markdown`](#option-markdown) + - [`alert`](#option-alert) + - [`button`](#option-button) + - inputs types: + - [`string`](#option-string) + - [`text`](#option-text) + - [`password`](#option-password) + - [`color`](#option-color) + - [`number`](#option-number) + - [`range`](#option-range) + - [`boolean`](#option-boolean) + - [`date`](#option-date) + - [`time`](#option-time) + - [`email`](#option-email) + - [`path`](#option-path) + - [`url`](#option-url) + - [`file`](#option-file) + - [`select`](#option-select) + - [`tags`](#option-tags) + - [`domain`](#option-domain) + - [`app`](#option-app) + - [`user`](#option-user) + - [`group`](#option-group) + - `ask`: `Translation` (default to the option's `id` if not defined): + - text to display as the option's label for inputs or text to display for readonly options + - `visible` (optional): `bool` or `JSExpression` (default: `true`) + - define if the option is diplayed/asked + - if `false` and used alongside `readonly = true`, you get a context only value that can still be used in `JSExpression`s + - `readonly` (optional): `bool` (default: `false`, forced to `true` for readonly types): + - If `true` for input types: forbid mutation of its value + - `bind` (optional): `Binding` (default: `None`): + - (config panels only!) allow to choose where an option's is gathered/stored: + - if not specified, the value will be gathered/stored in the `settings.yml` + - if `"null"`: + - the value will not be stored at all (can still be used in context evaluations) + - if in `scripts/config` there's a function named: + - `get__my_option_id`: the value will be gathered from this custom getter + - `set__my_option_id`: the value will be passed to this custom setter where you can do whatever you want with the value + - `validate__my_option_id`: the value will be passed to this custom validator before any custom setter + - if `bind` is a file path: + - if the path starts with `:`, the value be saved as its id's variable/property counterpart + - this only works for first level variables/properties and simple types (no array) + - else the value will be stored as the whole content of the file + - you can use `__FINALPATH__` in your path to point to dynamic install paths + - FIXME are other global variables accessible? + """ + type: OptionType id: str ask: Union[Translation, None] @@ -364,10 +434,34 @@ class BaseReadonlyOption(BaseOption): class DisplayTextOption(BaseReadonlyOption): + """ + Display simple multi-line content. + + ##### Examples + + ```toml + [my_option_id] + type = "display_text" + ask = "Simple text rendered as is." + ``` + """ + type: Literal[OptionType.display_text] = OptionType.display_text class MarkdownOption(BaseReadonlyOption): + """ + Display markdown multi-line content. + Markdown is currently only rendered in the web-admin + + ##### Examples + ```toml + [my_option_id] + type = "display_text" + ask = "Text **rendered** in markdown." + ``` + """ + type: Literal[OptionType.markdown] = OptionType.markdown @@ -379,6 +473,27 @@ class State(str, Enum): class AlertOption(BaseReadonlyOption): + """ + Alerts displays a important message with a level of severity. + You can use markdown in `ask` but will only be rendered in the web-admin. + + ##### Examples + + ```toml + [my_option_id] + type = "alert" + ask = "The configuration seems to be manually modified..." + style = "warning" + icon = "warning" + ``` + ##### Properties + + - [common properties](#common-option-properties) + - `style`: any of `"success|info|warning|danger"` (default: `"info"`) + - `icon` (optional): any icon name from [Fork Awesome](https://forkaweso.me/Fork-Awesome/icons/) + - Currently only displayed in the web-admin + """ + type: Literal[OptionType.alert] = OptionType.alert style: State = State.info icon: Union[str, None] = None @@ -395,6 +510,45 @@ class AlertOption(BaseReadonlyOption): class ButtonOption(BaseReadonlyOption): + """ + Triggers actions. + Available only in config panels. + Renders as a `button` in the web-admin and can be called with `yunohost [app|domain|settings] action run ` in CLI. + + Every options defined in an action section (a config panel section with at least one `button`) is guaranted to be shown/asked to the user and available in `scripts/config`'s scope. + [check examples in advanced use cases](#actions). + + ##### Examples + + ```toml + [my_action_id] + type = "button" + ask = "Break the system" + style = "danger" + icon = "bug" + # enabled only if another option's value (a `boolean` for example) is positive + enabled = "aknowledged" + ``` + + To be able to trigger an action we have to add a bash function starting with `run__` in your `scripts/config` + + ```bash + run__my_action_id() { + ynh_print_info "Running 'my_action_id' action" + } + ``` + + ##### Properties + + - [common properties](#common-option-properties) + - `bind`: forced to `"null"` + - `style`: any of `"success|info|warning|danger"` (default: `"success"`) + - `enabled`: `Binding` or `bool` (default: `true`) + - when used with `Binding` you can enable/disable the button depending on context + - `icon` (optional): any icon name from [Fork Awesome](https://forkaweso.me/Fork-Awesome/icons/) + - Currently only displayed in the web-admin + """ + type: Literal[OptionType.button] = OptionType.button bind: Literal["null"] = "null" help: Union[Translation, None] = None @@ -415,6 +569,35 @@ class ButtonOption(BaseReadonlyOption): class BaseInputOption(BaseOption): + """ + Rest of the option types available are considered `inputs`. + + ##### Examples + + ```toml + [my_option_id] + type = "string" + # …any common props… + + optional = false + redact = False + default = "some default string" + help = "You can enter almost anything!" + example = "an example string" + placeholder = "write something…" + ``` + + ##### Properties + + - [common properties](#common-option-properties) + - `optional`: `bool` (default: `false`, but `true` in config panels) + - `redact`: `bool` (default: `false`), to redact the value in the logs when the value contain private information + - `default`: depends on `type`, the default value to assign to the option + - in case of readonly values, you can use this `default` to assign a value (or return a dynamic `default` from a custom getter) + - `help` (optional): `Translation`, to display a short help message in cli and web-admin + - `example` (optional): `str`, to display an example value in web-admin only + - `placeholder` (optional): `str`, shown in the web-admin fields only + """ + help: Union[Translation, None] = None example: Union[str, None] = None placeholder: Union[str, None] = None @@ -563,10 +746,44 @@ class BaseStringOption(BaseInputOption): class StringOption(BaseStringOption): + """ + Ask for a simple string. + + ##### Examples + ```toml + [my_option_id] + type = "string" + default = "E10" + pattern.regexp = "^[A-F]\d\d$" + pattern.error = "Provide a room like F12 : one uppercase and 2 numbers" + ``` + + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + type: Literal[OptionType.string] = OptionType.string class TextOption(BaseStringOption): + """ + Ask for a multiline string. + Renders as a `textarea` in the web-admin and by opening a text editor on the CLI. + + ##### Examples + ```toml + [my_option_id] + type = "text" + default = "multi\\nline\\ncontent" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + type: Literal[OptionType.text] = OptionType.text @@ -574,6 +791,22 @@ FORBIDDEN_PASSWORD_CHARS = r"{}" class PasswordOption(BaseInputOption): + """ + Ask for a password. + The password is tested as a regular user password (at least 8 chars) + + ##### Examples + ```toml + [my_option_id] + type = "password" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: forced to `""` + - `redact`: forced to `true` + - `example`: forbidden + """ + type: Literal[OptionType.password] = OptionType.password example: Literal[None] = None default: Literal[None] = None @@ -610,6 +843,21 @@ class PasswordOption(BaseInputOption): class ColorOption(BaseInputOption): + """ + Ask for a color represented as a hex value (with possibly an alpha channel). + Renders as color picker in the web-admin and as a prompt that accept named color like `yellow` in CLI. + + ##### Examples + ```toml + [my_option_id] + type = "color" + default = "#ff0" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + type: Literal[OptionType.color] = OptionType.color default: Union[str, None] _annotation = Color @@ -642,6 +890,26 @@ class ColorOption(BaseInputOption): class NumberOption(BaseInputOption): + """ + Ask for a number (an integer). + + ##### Examples + ```toml + [my_option_id] + type = "number" + default = 100 + min = 50 + max = 200 + step = 5 + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `type`: `number` or `range` (input or slider in the web-admin) + - `min` (optional): minimal int value inclusive + - `max` (optional): maximal int value inclusive + - `step` (optional): currently only used in the webadmin as the `` step jump + """ + # `number` and `range` are exactly the same, but `range` does render as a slider in web-admin type: Literal[OptionType.number, OptionType.range] = OptionType.number default: Union[int, None] @@ -696,6 +964,27 @@ class NumberOption(BaseInputOption): class BooleanOption(BaseInputOption): + """ + Ask for a boolean. + Renders as a switch in the web-admin and a yes/no prompt in CLI. + + ##### Examples + ```toml + [my_option_id] + type = "boolean" + default = 1 + yes = "agree" + no = "disagree" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `0` + - `yes` (optional): (default: `1`) define as what the thruthy value should output + - can be `true`, `True`, `"yes"`, etc. + - `no` (optional): (default: `0`) define as what the thruthy value should output + - can be `0`, `"false"`, `"n"`, etc. + """ + type: Literal[OptionType.boolean] = OptionType.boolean yes: Any = 1 no: Any = 0 @@ -801,6 +1090,23 @@ class BooleanOption(BaseInputOption): class DateOption(BaseInputOption): + """ + Ask for a date in the form `"2025-06-14"`. + Renders as a date-picker in the web-admin and a regular prompt in CLI. + + Can also take a timestamp as value that will output as an ISO date string. + + ##### Examples + ```toml + [my_option_id] + type = "date" + default = "2070-12-31" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + type: Literal[OptionType.date] = OptionType.date default: Union[str, None] _annotation = datetime.date @@ -816,6 +1122,21 @@ class DateOption(BaseInputOption): class TimeOption(BaseInputOption): + """ + Ask for an hour in the form `"22:35"`. + Renders as a date-picker in the web-admin and a regular prompt in CLI. + + ##### Examples + ```toml + [my_option_id] + type = "time" + default = "12:26" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + type: Literal[OptionType.time] = OptionType.time default: Union[str, int, None] _annotation = datetime.time @@ -835,12 +1156,41 @@ class TimeOption(BaseInputOption): class EmailOption(BaseInputOption): + """ + Ask for an email. Validation made with [python-email-validator](https://github.com/JoshData/python-email-validator) + + ##### Examples + ```toml + [my_option_id] + type = "email" + default = "Abc.123@test-example.com" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + type: Literal[OptionType.email] = OptionType.email default: Union[EmailStr, None] _annotation = EmailStr class WebPathOption(BaseStringOption): + """ + Ask for an web path (the part of an url after the domain). Used by default in app install to define from where the app will be accessible. + + ##### Examples + ```toml + [my_option_id] + type = "path" + default = "/" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + type: Literal[OptionType.path] = OptionType.path @staticmethod @@ -874,6 +1224,21 @@ class WebPathOption(BaseStringOption): class URLOption(BaseStringOption): + """ + Ask for any url. + + ##### Examples + ```toml + [my_option_id] + type = "url" + default = "https://example.xn--zfr164b/@handle/" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + type: Literal[OptionType.url] = OptionType.url _annotation = HttpUrl @@ -882,6 +1247,25 @@ class URLOption(BaseStringOption): class FileOption(BaseInputOption): + """ + Ask for file. + Renders a file prompt in the web-admin and ask for a path in CLI. + + ##### Examples + ```toml + [my_option_id] + type = "file" + accept = ".json" + # bind the file to a location to save the file there + bind = "/tmp/my_file.json" + ``` + ##### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `accept`: a comma separated list of extension to accept like `".conf, .ini` + - /!\ currently only work on the web-admin + """ + type: Literal[OptionType.file] = OptionType.file # `FilePath` for CLI (path must exists and must be a file) # `bytes` for API (a base64 encoded file actually) @@ -991,6 +1375,24 @@ class BaseChoicesOption(BaseInputOption): class SelectOption(BaseChoicesOption): + """ + Ask for value from a limited set of values. + Renders as a regular `` in the web-admin and as a regular prompt in CLI with autocompletion of `choices`. - ##### Examples + #### Examples ```toml [section.my_option_id] type = "select" @@ -1398,7 +1395,7 @@ class SelectOption(BaseChoicesOption): choices = "one,two,three" default = "two" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: `""`, obviously the default has to be empty or an available `choices` item. - `choices`: a (coma separated) list of values @@ -1418,7 +1415,7 @@ class TagsOption(BaseChoicesOption): This output as a coma separated list of strings `"one,two,three"` - ##### Examples + #### Examples ```toml [section.my_option_id] type = "tags" @@ -1430,7 +1427,7 @@ class TagsOption(BaseChoicesOption): # choices = "one,two,three" default = "two,three" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: `""`, obviously the default has to be empty or an available `choices` item. - `pattern` (optional): `Pattern`, a regex to match all the values against @@ -1523,12 +1520,12 @@ class DomainOption(BaseChoicesOption): Ask for a user domain. Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of registered domains. - ##### Examples + #### Examples ```toml [section.my_option_id] type = "domain" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: forced to the instance main domain """ @@ -1577,13 +1574,13 @@ class AppOption(BaseChoicesOption): Ask for a user app. Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of installed apps. - ##### Examples + #### Examples ```toml [section.my_option_id] type = "app" filter = "is_webapp" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: `""` - `filter` (optional): `JSExpression` with what `yunohost app info --full` returns as context (only first level keys) @@ -1630,12 +1627,12 @@ class UserOption(BaseChoicesOption): Ask for a user. Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of available usernames. - ##### Examples + #### Examples ```toml [section.my_option_id] type = "user" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: the first admin user found """ @@ -1690,13 +1687,13 @@ class GroupOption(BaseChoicesOption): Ask for a group. Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of available groups. - ##### Examples + #### Examples ```toml [section.my_option_id] type = "group" default = "visitors" ``` - ##### Properties + #### Properties - [common inputs properties](#common-inputs-properties) - `default`: `"all_users"`, `"visitors"` or `"admins"` (default: `"all_users"`) """ From 02619e8284bfa068202a5aa941448bd70cc92ecb Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 24 Oct 2023 15:05:26 +0200 Subject: [PATCH 0391/1116] doc:config fix missing aleks additions --- doc/generate_configpanel_doc.py | 58 +++++----- doc/generate_options_doc.py | 197 ++++++++++++++++---------------- src/utils/configpanel.py | 22 ++-- 3 files changed, 139 insertions(+), 138 deletions(-) diff --git a/doc/generate_configpanel_doc.py b/doc/generate_configpanel_doc.py index e29a80dbc..1eb7b5ebb 100644 --- a/doc/generate_configpanel_doc.py +++ b/doc/generate_configpanel_doc.py @@ -88,41 +88,41 @@ print( """ ## Full example -We supposed we have an upstream app with this simple config.yml file: +Let's imagine that the upstream app is configured using this simple `config.yml` file stored in the app's install directory (typically `/var/www/$app/config.yml`): ```yaml -title: 'My dummy apps' +title: 'My dummy app' theme: 'white' max_rate: 10 max_age: 365 ``` -We could for example create a simple configuration panel for it like this one, by following the syntax `\[PANEL.SECTION.QUESTION\]`: +We could for example create a simple configuration panel for it like this one, by following the syntax `[PANEL.SECTION.QUESTION]`: ```toml version = "1.0" [main] -[main.main] -[main.main.title] -ask.en = "Title" -type = "string" -bind = ":__INSTALL_DIR__/config.yml" + [main.main] + [main.main.title] + ask.en = "Title" + type = "string" + bind = ":__INSTALL_DIR__/config.yml" -[main.main.theme] -ask.en = "Theme" -type = "select" -choices = ["white", "dark"] -bind = ":__INSTALL_DIR__/config.yml" + [main.main.theme] + ask.en = "Theme" + type = "select" + choices = ["white", "dark"] + bind = ":__INSTALL_DIR__/config.yml" -[main.limits] -[main.limits.max_rate] -ask.en = "Maximum display rate" -type = "number" -bind = ":__INSTALL_DIR__/config.yml" + [main.limits] + [main.limits.max_rate] + ask.en = "Maximum display rate" + type = "number" + bind = ":__INSTALL_DIR__/config.yml" -[main.limits.max_age] -ask.en = "Duration of a dummy" -type = "number" -bind = ":__INSTALL_DIR__/config.yml" + [main.limits.max_age] + ask.en = "Duration of a dummy" + type = "number" + bind = ":__INSTALL_DIR__/config.yml" ``` Here we have created one `main` panel, containing the `main` and `limits` sections, containing questions according to params name of our `config.yml` file. Thanks to the `bind` properties, all those questions are bind to their values in the `config.yml` file. @@ -147,13 +147,13 @@ ynh_app_config_apply() { } ``` -List of main configuration helpers -* ynh_app_config_get -* ynh_app_config_show -* ynh_app_config_validate -* ynh_app_config_apply -* ynh_app_config_run +List of main configuration helpers: + * `ynh_app_config_get` + * `ynh_app_config_show` + * `ynh_app_config_validate` + * `ynh_app_config_apply` + * `ynh_app_config_run` -More info on this could be found by reading [vpnclient_ynh config script](https://github.com/YunoHost-Apps/vpnclient_ynh/blob/master/scripts/config) +More info on this can be found by reading [vpnclient_ynh config script](https://github.com/YunoHost-Apps/vpnclient_ynh/blob/master/scripts/config) """ ) diff --git a/doc/generate_options_doc.py b/doc/generate_options_doc.py index 88f6deb20..ea7febe6d 100644 --- a/doc/generate_options_doc.py +++ b/doc/generate_options_doc.py @@ -132,62 +132,70 @@ print( """ ---------------- -## Read and write values: the `bind` property +## Reading and writing values ! Config panels only You can read and write values with 2 mechanisms: the `bind` property in the `config_panel.toml` and for complex use cases the getter/setter in a `config` script. -`bind` allows us to alter the default behavior of applying option's values, which is: get from and set in the app `settings.yml`. +If you did not define a specific getter/setter (see below), and no `bind` argument was defined, YunoHost will read/write the value from/to the app's `/etc/yunohost/$app/settings.yml` file. -We can: +With `bind`, we can: - alter the source the value comes from with binds to file or custom getters. -- alter the destination with binds to file or settings. +- alter the destination with binds to file or custom setters. - parse/validate the value before destination with validators -! IMPORTANT: with the exception of `bind = "null"` options, options ids should almost **always** correspond to an app setting initialized / reused during install/upgrade. +! IMPORTANT: with the exception of `bind = "null"` options, options ids should almost **always** correspond to an app setting initialized/reused during install/upgrade. Not doing so may result in inconsistencies between the config panel mechanism and the use of ynh_add_config -### Read / write into a var of a configuration file +### Read / write into a var of an actual configuration file Settings usually correspond to key/values in actual app configurations. Hence, a more useful mode is to have `bind = ":FILENAME"` with a colon `:` before. In that case, YunoHost will automagically find a line with `KEY=VALUE` in `FILENAME` (with the adequate separator between `KEY` and `VALUE`). YunoHost will then use this value for the read/get operation. During write/set operations, YunoHost will overwrite the value in **both** FILENAME and in the app's settings.yml -Configuration file format supported: `yaml`, `toml`, `json`, `ini`, `env`, `php`, `python`. +Configuration file format supported: `YAML`, `TOML`, `JSON`, `INI`, `PHP`, `.env`-like, `.py`. The feature probably works with others formats, but should be tested carefully. -Note that this feature only works with relatively simple cases such as `KEY: VALUE`, but won't properly work with complex data structures like multiline array/lists or dictionnaries. -It also doesn't work with XML format, custom config function call, php define(), … -If you need to save complex/multiline content in a configuration variable, you should do it via a specific getter/setter. - ```toml -[panel.section.config_value] +[main.main.theme] # Do not use `file` for this since we only want to insert/save a value type = "string" -bind = ":__FINALPATH__/config.ini" -default = "" +bind = ":__INSTALL_DIR__/config.yml" +``` +In which case, YunoHost will look for something like a key/value, with the key being `theme`. + +If the question id in the config panel (here, `theme`) differs from the key in the actual conf file (let's say it's not `theme` but `css_theme`), then you can write: +```toml +[main.main.theme] +type = "string" +bind = "css_theme:__FINALPATH__/config.yml" ``` -By default, `bind = ":FILENAME"` will use the option id as `KEY` but the option id may sometime not be the exact same `KEY` name in the configuration file. -For example, [In pepettes app](https://github.com/YunoHost-Apps/pepettes_ynh/blob/5cc2d3ffd6529cc7356ff93af92dbb6785c3ab9a/conf/settings.py##L11), the python variable is `name` and not `project_name`. In that case, the key name can be specified before the colon `:`. - -```toml -[panel.section.project_name] -bind = "name:__FINALPATH__/config.ini" +!!!! Note: This mechanism is quasi language agnostic and will use regexes to find something that looks like a key=value or common variants. However, it does assume that the key and value are stored on the same line. It doesn't support multiline text or file in a variable with this method. If you need to save multiline content in a configuration variable, you should create a custom getter/setter (see below). +Nested syntax is also supported, which may be useful for example to remove ambiguities about stuff looking like: +```json +{ + "foo": { + "max": 123 + }, + "bar": { + "max": 456 + } +} ``` -Sometimes, you want to read and save a value in a variable name that appears several time in the configuration file (for example variables called `max`). The `bind` property allows you to change the value on the variable following a regex in a the file: +which we can `bind` to using: ```toml -bind = "importExportRateLimiting>max:__INSTALL_DIR__/conf.json" +bind = "foo>max:__INSTALL_DIR__/conf.json" ``` ### Read / write an entire file -You can bind a `text` or directly a `file` to a specific file by using `bind = "FILEPATH`. +Useful when using a question `file` or `text` for which you want to save the raw content directly as a file on the system. ```toml [panel.section.config_file] @@ -207,17 +215,14 @@ default = "key: 'value'" Sometimes the `bind` mechanism is not enough: * the config file format is not supported (e.g. xml, csv) * the data is not contained in a config file (e.g. database, directory, web resources...) - * the data should be writen but not read (e.g. password) - * the data should be read but not writen (e.g. status information) + * the data should be written but not read (e.g. password) + * the data should be read but not written (e.g. fetching status information) * we want to change other things than the value (e.g. the choices list of a select) * the question answer contains several values to dispatch in several places * and so on -For all of those use cases, there are the specific getter or setter mechanism for an option! +You can create specific getter/setters functions inside the `scripts/config` of your app to customize how the information is read/written. -To create specific getter/setter, you first need to create a `config` script inside the `scripts` directory - -`scripts/config` ```bash #!/bin/bash source /usr/share/yunohost/helpers @@ -232,54 +237,65 @@ ynh_app_config_run $1 ### Getters -Define an option's custom getter in a bash script `script/config`. -It has to be named after an option's `id` prepended by `get__`. +A question's getter is the function used to read the current value/state. Custom getters are defined using bash functions called `getter__QUESTION_SHORT_KEY()` which returns data through stdout. -The function should returns one of these two formats: - * a raw format, in this case the return is binded directly to the value of the question - * a yaml format, in this case you can rewrite several properties of your option (like the `style` of an `alert`, the list of `choices` of a `select`, etc.) +Stdout can generated using one of those formats: + 1) either a raw format, in which case the return is binded directly to the value of the question + 2) or a yaml format, in this case you dynamically provide properties for your question (for example the `style` of an `alert`, the list of available `choices` of a `select`, etc.) -[details summary="Basic example : Get the login inside the first line of a file " class="helper-card-subtitle text-muted"] -scripts/config -```bash -get__login_user() { - if [ -s /etc/openvpn/keys/credentials ] - then - echo "$(sed -n 1p /etc/openvpn/keys/credentials)" - else - echo "" - fi -} +[details summary="Basic example with raw stdout: get the timezone on the system" class="helper-card-subtitle text-muted"] + +`config_panel.toml` + +```toml +[main.main.timezone] +ask = "Timezone" +type = "string" ``` -config_panel.toml -```toml -[main.auth.login_user] -ask = "Username" -type = "string" +`scripts/config` + +```bash +get__timezone() { + echo "$(cat /etc/timezone)" +} ``` [/details] -[details summary="Advanced example 1 : Display a list of available plugins" class="helper-card-subtitle text-muted"] -scripts/config -```bash -get__plugins() { - echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]" -} -``` +[details summary="Basic example with yaml-formated stdout : Display a list of available plugins" class="helper-card-subtitle text-muted"] -config_panel.toml +`config_panel.toml` ```toml [main.plugins.plugins] ask = "Plugin to activate" type = "tags" choices = [] ``` + +`scripts/config` + +```bash +get__plugins() { + echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]" +} +``` + [/details] -[details summary="Example 2 : Display the status of a VPN" class="helper-card-subtitle text-muted"] -scripts/config +[details summary="Advanced example with yaml-formated stdout : Display the status of a VPN" class="helper-card-subtitle text-muted"] + +`config_panel.toml` + +```toml +[main.cube.status] +ask = "Custom getter alert" +type = "alert" +style = "info" +bind = "null" # no behaviour on +``` + +`scripts/config` ```bash get__status() { if [ -f "/sys/class/net/tun0/operstate" ] && [ "$(cat /sys/class/net/tun0/operstate)" == "up" ] @@ -298,50 +314,38 @@ EOF fi } ``` - -config_panel.toml -```toml -[main.cube.status] -ask = "Custom getter alert" -type = "alert" -style = "info" -bind = "null" # no behaviour on -``` [/details] ### Setters -Define an option's custom setter in a bash script `script/config`. -It has to be named after an option's id prepended by `set__`. +A question's setter is the function used to set new value/state. Custom setters are defined using bash functions called `setter__QUESTION_SHORT_KEY()`. In the context of the setter function, variables named with the various quetion's short keys are avaible ... for example the user-specified date for question `[main.main.theme]` is available as `$theme`. -This function could access new values defined by the users by using bash variable with the same name as the short key of a question. - -You probably should use `ynh_print_info` in order to display info for user about change that has been made to help them to understand a bit what's going. +When doing non-trivial operations to set a value, you may want to use `ynh_print_info` to inform the admin about what's going on. -[details summary="Basic example : Set the login into the first line of a file " class="helper-card-subtitle text-muted"] -scripts/config -```bash -set__login_user() { - if [ -z "${login_user}" ] - then - echo "${login_user}" > /etc/openvpn/keys/credentials - ynh_print_info "The user login has been registered in /etc/openvpn/keys/credentials" - fi -} +[details summary="Basic example : Set the system timezone" class="helper-card-subtitle text-muted"] + +`config_panel.toml` + +```toml +[main.main.timezone] +ask = "Timezone" +type = "string" ``` -config_panel.toml -```toml -[main.auth.login_user] -ask = "Username" -type = "string" +`scripts/config` + +```bash +set__timezone() { + echo "$timezone" > /etc/timezone + ynh_print_info "The timezone has been changed to $timezone" +} ``` [/details] -#### Validation +### Validation You will often need to validate data answered by the user before to save it somewhere. @@ -353,9 +357,9 @@ pattern.error = 'An email is required for this field' You can also restrict several types with a choices list. ```toml -choices.option1 = "Plop1" -choices.option2 = "Plop2" -choices.option3 = "Plop3" +choices.foo = "Foo (some explanation)" +choices.bar = "Bar (moar explanation)" +choices.loremipsum = "Lorem Ipsum Dolor Sit Amet" ``` Some other type specific argument exist like @@ -366,15 +370,12 @@ Some other type specific argument exist like | `boolean` | `yes` `no` | -If you need more control over validation, you can use custom validators. -Define an option's custom validator in a bash script `script/config`. -It has to be named after an option's id prepended by `validate__`. - +Finally, if you need specific or multi variable validation, you can use custom validators function. Validators allows us to return custom error messages depending on the value. ```bash validate__login_user() { - if [[ "${#login_user}" -lt 4 ]]; then echo 'Too short user login'; fi + if [[ "${#login_user}" -lt 4 ]]; then echo 'User login is too short, should be at least 4 chars'; fi } ``` diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 4f333cc5a..47c97a808 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -217,30 +217,30 @@ class PanelModel(ContainerModel): class ConfigPanelModel(BaseModel): """ - Configuration panels allows instances adminitrators to manage some parameters or runs some actions for which the app's upstream doesn't provide any configuration panels itself. It's a good way to reduce manual change on config files and avoid conflicts on it. + Configuration panels allows admins to manage parameters or runs actions for which the upstream's app doesn't provide any appropriate UI itself. It's a good way to reduce manual change on config files and avoid conflicts on it. - Those panels could also be used as interface generator to extend quickly capabilities of YunoHost (e.g. VPN Client, Hotspost, Borg, etc.). + Those panels can also be used to quickly create interfaces that extend the capabilities of YunoHost (e.g. VPN Client, Hotspost, Borg, etc.). From a packager perspective, this `config_panel.toml` is coupled to the `scripts/config` script, which may be used to define custom getters/setters/validations/actions. However, most use cases should be covered automagically by the core, thus it may not be necessary to define a scripts/config at all! - ! IMPORTANT: Please: Keep in mind the YunoHost spirit, and try to build your panels in such a way as to expose only really useful parameters, and if there are many of them, to relegate those corresponding to rarer use cases to "Advanced" sub-sections. + ! Please: Keep in mind the YunoHost spirit, and try to build your panels in such a way as to expose only really useful, "high-level" parameters, and if there are many of them, to relegate those corresponding to rarer use cases to "Advanced" sub-sections. Keep it simple, focus on common needs, don't expect the admins to have 3 PhDs in computer science. - ### How does `config_panel.toml` work - Basically, configuration panels for apps uses at least a `config_panel.toml` at the root of your package. For advanced usecases, this TOML file could also be paired with a `scripts/config` to define custom getters/setters/validators/actions. However, most use cases should be covered automagically by the core, thus it may not be necessary to define a `scripts/config` at all! + ### `config_panel.toml`'s principle and general format + To create configuration panels for apps, you should at least create a `config_panel.toml` at the root of the package. For more complex cases, this TOML file can be paired with a `config` script inside the scripts directory of your package, which will handle specific controller logic. - The `config_panel.toml` file describes one or several panels, containing some sections, containing some options generally binded to a params in a configuration file. + The `config_panel.toml` describes one or several panels, containing sections, each containing questions generally binded to a params in the app's actual configuration files. ### Options short keys have to be unique - For performance reasons, questions short keys should be unique in all the `config_panel.toml` file, not just inside its panel or its section. - - So you can't have + For performance reasons, questions short keys have to be unique in all the `config_panel.toml` file, not just inside its panel or its section. Hence it's not possible to have: ```toml [manual.vpn.server_ip] [advanced.dns.server_ip] ``` - Indeed the real variable name is server_ip and here you have a conflict. + In which two questions have "real variable name" `is server_ip` and therefore conflict with each other. - ### Options + ! Some short keys are forbidden cause it can interfer with config scripts (`old`, `file_hash`, `types`, `binds`, `formats`, `changed`) and you probably should avoid to use common settings name to avoid to bind your question to this settings (e.g. `id`, `install_time`, `mysql_pwd`, `path`, `domain`, `port`, `db_name`, `current_revision`, `admin`) + + ### Supported questions types and properties [Learn more about Options](/dev/forms) in their dedicated doc page as those are also used in app install forms and core config panels. From c4c79c61fe30491388a2cda4fe5627de81428263 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 25 Oct 2023 15:06:10 +0200 Subject: [PATCH 0392/1116] configpanel: forbid extra props on BaseOption + accordingly fix tests --- src/tests/test_questions.py | 4 ++++ src/utils/form.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index fbbf757c9..afd73c861 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -16,6 +16,7 @@ from yunohost import app, domain, user from yunohost.utils.form import ( OPTIONS, FORBIDDEN_PASSWORD_CHARS, + READONLY_TYPES, ask_questions_and_parse_answers, BaseChoicesOption, BaseInputOption, @@ -444,6 +445,9 @@ class BaseTest: def _test_basic_attrs(self): raw_option = self.get_raw_option(optional=True) + if raw_option["type"] in READONLY_TYPES: + del raw_option["optional"] + if raw_option["type"] == "select": raw_option["choices"] = ["one"] diff --git a/src/utils/form.py b/src/utils/form.py index 07be55312..70afeb8f3 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -258,6 +258,12 @@ class OptionType(str, Enum): group = "group" +READONLY_TYPES = { + OptionType.display_text, + OptionType.markdown, + OptionType.alert, + OptionType.button, +} FORBIDDEN_READONLY_TYPES = { OptionType.password, OptionType.app, @@ -310,6 +316,7 @@ class BaseOption(BaseModel): arbitrary_types_allowed = True use_enum_values = True validate_assignment = True + extra = Extra.forbid @staticmethod def schema_extra(schema: dict[str, Any], model: Type["BaseOption"]) -> None: @@ -1314,9 +1321,11 @@ class OptionsModel(BaseModel): "type", OptionType.select if "choices" in data else OptionType.string, ), - "optional": data.get("optional", optional), } + if option["type"] not in READONLY_TYPES: + option["optional"] = option.get("optional", optional) + # LEGACY (`choices` in option `string` used to be valid) if "choices" in option and option["type"] == OptionType.string: logger.warning( From 3faa5742674074562f168183560a5daf5ebc0968 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 25 Oct 2023 15:07:31 +0200 Subject: [PATCH 0393/1116] configpanel: add proper schema definition --- doc/generate_json_schema.py | 4 ++++ src/utils/configpanel.py | 35 +++++++++++++++++++++++++++++++++++ src/utils/form.py | 12 ++++++++---- 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 doc/generate_json_schema.py diff --git a/doc/generate_json_schema.py b/doc/generate_json_schema.py new file mode 100644 index 000000000..1abf88915 --- /dev/null +++ b/doc/generate_json_schema.py @@ -0,0 +1,4 @@ +from yunohost.utils.configpanel import ConfigPanelModel + + +print(ConfigPanelModel.schema_json(indent=2)) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 325f6579d..e3ceeff88 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -88,6 +88,14 @@ class SectionModel(ContainerModel, OptionsModel): optional: bool = True is_action_section: bool = False + class Config: + @staticmethod + def schema_extra(schema: dict[str, Any]) -> None: + del schema["properties"]["id"] + options = schema["properties"].pop("options") + del schema["required"] + schema["additionalProperties"] = options["items"] + # Don't forget to pass arguments to super init def __init__( self, @@ -137,6 +145,13 @@ class PanelModel(ContainerModel): class Config: extra = Extra.allow + @staticmethod + def schema_extra(schema: dict[str, Any]) -> None: + del schema["properties"]["id"] + del schema["properties"]["sections"] + del schema["required"] + schema["additionalProperties"] = {"$ref": "#/definitions/SectionModel"} + # Don't forget to pass arguments to super init def __init__( self, @@ -170,6 +185,26 @@ class ConfigPanelModel(BaseModel): arbitrary_types_allowed = True extra = Extra.allow + @staticmethod + def schema_extra(schema: dict[str, Any]) -> None: + """Update the schema to the expected input + In actual TOML definition, schema is like: + ```toml + [panel_1] + [panel_1.section_1] + [panel_1.section_1.option_1] + ``` + Which is equivalent to `{"panel_1": {"section_1": {"option_1": {}}}}` + so `section_id` (and `option_id`) are additional property of `panel_id`, + which is convinient to write but not ideal to iterate. + In ConfigPanelModel we gather additional properties of panels, sections + and options as lists so that structure looks like: + `{"panels`: [{"id": "panel_1", "sections": [{"id": "section_1", "options": [{"id": "option_1"}]}]}] + """ + del schema["properties"]["panels"] + del schema["required"] + schema["additionalProperties"] = {"$ref": "#/definitions/PanelModel"} + # Don't forget to pass arguments to super init def __init__( self, diff --git a/src/utils/form.py b/src/utils/form.py index 70afeb8f3..bd373badb 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -319,10 +319,14 @@ class BaseOption(BaseModel): extra = Extra.forbid @staticmethod - def schema_extra(schema: dict[str, Any], model: Type["BaseOption"]) -> None: - # FIXME Do proper doctstring for Options - del schema["description"] - schema["additionalProperties"] = False + def schema_extra(schema: dict[str, Any]) -> None: + del schema["properties"]["id"] + del schema["properties"]["name"] + schema["required"] = [ + required for required in schema.get("required", []) if required != "id" + ] + if not schema["required"]: + del schema["required"] @validator("id", pre=True) def check_id_is_not_forbidden(cls, value: str) -> str: From 6fdfd81cbb4cdd4de69b97f30b81583d7246ce68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Thu, 26 Oct 2023 03:16:08 +0000 Subject: [PATCH 0394/1116] Translated using Weblate (Galician) Currently translated at 100.0% (782 of 782 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/gl.json b/locales/gl.json index acbb50fe4..5d28e06e4 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -714,7 +714,7 @@ "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_stable_codename_details": "Normalmente isto débese a unha configuración incorrecta do teu provedor de hospedaxe. Isoto é perigoso, porque tan pronto como a nova versión de Debian se convirta en 'stable', apt vai querer actualizar todos os paquetes do sistema sen realizar o procedemento de migración axeitado. É 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", From 5547208be495d34091551545af8187005cedfbf1 Mon Sep 17 00:00:00 2001 From: Thomas <51749973+Thovi98@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:15:09 +0200 Subject: [PATCH 0395/1116] Update docker-image-extract --- helpers/vendor/docker-image-extract/LICENSE | 26 +++++---- helpers/vendor/docker-image-extract/README.md | 2 +- .../docker-image-extract/docker-image-extract | 54 ++++++++++++++----- 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/helpers/vendor/docker-image-extract/LICENSE b/helpers/vendor/docker-image-extract/LICENSE index 82579b059..986360f1a 100644 --- a/helpers/vendor/docker-image-extract/LICENSE +++ b/helpers/vendor/docker-image-extract/LICENSE @@ -1,21 +1,19 @@ -MIT License +Copyright (c) 2020-2023, Jeremy Lin -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: -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 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. +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. \ No newline at end of file diff --git a/helpers/vendor/docker-image-extract/README.md b/helpers/vendor/docker-image-extract/README.md index 6f6cb5074..4c4fa301f 100644 --- a/helpers/vendor/docker-image-extract/README.md +++ b/helpers/vendor/docker-image-extract/README.md @@ -1 +1 @@ -This is taken from https://github.com/efrecon/docker-image-extract +This is taken from https://github.com/jjlin/docker-image-extract, under MIT license. \ No newline at end of file diff --git a/helpers/vendor/docker-image-extract/docker-image-extract b/helpers/vendor/docker-image-extract/docker-image-extract index 4842a8e04..b5dfdb7a7 100755 --- a/helpers/vendor/docker-image-extract/docker-image-extract +++ b/helpers/vendor/docker-image-extract/docker-image-extract @@ -2,7 +2,7 @@ # # This script pulls and extracts all files from an image in Docker Hub. # -# Copyright (c) 2020-2022, Jeremy Lin +# Copyright (c) 2020-2023, Jeremy Lin # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -103,6 +103,17 @@ if [ $# -eq 0 ]; then exit 1 fi +if [ -e "${OUT_DIR}" ]; then + if [ -d "${OUT_DIR}" ]; then + echo "WARNING: Output dir already exists. If it contains a previous extracted image," + echo "there may be errors when trying to overwrite files with read-only permissions." + echo + else + echo "ERROR: Output dir already exists, but is not a directory." + exit 1 + fi +fi + have_curl() { command -v curl >/dev/null } @@ -173,16 +184,20 @@ fetch() { # 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 +# If the ref is already a SHA-256 image digest, then we don't need to look up anything. +if [ -z "${ref##sha256:*}" ]; then digest="${ref}" else echo "Getting multi-arch manifest list..." + NL=' +' 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' | + # This uses the \${NL} syntax because some BSD variants of sed don't + # support \n syntax in the replacement string, but instead require + # a literal newline preceded by a backslash. + sed -e 's/\([][{}]\)/\'"${NL}"'\1\'"${NL}"'/g' | # Extract the "images":[...] list. sed -n '/"images":/,/]/ p' | # Each image's details are now on a separate line, e.g. @@ -205,13 +220,13 @@ else 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 + 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 fi # https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate @@ -226,10 +241,21 @@ 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" + +# https://github.com/distribution/distribution/blob/main/docs/spec/manifest-v2-2.md +docker_manifest_v2="application/vnd.docker.distribution.manifest.v2+json" + +# https://github.com/opencontainers/image-spec/blob/main/manifest.md +oci_manifest_v1="application/vnd.oci.image.manifest.v1+json" + +# Docker Hub can return either type of manifest format. Most images seem to +# use the Docker format for now, but the OCI format will likely become more +# common as features that require that format become enabled by default +# (e.g., https://github.com/docker/build-push-action/releases/tag/v3.3.0). +accept_header="Accept: ${docker_manifest_v2},${oci_manifest_v1}" echo "Getting image manifest for $image:$ref..." -layers=$(fetch "${manifest_url}" "${auth_header}" "${v2_header}" | +layers=$(fetch "${manifest_url}" "${auth_header}" "${accept_header}" | # Extract `digest` values only after the `layers` section appears. sed -n '/"layers":/,$ p' | extract 'digest') @@ -259,4 +285,4 @@ for layer in $layers; do IFS="${OLD_IFS}" done -echo "Image contents extracted into ${OUT_DIR}." +echo "Image contents extracted into ${OUT_DIR}." \ No newline at end of file From 7f954af6b6e5781ee7e7db4853ac2dcc25e2baad Mon Sep 17 00:00:00 2001 From: Kay0u Date: Mon, 2 Oct 2023 04:10:58 +0200 Subject: [PATCH 0396/1116] fix an error in dump_script_log_extract_for_debugging --- src/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/log.py b/src/log.py index 13683d8ef..e429bbd4a 100644 --- a/src/log.py +++ b/src/log.py @@ -745,7 +745,7 @@ class OperationLogger: # 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo # And we just want the part starting by "DEBUG - " lines = [line for line in lines if ":" in line.strip()] - lines = [line.strip().split(": ", 1)[1] for line in lines] + lines = [line.strip().split(": ", 1)[-1] for line in lines] # And we ignore boring/irrelevant lines # Annnnnnd we also ignore lines matching [number] + such as # 72971 DEBUG 29739 + ynh_exit_properly From bfb7dda42e11b294552ca09564ef48910518285d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin <4533074+alexAubin@users.noreply.github.com> Date: Mon, 30 Oct 2023 14:03:26 +0100 Subject: [PATCH 0397/1116] Rename helper into "ynh_exec_and_print_stderr_only_if_error" --- helpers/logging | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helpers/logging b/helpers/logging index 8989140f6..accb8f9b0 100644 --- a/helpers/logging +++ b/helpers/logging @@ -188,13 +188,13 @@ ynh_exec_fully_quiet() { # Execute a command and redirect stderr in /dev/null. Print stderr on error. # -# usage: ynh_exec_stderr_on_error your command and args +# usage: ynh_exec_and_print_stderr_only_if_error your command and args # | arg: command - command to execute # -# Note that you should NOT quote the command but only prefix it with ynh_exec_stderr_on_error +# Note that you should NOT quote the command but only prefix it with ynh_exec_and_print_stderr_only_if_error # # Requires YunoHost version 11.2 or higher. -ynh_exec_stderr_on_error() { +ynh_exec_and_print_stderr_only_if_error() { logfile="$(mktemp)" rc=0 # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 From 418df4c05f82636b40da14768f3400cc3d363fb5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Oct 2023 15:11:42 +0100 Subject: [PATCH 0398/1116] debian: move yunohost-portal to 'Recommends' ... mainly to bypass issue on the CI, but also because it sounds legit ... not 100% about this ? --- debian/control | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index 9f156ddea..6a72998dc 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Package: yunohost Essential: yes Architecture: all Depends: ${python3:Depends}, ${misc:Depends} - , moulinette (>= 11.1), ssowat (>= 11.1), yunohost-portal (>= 11.1) + , moulinette (>= 11.1), ssowat (>= 11.1) , python3-psutil, python3-requests, python3-dnspython, python3-openssl , python3-miniupnpc, python3-dbus, python3-jinja2 , python3-toml, python3-packaging, python3-publicsuffix2 @@ -30,7 +30,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , acl , git, curl, wget, cron, unzip, jq, bc, at, procps , lsb-release, haveged, fake-hwclock, equivs, lsof, whois -Recommends: yunohost-admin +Recommends: yunohost-admin, yunohost-portal (>= 11.1) , ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog , unattended-upgrades From e4182bb362776bbdab221990b299c1e28b93d3c9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Oct 2023 15:12:15 +0100 Subject: [PATCH 0399/1116] debian: require moulinette, ssowat, yunohost-portal to be >= 12.0 --- debian/control | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index 6a72998dc..a9f7f4cc6 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Package: yunohost Essential: yes Architecture: all Depends: ${python3:Depends}, ${misc:Depends} - , moulinette (>= 11.1), ssowat (>= 11.1) + , moulinette (>= 12.0), ssowat (>= 12.0) , python3-psutil, python3-requests, python3-dnspython, python3-openssl , python3-miniupnpc, python3-dbus, python3-jinja2 , python3-toml, python3-packaging, python3-publicsuffix2 @@ -30,7 +30,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , acl , git, curl, wget, cron, unzip, jq, bc, at, procps , lsb-release, haveged, fake-hwclock, equivs, lsof, whois -Recommends: yunohost-admin, yunohost-portal (>= 11.1) +Recommends: yunohost-admin, yunohost-portal (>= 12.0) , ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog , unattended-upgrades From 9423168aaf836b376783692941e6e9bcf5b2f042 Mon Sep 17 00:00:00 2001 From: axolotle Date: Mon, 30 Oct 2023 15:17:01 +0100 Subject: [PATCH 0400/1116] configpanels: fix app `is_default` + dns alert style --- src/app.py | 6 ++++-- src/dns.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app.py b/src/app.py index 21219b2ce..52e1ebede 100644 --- a/src/app.py +++ b/src/app.py @@ -26,7 +26,6 @@ import re import subprocess import tempfile import copy -from collections import OrderedDict from typing import TYPE_CHECKING, List, Tuple, Dict, Any, Iterator, Optional, Union from packaging import version from logging import getLogger @@ -129,6 +128,7 @@ def app_info(app, full=False, upgradable=False): """ Get info for a specific app """ + from yunohost.domain import domain_config_get from yunohost.permission import user_permission_list _assert_is_installed(app) @@ -228,7 +228,9 @@ def app_info(app, full=False, upgradable=False): ret["is_webapp"] = "domain" in settings and "path" in settings if ret["is_webapp"]: - ret["is_default"] = settings.get("default_app", "_none") + ret["is_default"] = ( + domain_config_get(settings["domain"], "feature.app.default_app") == app + ) ret["supports_change_url"] = os.path.exists( os.path.join(setting_path, "scripts", "change_url") diff --git a/src/dns.py b/src/dns.py index fc4b26a75..126f81dbf 100644 --- a/src/dns.py +++ b/src/dns.py @@ -558,6 +558,7 @@ def _get_registrar_config_section(domain): except ValueError: registrar_infos["registrar"]["default"] = None registrar_infos["infos"]["ask"] = m18n.n("domain_dns_registrar_not_supported") + registrar_infos["infos"]["style"] = "warning" else: registrar_infos["registrar"]["default"] = registrar registrar_infos["infos"]["ask"] = m18n.n( From 8aee337d0f18f77876e539a3a2b96e8d21f39546 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Oct 2023 17:04:17 +0100 Subject: [PATCH 0401/1116] regenconf/portal: fix attempt to chown before the user is created --- hooks/conf_regen/01-yunohost | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 3b810de30..74331edc2 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -29,11 +29,6 @@ do_init_regen() { chown -R root:ssl-cert /etc/yunohost/certs chmod 750 /etc/yunohost/certs - # Portal folder - mkdir -p /etc/yunohost/portal - chmod 500 /etc/yunohost/portal - chown ynh-portal:ynh-portal /etc/yunohost/portal - # App folders mkdir -p /etc/yunohost/apps chmod 700 /etc/yunohost/apps @@ -72,6 +67,12 @@ do_init_regen() { chown ynh-portal:root /etc/yunohost/.ssowat_cookie_secret chmod 400 /etc/yunohost/.ssowat_cookie_secret + # Portal folder + mkdir -p /etc/yunohost/portal + chmod 500 /etc/yunohost/portal + chown ynh-portal:ynh-portal /etc/yunohost/portal + + # YunoHost services cp yunohost-api.service /etc/systemd/system/yunohost-api.service cp yunohost-portal-api.service /etc/systemd/system/yunohost-portal-api.service cp yunohost-firewall.service /etc/systemd/system/yunohost-firewall.service From f02538cef05744c4103fcf1af31d0114e839351e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Oct 2023 18:39:31 +0100 Subject: [PATCH 0402/1116] doc: iterate on configpanel/form documentation --- ...enerate_configpanel_and_formoptions_doc.py | 171 ++++++ doc/generate_configpanel_doc.py | 159 ------ doc/generate_options_doc.py | 486 ------------------ src/utils/configpanel.py | 57 +- src/utils/form.py | 83 ++- 5 files changed, 211 insertions(+), 745 deletions(-) create mode 100644 doc/generate_configpanel_and_formoptions_doc.py delete mode 100644 doc/generate_configpanel_doc.py delete mode 100644 doc/generate_options_doc.py diff --git a/doc/generate_configpanel_and_formoptions_doc.py b/doc/generate_configpanel_and_formoptions_doc.py new file mode 100644 index 000000000..061ebf77c --- /dev/null +++ b/doc/generate_configpanel_and_formoptions_doc.py @@ -0,0 +1,171 @@ +import sys +import ast +import datetime +import subprocess + +version = open("../debian/changelog").readlines()[0].split()[1].strip("()") +today = datetime.datetime.now().strftime("%d/%m/%Y") + + +def get_current_commit(): + p = subprocess.Popen( + "git rev-parse --verify HEAD", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + stdout, stderr = p.communicate() + + current_commit = stdout.strip().decode("utf-8") + return current_commit + + +current_commit = get_current_commit() + +def print_config_panel_docs(): + + fname = "../src/utils/configpanel.py" + content = open(fname).read() + + # NB: This magic is because we want to be able to run this script outside of a YunoHost context, + # in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports... + tree = ast.parse(content) + + ConfigPanelClasses = reversed( + [ + c + for c in tree.body + if isinstance(c, ast.ClassDef) + and c.name in {"SectionModel", "PanelModel", "ConfigPanelModel"} + ] + ) + + + print("## Configuration panel structure") + + for c in ConfigPanelClasses: + doc = ast.get_docstring(c) + print("") + print(f"### {c.name.replace('Model', '')}") + print("") + print(doc) + print("") + print("---") + + +def print_form_doc(): + + fname = "../src/utils/form.py" + content = open(fname).read() + + # NB: This magic is because we want to be able to run this script outside of a YunoHost context, + # in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports... + tree = ast.parse(content) + + OptionClasses = [ + c for c in tree.body if isinstance(c, ast.ClassDef) and c.name.endswith("Option") + ] + + OptionDocString = {} + + print("## List of all option types") + + for c in OptionClasses: + if not isinstance(c.body[0], ast.Expr): + continue + option_type = None + + if c.name in {"BaseOption", "BaseInputOption"}: + option_type = c.name + elif c.body[1].target.id == "type": + option_type = c.body[1].value.attr + + generaltype = c.bases[0].id.replace("Option", "").replace("Base", "").lower() if c.bases else None + + docstring = ast.get_docstring(c) + if docstring: + if "#### Properties" not in docstring: + docstring += """ +#### Properties + +- [common properties](#common-properties)""" + OptionDocString[option_type] = {"doc": docstring, "generaltype": generaltype} + + # Dirty hack to have "BaseOption" as first and "BaseInputOption" as 2nd in list + + base = OptionDocString.pop("BaseOption") + baseinput = OptionDocString.pop("BaseInputOption") + OptionDocString2 = { + "BaseOption": base, + "BaseInputOption": baseinput, + } + OptionDocString2.update(OptionDocString) + + for option_type, infos in OptionDocString2.items(): + if option_type == "display_text": + # display_text is kind of legacy x_x + continue + print("") + if option_type == "BaseOption": + print("### Common properties") + elif option_type == "BaseInputOption": + print("### Common inputs properties") + else: + print(f"### `{option_type}`" + (f" ({infos['generaltype']})" if infos["generaltype"] else "")) + print("") + print(infos["doc"]) + print("") + print("---") + +print( + f"""--- +title: Technical details for config panel structure and form option types +template: docs +taxonomy: + category: docs +routes: + default: '/dev/forms' +--- + +Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_options_doc.py) on {today} (YunoHost version {version}) + +## Glossary + +You may encounter some named types which are used for simplicity. + +- `Translation`: a translated property + - used for properties: `ask`, `help` and `Pattern.error` + - a `dict` with locales as keys and translations as values: + ```toml + ask.en = "The text in english" + ask.fr = "Le texte en français" + ``` + It is not currently possible for translators to translate those string in weblate. + - a single `str` for a single english default string + ```toml + help = "The text in english" + ``` +- `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`: + - used for properties: `visible` and `enabled` + - operators availables: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()` +- `Binding`: bind a value to a file/property/variable/getter/setter/validator + - save the value in `settings.yaml` when not defined + - nothing at all with `"null"` + - a custom getter/setter/validator with `"null"` + a function starting with `get__`, `set__`, `validate__` in `scripts/config` + - a variable/property in a file with `:__FINALPATH__/my_file.php` + - a whole file with `__FINALPATH__/my_file.php` +- `Pattern`: a `dict` with a regex to match the value against and an error message + ```toml + pattern.regexp = '^[A-F]\d\d$' + pattern.error = "Provide a room number such as F12: one uppercase and 2 numbers" + # or with translated error + pattern.error.en = "Provide a room number such as F12: one uppercase and 2 numbers" + pattern.error.fr = "Entrez un numéro de salle comme F12: une lettre majuscule et deux chiffres." + ``` + - IMPORTANT: your `pattern.regexp` should be between simple quote, not double. + +""" +) + +print_config_panel_docs() +print_form_doc() diff --git a/doc/generate_configpanel_doc.py b/doc/generate_configpanel_doc.py deleted file mode 100644 index 1eb7b5ebb..000000000 --- a/doc/generate_configpanel_doc.py +++ /dev/null @@ -1,159 +0,0 @@ -import ast -import datetime -import subprocess - -version = open("../debian/changelog").readlines()[0].split()[1].strip("()") -today = datetime.datetime.now().strftime("%d/%m/%Y") - - -def get_current_commit(): - p = subprocess.Popen( - "git rev-parse --verify HEAD", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - stdout, stderr = p.communicate() - - current_commit = stdout.strip().decode("utf-8") - return current_commit - - -current_commit = get_current_commit() - - -print( - f"""--- -title: Config Panels -template: docs -taxonomy: - category: docs -routes: - default: '/packaging_config_panels' ---- - -Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_configpanel_doc.py) on {today} (YunoHost version {version}) - -## Glossary - -You may encounter some named types which are used for simplicity. - -- `Translation`: a translated property - - used for properties: `ask`, `help` and `Pattern.error` - - a `dict` with locales as keys and translations as values: - ```toml - ask.en = "The text in english" - ask.fr = "Le texte en français" - ``` - It is not currently possible for translators to translate those string in weblate. - - a single `str` for a single english default string - ```toml - help = "The text in english" - ``` -- `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`: - - used for properties: `visible` and `enabled` - - operators availables: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()` - - [examples available in the advanced section of Options](/packaging_apps_options#advanced-use-cases) -""" -) - - -fname = "../src/utils/configpanel.py" -content = open(fname).read() - -# NB: This magic is because we want to be able to run this script outside of a YunoHost context, -# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports... -tree = ast.parse(content) - -OptionClasses = reversed( - [ - c - for c in tree.body - if isinstance(c, ast.ClassDef) - and c.name in {"SectionModel", "PanelModel", "ConfigPanelModel"} - ] -) - -for c in OptionClasses: - doc = ast.get_docstring(c) - print("") - print("----------------") - print(f"## {c.name.replace('Model', '')}") - print("") - print(doc) - print("") - - -print( - """ -## Full example - -Let's imagine that the upstream app is configured using this simple `config.yml` file stored in the app's install directory (typically `/var/www/$app/config.yml`): -```yaml -title: 'My dummy app' -theme: 'white' -max_rate: 10 -max_age: 365 -``` - -We could for example create a simple configuration panel for it like this one, by following the syntax `[PANEL.SECTION.QUESTION]`: -```toml -version = "1.0" -[main] - - [main.main] - [main.main.title] - ask.en = "Title" - type = "string" - bind = ":__INSTALL_DIR__/config.yml" - - [main.main.theme] - ask.en = "Theme" - type = "select" - choices = ["white", "dark"] - bind = ":__INSTALL_DIR__/config.yml" - - [main.limits] - [main.limits.max_rate] - ask.en = "Maximum display rate" - type = "number" - bind = ":__INSTALL_DIR__/config.yml" - - [main.limits.max_age] - ask.en = "Duration of a dummy" - type = "number" - bind = ":__INSTALL_DIR__/config.yml" -``` - -Here we have created one `main` panel, containing the `main` and `limits` sections, containing questions according to params name of our `config.yml` file. Thanks to the `bind` properties, all those questions are bind to their values in the `config.yml` file. - -## Overwrite config panel mechanism - -All main configuration helpers are overwritable, example: - -```bash -ynh_app_config_apply() { - - # Stop vpn client - touch /tmp/.ynh-vpnclient-stopped - systemctl stop ynh-vpnclient - - _ynh_app_config_apply - - # Start vpn client - systemctl start ynh-vpnclient - rm -f /tmp/.ynh-vpnclient-stopped - -} -``` - -List of main configuration helpers: - * `ynh_app_config_get` - * `ynh_app_config_show` - * `ynh_app_config_validate` - * `ynh_app_config_apply` - * `ynh_app_config_run` - -More info on this can be found by reading [vpnclient_ynh config script](https://github.com/YunoHost-Apps/vpnclient_ynh/blob/master/scripts/config) -""" -) diff --git a/doc/generate_options_doc.py b/doc/generate_options_doc.py deleted file mode 100644 index ea7febe6d..000000000 --- a/doc/generate_options_doc.py +++ /dev/null @@ -1,486 +0,0 @@ -import ast -import datetime -import subprocess - -version = open("../debian/changelog").readlines()[0].split()[1].strip("()") -today = datetime.datetime.now().strftime("%d/%m/%Y") - - -def get_current_commit(): - p = subprocess.Popen( - "git rev-parse --verify HEAD", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - stdout, stderr = p.communicate() - - current_commit = stdout.strip().decode("utf-8") - return current_commit - - -current_commit = get_current_commit() - - -print( - f"""--- -title: Options -template: docs -taxonomy: - category: docs -routes: - default: '/dev/forms' ---- - -Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_options_doc.py) on {today} (YunoHost version {version}) - -## Glossary - -You may encounter some named types which are used for simplicity. - -- `Translation`: a translated property - - used for properties: `ask`, `help` and `Pattern.error` - - a `dict` with locales as keys and translations as values: - ```toml - ask.en = "The text in english" - ask.fr = "Le texte en français" - ``` - It is not currently possible for translators to translate those string in weblate. - - a single `str` for a single english default string - ```toml - help = "The text in english" - ``` -- `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`: - - used for properties: `visible` and `enabled` - - operators availables: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()` - - [examples available in the advanced section](#advanced-use-cases) -- `Binding`: bind a value to a file/property/variable/getter/setter/validator - - save the value in `settings.yaml` when not defined - - nothing at all with `"null"` - - a custom getter/setter/validator with `"null"` + a function starting with `get__`, `set__`, `validate__` in `scripts/config` - - a variable/property in a file with `:__FINALPATH__/my_file.php` - - a whole file with `__FINALPATH__/my_file.php` - - [examples available in the advanced section](#bind) -- `Pattern`: a `dict` with a regex to match the value against and an error message - ```toml - pattern.regexp = '^[A-F]\d\d$' - pattern.error = "Provide a room like F12: one uppercase and 2 numbers" - # or with translated error - pattern.error.en = "Provide a room like F12: one uppercase and 2 numbers" - pattern.error.fr = "Entrez un numéro de salle comme F12: une lettre majuscule et deux chiffres." - ``` - - IMPORTANT: your `pattern.regexp` should be between simple quote, not double. - -""" -) - - -fname = "../src/utils/form.py" -content = open(fname).read() - -# NB: This magic is because we want to be able to run this script outside of a YunoHost context, -# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports... -tree = ast.parse(content) - -OptionClasses = [ - c for c in tree.body if isinstance(c, ast.ClassDef) and c.name.endswith("Option") -] - -OptionDocString = {} - -for c in OptionClasses: - if not isinstance(c.body[0], ast.Expr): - continue - option_type = None - - if c.name in {"BaseOption", "BaseInputOption"}: - option_type = c.name - elif c.body[1].target.id == "type": - option_type = c.body[1].value.attr - - docstring = ast.get_docstring(c) - if docstring: - if "##### Properties" not in docstring: - docstring += """ -##### Properties - -- [common properties](#common-option-properties) - """ - OptionDocString[option_type] = docstring - -for option_type, doc in OptionDocString.items(): - print("") - if option_type == "BaseOption": - print("## Common Option properties") - elif option_type == "BaseInputOption": - print("## Common Inputs properties") - elif option_type == "display_text": - print("----------------") - print("## Readonly Options") - print(f"### Option `{option_type}`") - elif option_type == "string": - print("----------------") - print("## Input Options") - print(f"### Option `{option_type}`") - else: - print(f"### Option `{option_type}`") - print("") - print(doc) - print("") - -print( - """ ----------------- - -## Reading and writing values - -! Config panels only - -You can read and write values with 2 mechanisms: the `bind` property in the `config_panel.toml` and for complex use cases the getter/setter in a `config` script. - -If you did not define a specific getter/setter (see below), and no `bind` argument was defined, YunoHost will read/write the value from/to the app's `/etc/yunohost/$app/settings.yml` file. - -With `bind`, we can: -- alter the source the value comes from with binds to file or custom getters. -- alter the destination with binds to file or custom setters. -- parse/validate the value before destination with validators - -! IMPORTANT: with the exception of `bind = "null"` options, options ids should almost **always** correspond to an app setting initialized/reused during install/upgrade. -Not doing so may result in inconsistencies between the config panel mechanism and the use of ynh_add_config - - -### Read / write into a var of an actual configuration file - -Settings usually correspond to key/values in actual app configurations. Hence, a more useful mode is to have `bind = ":FILENAME"` with a colon `:` before. In that case, YunoHost will automagically find a line with `KEY=VALUE` in `FILENAME` (with the adequate separator between `KEY` and `VALUE`). - -YunoHost will then use this value for the read/get operation. During write/set operations, YunoHost will overwrite the value in **both** FILENAME and in the app's settings.yml - -Configuration file format supported: `YAML`, `TOML`, `JSON`, `INI`, `PHP`, `.env`-like, `.py`. -The feature probably works with others formats, but should be tested carefully. - -```toml -[main.main.theme] -# Do not use `file` for this since we only want to insert/save a value -type = "string" -bind = ":__INSTALL_DIR__/config.yml" -``` -In which case, YunoHost will look for something like a key/value, with the key being `theme`. - -If the question id in the config panel (here, `theme`) differs from the key in the actual conf file (let's say it's not `theme` but `css_theme`), then you can write: -```toml -[main.main.theme] -type = "string" -bind = "css_theme:__FINALPATH__/config.yml" -``` - -!!!! Note: This mechanism is quasi language agnostic and will use regexes to find something that looks like a key=value or common variants. However, it does assume that the key and value are stored on the same line. It doesn't support multiline text or file in a variable with this method. If you need to save multiline content in a configuration variable, you should create a custom getter/setter (see below). - -Nested syntax is also supported, which may be useful for example to remove ambiguities about stuff looking like: -```json -{ - "foo": { - "max": 123 - }, - "bar": { - "max": 456 - } -} -``` - -which we can `bind` to using: - -```toml -bind = "foo>max:__INSTALL_DIR__/conf.json" -``` - -### Read / write an entire file - -Useful when using a question `file` or `text` for which you want to save the raw content directly as a file on the system. - -```toml -[panel.section.config_file] -type = "file" -bind = "__FINALPATH__/config.ini" -``` - -```toml -[panel.section.config_content] -type = "text" -bind = "__FINALPATH__/config.ini" -default = "key: 'value'" -``` - -## Advanced use cases - -Sometimes the `bind` mechanism is not enough: - * the config file format is not supported (e.g. xml, csv) - * the data is not contained in a config file (e.g. database, directory, web resources...) - * the data should be written but not read (e.g. password) - * the data should be read but not written (e.g. fetching status information) - * we want to change other things than the value (e.g. the choices list of a select) - * the question answer contains several values to dispatch in several places - * and so on - -You can create specific getter/setters functions inside the `scripts/config` of your app to customize how the information is read/written. - -```bash -#!/bin/bash -source /usr/share/yunohost/helpers - -ynh_abort_if_errors - -# Put your getter, setter, validator or action here - -# Keep this last line -ynh_app_config_run $1 -``` - -### Getters - -A question's getter is the function used to read the current value/state. Custom getters are defined using bash functions called `getter__QUESTION_SHORT_KEY()` which returns data through stdout. - -Stdout can generated using one of those formats: - 1) either a raw format, in which case the return is binded directly to the value of the question - 2) or a yaml format, in this case you dynamically provide properties for your question (for example the `style` of an `alert`, the list of available `choices` of a `select`, etc.) - - -[details summary="Basic example with raw stdout: get the timezone on the system" class="helper-card-subtitle text-muted"] - -`config_panel.toml` - -```toml -[main.main.timezone] -ask = "Timezone" -type = "string" -``` - -`scripts/config` - -```bash -get__timezone() { - echo "$(cat /etc/timezone)" -} -``` -[/details] - -[details summary="Basic example with yaml-formated stdout : Display a list of available plugins" class="helper-card-subtitle text-muted"] - -`config_panel.toml` -```toml -[main.plugins.plugins] -ask = "Plugin to activate" -type = "tags" -choices = [] -``` - -`scripts/config` - -```bash -get__plugins() { - echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]" -} -``` - -[/details] - -[details summary="Advanced example with yaml-formated stdout : Display the status of a VPN" class="helper-card-subtitle text-muted"] - -`config_panel.toml` - -```toml -[main.cube.status] -ask = "Custom getter alert" -type = "alert" -style = "info" -bind = "null" # no behaviour on -``` - -`scripts/config` -```bash -get__status() { - if [ -f "/sys/class/net/tun0/operstate" ] && [ "$(cat /sys/class/net/tun0/operstate)" == "up" ] - then - cat << EOF -style: success -ask: - en: Your VPN is running :) -EOF - else - cat << EOF -style: danger -ask: - en: Your VPN is down -EOF - fi -} -``` -[/details] - - -### Setters - -A question's setter is the function used to set new value/state. Custom setters are defined using bash functions called `setter__QUESTION_SHORT_KEY()`. In the context of the setter function, variables named with the various quetion's short keys are avaible ... for example the user-specified date for question `[main.main.theme]` is available as `$theme`. - -When doing non-trivial operations to set a value, you may want to use `ynh_print_info` to inform the admin about what's going on. - - -[details summary="Basic example : Set the system timezone" class="helper-card-subtitle text-muted"] - -`config_panel.toml` - -```toml -[main.main.timezone] -ask = "Timezone" -type = "string" -``` - -`scripts/config` - -```bash -set__timezone() { - echo "$timezone" > /etc/timezone - ynh_print_info "The timezone has been changed to $timezone" -} -``` -[/details] - - -### Validation - -You will often need to validate data answered by the user before to save it somewhere. - -Validation can be made with regex through `pattern` argument -```toml -pattern.regexp = '^.+@.+$' -pattern.error = 'An email is required for this field' -``` - -You can also restrict several types with a choices list. -```toml -choices.foo = "Foo (some explanation)" -choices.bar = "Bar (moar explanation)" -choices.loremipsum = "Lorem Ipsum Dolor Sit Amet" -``` - -Some other type specific argument exist like -| type | validation arguments | -| ----- | --------------------------- | -| `number`, `range` | `min`, `max`, `step` | -| `file` | `accept` | -| `boolean` | `yes` `no` | - - -Finally, if you need specific or multi variable validation, you can use custom validators function. -Validators allows us to return custom error messages depending on the value. - -```bash -validate__login_user() { - if [[ "${#login_user}" -lt 4 ]]; then echo 'User login is too short, should be at least 4 chars'; fi -} -``` - -### Actions - -Define an option's action in a bash script `script/config`. -It has to be named after a `button`'s id prepended by `run__`. - -```toml -[panel.section.my_action] -type = "button" -# no need to set `bind` to "null" it is its hard default -ask = "Run action" -``` - -```bash -run__my_action() { - ynh_print_info "Running 'my_action'..." -} -``` - -A more advanced example could look like: - -```toml -[panel.my_action_section] -name = "Action section" - [panel.my_action_section.my_repo] - type = "url" - bind = "null" # value will not be saved as a setting - ask = "gimme a repo link" - - [panel.my_action_section.my_repo_name] - type = "string" - bind = "null" # value will not be saved as a setting - ask = "gimme a custom folder name" - - [panel.my_action_section.my_action] - type = "button" - ask = "Clone the repo" - # enabled the button only if the above values is defined - enabled = "my_repo && my_repo_name" -``` - -```bash -run__my_action() { - ynh_print_info "Cloning '$my_repo'..." - cd /tmp - git clone "$my_repo" "$my_repo_name" -} -``` - -### `visible` & `enabled` expression evaluation - -Sometimes we may want to conditionaly display a message or prompt for a value, for this we have the `visible` prop. -And we may want to allow a user to trigger an action only if some condition are met, for this we have the `enabled` prop. - -Expressions are evaluated against a context containing previous values of the current section's options. This quite limited current design exists because on the web-admin or on the CLI we cannot guarantee that a value will be present in the form if the user queried only a single panel/section/option. -In the case of an action, the user will be shown or asked for each of the options of the section in which the button is present. - -The expression has to be written in javascript (this has been designed for the web-admin first and is converted to python on the fly on the cli). - -Available operators are: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`. - -#### Examples - -```toml -# simple "my_option_id" is thruthy/falsy -visible = "my_option_id" -visible = "!my_option_id" -# misc -visible = "my_value >= 10" -visible = "-(my_value + 1) < 0" -visible = "!!my_value || my_other_value" -``` -For a more complete set of examples, [check the tests at the end of the file](https://github.com/YunoHost/yunohost/blob/dev/src/tests/test_questions.py). - -#### match() - -For more complex evaluation we can use regex matching. - -```toml -[my_string] -default = "Lorem ipsum dolor et si qua met!" - -[my_boolean] -type = "boolean" -visible = "my_string && match(my_string, '^Lorem [ia]psumE?')" -``` - -Match the content of a file. - -```toml -[my_file] -type = "file" -accept = ".txt" -bind = "/etc/random/lorem.txt" - -[my_boolean] -type = "boolean" -visible = "my_file && match(my_file, '^Lorem [ia]psumE?')" -``` - -with a file with content like: -```txt -Lorem ipsum dolor et si qua met! -``` -""" -) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 47c97a808..bfad41280 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -85,13 +85,13 @@ class ContainerModel(BaseModel): class SectionModel(ContainerModel, OptionsModel): """ - Group options. Sections are `dict`s defined inside a Panel and require a unique id (in the below example, the id is `customization` prepended by the panel's id `main`). Keep in mind that this combined id will be used in CLI to refer to the section, so choose something short and meaningfull. Also make sure to not make a typo in the panel id, which would implicitly create an other entire panel. + Sections are, basically, options grouped together. Sections are `dict`s defined inside a Panel and require a unique id (in the below example, the id is `customization` prepended by the panel's id `main`). Keep in mind that this combined id will be used in CLI to refer to the section, so choose something short and meaningfull. Also make sure to not make a typo in the panel id, which would implicitly create an other entire panel. If at least one `button` is present it then become an action section. Options in action sections are not considered settings and therefor are not saved, they are more like parameters that exists only during the execution of an action. FIXME i'm not sure we have this in code. - ### Examples + #### Examples ```toml [main] @@ -106,7 +106,7 @@ class SectionModel(ContainerModel, OptionsModel): # …refer to Options doc ``` - ### Properties + #### Properties - `name` (optional): `Translation` or `str`, displayed as the section's title if any - `help`: `Translation` or `str`, text to display before the first option - `services` (optional): `list` of services names to `reload-or-restart` when any option's value contained in the section changes @@ -163,9 +163,9 @@ class SectionModel(ContainerModel, OptionsModel): class PanelModel(ContainerModel): """ - Group sections. Panels are `dict`s defined inside a ConfigPanel file and require a unique id (in the below example, the id is `main`). Keep in mind that this id will be used in CLI to refer to the panel, so choose something short and meaningfull. + Panels are, basically, sections grouped together. Panels are `dict`s defined inside a ConfigPanel file and require a unique id (in the below example, the id is `main`). Keep in mind that this id will be used in CLI to refer to the panel, so choose something short and meaningfull. - ### Examples + #### Examples ```toml [main] name.en = "Main configuration" @@ -176,7 +176,7 @@ class PanelModel(ContainerModel): [main.customization] # …refer to Sections doc ``` - ### Properties + #### Properties - `name`: `Translation` or `str`, displayed as the panel title - `help` (optional): `Translation` or `str`, text to display before the first section - `services` (optional): `list` of services names to `reload-or-restart` when any option's value contained in the panel changes @@ -217,58 +217,23 @@ class PanelModel(ContainerModel): class ConfigPanelModel(BaseModel): """ - Configuration panels allows admins to manage parameters or runs actions for which the upstream's app doesn't provide any appropriate UI itself. It's a good way to reduce manual change on config files and avoid conflicts on it. + This is the 'root' level of the config panel toml file - Those panels can also be used to quickly create interfaces that extend the capabilities of YunoHost (e.g. VPN Client, Hotspost, Borg, etc.). + #### Examples - From a packager perspective, this `config_panel.toml` is coupled to the `scripts/config` script, which may be used to define custom getters/setters/validations/actions. However, most use cases should be covered automagically by the core, thus it may not be necessary to define a scripts/config at all! - - ! Please: Keep in mind the YunoHost spirit, and try to build your panels in such a way as to expose only really useful, "high-level" parameters, and if there are many of them, to relegate those corresponding to rarer use cases to "Advanced" sub-sections. Keep it simple, focus on common needs, don't expect the admins to have 3 PhDs in computer science. - - ### `config_panel.toml`'s principle and general format - To create configuration panels for apps, you should at least create a `config_panel.toml` at the root of the package. For more complex cases, this TOML file can be paired with a `config` script inside the scripts directory of your package, which will handle specific controller logic. - - The `config_panel.toml` describes one or several panels, containing sections, each containing questions generally binded to a params in the app's actual configuration files. - - ### Options short keys have to be unique - For performance reasons, questions short keys have to be unique in all the `config_panel.toml` file, not just inside its panel or its section. Hence it's not possible to have: - ```toml - [manual.vpn.server_ip] - [advanced.dns.server_ip] - ``` - In which two questions have "real variable name" `is server_ip` and therefore conflict with each other. - - ! Some short keys are forbidden cause it can interfer with config scripts (`old`, `file_hash`, `types`, `binds`, `formats`, `changed`) and you probably should avoid to use common settings name to avoid to bind your question to this settings (e.g. `id`, `install_time`, `mysql_pwd`, `path`, `domain`, `port`, `db_name`, `current_revision`, `admin`) - - ### Supported questions types and properties - - [Learn more about Options](/dev/forms) in their dedicated doc page as those are also used in app install forms and core config panels. - - ### YunoHost community examples - - [Check the basic example at the end of this doc](#basic-example) - - [Check the example_ynh app toml](https://github.com/YunoHost/example_ynh/blob/master/config_panel.toml.example) and the [basic `scripts/config` example](https://github.com/YunoHost/example_ynh/blob/master/scripts/config) - - [Check config panels of other apps](https://grep.app/search?q=version&filter[repo.pattern][0]=YunoHost-Apps&filter[lang][0]=TOML) - - [Check `scripts/config` of other apps](https://grep.app/search?q=ynh_app_config_apply&filter[repo.pattern][0]=YunoHost-Apps&filter[lang][0]=Shell) - - ### Examples ```toml version = 1.0 [config] # …refer to Panels doc ``` - ### Properties + + #### Properties + - `version`: `float` (default: `1.0`), version that the config panel supports in terms of features. - `i18n` (optional): `str`, an i18n property that let you internationalize options text. - However this feature is only available in core configuration panel (like `yunohost domain config`), prefer the use `Translation` in `name`, `help`, etc. - #### Version - Here a small reminder to associate config panel version with YunoHost version. - - | Config | YNH | Config panel small change log | - | ------ | --- | ------------------------------------------------------- | - | 0.1 | 3.x | 0.1 config script not compatible with YNH >= 4.3 | - | 1.0 | 4.3.x | The new config panel system with 'bind' property | """ version: float = CONFIG_PANEL_VERSION_SUPPORTED diff --git a/src/utils/form.py b/src/utils/form.py index 28c5c4d24..1197bae01 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -306,7 +306,7 @@ class BaseOption(BaseModel): ! IMPORTANT: as for Panels and Sections you have to choose an id, but this one should be unique in all this document, even if the question is in an other panel. - #### Examples + #### Example ```toml [section.my_option_id] @@ -325,32 +325,7 @@ class BaseOption(BaseModel): #### Properties - - `type`: - - readonly types: - - [`display_text`](#option-display_text) - - [`markdown`](#option-markdown) - - [`alert`](#option-alert) - - [`button`](#option-button) - - inputs types: - - [`string`](#option-string) - - [`text`](#option-text) - - [`password`](#option-password) - - [`color`](#option-color) - - [`number`](#option-number) - - [`range`](#option-range) - - [`boolean`](#option-boolean) - - [`date`](#option-date) - - [`time`](#option-time) - - [`email`](#option-email) - - [`path`](#option-path) - - [`url`](#option-url) - - [`file`](#option-file) - - [`select`](#option-select) - - [`tags`](#option-tags) - - [`domain`](#option-domain) - - [`app`](#option-app) - - [`user`](#option-user) - - [`group`](#option-group) + - `type`: the actual type of the option, such as 'markdown', 'password', 'number', 'email', ... - `ask`: `Translation` (default to the option's `id` if not defined): - text to display as the option's label for inputs or text to display for readonly options - in config panels, questions are displayed on the left side and therefore have not much space to be rendered. Therefore, it is better to use a short question, and use the `help` property to provide additional details if necessary. @@ -445,7 +420,7 @@ class DisplayTextOption(BaseReadonlyOption): """ Display simple multi-line content. - #### Examples + #### Example ```toml [section.my_option_id] @@ -462,7 +437,7 @@ class MarkdownOption(BaseReadonlyOption): Display markdown multi-line content. Markdown is currently only rendered in the web-admin - #### Examples + #### Example ```toml [section.my_option_id] type = "display_text" @@ -485,7 +460,7 @@ class AlertOption(BaseReadonlyOption): Alerts displays a important message with a level of severity. You can use markdown in `ask` but will only be rendered in the web-admin. - #### Examples + #### Example ```toml [section.my_option_id] @@ -496,7 +471,7 @@ class AlertOption(BaseReadonlyOption): ``` #### Properties - - [common properties](#common-option-properties) + - [common properties](#common-properties) - `style`: any of `"success|info|warning|danger"` (default: `"info"`) - `icon` (optional): any icon name from [Fork Awesome](https://forkaweso.me/Fork-Awesome/icons/) - Currently only displayed in the web-admin @@ -526,7 +501,7 @@ class ButtonOption(BaseReadonlyOption): Every options defined in an action section (a config panel section with at least one `button`) is guaranted to be shown/asked to the user and available in `scripts/config`'s scope. [check examples in advanced use cases](#actions). - #### Examples + #### Example ```toml [section.my_option_id] @@ -548,7 +523,7 @@ class ButtonOption(BaseReadonlyOption): #### Properties - - [common properties](#common-option-properties) + - [common properties](#common-properties) - `bind`: forced to `"null"` - `style`: any of `"success|info|warning|danger"` (default: `"success"`) - `enabled`: `JSExpression` or `bool` (default: `true`) @@ -580,14 +555,14 @@ class BaseInputOption(BaseOption): """ Rest of the option types available are considered `inputs`. - #### Examples + #### Example ```toml [section.my_option_id] type = "string" # …any common props… + optional = false - redact = False + redact = false default = "some default string" help = "You can enter almost anything!" example = "an example string" @@ -596,7 +571,7 @@ class BaseInputOption(BaseOption): #### Properties - - [common properties](#common-option-properties) + - [common properties](#common-properties) - `optional`: `bool` (default: `false`, but `true` in config panels) - `redact`: `bool` (default: `false`), to redact the value in the logs when the value contain private information - `default`: depends on `type`, the default value to assign to the option @@ -757,7 +732,7 @@ class StringOption(BaseStringOption): """ Ask for a simple string. - #### Examples + #### Example ```toml [section.my_option_id] type = "string" @@ -780,7 +755,7 @@ class TextOption(BaseStringOption): Ask for a multiline string. Renders as a `textarea` in the web-admin and by opening a text editor on the CLI. - #### Examples + #### Example ```toml [section.my_option_id] type = "text" @@ -803,7 +778,7 @@ class PasswordOption(BaseInputOption): Ask for a password. The password is tested as a regular user password (at least 8 chars) - #### Examples + #### Example ```toml [section.my_option_id] type = "password" @@ -855,7 +830,7 @@ class ColorOption(BaseInputOption): Ask for a color represented as a hex value (with possibly an alpha channel). Renders as color picker in the web-admin and as a prompt that accept named color like `yellow` in CLI. - #### Examples + #### Example ```toml [section.my_option_id] type = "color" @@ -901,7 +876,7 @@ class NumberOption(BaseInputOption): """ Ask for a number (an integer). - #### Examples + #### Example ```toml [section.my_option_id] type = "number" @@ -976,7 +951,7 @@ class BooleanOption(BaseInputOption): Ask for a boolean. Renders as a switch in the web-admin and a yes/no prompt in CLI. - #### Examples + #### Example ```toml [section.my_option_id] type = "boolean" @@ -1104,7 +1079,7 @@ class DateOption(BaseInputOption): Can also take a timestamp as value that will output as an ISO date string. - #### Examples + #### Example ```toml [section.my_option_id] type = "date" @@ -1134,7 +1109,7 @@ class TimeOption(BaseInputOption): Ask for an hour in the form `"22:35"`. Renders as a date-picker in the web-admin and a regular prompt in CLI. - #### Examples + #### Example ```toml [section.my_option_id] type = "time" @@ -1167,7 +1142,7 @@ class EmailOption(BaseInputOption): """ Ask for an email. Validation made with [python-email-validator](https://github.com/JoshData/python-email-validator) - #### Examples + #### Example ```toml [section.my_option_id] type = "email" @@ -1187,7 +1162,7 @@ class WebPathOption(BaseStringOption): """ Ask for an web path (the part of an url after the domain). Used by default in app install to define from where the app will be accessible. - #### Examples + #### Example ```toml [section.my_option_id] type = "path" @@ -1235,7 +1210,7 @@ class URLOption(BaseStringOption): """ Ask for any url. - #### Examples + #### Example ```toml [section.my_option_id] type = "url" @@ -1259,7 +1234,7 @@ class FileOption(BaseInputOption): Ask for file. Renders a file prompt in the web-admin and ask for a path in CLI. - #### Examples + #### Example ```toml [section.my_option_id] type = "file" @@ -1387,7 +1362,7 @@ class SelectOption(BaseChoicesOption): Ask for value from a limited set of values. Renders as a regular `