From 7372dc207962c6e16a5f9e73c6d082b39861b9cd Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 24 Nov 2022 14:42:43 +0100 Subject: [PATCH 001/319] be able to change the loginShell of a user --- src/user.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/user.py b/src/user.py index 84923106c..74a11e99d 100644 --- a/src/user.py +++ b/src/user.py @@ -134,6 +134,7 @@ def user_create( lastname=None, mailbox_quota="0", admin=False, + loginShell="/bin/bash", from_import=False, ): @@ -253,7 +254,7 @@ def user_create( "gidNumber": [uid], "uidNumber": [uid], "homeDirectory": ["/home/" + username], - "loginShell": ["/bin/bash"], + "loginShell": [loginShell], } try: @@ -363,6 +364,7 @@ def user_update( mailbox_quota=None, from_import=False, fullname=None, + loginShell=None, ): if firstname or lastname: @@ -524,6 +526,10 @@ def user_update( new_attr_dict["mailuserquota"] = [mailbox_quota] env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota + if loginShell is not None: + new_attr_dict["loginShell"] = [loginShell] + env_dict["YNH_USER_LOGINSHELL"] = loginShell + if not from_import: operation_logger.start() @@ -532,6 +538,10 @@ def user_update( except Exception as e: raise YunohostError("user_update_failed", user=username, error=e) + # Invalidate passwd and group to update the loginShell + subprocess.call(["nscd", "-i", "passwd"]) + subprocess.call(["nscd", "-i", "group"]) + # Trigger post_user_update hooks hook_callback("post_user_update", env=env_dict) From af1c1d8c02507a36d8e882fd79e08c0506704582 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 24 Nov 2022 15:13:59 +0100 Subject: [PATCH 002/319] check if the shell exists --- locales/en.json | 1 + src/user.py | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index d18f8791e..4dc4037f8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -459,6 +459,7 @@ "invalid_number_max": "Must be lesser than {max}", "invalid_number_min": "Must be greater than {min}", "invalid_regex": "Invalid regex:'{regex}'", + "invalid_shell": "Invalid shell: {shell}", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "ldap_attribute_already_exists": "LDAP attribute '{attribute}' already exists with value '{value}'", diff --git a/src/user.py b/src/user.py index 74a11e99d..6583b32e8 100644 --- a/src/user.py +++ b/src/user.py @@ -122,6 +122,29 @@ def user_list(fields=None): return {"users": users} +def list_shells(): + import ctypes + import ctypes.util + import os + import sys + + """List the shells from /etc/shells.""" + libc = ctypes.CDLL(ctypes.util.find_library("c")) + getusershell = libc.getusershell + getusershell.restype = ctypes.c_char_p + libc.setusershell() + while True: + shell = getusershell() + if not shell: + break + yield shell.decode() + libc.endusershell() + + +def shellexists(shell): + """Check if the provided shell exists and is executable.""" + return os.path.isfile(shell) and os.access(shell, os.X_OK) + @is_unit_operation([("username", "user")]) def user_create( @@ -134,8 +157,8 @@ def user_create( lastname=None, mailbox_quota="0", admin=False, - loginShell="/bin/bash", from_import=False, + loginShell=None, ): if firstname or lastname: @@ -235,6 +258,12 @@ def user_create( uid = str(random.randint(1001, 65000)) uid_guid_found = uid not in all_uid and uid not in all_gid + if not loginShell: + loginShell = "/bin/bash" + else: + if not shellexists(loginShell) or loginShell not in list_shells(): + raise YunohostValidationError("invalid_shell", shell=loginShell) + attr_dict = { "objectClass": [ "mailAccount", @@ -527,6 +556,8 @@ def user_update( env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota if loginShell is not None: + if not shellexists(loginShell) or loginShell not in list_shells(): + raise YunohostValidationError("invalid_shell", shell=loginShell) new_attr_dict["loginShell"] = [loginShell] env_dict["YNH_USER_LOGINSHELL"] = loginShell @@ -563,7 +594,7 @@ def user_info(username): ldap = _get_ldap_interface() - user_attrs = ["cn", "mail", "uid", "maildrop", "mailuserquota"] + user_attrs = ["cn", "mail", "uid", "maildrop", "mailuserquota", "loginShell"] if len(username.split("@")) == 2: filter = "mail=" + username @@ -581,6 +612,7 @@ def user_info(username): "username": user["uid"][0], "fullname": user["cn"][0], "mail": user["mail"][0], + "loginShell": user["loginShell"][0], "mail-aliases": [], "mail-forward": [], } From dda5095157b4d6e5d947df4a65d54c9eb17c0dfa Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 24 Nov 2022 15:14:06 +0100 Subject: [PATCH 003/319] add actionsmap parameters --- share/actionsmap.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 98ae59a7b..72c515e6f 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -124,6 +124,11 @@ user: pattern: &pattern_mailbox_quota - !!str ^(\d+[bkMGT])|0$ - "pattern_mailbox_quota" + -s: + full: --loginShell + help: The login shell used + default: "/bin/bash" + ### user_delete() delete: @@ -203,6 +208,10 @@ user: metavar: "{SIZE|0}" extra: pattern: *pattern_mailbox_quota + -s: + full: --loginShell + help: The login shell used + default: "/bin/bash" ### user_info() info: From 21c72ad1c5378da21513f45c15addad2dd9e7596 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 24 Nov 2022 17:30:05 +0100 Subject: [PATCH 004/319] fix linter --- src/user.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/user.py b/src/user.py index 6583b32e8..61060a9ef 100644 --- a/src/user.py +++ b/src/user.py @@ -125,8 +125,6 @@ def user_list(fields=None): def list_shells(): import ctypes import ctypes.util - import os - import sys """List the shells from /etc/shells.""" libc = ctypes.CDLL(ctypes.util.find_library("c")) From a6db52b7b42cb0e2286f4df29441a72557326b68 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 8 Jan 2023 14:58:53 +0100 Subject: [PATCH 005/319] apps: don't clone 'master' branch by default, use git ls-remote to check what's the default branch instead --- src/app.py | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/app.py b/src/app.py index ed1432685..b1ae7410b 100644 --- a/src/app.py +++ b/src/app.py @@ -29,7 +29,7 @@ import subprocess import tempfile import copy from collections import OrderedDict -from typing import List, Tuple, Dict, Any, Iterator +from typing import List, Tuple, Dict, Any, Iterator, Optional from packaging import version from moulinette import Moulinette, m18n @@ -2300,19 +2300,19 @@ def _extract_app(src: str) -> Tuple[Dict, str]: url = app_info["git"]["url"] branch = app_info["git"]["branch"] revision = str(app_info["git"]["revision"]) - return _extract_app_from_gitrepo(url, branch, revision, app_info) + return _extract_app_from_gitrepo(url, branch=branch, revision=revision, app_info=app_info) # App is a git repo url elif _is_app_repo_url(src): url = src.strip().strip("/") - branch = "master" - revision = "HEAD" # gitlab urls may look like 'https://domain/org/group/repo/-/tree/testing' # compated to github urls looking like 'https://domain/org/repo/tree/testing' if "/-/" in url: url = url.replace("/-/", "/") if "/tree/" in url: url, branch = url.split("/tree/", 1) - return _extract_app_from_gitrepo(url, branch, revision, {}) + else: + branch = None + return _extract_app_from_gitrepo(url, branch=branch) # App is a local folder elif os.path.exists(src): return _extract_app_from_folder(src) @@ -2369,9 +2369,36 @@ def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: def _extract_app_from_gitrepo( - url: str, branch: str, revision: str, app_info: Dict = {} + url: str, branch: Optional[str] = None, revision: str = "HEAD", app_info: Dict = {} ) -> Tuple[Dict, str]: + + logger.debug("Checking default branch") + + try: + git_remote_show = check_output(["git", "remote", "show", url], env={"GIT_TERMINAL_PROMPT": "0", "LC_ALL": "C"}, shell=False) + except Exception: + raise YunohostError("app_sources_fetch_failed") + + if not branch: + default_branch = None + try: + for line in git_remote_show.split('\n'): + if "HEAD branch:" in line: + default_branch = line.split()[-1] + except Exception: + pass + + if not default_branch: + logger.warning("Failed to parse default branch, trying 'main'") + branch = 'main' + else: + if default_branch in ['testing', 'dev']: + logger.warning(f"Trying 'master' branch instead of default '{default_branch}'") + branch = 'master' + else: + branch = default_branch + logger.debug(m18n.n("downloading")) extracted_app_folder = _make_tmp_workdir_for_app() From b9be18c781c31c93d1f025992e595cd345005880 Mon Sep 17 00:00:00 2001 From: YunoHost Bot Date: Sun, 8 Jan 2023 15:52:48 +0100 Subject: [PATCH 006/319] [CI] Format code with Black (#1562) --- doc/generate_api_doc.py | 348 ++++++++++++++++++---------------------- src/app.py | 23 ++- 2 files changed, 175 insertions(+), 196 deletions(-) diff --git a/doc/generate_api_doc.py b/doc/generate_api_doc.py index 939dd90bd..fc44ffbcd 100644 --- a/doc/generate_api_doc.py +++ b/doc/generate_api_doc.py @@ -24,289 +24,261 @@ import yaml import json import requests + def main(): - """ - """ - with open('../share/actionsmap.yml') as f: + """ """ + with open("../share/actionsmap.yml") as f: action_map = yaml.safe_load(f) try: - with open('/etc/yunohost/current_host', 'r') as f: + with open("/etc/yunohost/current_host", "r") as f: domain = f.readline().rstrip() except IOError: - domain = requests.get('http://ip.yunohost.org').text - with open('../debian/changelog') as f: + domain = requests.get("http://ip.yunohost.org").text + with open("../debian/changelog") as f: top_changelog = f.readline() - api_version = top_changelog[top_changelog.find("(")+1:top_changelog.find(")")] + api_version = top_changelog[top_changelog.find("(") + 1 : top_changelog.find(")")] csrf = { - 'name': 'X-Requested-With', - 'in': 'header', - 'required': True, - 'schema': { - 'type': 'string', - 'default': 'Swagger API' - } - + "name": "X-Requested-With", + "in": "header", + "required": True, + "schema": {"type": "string", "default": "Swagger API"}, } resource_list = { - 'openapi': '3.0.3', - 'info': { - 'title': 'YunoHost API', - 'description': 'This is the YunoHost API used on all YunoHost instances. This API is essentially used by YunoHost Webadmin.', - 'version': api_version, - + "openapi": "3.0.3", + "info": { + "title": "YunoHost API", + "description": "This is the YunoHost API used on all YunoHost instances. This API is essentially used by YunoHost Webadmin.", + "version": api_version, }, - 'servers': [ + "servers": [ { - 'url': "https://{domain}/yunohost/api", - 'variables': { - 'domain': { - 'default': 'demo.yunohost.org', - 'description': 'Your yunohost domain' + "url": "https://{domain}/yunohost/api", + "variables": { + "domain": { + "default": "demo.yunohost.org", + "description": "Your yunohost domain", } - } + }, } ], - 'tags': [ - { - 'name': 'public', - 'description': 'Public route' - } - ], - 'paths': { - '/login': { - 'post': { - 'tags': ['public'], - 'summary': 'Logs in and returns the authentication cookie', - 'parameters': [csrf], - 'requestBody': { - 'required': True, - 'content': { - 'multipart/form-data': { - 'schema': { - 'type': 'object', - 'properties': { - 'credentials': { - 'type': 'string', - 'format': 'password' + "tags": [{"name": "public", "description": "Public route"}], + "paths": { + "/login": { + "post": { + "tags": ["public"], + "summary": "Logs in and returns the authentication cookie", + "parameters": [csrf], + "requestBody": { + "required": True, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "credentials": { + "type": "string", + "format": "password", } }, - 'required': [ - 'credentials' - ] + "required": ["credentials"], } } + }, + }, + "security": [], + "responses": { + "200": { + "description": "Successfully login", + "headers": {"Set-Cookie": {"schema": {"type": "string"}}}, } }, - 'security': [], - 'responses': { - '200': { - 'description': 'Successfully login', - 'headers': { - 'Set-Cookie': { - 'schema': { - 'type': 'string' - } - } - } - } - } } }, - '/installed': { - 'get': { - 'tags': ['public'], - 'summary': 'Test if the API is working', - 'parameters': [], - 'security': [], - 'responses': { - '200': { - 'description': 'Successfully working', + "/installed": { + "get": { + "tags": ["public"], + "summary": "Test if the API is working", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Successfully working", } - } + }, } - } + }, }, } - def convert_categories(categories, parent_category=""): for category, category_params in categories.items(): if parent_category: category = f"{parent_category} {category}" - if 'subcategory_help' in category_params: - category_params['category_help'] = category_params['subcategory_help'] + if "subcategory_help" in category_params: + category_params["category_help"] = category_params["subcategory_help"] - if 'category_help' not in category_params: - category_params['category_help'] = '' - resource_list['tags'].append({ - 'name': category, - 'description': category_params['category_help'] - }) + if "category_help" not in category_params: + category_params["category_help"] = "" + resource_list["tags"].append( + {"name": category, "description": category_params["category_help"]} + ) - - for action, action_params in category_params['actions'].items(): - if 'action_help' not in action_params: - action_params['action_help'] = '' - if 'api' not in action_params: + for action, action_params in category_params["actions"].items(): + if "action_help" not in action_params: + action_params["action_help"] = "" + if "api" not in action_params: continue - if not isinstance(action_params['api'], list): - action_params['api'] = [action_params['api']] + if not isinstance(action_params["api"], list): + action_params["api"] = [action_params["api"]] - for i, api in enumerate(action_params['api']): + for i, api in enumerate(action_params["api"]): print(api) - method, path = api.split(' ') + method, path = api.split(" ") method = method.lower() - key_param = '' - if '{' in path: - key_param = path[path.find("{")+1:path.find("}")] - resource_list['paths'].setdefault(path, {}) + key_param = "" + if "{" in path: + key_param = path[path.find("{") + 1 : path.find("}")] + resource_list["paths"].setdefault(path, {}) - notes = '' + notes = "" operationId = f"{category}_{action}" if i > 0: operationId += f"_{i}" operation = { - 'tags': [category], - 'operationId': operationId, - 'summary': action_params['action_help'], - 'description': notes, - 'responses': { - '200': { - 'description': 'successful operation' - } - } + "tags": [category], + "operationId": operationId, + "summary": action_params["action_help"], + "description": notes, + "responses": {"200": {"description": "successful operation"}}, } - if action_params.get('deprecated'): - operation['deprecated'] = True + if action_params.get("deprecated"): + operation["deprecated"] = True - operation['parameters'] = [] - if method == 'post': - operation['parameters'] = [csrf] + operation["parameters"] = [] + if method == "post": + operation["parameters"] = [csrf] - if 'arguments' in action_params: - if method in ['put', 'post', 'patch']: - operation['requestBody'] = { - 'required': True, - 'content': { - 'multipart/form-data': { - 'schema': { - 'type': 'object', - 'properties': { - }, - 'required': [] + if "arguments" in action_params: + if method in ["put", "post", "patch"]: + operation["requestBody"] = { + "required": True, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": {}, + "required": [], } } - } + }, } - for arg_name, arg_params in action_params['arguments'].items(): - if 'help' not in arg_params: - arg_params['help'] = '' - param_type = 'query' + for arg_name, arg_params in action_params["arguments"].items(): + if "help" not in arg_params: + arg_params["help"] = "" + param_type = "query" allow_multiple = False required = True allowable_values = None - name = str(arg_name).replace('-', '_') - if name[0] == '_': + name = str(arg_name).replace("-", "_") + if name[0] == "_": required = False - if 'full' in arg_params: - name = arg_params['full'][2:] + if "full" in arg_params: + name = arg_params["full"][2:] else: name = name[2:] - name = name.replace('-', '_') + name = name.replace("-", "_") - if 'choices' in arg_params: - allowable_values = arg_params['choices'] - _type = 'string' - if 'type' in arg_params: - types = { - 'open': 'file', - 'int': 'int' - } - _type = types[arg_params['type']] - if 'action' in arg_params and arg_params['action'] == 'store_true': - _type = 'boolean' + if "choices" in arg_params: + allowable_values = arg_params["choices"] + _type = "string" + if "type" in arg_params: + types = {"open": "file", "int": "int"} + _type = types[arg_params["type"]] + if ( + "action" in arg_params + and arg_params["action"] == "store_true" + ): + _type = "boolean" - if 'nargs' in arg_params: - if arg_params['nargs'] == '*': + if "nargs" in arg_params: + if arg_params["nargs"] == "*": allow_multiple = True required = False - _type = 'array' - if arg_params['nargs'] == '+': + _type = "array" + if arg_params["nargs"] == "+": allow_multiple = True required = True - _type = 'array' - if arg_params['nargs'] == '?': + _type = "array" + if arg_params["nargs"] == "?": allow_multiple = False required = False else: allow_multiple = False - if name == key_param: - param_type = 'path' + param_type = "path" required = True allow_multiple = False - if method in ['put', 'post', 'patch']: - schema = operation['requestBody']['content']['multipart/form-data']['schema'] - schema['properties'][name] = { - 'type': _type, - 'description': arg_params['help'] + if method in ["put", "post", "patch"]: + schema = operation["requestBody"]["content"][ + "multipart/form-data" + ]["schema"] + schema["properties"][name] = { + "type": _type, + "description": arg_params["help"], } if required: - schema['required'].append(name) - prop_schema = schema['properties'][name] + schema["required"].append(name) + prop_schema = schema["properties"][name] else: parameters = { - 'name': name, - 'in': param_type, - 'description': arg_params['help'], - 'required': required, - 'schema': { - 'type': _type, + "name": name, + "in": param_type, + "description": arg_params["help"], + "required": required, + "schema": { + "type": _type, }, - 'explode': allow_multiple + "explode": allow_multiple, } - prop_schema = parameters['schema'] - operation['parameters'].append(parameters) + prop_schema = parameters["schema"] + operation["parameters"].append(parameters) if allowable_values is not None: - prop_schema['enum'] = allowable_values - if 'default' in arg_params: - prop_schema['default'] = arg_params['default'] - if arg_params.get('metavar') == 'PASSWORD': - prop_schema['format'] = 'password' - if arg_params.get('metavar') == 'MAIL': - prop_schema['format'] = 'mail' + prop_schema["enum"] = allowable_values + if "default" in arg_params: + prop_schema["default"] = arg_params["default"] + if arg_params.get("metavar") == "PASSWORD": + prop_schema["format"] = "password" + if arg_params.get("metavar") == "MAIL": + prop_schema["format"] = "mail" # Those lines seems to slow swagger ui too much - #if 'pattern' in arg_params.get('extra', {}): + # if 'pattern' in arg_params.get('extra', {}): # prop_schema['pattern'] = arg_params['extra']['pattern'][0] - - - resource_list['paths'][path][method.lower()] = operation + resource_list["paths"][path][method.lower()] = operation # Includes subcategories - if 'subcategories' in category_params: - convert_categories(category_params['subcategories'], category) + if "subcategories" in category_params: + convert_categories(category_params["subcategories"], category) - del action_map['_global'] + del action_map["_global"] convert_categories(action_map) openapi_json = json.dumps(resource_list) # Save the OpenAPI json - with open(os.getcwd() + '/openapi.json', 'w') as f: + with open(os.getcwd() + "/openapi.json", "w") as f: f.write(openapi_json) openapi_js = f"var openapiJSON = {openapi_json}" - with open(os.getcwd() + '/openapi.js', 'w') as f: + with open(os.getcwd() + "/openapi.js", "w") as f: f.write(openapi_js) - -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff --git a/src/app.py b/src/app.py index b1ae7410b..dfaa36a1e 100644 --- a/src/app.py +++ b/src/app.py @@ -2300,7 +2300,9 @@ def _extract_app(src: str) -> Tuple[Dict, str]: url = app_info["git"]["url"] branch = app_info["git"]["branch"] revision = str(app_info["git"]["revision"]) - return _extract_app_from_gitrepo(url, branch=branch, revision=revision, app_info=app_info) + return _extract_app_from_gitrepo( + url, branch=branch, revision=revision, app_info=app_info + ) # App is a git repo url elif _is_app_repo_url(src): url = src.strip().strip("/") @@ -2372,18 +2374,21 @@ def _extract_app_from_gitrepo( url: str, branch: Optional[str] = None, revision: str = "HEAD", app_info: Dict = {} ) -> Tuple[Dict, str]: - logger.debug("Checking default branch") try: - git_remote_show = check_output(["git", "remote", "show", url], env={"GIT_TERMINAL_PROMPT": "0", "LC_ALL": "C"}, shell=False) + git_remote_show = check_output( + ["git", "remote", "show", url], + env={"GIT_TERMINAL_PROMPT": "0", "LC_ALL": "C"}, + shell=False, + ) except Exception: raise YunohostError("app_sources_fetch_failed") if not branch: default_branch = None try: - for line in git_remote_show.split('\n'): + for line in git_remote_show.split("\n"): if "HEAD branch:" in line: default_branch = line.split()[-1] except Exception: @@ -2391,11 +2396,13 @@ def _extract_app_from_gitrepo( if not default_branch: logger.warning("Failed to parse default branch, trying 'main'") - branch = 'main' + branch = "main" else: - if default_branch in ['testing', 'dev']: - logger.warning(f"Trying 'master' branch instead of default '{default_branch}'") - branch = 'master' + if default_branch in ["testing", "dev"]: + logger.warning( + f"Trying 'master' branch instead of default '{default_branch}'" + ) + branch = "master" else: branch = default_branch From f258eab6c286d5370c4047f5894ff32a8d09c3ba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 9 Jan 2023 23:58:45 +0100 Subject: [PATCH 007/319] ssowat: add use_remote_user_var_in_nginx_conf flag on permission --- src/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app.py b/src/app.py index dfaa36a1e..fe49932f0 100644 --- a/src/app.py +++ b/src/app.py @@ -1595,6 +1595,8 @@ def app_ssowatconf(): } redirected_urls = {} + apps_using_remote_user_var_in_nginx = check_output('grep -nri \'$remote_user\' /etc/yunohost/apps/*/conf/*nginx*conf | awk -F/ \'{print $5}\' || true').strip().split("\n") + for app in _installed_apps(): app_settings = read_yaml(APPS_SETTING_PATH + app + "/settings.yml") or {} @@ -1633,7 +1635,10 @@ def app_ssowatconf(): if not uris: continue + app_id = perm_name.split(".")[0] + permissions[perm_name] = { + "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"] From 37b424e9680668564c8393c4d56352fbe4747599 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 10 Jan 2023 00:00:07 +0100 Subject: [PATCH 008/319] Update changelog for 11.1.2.1 --- debian/changelog | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/debian/changelog b/debian/changelog index 24a1969ed..fbcddc9fb 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,14 @@ +yunohost (11.1.2.1) testing; urgency=low + + - i18n: fix (un)defined string issues (dd33476f) + - doc: Revive the old auto documentation of API with swagger + - apps: don't clone 'master' branch by default, use git ls-remote to check what's the default branch instead (a6db52b7) + - ssowat: add use_remote_user_var_in_nginx_conf flag on permission (f258eab6) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Mon, 09 Jan 2023 23:58:51 +0100 + yunohost (11.1.2) testing; urgency=low - apps: Various fixes/improvements for appsv2, mostly related to webadmin integration ([#1526](https://github.com/yunohost/yunohost/pull/1526)) From b37d4baf64ad043b4e7c53fb20d3fb9e3b5ecbb8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 10 Jan 2023 00:18:23 +0100 Subject: [PATCH 009/319] Fix boring issues in tools_upgrade --- src/tools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tools.py b/src/tools.py index c52cad675..5c5b8077b 100644 --- a/src/tools.py +++ b/src/tools.py @@ -416,7 +416,8 @@ def tools_upgrade(operation_logger, target=None): if target not in ["apps", "system"]: raise YunohostValidationError( - "Uhoh ?! tools_upgrade should have 'apps' or 'system' value for argument target" + "Uhoh ?! tools_upgrade should have 'apps' or 'system' value for argument target", + raw_msg=True ) # @@ -510,7 +511,7 @@ def tools_upgrade(operation_logger, target=None): logger.warning( m18n.n( "tools_upgrade_failed", - packages_list=", ".join(upgradables), + packages_list=", ".join([p["name"] for p in upgradables]), ) ) From 683421719fb8d4404c8b8e7bca4e9411c2197c08 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 10 Jan 2023 00:39:54 +0100 Subject: [PATCH 010/319] configpanel: key 'type' may not exist? --- src/utils/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/config.py b/src/utils/config.py index 27e4b9509..721da443b 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -575,7 +575,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["type"] == "button": + if subnode.get("type") == "button": out["is_action_section"] = True out.setdefault(sublevel, []).append(subnode) # Key/value are a property From f21fbed2f72947d57b1d795589d0a9504e004d5c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 10 Jan 2023 00:42:40 +0100 Subject: [PATCH 011/319] configpanel: stop the madness of returning a 500 error when trying to load config panel 0.1 ... otherwise this will crash the new app info view ... --- locales/en.json | 1 - src/utils/config.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index 1d4d37d92..e655acb83 100644 --- a/locales/en.json +++ b/locales/en.json @@ -162,7 +162,6 @@ "config_validate_email": "Should be a valid email", "config_validate_time": "Should be a valid time like HH:MM", "config_validate_url": "Should be a valid web URL", - "config_version_not_supported": "Config panel versions '{version}' are not supported.", "confirm_app_install_danger": "DANGER! This app is known to be still experimental (if not explicitly not working)! You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system... If you are willing to take that risk anyway, type '{answers}'", "confirm_app_install_thirdparty": "DANGER! This app is not part of YunoHost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system... If you are willing to take that risk anyway, type '{answers}'", "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated into YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ", diff --git a/src/utils/config.py b/src/utils/config.py index 721da443b..da3c68ad8 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -479,9 +479,8 @@ class ConfigPanel: # Check TOML config panel is in a supported version if float(toml_config_panel["version"]) < CONFIG_PANEL_VERSION_SUPPORTED: - raise YunohostError( - "config_version_not_supported", version=toml_config_panel["version"] - ) + logger.error(f"Config panels version {toml_config_panel['version']} are not supported") + return None # Transform toml format into internal format format_description = { From 25c10166cfc078461d0ef23c4dff201d231f5abb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 10 Jan 2023 00:48:39 +0100 Subject: [PATCH 012/319] apps: fix trick to find the default branch from git repo @_@ --- src/app.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app.py b/src/app.py index fe49932f0..7458808fc 100644 --- a/src/app.py +++ b/src/app.py @@ -2382,20 +2382,24 @@ def _extract_app_from_gitrepo( logger.debug("Checking default branch") try: - git_remote_show = check_output( - ["git", "remote", "show", url], + git_ls_remote = check_output( + ["git", "ls-remote", "--symref", url, "HEAD"], env={"GIT_TERMINAL_PROMPT": "0", "LC_ALL": "C"}, shell=False, ) - except Exception: + except Exception as e: + logger.error(str(e)) raise YunohostError("app_sources_fetch_failed") if not branch: default_branch = None try: - for line in git_remote_show.split("\n"): - if "HEAD branch:" in line: - default_branch = line.split()[-1] + for line in git_ls_remote.split("\n"): + # Look for the line formated like : + # ref: refs/heads/master HEAD + if "ref: refs/heads/" in line: + line = line.replace("/", " ").replace("\t", " ") + default_branch = line.split()[3] except Exception: pass From 4c17220764d5e7097fa5b8d544d00ba842b61a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= Date: Thu, 5 Jan 2023 18:26:39 +0000 Subject: [PATCH 013/319] Translated using Weblate (French) Currently translated at 100.0% (743 of 743 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 959ef1a8d..f6e4078c1 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -738,7 +738,7 @@ "global_settings_setting_smtp_allow_ipv6": "Autoriser l'IPv6", "password_too_long": "Veuillez choisir un mot de passe de moins de 127 caractères", "domain_cannot_add_muc_upload": "Vous ne pouvez pas ajouter de domaines commençant par 'muc.'. Ce type de nom est réservé à la fonction de chat XMPP multi-utilisateurs intégrée à YunoHost.", - "group_update_aliases": "Mise à jour des alias du groupe '{group}'.", + "group_update_aliases": "Mise à jour des alias du groupe '{group}'", "group_no_change": "Rien à mettre à jour pour le groupe '{group}'", "global_settings_setting_portal_theme": "Thème du portail", "global_settings_setting_portal_theme_help": "Pour plus d'informations sur la création de thèmes de portail personnalisés, voir https://yunohost.org/theming", From 7ceda87692aee666bfb99650aeb23b1fd60fa6c0 Mon Sep 17 00:00:00 2001 From: ppr Date: Sun, 8 Jan 2023 21:08:17 +0000 Subject: [PATCH 014/319] Translated using Weblate (French) Currently translated at 99.2% (744 of 750 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index f6e4078c1..705f39083 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -742,5 +742,12 @@ "group_no_change": "Rien à mettre à jour pour le groupe '{group}'", "global_settings_setting_portal_theme": "Thème du portail", "global_settings_setting_portal_theme_help": "Pour plus d'informations sur la création de thèmes de portail personnalisés, voir https://yunohost.org/theming", - "global_settings_setting_passwordless_sudo": "Permettre aux administrateurs d'utiliser 'sudo' sans retaper leur mot de passe" + "global_settings_setting_passwordless_sudo": "Permettre aux administrateurs d'utiliser 'sudo' sans retaper leur mot de passe", + "app_arch_not_supported": "Cette application ne peut être installée que sur les architectures {', '.join(required)}. L'architecture de votre serveur est {current}", + "app_resource_failed": "L'allocation automatique des ressources (provisioning), la suppression d'accès à ces ressources (déprovisioning) ou la mise à jour des ressources pour {app} a échoué : {error}", + "confirm_app_insufficient_ram": "ATTENTION ! Cette application requiert {required} de RAM pour l'installation/mise à niveau mais il n'y a que {current} de disponible actuellement. Même si cette application pouvait fonctionner, son processus d'installation/mise à niveau nécessite une grande quantité de RAM. Votre serveur pourrait donc geler (freezer) et planter lamentablement. Si vous êtes prêt à prendre ce risque, tapez '{answers}'", + "app_not_enough_disk": "Cette application nécessite {required} d'espace libre.", + "app_not_enough_ram": "Cette application nécessite {required} de mémoire vive (RAM) pour être installée/mise à niveau mais seule {current} de mémoire est disponible actuellement.", + "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}.", + "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des éléments d'information importants à connaître. [{answers}]" } From 27d61062592aadb3ddb5a7704c492794bfd8adf5 Mon Sep 17 00:00:00 2001 From: ppr Date: Mon, 9 Jan 2023 18:37:06 +0000 Subject: [PATCH 015/319] Translated using Weblate (French) Currently translated at 99.2% (744 of 750 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 705f39083..9eaca4bb2 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -748,6 +748,6 @@ "confirm_app_insufficient_ram": "ATTENTION ! Cette application requiert {required} de RAM pour l'installation/mise à niveau mais il n'y a que {current} de disponible actuellement. Même si cette application pouvait fonctionner, son processus d'installation/mise à niveau nécessite une grande quantité de RAM. Votre serveur pourrait donc geler (freezer) et planter lamentablement. Si vous êtes prêt à prendre ce risque, tapez '{answers}'", "app_not_enough_disk": "Cette application nécessite {required} d'espace libre.", "app_not_enough_ram": "Cette application nécessite {required} de mémoire vive (RAM) pour être installée/mise à niveau mais seule {current} de mémoire est disponible actuellement.", - "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}.", + "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}", "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des éléments d'information importants à connaître. [{answers}]" } From 8859038a41b94700bd071689eabb88d05a438c72 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Tue, 10 Jan 2023 01:30:51 +0000 Subject: [PATCH 016/319] [CI] Format code with Black --- src/app.py | 11 +++++++++-- src/tools.py | 2 +- src/utils/config.py | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/app.py b/src/app.py index 7458808fc..5b2e63e44 100644 --- a/src/app.py +++ b/src/app.py @@ -1595,7 +1595,13 @@ def app_ssowatconf(): } redirected_urls = {} - apps_using_remote_user_var_in_nginx = check_output('grep -nri \'$remote_user\' /etc/yunohost/apps/*/conf/*nginx*conf | awk -F/ \'{print $5}\' || true').strip().split("\n") + apps_using_remote_user_var_in_nginx = ( + check_output( + "grep -nri '$remote_user' /etc/yunohost/apps/*/conf/*nginx*conf | awk -F/ '{print $5}' || true" + ) + .strip() + .split("\n") + ) for app in _installed_apps(): @@ -1638,7 +1644,8 @@ def app_ssowatconf(): app_id = perm_name.split(".")[0] permissions[perm_name] = { - "use_remote_user_var_in_nginx_conf": app_id in apps_using_remote_user_var_in_nginx, + "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"] diff --git a/src/tools.py b/src/tools.py index 5c5b8077b..eb385f4a8 100644 --- a/src/tools.py +++ b/src/tools.py @@ -417,7 +417,7 @@ def tools_upgrade(operation_logger, target=None): if target not in ["apps", "system"]: raise YunohostValidationError( "Uhoh ?! tools_upgrade should have 'apps' or 'system' value for argument target", - raw_msg=True + raw_msg=True, ) # diff --git a/src/utils/config.py b/src/utils/config.py index da3c68ad8..bd3a6b6a9 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -479,7 +479,9 @@ class ConfigPanel: # 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") + logger.error( + f"Config panels version {toml_config_panel['version']} are not supported" + ) return None # Transform toml format into internal format From ea20b1581d6998ed6aa8d6c9cd6c8fc5d8b3cb9a Mon Sep 17 00:00:00 2001 From: Tagadda <36127788+Tagadda@users.noreply.github.com> Date: Sat, 26 Mar 2022 14:11:37 +0000 Subject: [PATCH 017/319] enh: ipv6 only global setting --- share/config_global.toml | 6 ++++++ src/diagnosers/10-ip.py | 5 +++-- src/diagnosers/14-ports.py | 5 +++-- src/diagnosers/21-web.py | 9 +++++---- src/diagnosers/24-mail.py | 5 +++-- src/dns.py | 5 +++-- src/settings.py | 2 ++ 7 files changed, 25 insertions(+), 12 deletions(-) diff --git a/share/config_global.toml b/share/config_global.toml index 1f3cc1b39..405157c5f 100644 --- a/share/config_global.toml +++ b/share/config_global.toml @@ -160,3 +160,9 @@ name = "Other" [misc.backup.backup_compress_tar_archives] type = "boolean" default = false + + [misc.network] + name = "Network" + [misc.network.network_ipv6_only] + type = "boolean" + default = false diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index b2bedc802..098bd569c 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -28,6 +28,7 @@ from moulinette.utils.filesystem import read_file from yunohost.diagnosis import Diagnoser from yunohost.utils.network import get_network_interfaces +from yunohost.settings import settings_get logger = log.getActionLogger("yunohost.diagnosis") @@ -121,7 +122,7 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv4"}, data={"global": ipv4, "local": get_local_ip("ipv4")}, - status="SUCCESS" if ipv4 else "ERROR", + status="SUCCESS" if ipv4 else "WARNING" if settings_get("network_ipv6_only") else "ERROR", summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None, ) @@ -129,7 +130,7 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv6"}, data={"global": ipv6, "local": get_local_ip("ipv6")}, - status="SUCCESS" if ipv6 else "WARNING", + status="SUCCESS" if ipv6 else "ERROR" if settings_get("network_ipv6_only") else "WARNING", summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv6 diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index 5671211b5..0ca39a42c 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -21,6 +21,7 @@ from typing import List from yunohost.diagnosis import Diagnoser from yunohost.service import _get_services +from yunohost.settings import settings_get class MyDiagnoser(Diagnoser): @@ -46,7 +47,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS": + if ipv4.get("status") == "SUCCESS" and not settings_get("network_ipv6_only"): ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -120,7 +121,7 @@ class MyDiagnoser(Diagnoser): for record in dnsrecords.get("items", []) ) - if failed == 4 or ipv6_is_important(): + if failed == 4 and not settings_get("network_ipv6_only") or ipv6_is_important(): yield dict( meta={"port": port}, data={ diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index 4a69895b2..bdba89f78 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -26,6 +26,7 @@ from moulinette.utils.filesystem import read_file, mkdir, rm from yunohost.diagnosis import Diagnoser from yunohost.domain import domain_list from yunohost.utils.dns import is_special_use_tld +from yunohost.settings import settings_get DIAGNOSIS_SERVER = "diagnosis.yunohost.org" @@ -76,7 +77,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS": + if ipv4.get("status") == "SUCCESS" and not settings_get("network_ipv6_only"): ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -96,7 +97,7 @@ class MyDiagnoser(Diagnoser): # "curl --head the.global.ip" will simply timeout... if self.do_hairpinning_test: global_ipv4 = ipv4.get("data", {}).get("global", None) - if global_ipv4: + if global_ipv4 and not settings_get("network_ipv6_only"): try: requests.head("http://" + global_ipv4, timeout=5) except requests.exceptions.Timeout: @@ -147,7 +148,7 @@ class MyDiagnoser(Diagnoser): if all( results[ipversion][domain]["status"] == "ok" for ipversion in ipversions ): - if 4 in ipversions: + if 4 in ipversions and not settings_get("network_ipv6_only"): self.do_hairpinning_test = True yield dict( meta={"domain": domain}, @@ -185,7 +186,7 @@ class MyDiagnoser(Diagnoser): ) AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") - return AAAA_status in ["OK", "WRONG"] + return AAAA_status in ["OK", "WRONG"] or settings_get("network_ipv6_only") if failed == 4 or ipv6_is_important_for_this_domain(): yield dict( diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 88d6a8259..536f870b3 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -31,6 +31,7 @@ from yunohost.diagnosis import Diagnoser from yunohost.domain import _get_maindomain, domain_list from yunohost.settings import settings_get from yunohost.utils.dns import dig +from yunohost.settings import settings_get DEFAULT_DNS_BLACKLIST = "/usr/share/yunohost/dnsbl_list.yml" @@ -301,13 +302,13 @@ class MyDiagnoser(Diagnoser): outgoing_ipversions = [] outgoing_ips = [] ipv4 = Diagnoser.get_cached_report("ip", {"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS": + if ipv4.get("status") == "SUCCESS" and not settings_get("network_ipv6_only"): outgoing_ipversions.append(4) global_ipv4 = ipv4.get("data", {}).get("global", {}) if global_ipv4: outgoing_ips.append(global_ipv4) - if settings_get("email.smtp.smtp_allow_ipv6"): + if settings_get("email.smtp.smtp_allow_ipv6") or settings_get("network_ipv6_only"): ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": outgoing_ipversions.append(6) diff --git a/src/dns.py b/src/dns.py index 1c6b99cf0..cc7ebd7e7 100644 --- a/src/dns.py +++ b/src/dns.py @@ -38,6 +38,7 @@ from yunohost.domain import ( from yunohost.utils.dns import dig, is_yunohost_dyndns_domain, is_special_use_tld from yunohost.utils.error import YunohostValidationError, YunohostError from yunohost.utils.network import get_public_ip +from yunohost.settings import settings_get from yunohost.log import is_unit_operation from yunohost.hook import hook_callback @@ -185,7 +186,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): ########################### # Basic ipv4/ipv6 records # ########################### - if ipv4: + if ipv4 and not settings_get("network_ipv6_only"): basic.append([basename, ttl, "A", ipv4]) if ipv6: @@ -240,7 +241,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): # Only recommend wildcard and CAA for the top level if domain == base_domain: - if ipv4: + if ipv4 and not settings_get("network_ipv6_only"): extra.append([f"*{suffix}", ttl, "A", ipv4]) if ipv6: diff --git a/src/settings.py b/src/settings.py index d9ea600a4..f52574785 100644 --- a/src/settings.py +++ b/src/settings.py @@ -310,6 +310,7 @@ def regen_ssowatconf(setting_name, old_value, new_value): @post_change_hook("nginx_compatibility") @post_change_hook("webadmin_allowlist_enabled") @post_change_hook("webadmin_allowlist") +@post_change_hook("network_ipv6_only") def reconfigure_nginx(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["nginx"]) @@ -341,6 +342,7 @@ def reconfigure_ssh_and_fail2ban(setting_name, old_value, new_value): @post_change_hook("smtp_relay_user") @post_change_hook("smtp_relay_password") @post_change_hook("postfix_compatibility") +@post_change_hook("network_ipv6_only") def reconfigure_postfix(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["postfix"]) From 029c3b76863cf0646a2f8e1137a2b9325fbdaf79 Mon Sep 17 00:00:00 2001 From: Tagadda <36127788+Tagadda@users.noreply.github.com> Date: Tue, 11 Oct 2022 18:01:56 +0000 Subject: [PATCH 018/319] Change to select --- share/config_global.toml | 9 ++++++--- src/diagnosers/10-ip.py | 4 ++-- src/diagnosers/14-ports.py | 4 ++-- src/diagnosers/21-web.py | 8 ++++---- src/diagnosers/24-mail.py | 4 ++-- src/dns.py | 4 ++-- src/settings.py | 4 ++-- 7 files changed, 20 insertions(+), 17 deletions(-) diff --git a/share/config_global.toml b/share/config_global.toml index 405157c5f..40b71ab19 100644 --- a/share/config_global.toml +++ b/share/config_global.toml @@ -163,6 +163,9 @@ name = "Other" [misc.network] name = "Network" - [misc.network.network_ipv6_only] - type = "boolean" - default = false + [misc.network.dns_exposure] + type = "select" + choices.both = "Both" + choices.ipv4 = "IPv4 Only" + choices.ipv6 = "IPv6 Only" + default = "both" diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index 098bd569c..7de462334 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -122,7 +122,7 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv4"}, data={"global": ipv4, "local": get_local_ip("ipv4")}, - status="SUCCESS" if ipv4 else "WARNING" if settings_get("network_ipv6_only") else "ERROR", + status="SUCCESS" if ipv4 else "ERROR" if settings_get("dns_exposure") == "ipv4" else "WARNING", summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None, ) @@ -130,7 +130,7 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv6"}, data={"global": ipv6, "local": get_local_ip("ipv6")}, - status="SUCCESS" if ipv6 else "ERROR" if settings_get("network_ipv6_only") else "WARNING", + status="SUCCESS" if ipv6 else "ERROR" if settings_get("dns_exposure") == "ipv6" else "WARNING", summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv6 diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index 0ca39a42c..2d7eee717 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -47,7 +47,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and not settings_get("network_ipv6_only"): + if ipv4.get("status") == "SUCCESS" or not settings_get("dns_exposure") == "ipv6": ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -121,7 +121,7 @@ class MyDiagnoser(Diagnoser): for record in dnsrecords.get("items", []) ) - if failed == 4 and not settings_get("network_ipv6_only") or ipv6_is_important(): + if failed == 4 and not settings_get("dns_exposure") == "ipv6" or ipv6_is_important(): yield dict( meta={"port": port}, data={ diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index bdba89f78..eaac0d25f 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -77,7 +77,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and not settings_get("network_ipv6_only"): + if ipv4.get("status") == "SUCCESS" and not settings_get("dns_exposure") == "ipv6": ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -97,7 +97,7 @@ class MyDiagnoser(Diagnoser): # "curl --head the.global.ip" will simply timeout... if self.do_hairpinning_test: global_ipv4 = ipv4.get("data", {}).get("global", None) - if global_ipv4 and not settings_get("network_ipv6_only"): + if global_ipv4 and settings_get("dns_exposure") != "ipv6": try: requests.head("http://" + global_ipv4, timeout=5) except requests.exceptions.Timeout: @@ -148,7 +148,7 @@ class MyDiagnoser(Diagnoser): if all( results[ipversion][domain]["status"] == "ok" for ipversion in ipversions ): - if 4 in ipversions and not settings_get("network_ipv6_only"): + if 4 in ipversions and settings_get("dns_exposure") != "ipv6": self.do_hairpinning_test = True yield dict( meta={"domain": domain}, @@ -186,7 +186,7 @@ class MyDiagnoser(Diagnoser): ) AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") - return AAAA_status in ["OK", "WRONG"] or settings_get("network_ipv6_only") + return AAAA_status in ["OK", "WRONG"] or settings_get("dns_exposure") != "ipv4" if failed == 4 or ipv6_is_important_for_this_domain(): yield dict( diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 536f870b3..43273aebf 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -302,13 +302,13 @@ class MyDiagnoser(Diagnoser): outgoing_ipversions = [] outgoing_ips = [] ipv4 = Diagnoser.get_cached_report("ip", {"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and not settings_get("network_ipv6_only"): + if ipv4.get("status") == "SUCCESS" and settings_get("dns_exposure") != "ipv6": outgoing_ipversions.append(4) global_ipv4 = ipv4.get("data", {}).get("global", {}) if global_ipv4: outgoing_ips.append(global_ipv4) - if settings_get("email.smtp.smtp_allow_ipv6") or settings_get("network_ipv6_only"): + if settings_get("email.smtp.smtp_allow_ipv6") or settings_get("dns_exposure") != "ipv4": ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": outgoing_ipversions.append(6) diff --git a/src/dns.py b/src/dns.py index cc7ebd7e7..31c91d590 100644 --- a/src/dns.py +++ b/src/dns.py @@ -186,7 +186,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): ########################### # Basic ipv4/ipv6 records # ########################### - if ipv4 and not settings_get("network_ipv6_only"): + if ipv4 and not settings_get("dns_exposure") == "ipv6": basic.append([basename, ttl, "A", ipv4]) if ipv6: @@ -241,7 +241,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): # Only recommend wildcard and CAA for the top level if domain == base_domain: - if ipv4 and not settings_get("network_ipv6_only"): + if ipv4 and settings_get("dns_exposure") != "ipv6": extra.append([f"*{suffix}", ttl, "A", ipv4]) if ipv6: diff --git a/src/settings.py b/src/settings.py index f52574785..96f11caeb 100644 --- a/src/settings.py +++ b/src/settings.py @@ -310,7 +310,7 @@ def regen_ssowatconf(setting_name, old_value, new_value): @post_change_hook("nginx_compatibility") @post_change_hook("webadmin_allowlist_enabled") @post_change_hook("webadmin_allowlist") -@post_change_hook("network_ipv6_only") +@post_change_hook("dns_exposure") def reconfigure_nginx(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["nginx"]) @@ -342,7 +342,7 @@ def reconfigure_ssh_and_fail2ban(setting_name, old_value, new_value): @post_change_hook("smtp_relay_user") @post_change_hook("smtp_relay_password") @post_change_hook("postfix_compatibility") -@post_change_hook("network_ipv6_only") +@post_change_hook("dns_exposure") def reconfigure_postfix(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["postfix"]) From f4b396219cbf76e3785f0b24dc130b590a2d2464 Mon Sep 17 00:00:00 2001 From: Tagadda <36127788+Tagadda@users.noreply.github.com> Date: Tue, 11 Oct 2022 18:06:57 +0000 Subject: [PATCH 019/319] Clarify conditions --- src/diagnosers/14-ports.py | 4 ++-- src/diagnosers/21-web.py | 2 +- src/dns.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index 2d7eee717..1483dbb96 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -47,7 +47,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" or not settings_get("dns_exposure") == "ipv6": + if ipv4.get("status") == "SUCCESS" or settings_get("dns_exposure") != "ipv6": ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -121,7 +121,7 @@ class MyDiagnoser(Diagnoser): for record in dnsrecords.get("items", []) ) - if failed == 4 and not settings_get("dns_exposure") == "ipv6" or ipv6_is_important(): + if failed == 4 and settings_get("dns_exposure") != "ipv6" or ipv6_is_important(): yield dict( meta={"port": port}, data={ diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index eaac0d25f..f62d182bc 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -77,7 +77,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and not settings_get("dns_exposure") == "ipv6": + if ipv4.get("status") == "SUCCESS" and settings_get("dns_exposure") != "ipv6": ipversions.append(4) # To be discussed: we could also make this check dependent on the diff --git a/src/dns.py b/src/dns.py index 31c91d590..a007f69be 100644 --- a/src/dns.py +++ b/src/dns.py @@ -186,7 +186,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): ########################### # Basic ipv4/ipv6 records # ########################### - if ipv4 and not settings_get("dns_exposure") == "ipv6": + if ipv4 and settings_get("dns_exposure") != "ipv6": basic.append([basename, ttl, "A", ipv4]) if ipv6: From c4c78f5daa785a9d3356e9365fdc8f6d1900fc92 Mon Sep 17 00:00:00 2001 From: Tagadda <36127788+Tagadda@users.noreply.github.com> Date: Mon, 24 Oct 2022 21:55:05 +0000 Subject: [PATCH 020/319] oops --- src/diagnosers/10-ip.py | 4 ++-- src/diagnosers/14-ports.py | 4 ++-- src/diagnosers/21-web.py | 8 ++++---- src/diagnosers/24-mail.py | 4 ++-- src/dns.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index 7de462334..954b9b4e8 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -122,7 +122,7 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv4"}, data={"global": ipv4, "local": get_local_ip("ipv4")}, - status="SUCCESS" if ipv4 else "ERROR" if settings_get("dns_exposure") == "ipv4" else "WARNING", + status="SUCCESS" if ipv4 else "ERROR" if settings_get("misc.network.dns_exposure") == "ipv4" else "WARNING", summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None, ) @@ -130,7 +130,7 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv6"}, data={"global": ipv6, "local": get_local_ip("ipv6")}, - status="SUCCESS" if ipv6 else "ERROR" if settings_get("dns_exposure") == "ipv6" else "WARNING", + status="SUCCESS" if ipv6 else "ERROR" if settings_get("misc.network.dns_exposure") == "ipv6" else "WARNING", summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv6 diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index 1483dbb96..1e265f78e 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -47,7 +47,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" or settings_get("dns_exposure") != "ipv6": + if ipv4.get("status") == "SUCCESS" or settings_get(misc.network.dns_exposure") != "ipv6": ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -121,7 +121,7 @@ class MyDiagnoser(Diagnoser): for record in dnsrecords.get("items", []) ) - if failed == 4 and settings_get("dns_exposure") != "ipv6" or ipv6_is_important(): + if failed == 4 and settings_get(misc.network.dns_exposure") != "ipv6" or ipv6_is_important(): yield dict( meta={"port": port}, data={ diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index f62d182bc..2024cf6ce 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -77,7 +77,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and settings_get("dns_exposure") != "ipv6": + if ipv4.get("status") == "SUCCESS" and settings_get(misc.network.dns_exposure") != "ipv6": ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -97,7 +97,7 @@ class MyDiagnoser(Diagnoser): # "curl --head the.global.ip" will simply timeout... if self.do_hairpinning_test: global_ipv4 = ipv4.get("data", {}).get("global", None) - if global_ipv4 and settings_get("dns_exposure") != "ipv6": + if global_ipv4 and settings_get(misc.network.dns_exposure") != "ipv6": try: requests.head("http://" + global_ipv4, timeout=5) except requests.exceptions.Timeout: @@ -148,7 +148,7 @@ class MyDiagnoser(Diagnoser): if all( results[ipversion][domain]["status"] == "ok" for ipversion in ipversions ): - if 4 in ipversions and settings_get("dns_exposure") != "ipv6": + if 4 in ipversions and settings_get(misc.network.dns_exposure") != "ipv6": self.do_hairpinning_test = True yield dict( meta={"domain": domain}, @@ -186,7 +186,7 @@ class MyDiagnoser(Diagnoser): ) AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") - return AAAA_status in ["OK", "WRONG"] or settings_get("dns_exposure") != "ipv4" + return AAAA_status in ["OK", "WRONG"] or settings_get(misc.network.dns_exposure") != "ipv4" if failed == 4 or ipv6_is_important_for_this_domain(): yield dict( diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 43273aebf..590b0d9ba 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -302,13 +302,13 @@ class MyDiagnoser(Diagnoser): outgoing_ipversions = [] outgoing_ips = [] ipv4 = Diagnoser.get_cached_report("ip", {"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and settings_get("dns_exposure") != "ipv6": + if ipv4.get("status") == "SUCCESS" and settings_get(misc.network.dns_exposure") != "ipv6": outgoing_ipversions.append(4) global_ipv4 = ipv4.get("data", {}).get("global", {}) if global_ipv4: outgoing_ips.append(global_ipv4) - if settings_get("email.smtp.smtp_allow_ipv6") or settings_get("dns_exposure") != "ipv4": + if settings_get("email.smtp.smtp_allow_ipv6") or settings_get(misc.network.dns_exposure") != "ipv4": ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": outgoing_ipversions.append(6) diff --git a/src/dns.py b/src/dns.py index a007f69be..9d81391e5 100644 --- a/src/dns.py +++ b/src/dns.py @@ -186,7 +186,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): ########################### # Basic ipv4/ipv6 records # ########################### - if ipv4 and settings_get("dns_exposure") != "ipv6": + if ipv4 and settings_get("misc.network.dns_exposure") != "ipv6": basic.append([basename, ttl, "A", ipv4]) if ipv6: @@ -241,7 +241,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): # Only recommend wildcard and CAA for the top level if domain == base_domain: - if ipv4 and settings_get("dns_exposure") != "ipv6": + if ipv4 and settings_get("misc.network.dns_exposure") != "ipv6": extra.append([f"*{suffix}", ttl, "A", ipv4]) if ipv6: From 28e4b458065d39dc086dae105fa8596b8c3f1b25 Mon Sep 17 00:00:00 2001 From: Tagadda <36127788+Tagadda@users.noreply.github.com> Date: Mon, 24 Oct 2022 22:00:30 +0000 Subject: [PATCH 021/319] oops again... --- src/diagnosers/14-ports.py | 4 ++-- src/diagnosers/21-web.py | 8 ++++---- src/diagnosers/24-mail.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index 1e265f78e..0cd54efba 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -47,7 +47,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" or settings_get(misc.network.dns_exposure") != "ipv6": + if ipv4.get("status") == "SUCCESS" or settings_get("misc.network.dns_exposure") != "ipv6": ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -121,7 +121,7 @@ class MyDiagnoser(Diagnoser): for record in dnsrecords.get("items", []) ) - if failed == 4 and settings_get(misc.network.dns_exposure") != "ipv6" or ipv6_is_important(): + if failed == 4 and settings_get("misc.network.dns_exposure") != "ipv6" or ipv6_is_important(): yield dict( meta={"port": port}, data={ diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index 2024cf6ce..74e3ca483 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -77,7 +77,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and settings_get(misc.network.dns_exposure") != "ipv6": + if ipv4.get("status") == "SUCCESS" and settings_get("misc.network.dns_exposure") != "ipv6": ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -97,7 +97,7 @@ class MyDiagnoser(Diagnoser): # "curl --head the.global.ip" will simply timeout... if self.do_hairpinning_test: global_ipv4 = ipv4.get("data", {}).get("global", None) - if global_ipv4 and settings_get(misc.network.dns_exposure") != "ipv6": + if global_ipv4 and settings_get("misc.network.dns_exposure") != "ipv6": try: requests.head("http://" + global_ipv4, timeout=5) except requests.exceptions.Timeout: @@ -148,7 +148,7 @@ class MyDiagnoser(Diagnoser): if all( results[ipversion][domain]["status"] == "ok" for ipversion in ipversions ): - if 4 in ipversions and settings_get(misc.network.dns_exposure") != "ipv6": + if 4 in ipversions and settings_get("misc.network.dns_exposure") != "ipv6": self.do_hairpinning_test = True yield dict( meta={"domain": domain}, @@ -186,7 +186,7 @@ class MyDiagnoser(Diagnoser): ) AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") - return AAAA_status in ["OK", "WRONG"] or settings_get(misc.network.dns_exposure") != "ipv4" + return AAAA_status in ["OK", "WRONG"] or settings_get("misc.network.dns_exposure") != "ipv4" if failed == 4 or ipv6_is_important_for_this_domain(): yield dict( diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 590b0d9ba..283f80681 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -302,13 +302,13 @@ class MyDiagnoser(Diagnoser): outgoing_ipversions = [] outgoing_ips = [] ipv4 = Diagnoser.get_cached_report("ip", {"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and settings_get(misc.network.dns_exposure") != "ipv6": + if ipv4.get("status") == "SUCCESS" and settings_get("misc.network.dns_exposure") != "ipv6": outgoing_ipversions.append(4) global_ipv4 = ipv4.get("data", {}).get("global", {}) if global_ipv4: outgoing_ips.append(global_ipv4) - if settings_get("email.smtp.smtp_allow_ipv6") or settings_get(misc.network.dns_exposure") != "ipv4": + if settings_get("email.smtp.smtp_allow_ipv6") or settings_get("misc.network.dns_exposure") != "ipv4": ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": outgoing_ipversions.append(6) From 1a07839b5fa5d390fe76314d2f052d0416d28c13 Mon Sep 17 00:00:00 2001 From: Tagadda <36127788+Tagadda@users.noreply.github.com> Date: Mon, 24 Oct 2022 22:20:12 +0000 Subject: [PATCH 022/319] diagnosis --- locales/en.json | 1 + src/diagnosers/10-ip.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index e655acb83..789ec5a4b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -245,6 +245,7 @@ "diagnosis_ip_no_ipv4": "The server does not have working IPv4.", "diagnosis_ip_no_ipv6": "The server does not have working IPv6.", "diagnosis_ip_no_ipv6_tip": "Having a working IPv6 is not mandatory for your server to work, but it is better for the health of the Internet as a whole. IPv6 should usually be automatically configured by the system or your provider if it's available. Otherwise, you might need to configure a few things manually as explained in the documentation here: https://yunohost.org/#/ipv6. If you cannot enable IPv6 or if it seems too technical for you, you can also safely ignore this warning.", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 should usually be automatically configured by the system or your provider if it's available. Otherwise, you might need to configure a few things manually as explained in the documentation here: https://yunohost.org/#/ipv6.", "diagnosis_ip_not_connected_at_all": "The server does not seem to be connected to the Internet at all!?", "diagnosis_ip_weird_resolvconf": "DNS resolution seems to be working, but it looks like you're using a custom /etc/resolv.conf.", "diagnosis_ip_weird_resolvconf_details": "The file /etc/resolv.conf should be a symlink to /etc/resolvconf/run/resolv.conf itself pointing to 127.0.0.1 (dnsmasq). If you want to manually configure DNS resolvers, please edit /etc/resolv.dnsmasq.conf.", diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index 954b9b4e8..1d28be143 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -119,10 +119,13 @@ class MyDiagnoser(Diagnoser): else: return local_ip + def is_ipvx_important(x): + return settings_get("misc.network.dns_exposure") == "both" or "ipv"+str(x) + yield dict( meta={"test": "ipv4"}, data={"global": ipv4, "local": get_local_ip("ipv4")}, - status="SUCCESS" if ipv4 else "ERROR" if settings_get("misc.network.dns_exposure") == "ipv4" else "WARNING", + status="SUCCESS" if ipv4 else "ERROR" if is_ipvx_important(4) else "WARNING", summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None, ) @@ -130,11 +133,11 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv6"}, data={"global": ipv6, "local": get_local_ip("ipv6")}, - status="SUCCESS" if ipv6 else "ERROR" if settings_get("misc.network.dns_exposure") == "ipv6" else "WARNING", + status="SUCCESS" if ipv6 else "ERROR" if is_ipvx_important(6) else "WARNING", summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv6 - else ["diagnosis_ip_no_ipv6_tip"], + else ["diagnosis_ip_no_ipv6_tip_important" if is_ipvx_important(6) else "diagnosis_ip_no_ipv6_tip"], ) # TODO / FIXME : add some attempt to detect ISP (using whois ?) ? From e82849492baa6dff14cba2e34ee88d3a4fa9a4e9 Mon Sep 17 00:00:00 2001 From: Tagadda <36127788+Tagadda@users.noreply.github.com> Date: Mon, 24 Oct 2022 22:25:09 +0000 Subject: [PATCH 023/319] zblerg --- src/diagnosers/10-ip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index 1d28be143..6b35731a0 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -120,7 +120,7 @@ class MyDiagnoser(Diagnoser): return local_ip def is_ipvx_important(x): - return settings_get("misc.network.dns_exposure") == "both" or "ipv"+str(x) + return settings_get("misc.network.dns_exposure") in ["both", "ipv"+str(x)] yield dict( meta={"test": "ipv4"}, From 28256f39de0a07ce1547fc39660084a470c7d247 Mon Sep 17 00:00:00 2001 From: YunoHost Bot Date: Tue, 10 Jan 2023 13:12:41 +0100 Subject: [PATCH 024/319] [CI] Reformat / remove stale translated strings (#1557) Co-authored-by: Alexandre Aubin --- locales/ar.json | 2 +- locales/de.json | 3 +-- locales/en.json | 15 ++++++++------- locales/es.json | 3 +-- locales/eu.json | 3 +-- locales/fr.json | 9 ++++----- locales/gl.json | 3 +-- locales/he.json | 2 +- locales/it.json | 1 - locales/nl.json | 2 +- locales/pt.json | 2 +- locales/pt_BR.json | 2 +- locales/sk.json | 2 +- locales/uk.json | 3 +-- locales/zh_Hans.json | 2 +- 15 files changed, 24 insertions(+), 30 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 673176cdf..aa40f2420 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -194,4 +194,4 @@ "global_settings_setting_smtp_allow_ipv6": "سماح IPv6", "disk_space_not_sufficient_update": "ليس هناك مساحة كافية لتحديث هذا التطبيق", "domain_cert_gen_failed": "لا يمكن إعادة توليد الشهادة" -} +} \ No newline at end of file diff --git a/locales/de.json b/locales/de.json index 5baa41687..e09214f04 100644 --- a/locales/de.json +++ b/locales/de.json @@ -605,7 +605,6 @@ "domain_dns_push_success": "DNS-Einträge aktualisiert!", "domain_dns_push_failed": "Die Aktualisierung der DNS-Einträge ist leider gescheitert.", "domain_dns_push_partial_failure": "DNS-Einträge teilweise aktualisiert: einige Warnungen/Fehler wurden gemeldet.", - "domain_config_features_disclaimer": "Bisher hat das Aktivieren/Deaktivieren von Mail- oder XMPP-Funktionen nur Auswirkungen auf die empfohlene und automatische DNS-Konfiguration, nicht auf die Systemkonfigurationen!", "domain_config_mail_in": "Eingehende E-Mails", "domain_config_mail_out": "Ausgehende E-Mails", "domain_config_xmpp": "Instant Messaging (XMPP)", @@ -696,4 +695,4 @@ "domain_config_cert_summary_expired": "ACHTUNG: Das aktuelle Zertifikat ist nicht gültig! HTTPS wird gar nicht funktionieren!", "domain_config_cert_summary_letsencrypt": "Toll! Sie benutzen ein gültiges Let's Encrypt-Zertifikat!", "domain_config_cert_summary_ok": "Gut, das aktuelle Zertifikat sieht gut aus!" -} +} \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index e655acb83..3467132a8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -316,8 +316,8 @@ "diagnosis_using_yunohost_testing_details": "This is probably OK if you know what you are doing, but pay attention to the release notes before installing YunoHost upgrades! If you want to disable 'testing' upgrades, you should remove the testing keyword from /etc/apt/sources.list.d/yunohost.list.", "disk_space_not_sufficient_install": "There is not enough disk space left to install this application", "disk_space_not_sufficient_update": "There is not enough disk space left to update this application", - "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated into YunoHost.", "domain_cannot_add_muc_upload": "You cannot add domains starting with 'muc.'. This kind of name is reserved for the XMPP multi-users chat feature integrated into YunoHost.", + "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated into YunoHost.", "domain_cannot_remove_main": "You cannot remove '{domain}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n '; here is the list of candidate domains: {other_domains}", "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add ', then set is as the main domain using 'yunohost domain main-domain -n ' and then you can remove the domain '{domain}' using 'yunohost domain remove {domain}'.'", "domain_cert_gen_failed": "Could not generate certificate", @@ -399,7 +399,6 @@ "firewall_reloaded": "Firewall reloaded", "firewall_rules_cmd_failed": "Some firewall rule commands have failed. More info in log.", "global_settings_reset_success": "Reset global settings", - "global_settings_setting_passwordless_sudo": "Allow admins to use 'sudo' without re-typing their passwords", "global_settings_setting_admin_strength": "Admin password strength requirements", "global_settings_setting_admin_strength_help": "These requirements are only enforced when initializing or changing the password", "global_settings_setting_backup_compress_tar_archives": "Compress backups", @@ -408,8 +407,12 @@ "global_settings_setting_nginx_compatibility_help": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", "global_settings_setting_nginx_redirect_to_https": "Force HTTPS", "global_settings_setting_nginx_redirect_to_https_help": "Redirect HTTP requests to HTTPs by default (DO NOT TURN OFF unless you really know what you're doing!)", + "global_settings_setting_passwordless_sudo": "Allow admins to use 'sudo' without re-typing their passwords", + "global_settings_setting_passwordless_sudo_help": "FIXME", "global_settings_setting_pop3_enabled": "Enable POP3", "global_settings_setting_pop3_enabled_help": "Enable the POP3 protocol for the mail server", + "global_settings_setting_portal_theme": "Portal theme", + "global_settings_setting_portal_theme_help": "More info regarding creating custom portal themes at https://yunohost.org/theming", "global_settings_setting_postfix_compatibility": "Postfix Compatibility", "global_settings_setting_postfix_compatibility_help": "Compatibility vs. security tradeoff for the Postfix server. Affects the ciphers (and other security-related aspects)", "global_settings_setting_root_access_explain": "On Linux systems, 'root' is the absolute admin. In YunoHost context, direct 'root' SSH login is by default disable - except from the local network of the server. Members of the 'admins' group can use the sudo command to act as root from the command line. However, it can be helpful to have a (robust) root password to debug the system if for some reason regular admins can not login anymore.", @@ -431,8 +434,6 @@ "global_settings_setting_ssh_password_authentication_help": "Allow password authentication for SSH", "global_settings_setting_ssh_port": "SSH port", "global_settings_setting_ssowat_panel_overlay_enabled": "Enable the small 'YunoHost' portal shortcut square on apps", - "global_settings_setting_portal_theme": "Portal theme", - "global_settings_setting_portal_theme_help": "More info regarding creating custom portal themes at https://yunohost.org/theming", "global_settings_setting_user_strength": "User password strength requirements", "global_settings_setting_user_strength_help": "These requirements are only enforced when initializing or changing the password", "global_settings_setting_webadmin_allowlist": "Webadmin IP allowlist", @@ -452,11 +453,11 @@ "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_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_update_aliases": "Updating aliases for group '{group}'", - "group_no_change": "Nothing to change for group '{group}'", "group_user_already_in_group": "User {user} is already in group {group}", "group_user_not_in_group": "User {user} is not in group {group}", "hook_exec_failed": "Could not run script: {path}", @@ -748,4 +749,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/locales/es.json b/locales/es.json index 8637c3da8..ae2eb39fe 100644 --- a/locales/es.json +++ b/locales/es.json @@ -574,7 +574,6 @@ "domain_dns_push_failed_to_authenticate": "No se pudo autenticar en la API del registrador para el dominio '{domain}'. ¿Lo más probable es que las credenciales sean incorrectas? (Error: {error})", "domain_dns_registrar_experimental": "Hasta ahora, la comunidad de YunoHost no ha probado ni revisado correctamente la interfaz con la API de **{registrar}**. El soporte es **muy experimental**. ¡Ten cuidado!", "domain_dns_push_record_failed": "No se pudo {action} registrar {type}/{name}: {error}", - "domain_config_features_disclaimer": "Hasta ahora, habilitar/deshabilitar las funciones de correo o XMPP solo afecta la configuración de DNS recomendada y automática, ¡no las configuraciones del sistema!", "domain_config_mail_in": "Correos entrantes", "domain_config_mail_out": "Correos salientes", "domain_config_xmpp": "Mensajería instantánea (XMPP)", @@ -680,4 +679,4 @@ "config_forbidden_readonly_type": "El tipo '{type}' no puede establecerse como solo lectura, utilice otro tipo para representar este valor (arg id relevante: '{id}').", "diagnosis_using_stable_codename": "apt (el gestor de paquetes del sistema) está configurado actualmente para instalar paquetes de nombre en clave 'estable', en lugar del nombre en clave de la versión actual de Debian (bullseye).", "diagnosis_using_stable_codename_details": "Esto suele deberse a una configuración incorrecta de su proveedor de alojamiento. Esto es peligroso, porque tan pronto como la siguiente versión de Debian se convierta en la nueva 'estable', apt querrá actualizar todos los paquetes del sistema sin pasar por un procedimiento de migración adecuado. Se recomienda arreglar esto editando la fuente de apt para el repositorio base de Debian, y reemplazar la palabra clave stable por bullseye. El fichero de configuración correspondiente debería ser /etc/apt/sources.list, o un fichero en /etc/apt/sources.list.d/." -} +} \ No newline at end of file diff --git a/locales/eu.json b/locales/eu.json index d58289bf4..f53da2b34 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -337,7 +337,6 @@ "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_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_features_disclaimer": "Oraingoz, posta elektronikoa edo XMPP funtzioak gaitu/desgaitzeak DNS ezarpenei soilik eragiten die, ez sistemaren konfigurazioari!", "domain_config_mail_out": "Bidalitako mezuak", "domain_config_xmpp": "Bat-bateko mezularitza (XMPP)", "good_practices_about_user_password": "Erabiltzaile-pasahitz berria ezartzear zaude. Pasahitzak 8 karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", @@ -738,4 +737,4 @@ "diagnosis_using_stable_codename_details": "Ostatatzaileak zerbait oker ezarri duenean gertatu ohi da hau. Arriskutsua da, Debianen datorren bertsioa 'estable' (egonkorra) bilakatzen denean, apt-ek sistemaren pakete guztiak bertsio-berritzen saiatuko da, beharrezko migrazio-prozedurarik burutu gabe. Debianen repositorioan apt iturria editatzen konpontzea da gomendioa, stable gakoa bullseye gakoarekin ordezkatuz. Ezarpen-fitxategia /etc/apt/sources.list izan beharko litzateke, edo /etc/apt/sources.list.d/ direktorioko fitxategiren bat.", "group_update_aliases": "'{group}' taldearen aliasak eguneratzen", "group_no_change": "Ez da ezer aldatu behar '{group}' talderako" -} +} \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index 9eaca4bb2..33949f1fd 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -259,7 +259,7 @@ "log_tools_upgrade": "Mettre à jour les paquets du système", "log_tools_shutdown": "Éteindre votre serveur", "log_tools_reboot": "Redémarrer votre serveur", - "mail_unavailable": "Cette adresse e-mail est réservée au groupe des administrateurs", + "mail_unavailable": "Cette adresse email est réservée au groupe des administrateurs", "good_practices_about_admin_password": "Vous êtes sur le point de définir un nouveau mot de passe administrateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou d'utiliser une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "good_practices_about_user_password": "Vous êtes sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "password_listed": "Ce mot de passe fait partie des mots de passe les plus utilisés dans le monde. Veuillez en choisir un autre moins commun et plus robuste.", @@ -389,7 +389,7 @@ "diagnosis_basesystem_ynh_single_version": "{package} version : {version} ({repo})", "diagnosis_basesystem_ynh_main_version": "Le serveur utilise YunoHost {main_version} ({repo})", "diagnosis_basesystem_ynh_inconsistent_versions": "Vous exécutez des versions incohérentes des packages YunoHost ... très probablement en raison d'une mise à niveau échouée ou partielle.", - "diagnosis_failed_for_category": "Échec du diagnostic pour la catégorie '{category}' : {error}", + "diagnosis_failed_for_category": "Échec du diagnostic pour la catégorie '{category}' : {error}", "diagnosis_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment !)", "diagnosis_ignored_issues": "(+ {nb_ignored} problème(s) ignoré(s))", "diagnosis_found_warnings": "Trouvé {warnings} objet(s) pouvant être amélioré(s) pour {category}.", @@ -483,7 +483,7 @@ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Certains opérateurs ne vous laisseront pas débloquer le port 25 parce qu'ils ne se soucient pas de la neutralité du Net.
- Certains d'entre eux offrent la possibilité d'utiliser un serveur de messagerie relai bien que cela implique que celui-ci sera en mesure d'espionner le trafic de votre messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Vous pouvez également envisager de passer à un fournisseur plus respectueux de la neutralité du net", "diagnosis_mail_ehlo_ok": "Le serveur de messagerie SMTP est accessible de l'extérieur et peut donc recevoir des emails !", "diagnosis_mail_ehlo_unreachable": "Le serveur de messagerie SMTP est inaccessible de l'extérieur en IPv{ipversion}. Il ne pourra pas recevoir des emails.", - "diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible.
1. La cause la plus courante de ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur.
2. Vous devez également vous assurer que le service postfix est en cours d'exécution.
3. Sur les configurations plus complexes : assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.", + "diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible.
1. La cause la plus courante de ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur.
2. Vous devez également vous assurer que le service postfix est en cours d'exécution.
3. Sur les configurations plus complexes : assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.", "diagnosis_mail_ehlo_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur.
EHLO reçu : {wrong_ehlo}
Attendu : {right_ehlo}
La cause la plus courante à ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur. Vous pouvez également vous assurer qu'aucun pare-feu ou reverse-proxy n'interfère.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains opérateurs ne vous laisseront pas configurer votre reverse-DNS (ou leur fonctionnalité pourrait être cassée ...). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes :
- Certains FAI offre cette possibilité à l'aide d'un relais de serveur de messagerie bien que cela implique que le relais pourra espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Enfin, il est également possible de changer d'opérateur", "diagnosis_mail_fcrdns_nok_alternatives_6": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si votre DNS inversé est correctement configuré en IPv4, vous pouvez essayer de désactiver l'utilisation d'IPv6 lors de l'envoi d'emails en exécutant yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Remarque : cette dernière solution signifie que vous ne pourrez pas envoyer ou recevoir d'emails avec les quelques serveurs qui ont uniquement de l'IPv6.", @@ -600,7 +600,6 @@ "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_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}.", "domain_dns_registrar_managed_in_parent_domain": "Ce domaine est un sous-domaine de {parent_domain_link}. La configuration du registrar DNS doit être gérée dans le panneau de configuration de {parent_domain}.", "domain_dns_registrar_not_supported": "YunoHost n'a pas pu détecter automatiquement le bureau d'enregistrement gérant ce domaine. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns.", @@ -750,4 +749,4 @@ "app_not_enough_ram": "Cette application nécessite {required} de mémoire vive (RAM) pour être installée/mise à niveau mais seule {current} de mémoire est disponible actuellement.", "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}", "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des éléments d'information importants à connaître. [{answers}]" -} +} \ No newline at end of file diff --git a/locales/gl.json b/locales/gl.json index 61af0b672..35419bcf4 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -604,7 +604,6 @@ "domain_dns_push_record_failed": "Fallou {action} do rexistro {type}/{name}: {error}", "domain_dns_push_success": "Rexistros DNS actualizados!", "domain_dns_push_failed": "Fallou completamente a actualización dos rexistros DNS.", - "domain_config_features_disclaimer": "Ata o momento, activar/desactivar as funcións de email ou XMPP só ten impacto na configuración automática da configuración DNS, non na configuración do sistema!", "domain_config_mail_in": "Emails entrantes", "domain_config_mail_out": "Emails saíntes", "domain_config_xmpp": "Mensaxería instantánea (XMPP)", @@ -742,4 +741,4 @@ "global_settings_setting_passwordless_sudo": "Permitir a Admins usar 'sudo' sen ter que volver a escribir o contrasinal", "global_settings_setting_portal_theme": "Decorado do Portal", "global_settings_setting_portal_theme_help": "Tes máis info acerca da creación de decorados para o portal de acceso en https://yunohost.org/theming" -} +} \ No newline at end of file diff --git a/locales/he.json b/locales/he.json index 0967ef424..9e26dfeeb 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1 +1 @@ -{} +{} \ No newline at end of file diff --git a/locales/it.json b/locales/it.json index 9bb923c2a..bc65612f0 100644 --- a/locales/it.json +++ b/locales/it.json @@ -612,7 +612,6 @@ "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.", - "domain_config_features_disclaimer": "Per ora, abilitare/disabilitare le impostazioni di posta o XMPP impatta unicamente sulle configurazioni DNS raccomandate o ottimizzate, non cambia quelle di sistema!", "domain_config_mail_in": "Email in arrivo", "domain_config_auth_application_key": "Chiave applicazione", "domain_config_auth_application_secret": "Chiave segreta applicazione", diff --git a/locales/nl.json b/locales/nl.json index 24ade2f5c..bcfb76acd 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -139,4 +139,4 @@ "group_already_exist_on_system": "Groep {group} bestaat al in de systeemgroepen", "good_practices_about_admin_password": "Je gaat nu een nieuw beheerderswachtwoordopgeven. Het wachtwoord moet minimaal 8 tekens lang zijn, hoewel het een goede gewoonte is om een langer wachtwoord te gebruiken (d.w.z. een wachtwoordzin) en/of een variatie van tekens te gebruiken (hoofdletters, kleine letters, cijfers en speciale tekens).", "good_practices_about_user_password": "Je gaat nu een nieuw gebruikerswachtwoord pgeven. Het wachtwoord moet minimaal 8 tekens lang zijn, hoewel het een goede gewoonte is om een langer wachtwoord te gebruiken (d.w.z. een wachtwoordzin) en/of een variatie van tekens te gebruiken (hoofdletters, kleine letters, cijfers en speciale tekens)." -} +} \ No newline at end of file diff --git a/locales/pt.json b/locales/pt.json index a7b574949..1df30f8e5 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -247,4 +247,4 @@ "certmanager_cert_renew_success": "Certificado Let's Encrypt renovado para o domínio '{domain}'", "certmanager_warning_subdomain_dns_record": "O subdomínio '{subdomain}' não resolve para o mesmo IP que '{domain}'. Algumas funcionalidades não estarão disponíveis até que você conserte isto e regenere o certificado.", "admins": "Admins" -} +} \ No newline at end of file diff --git a/locales/pt_BR.json b/locales/pt_BR.json index 0967ef424..9e26dfeeb 100644 --- a/locales/pt_BR.json +++ b/locales/pt_BR.json @@ -1 +1 @@ -{} +{} \ No newline at end of file diff --git a/locales/sk.json b/locales/sk.json index 25bd82988..544fb6c0e 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -250,4 +250,4 @@ "all_users": "Všetci používatelia YunoHost", "app_manifest_install_ask_init_main_permission": "Kto má mať prístup k tejto aplikácii? (Nastavenie môžete neskôr zmeniť)", "certmanager_cert_install_failed": "Inštalácia Let's Encrypt certifikátu pre {domains} skončila s chybou" -} +} \ No newline at end of file diff --git a/locales/uk.json b/locales/uk.json index 281f2dba7..02304a39c 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -614,7 +614,6 @@ "domain_dns_push_failed_to_authenticate": "Неможливо пройти автентифікацію на API реєстратора для домену '{domain}'. Ймовірно, облікові дані недійсні? (Помилка: {error})", "domain_dns_push_failed_to_list": "Не вдалося скласти список поточних записів за допомогою API реєстратора: {error}", "domain_dns_push_record_failed": "Не вдалося виконати дію {action} запису {type}/{name} : {error}", - "domain_config_features_disclaimer": "Поки що вмикання/вимикання функцій пошти або XMPP впливає тільки на рекомендовану та автоконфігурацію DNS, але не на конфігурацію системи!", "domain_config_xmpp": "Миттєвий обмін повідомленнями (XMPP)", "domain_config_auth_key": "Ключ автентифікації", "domain_config_auth_secret": "Секрет автентифікації", @@ -739,4 +738,4 @@ "password_confirmation_not_the_same": "Пароль і його підтвердження не збігаються", "password_too_long": "Будь ласка, виберіть пароль коротший за 127 символів", "pattern_fullname": "Має бути дійсне повне ім’я (принаймні 3 символи)" -} +} \ No newline at end of file diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index 8aecbbce3..59ceaf36f 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -593,4 +593,4 @@ "ask_admin_fullname": "管理员全名", "ask_admin_username": "管理员用户名", "ask_fullname": "全名" -} +} \ No newline at end of file From 4615d7b7b808b6368a929a20bf7b9ed32da2d99d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 10 Jan 2023 13:23:03 +0100 Subject: [PATCH 025/319] debian: regen ssowatconf during package upgrade --- debian/postinst | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/postinst b/debian/postinst index 9fb9b9977..9c168f8a3 100644 --- a/debian/postinst +++ b/debian/postinst @@ -20,6 +20,7 @@ do_configure() { fi else echo "Regenerating configuration, this might take a while..." + yunohost app ssowatconf yunohost tools regen-conf --output-as none echo "Launching migrations..." From 394907ff0df218ec926f43c2e2ba006c68cc3f8a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 10 Jan 2023 13:24:31 +0100 Subject: [PATCH 026/319] Update changlog for 11.1.2.2 --- debian/changelog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/debian/changelog b/debian/changelog index fbcddc9fb..536d1bdbf 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +yunohost (11.1.2.2) testing; urgency=low + + - Minor technical fixes (b37d4baf, 68342171) + - configpanel: stop the madness of returning a 500 error when trying to load config panel 0.1 ... otherwise this will crash the new app info view ... (f21fbed2) + - apps: fix trick to find the default branch from git repo @_@ (25c10166) + - debian: regen ssowatconf during package upgrade (4615d7b7) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Éric Gaspar, ppr) + + -- Alexandre Aubin Tue, 10 Jan 2023 13:23:28 +0100 + yunohost (11.1.2.1) testing; urgency=low - i18n: fix (un)defined string issues (dd33476f) From 5db49173f93c0424de6e0ab692c5b3caed5d30d1 Mon Sep 17 00:00:00 2001 From: Metin Bektas <30674934+methbkts@users.noreply.github.com> Date: Thu, 12 Jan 2023 14:49:58 +0000 Subject: [PATCH 027/319] chore: update actions version to use node 16 version --- .github/workflows/n_updater.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/n_updater.yml b/.github/workflows/n_updater.yml index 35afd8ae7..4c422c14c 100644 --- a/.github/workflows/n_updater.yml +++ b/.github/workflows/n_updater.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Fetch the source code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - name: Run the updater script From 17d870000067ab54f75528b6f8a8d2e71c3a7a79 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 13 Jan 2023 10:50:14 +0100 Subject: [PATCH 028/319] [fix] Remove some debug test --- debian/postinst | 1 - 1 file changed, 1 deletion(-) diff --git a/debian/postinst b/debian/postinst index 9c168f8a3..c9ad3d562 100644 --- a/debian/postinst +++ b/debian/postinst @@ -53,7 +53,6 @@ API_START_TIMESTAMP="\$(date --date="\$(systemctl show yunohost-api | grep ExecM if [ "\$(( \$(date +%s) - \$API_START_TIMESTAMP ))" -ge 60 ]; then - echo "restart" >> /var/log/testalex systemctl restart yunohost-api fi EOF From e73209b7c40214f1421842b6c7f865b3efc351ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= <46165813+ericgaspar@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:58:18 +0100 Subject: [PATCH 029/319] Add --routines flag --- helpers/mysql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/mysql b/helpers/mysql index 822159f27..a5290f794 100644 --- a/helpers/mysql +++ b/helpers/mysql @@ -133,7 +133,7 @@ ynh_mysql_dump_db() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - mysqldump --single-transaction --skip-dump-date "$database" + mysqldump --single-transaction --skip-dump-date --routines "$database" } # Create a user From 36b0f5899329a39e6e75204e91e08d26aa22be99 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 19 Jan 2023 11:15:02 +0100 Subject: [PATCH 030/319] rewrite list_shells --- src/user.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/user.py b/src/user.py index 61060a9ef..b3a2a22e6 100644 --- a/src/user.py +++ b/src/user.py @@ -122,21 +122,17 @@ def user_list(fields=None): return {"users": users} -def list_shells(): - import ctypes - import ctypes.util - """List the shells from /etc/shells.""" - libc = ctypes.CDLL(ctypes.util.find_library("c")) - getusershell = libc.getusershell - getusershell.restype = ctypes.c_char_p - libc.setusershell() - while True: - shell = getusershell() - if not shell: - break - yield shell.decode() - libc.endusershell() +def list_shells(): + with open("/etc/shells", "r") as f: + content = f.readlines() + + shells = [] + for line in content: + if line.startswith("/"): + shells.append(line.replace("\n","")) + return shells + def shellexists(shell): From 13be9af65f6e76ab00be1c6096093e8ce61d2aa7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 16:24:50 +0100 Subject: [PATCH 031/319] Simplify code --- src/user.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/user.py b/src/user.py index b3a2a22e6..05dce8f24 100644 --- a/src/user.py +++ b/src/user.py @@ -127,12 +127,7 @@ def list_shells(): with open("/etc/shells", "r") as f: content = f.readlines() - shells = [] - for line in content: - if line.startswith("/"): - shells.append(line.replace("\n","")) - return shells - + return [line.strip() for line in content if line.startswith("/")] def shellexists(shell): From 95f98a9c68aad0d125ede8c3cfb80c2b85c0d266 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 16:45:21 +0100 Subject: [PATCH 032/319] ipexposuresetting: replace confusing negations with explicit 'in' --- src/diagnosers/21-web.py | 8 ++++---- src/diagnosers/24-mail.py | 4 ++-- src/dns.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index 74e3ca483..25554fe9d 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -77,7 +77,7 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and settings_get("misc.network.dns_exposure") != "ipv6": + if ipv4.get("status") == "SUCCESS" and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -97,7 +97,7 @@ class MyDiagnoser(Diagnoser): # "curl --head the.global.ip" will simply timeout... if self.do_hairpinning_test: global_ipv4 = ipv4.get("data", {}).get("global", None) - if global_ipv4 and settings_get("misc.network.dns_exposure") != "ipv6": + if global_ipv4 and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: try: requests.head("http://" + global_ipv4, timeout=5) except requests.exceptions.Timeout: @@ -148,7 +148,7 @@ class MyDiagnoser(Diagnoser): if all( results[ipversion][domain]["status"] == "ok" for ipversion in ipversions ): - if 4 in ipversions and settings_get("misc.network.dns_exposure") != "ipv6": + if 4 in ipversions and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: self.do_hairpinning_test = True yield dict( meta={"domain": domain}, @@ -186,7 +186,7 @@ class MyDiagnoser(Diagnoser): ) AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") - return AAAA_status in ["OK", "WRONG"] or settings_get("misc.network.dns_exposure") != "ipv4" + return AAAA_status in ["OK", "WRONG"] or settings_get("misc.network.dns_exposure") in ["both", "ipv6"] if failed == 4 or ipv6_is_important_for_this_domain(): yield dict( diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 283f80681..1ae1da885 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -302,13 +302,13 @@ class MyDiagnoser(Diagnoser): outgoing_ipversions = [] outgoing_ips = [] ipv4 = Diagnoser.get_cached_report("ip", {"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and settings_get("misc.network.dns_exposure") != "ipv6": + if ipv4.get("status") == "SUCCESS" and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: outgoing_ipversions.append(4) global_ipv4 = ipv4.get("data", {}).get("global", {}) if global_ipv4: outgoing_ips.append(global_ipv4) - if settings_get("email.smtp.smtp_allow_ipv6") or settings_get("misc.network.dns_exposure") != "ipv4": + if settings_get("email.smtp.smtp_allow_ipv6") or settings_get("misc.network.dns_exposure") in ["both", "ipv6"]: ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": outgoing_ipversions.append(6) diff --git a/src/dns.py b/src/dns.py index 9d81391e5..d56e8e625 100644 --- a/src/dns.py +++ b/src/dns.py @@ -186,7 +186,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): ########################### # Basic ipv4/ipv6 records # ########################### - if ipv4 and settings_get("misc.network.dns_exposure") != "ipv6": + if ipv4 and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: basic.append([basename, ttl, "A", ipv4]) if ipv6: @@ -241,7 +241,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): # Only recommend wildcard and CAA for the top level if domain == base_domain: - if ipv4 and settings_get("misc.network.dns_exposure") != "ipv6": + if ipv4 and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: extra.append([f"*{suffix}", ttl, "A", ipv4]) if ipv6: From b41d623ed4f595be6eb16ceb14de887dfa1dab62 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Thu, 19 Jan 2023 15:53:22 +0000 Subject: [PATCH 033/319] [CI] Reformat / remove stale translated strings --- locales/de.json | 1 - locales/en.json | 3 +-- locales/es.json | 1 - locales/eu.json | 1 - locales/fr.json | 1 - locales/gl.json | 1 - locales/it.json | 1 - locales/pt.json | 1 - locales/ru.json | 1 - locales/sk.json | 1 - locales/uk.json | 1 - locales/zh_Hans.json | 1 - 12 files changed, 1 insertion(+), 13 deletions(-) diff --git a/locales/de.json b/locales/de.json index e09214f04..c666a7904 100644 --- a/locales/de.json +++ b/locales/de.json @@ -565,7 +565,6 @@ "diagnosis_apps_issue": "Ein Problem für die App {app} ist aufgetreten", "config_validate_time": "Sollte eine zulässige Zeit wie HH:MM sein", "config_validate_url": "Sollte eine zulässige web URL sein", - "config_version_not_supported": "Konfigurationspanel Versionen '{version}' sind nicht unterstützt.", "diagnosis_apps_allgood": "Alle installierten Apps berücksichtigen die grundlegenden Paketierungspraktiken", "diagnosis_apps_broken": "Diese App ist im YunoHost-Applikationskatalog momentan als defekt gekennzeichnet. Es könnte sich dabei um einen vorübergehendes Problem handeln. Während der/die Betreuer:in versucht das Problem zu beheben, ist die Upgrade-Funktion für diese App gesperrt.", "diagnosis_apps_not_in_app_catalog": "Diese 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.", diff --git a/locales/en.json b/locales/en.json index e36f13736..0eca7b9bc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -27,7 +27,6 @@ "app_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.", "app_id_invalid": "Invalid app ID", "app_install_failed": "Unable to install {app}: {error}", - "app_resource_failed": "Provisioning, deprovisioning, or updating resources for {app} failed: {error}", "app_install_files_invalid": "These files cannot be installed", "app_install_script_failed": "An error occurred inside the app installation script", "app_label_deprecated": "This command is deprecated! Please use the new command 'yunohost user permission update' to manage the app label.", @@ -50,6 +49,7 @@ "app_remove_after_failed_install": "Removing the app after installation failure...", "app_removed": "{app} uninstalled", "app_requirements_checking": "Checking requirements for {app}...", + "app_resource_failed": "Provisioning, deprovisioning, or updating resources for {app} failed: {error}", "app_restore_failed": "Could not restore {app}: {error}", "app_restore_script_failed": "An error occured inside the app restore script", "app_sources_fetch_failed": "Could not fetch source files, is the URL correct?", @@ -408,7 +408,6 @@ "global_settings_setting_nginx_redirect_to_https": "Force HTTPS", "global_settings_setting_nginx_redirect_to_https_help": "Redirect HTTP requests to HTTPs by default (DO NOT TURN OFF unless you really know what you're doing!)", "global_settings_setting_passwordless_sudo": "Allow admins to use 'sudo' without re-typing their passwords", - "global_settings_setting_passwordless_sudo_help": "FIXME", "global_settings_setting_pop3_enabled": "Enable POP3", "global_settings_setting_pop3_enabled_help": "Enable the POP3 protocol for the mail server", "global_settings_setting_portal_theme": "Portal theme", diff --git a/locales/es.json b/locales/es.json index ae2eb39fe..9a92908a4 100644 --- a/locales/es.json +++ b/locales/es.json @@ -552,7 +552,6 @@ "config_validate_email": "Debe ser una dirección de correo correcta", "config_validate_time": "Debe ser una hora valida en formato HH:MM", "config_validate_url": "Debe ser una URL válida", - "config_version_not_supported": "Las versiones del panel de configuración '{version}' no están soportadas.", "domain_remove_confirm_apps_removal": "La supresión de este dominio también eliminará las siguientes aplicaciones:\n{apps}\n\n¿Seguro? [{answers}]", "domain_registrar_is_not_configured": "El registrador aún no ha configurado el dominio {domain}.", "diagnosis_apps_not_in_app_catalog": "Esta aplicación se encuentra ausente o ya no figura en el catálogo de aplicaciones de YunoHost. Deberías considerar desinstalarla ya que no recibirá actualizaciones y podría comprometer la integridad y seguridad de tu sistema.", diff --git a/locales/eu.json b/locales/eu.json index f53da2b34..4d212bf58 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -49,7 +49,6 @@ "config_validate_email": "Benetazko posta elektronikoa izan behar da", "config_validate_time": "OO:MM formatua duen ordu bat izan behar da", "config_validate_url": "Benetazko URL bat izan behar da", - "config_version_not_supported": "Ezinezkoa da konfigurazio-panelaren '{version}' bertsioa erabiltzea.", "app_restore_script_failed": "Errorea gertatu da aplikazioa lehengoratzeko aginduan", "app_upgrade_some_app_failed": "Ezinezkoa izan da aplikazio batzuk eguneratzea", "app_install_failed": "Ezinezkoa izan da {app} instalatzea: {error}", diff --git a/locales/fr.json b/locales/fr.json index 33949f1fd..9c5b9a9e3 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -590,7 +590,6 @@ "config_validate_email": "Doit être un email valide", "config_validate_time": "Doit être une heure valide comme HH:MM", "config_validate_url": "Doit être une URL Web valide", - "config_version_not_supported": "Les versions du panneau de configuration '{version}' ne sont pas prises en charge.", "danger": "Danger :", "invalid_number_min": "Doit être supérieur à {min}", "invalid_number_max": "Doit être inférieur à {max}", diff --git a/locales/gl.json b/locales/gl.json index 35419bcf4..a4f9d9772 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -590,7 +590,6 @@ "log_app_config_set": "Aplicar a configuración á app '{}'", "app_config_unable_to_apply": "Fallou a aplicación dos valores de configuración.", "config_cant_set_value_on_section": "Non podes establecer un valor único na sección completa de configuración.", - "config_version_not_supported": "A versión do panel de configuración '{version}' non está soportada.", "invalid_number_max": "Ten que ser menor de {max}", "service_not_reloading_because_conf_broken": "Non se recargou/reiniciou o servizo '{name}' porque a súa configuración está estragada: {errors}", "diagnosis_http_special_use_tld": "O dominio {domain} baséase nun dominio de alto-nivel (TLD) especial como .local ou .test e por isto non é de agardar que esté exposto fóra da rede local.", diff --git a/locales/it.json b/locales/it.json index bc65612f0..e94a43a6d 100644 --- a/locales/it.json +++ b/locales/it.json @@ -618,7 +618,6 @@ "domain_config_auth_consumer_key": "Chiave consumatore", "ldap_attribute_already_exists": "L’attributo LDAP '{attribute}' esiste già con il valore '{value}'", "config_validate_time": "È necessario inserire un orario valido, come HH:MM", - "config_version_not_supported": "Le versioni '{version}' del pannello di configurazione non sono supportate.", "danger": "Attenzione:", "log_domain_config_set": "Aggiorna la configurazione per il dominio '{}'", "domain_dns_push_managed_in_parent_domain": "La configurazione automatica del DNS è gestita nel dominio genitore {parent_domain}.", diff --git a/locales/pt.json b/locales/pt.json index 1df30f8e5..0aa6b8223 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -204,7 +204,6 @@ "config_cant_set_value_on_section": "Você não pode setar um único valor na seção de configuração inteira.", "config_validate_time": "Deve ser um horário válido como HH:MM", "config_validate_url": "Deve ser uma URL válida", - "config_version_not_supported": "Versões do painel de configuração '{version}' não são suportadas.", "danger": "Perigo:", "diagnosis_basesystem_ynh_inconsistent_versions": "Você está executando versões inconsistentes dos pacotes YunoHost... provavelmente por causa de uma atualização parcial ou que falhou.", "diagnosis_description_basesystem": "Sistema base", diff --git a/locales/ru.json b/locales/ru.json index 40e7629e3..2c4e703da 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -106,7 +106,6 @@ "certmanager_domain_dns_ip_differs_from_public_ip": "DNS-записи для домена '{domain}' отличаются от IP этого сервера. Пожалуйста, проверьте категорию 'DNS-записи' (основные) в диагностике для получения дополнительной информации. Если вы недавно изменили свою A-запись, пожалуйста, подождите, пока она распространится (некоторые программы проверки распространения DNS доступны в интернете). (Если вы знаете, что делаете, используйте '--no-checks', чтобы отключить эти проверки.)", "certmanager_domain_not_diagnosed_yet": "Для домена {domain} еще нет результатов диагностики. Пожалуйста, перезапустите диагностику для категорий 'DNS-записи' и 'Домены', чтобы проверить, готов ли домен к Let's Encrypt. (Или, если вы знаете, что делаете, используйте '--no-checks', чтобы отключить эти проверки.)", "config_validate_url": "Должна быть правильная ссылка", - "config_version_not_supported": "Версии конфигурационной панели '{version}' не поддерживаются.", "confirm_app_install_danger": "ОПАСНО! Это приложение все еще является экспериментальным (если не сказать, что оно явно не работает)! Вам НЕ следует устанавливать его, если вы НЕ знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы НЕ будем оказывать техническую поддержку... Если вы все равно готовы рискнуть, введите '{answers}'", "confirm_app_install_thirdparty": "ВАЖНО! Это приложение не входит в каталог приложений YunoHost. Установка сторонних приложений может нарушить целостность и безопасность вашей системы. Вам НЕ следует устанавливать его, если вы НЕ знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы НЕ будем оказывать техническую поддержку... Если вы все равно готовы рискнуть, введите '{answers}'", "config_apply_failed": "Не удалось применить новую конфигурацию: {error}", diff --git a/locales/sk.json b/locales/sk.json index 544fb6c0e..359b2e562 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -144,7 +144,6 @@ "config_validate_email": "Toto by mal byť platný e-mail", "config_validate_time": "Toto by mal byť platný čas vo formáte HH:MM", "config_validate_url": "Toto by mala byť platná URL adresa webu", - "config_version_not_supported": "Verzie konfiguračného panela '{version}' nie sú podporované.", "danger": "Nebezpečenstvo:", "confirm_app_install_danger": "NEBEZPEČENSTVO! Táto aplikácia je experimentálna (ak vôbec funguje)! Pravdepodobne by ste ju NEMALI inštalovať, pokiaľ si nie ste istý, čo robíte. NEPOSKYTNEME VÁM ŽIADNU POMOC, ak táto aplikácia nebude fungovať alebo rozbije Váš systém… Ak sa rozhodnete i napriek tomu podstúpiť toto riziko, zadajte '{answers}'", "confirm_app_install_thirdparty": "NEBEZPEČENSTVO! Táto aplikácia nie je súčasťou katalógu aplikácií YunoHost. Inštalovaním aplikácií tretích strán môžete ohroziť integritu a bezpečnosť Vášho systému. Pravdepodobne by ste NEMALI pokračovať v inštalácií, pokiaľ neviete, čo robíte. NEPOSKYTNEME VÁM ŽIADNU POMOC, ak táto aplikácia nebude fungovať alebo rozbije Váš systém… Ak sa rozhodnete i napriek tomu podstúpiť toto riziko, zadajte '{answers}'", diff --git a/locales/uk.json b/locales/uk.json index 02304a39c..3c960e9fa 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -587,7 +587,6 @@ "config_validate_email": "Е-пошта має бути дійсною", "config_validate_time": "Час має бути дійсним, наприклад ГГ:ХХ", "config_validate_url": "Вебадреса має бути дійсною", - "config_version_not_supported": "Версії конфігураційної панелі '{version}' не підтримуються.", "danger": "Небезпека:", "invalid_number_min": "Має бути більшим за {min}", "invalid_number_max": "Має бути меншим за {max}", diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index 59ceaf36f..687064de6 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -571,7 +571,6 @@ "config_validate_email": "是有效的电子邮件", "config_validate_time": "应该是像 HH:MM 这样的有效时间", "config_validate_url": "应该是有效的URL", - "config_version_not_supported": "不支持配置面板版本“{ version }”。", "danger": "警告:", "diagnosis_apps_allgood": "所有已安装的应用程序都遵守基本的打包原则", "diagnosis_apps_deprecated_practices": "此应用程序的安装 版本仍然使用一些超旧的弃用打包原则。推荐您升级它。", From be5b1c1b69c2674cb1d0c2e335452140a1c09dca Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 16:49:48 +0100 Subject: [PATCH 034/319] debian: refresh catalog upon package upgrade --- debian/postinst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/debian/postinst b/debian/postinst index c9ad3d562..353317488 100644 --- a/debian/postinst +++ b/debian/postinst @@ -28,6 +28,9 @@ do_configure() { echo "Re-diagnosing server health..." yunohost diagnosis run --force + + echo "Refreshing app catalog..." + yunohost tools update apps || true fi # Trick to let yunohost handle the restart of the API, From 71be74ffe27723fce60b1062e3fac420a1d69d86 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 17:01:16 +0100 Subject: [PATCH 035/319] ci: Attempt to fix the CI, gitlab-ci had some changes related to artefacts paths --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4d0f30679..bb50f1c7a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,7 +37,7 @@ workflow: - when: always variables: - YNH_BUILD_DIR: "ynh-build" + YNH_BUILD_DIR: "./ynh-build" include: - template: Code-Quality.gitlab-ci.yml From 7addad59f0638e670e793e87e35077a373c8922a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 17:11:18 +0100 Subject: [PATCH 036/319] ci: friskies? --- .gitlab/ci/build.gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index db691b9d2..c603e95fb 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -31,7 +31,7 @@ build-yunohost: - mkdir -p $YNH_BUILD_DIR/$PACKAGE - cat archive.tar.gz | tar -xz -C $YNH_BUILD_DIR/$PACKAGE - rm archive.tar.gz - - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE + - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE - *build_script @@ -42,7 +42,7 @@ build-ssowat: script: - DEBIAN_DEPENDS=$(cat debian/control | tr "," "\n" | grep -Po "ssowat \([>,=,<]+ .*\)" | grep -Po "[0-9\.]+") - git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $DEBIAN_DEPENDS $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1 - - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE + - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE - *build_script build-moulinette: @@ -52,5 +52,5 @@ build-moulinette: script: - DEBIAN_DEPENDS=$(cat debian/control | tr "," "\n" | grep -Po "moulinette \([>,=,<]+ .*\)" | grep -Po "[0-9\.]+") - git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $DEBIAN_DEPENDS $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1 - - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE + - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE - *build_script From 312ded8873b10969660c4b91b9308d0e1cde8617 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 17:35:47 +0100 Subject: [PATCH 037/319] =?UTF-8?q?ci:=20friskies=3F=C2=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bb50f1c7a..0ec685143 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,7 +37,7 @@ workflow: - when: always variables: - YNH_BUILD_DIR: "./ynh-build" + YNH_BUILD_DIR: "$CI_PROJECT_DIR/ynh-build" include: - template: Code-Quality.gitlab-ci.yml From a65833647652119c7d6b6e76077da414876de9fe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 17:54:13 +0100 Subject: [PATCH 038/319] ci: add some boring debugging to have a clear view of where the .deb are -_-' --- .gitlab/ci/build.gitlab-ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index c603e95fb..855d94692 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -17,6 +17,11 @@ - VERSION_NIGHTLY="${VERSION}+$(date +%Y%m%d%H%M)" - dch --package "${PACKAGE}" --force-bad-version -v "${VERSION_NIGHTLY}" -D "unstable" --force-distribution "Daily build." - debuild --no-lintian -us -uc + - ls -l + - ls -l ../ + - ls -l $YNH_BUILD_DIR + - ls -l $YNH_BUILD_DIR/$PACKAGE/ + - ls -l $YNH_BUILD_DIR/*.deb ######################################## # BUILD DEB From bf07cd6c47140e1ac79d3b83ba84eb7aa43c9cc2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 18:14:46 +0100 Subject: [PATCH 039/319] =?UTF-8?q?ci:=20friskies=3F=C2=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab/ci/build.gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index 855d94692..4faa23814 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -8,7 +8,7 @@ - DEBIAN_FRONTEND=noninteractive apt update artifacts: paths: - - $YNH_BUILD_DIR/*.deb + - ./*.deb .build_script: &build_script - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" install devscripts --no-install-recommends @@ -22,6 +22,7 @@ - ls -l $YNH_BUILD_DIR - ls -l $YNH_BUILD_DIR/$PACKAGE/ - ls -l $YNH_BUILD_DIR/*.deb + - cd $YNH_BUILD_DIR/ ######################################## # BUILD DEB From ece8d65601dc9e2889993d7d9f88b26075834472 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 18:27:34 +0100 Subject: [PATCH 040/319] =?UTF-8?q?ci:=20friskies=3F=E2=81=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0ec685143..f8d2fcd97 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,7 +37,7 @@ workflow: - when: always variables: - YNH_BUILD_DIR: "$CI_PROJECT_DIR/ynh-build" + YNH_BUILD_DIR: "$PWD/ynh-build" include: - template: Code-Quality.gitlab-ci.yml From a568c7eecd338fa3ff09533cd85013ac2ef949e7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 18:36:38 +0100 Subject: [PATCH 041/319] =?UTF-8?q?ci:=20friskies=3F=E2=81=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab/ci/build.gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index 4faa23814..26324cb3d 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -22,7 +22,8 @@ - ls -l $YNH_BUILD_DIR - ls -l $YNH_BUILD_DIR/$PACKAGE/ - ls -l $YNH_BUILD_DIR/*.deb - - cd $YNH_BUILD_DIR/ + - cp ./*.deb ${CI_PROJECT_DIR}/ + - cd ${CI_PROJECT_DIR} ######################################## # BUILD DEB From 27305fe3fca38d798b8c19bb0ef7e125f0658b4f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 18:48:44 +0100 Subject: [PATCH 042/319] =?UTF-8?q?ci:=20friskies=3F=E2=81=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 2 +- .gitlab/ci/build.gitlab-ci.yml | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f8d2fcd97..11d920bd0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,7 +37,7 @@ workflow: - when: always variables: - YNH_BUILD_DIR: "$PWD/ynh-build" + YNH_BUILD_DIR: "/ynh-build" include: - template: Code-Quality.gitlab-ci.yml diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index 26324cb3d..610580dac 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -17,12 +17,7 @@ - VERSION_NIGHTLY="${VERSION}+$(date +%Y%m%d%H%M)" - dch --package "${PACKAGE}" --force-bad-version -v "${VERSION_NIGHTLY}" -D "unstable" --force-distribution "Daily build." - debuild --no-lintian -us -uc - - ls -l - - ls -l ../ - - ls -l $YNH_BUILD_DIR - - ls -l $YNH_BUILD_DIR/$PACKAGE/ - - ls -l $YNH_BUILD_DIR/*.deb - - cp ./*.deb ${CI_PROJECT_DIR}/ + - cp $YNH_BUILD_DIR/*.deb ${CI_PROJECT_DIR}/ - cd ${CI_PROJECT_DIR} ######################################## From a5de20d757498c34f1558e3b5edfc0b4fa4830a6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 19:06:44 +0100 Subject: [PATCH 043/319] =?UTF-8?q?ci:=20friskies=3F=E2=81=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab/ci/install.gitlab-ci.yml | 4 ++-- .gitlab/ci/test.gitlab-ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab/ci/install.gitlab-ci.yml b/.gitlab/ci/install.gitlab-ci.yml index ecdfecfcd..65409c6eb 100644 --- a/.gitlab/ci/install.gitlab-ci.yml +++ b/.gitlab/ci/install.gitlab-ci.yml @@ -17,7 +17,7 @@ upgrade: image: "after-install" 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 + - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb install-postinstall: @@ -25,5 +25,5 @@ install-postinstall: image: "before-install" 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 + - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb - 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 804940aa2..37edbda04 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 ./$YNH_BUILD_DIR/*.deb + - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb - pip3 install -U mock pip pytest pytest-cov pytest-mock pytest-sugar requests-mock tox ansi2html black jinja2 "packaging<22" .test-stage: From 5a412ce93c2f39aa959a4df9de87d8a24c713168 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 19 Jan 2023 23:09:50 +0100 Subject: [PATCH 044/319] Update changelog for 11.1.3 --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 536d1bdbf..10172fa9b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (11.1.3) testing; urgency=low + + - helpers: Include procedures in MySQL database backup ([#1570](https://github.com/yunohost/yunohost/pull/1570)) + - users: be able to change the loginShell of a user ([#1538](https://github.com/yunohost/yunohost/pull/1538)) + - debian: refresh catalog upon package upgrade (be5b1c1b) + + Thanks to all contributors <3 ! (Éric Gaspar, Kay0u, ljf, Metin Bektas) + + -- Alexandre Aubin Thu, 19 Jan 2023 23:08:10 +0100 + yunohost (11.1.2.2) testing; urgency=low - Minor technical fixes (b37d4baf, 68342171) From e00d60b0492ddea17e3be384ee30b3dcc96627f5 Mon Sep 17 00:00:00 2001 From: Tagada <36127788+Tagadda@users.noreply.github.com> Date: Sat, 21 Jan 2023 13:40:04 +0100 Subject: [PATCH 045/319] Apply suggestions from code review Co-authored-by: Alexandre Aubin --- src/diagnosers/14-ports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index 0cd54efba..57fb7cd98 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -121,7 +121,7 @@ class MyDiagnoser(Diagnoser): for record in dnsrecords.get("items", []) ) - if failed == 4 and settings_get("misc.network.dns_exposure") != "ipv6" or ipv6_is_important(): + if (failed == 4 and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]) or (failed == 6 and ipv6_is_important()): yield dict( meta={"port": port}, data={ From c444dee4fe7e4b1c273ec831b60c4a3b89becae9 Mon Sep 17 00:00:00 2001 From: Kayou Date: Mon, 23 Jan 2023 15:18:44 +0100 Subject: [PATCH 046/319] add xmpp-upload. and muc. server_name only if xmpp_enabled is enabled --- conf/nginx/server.tpl.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index 40f85b328..d3ff77714 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -6,7 +6,7 @@ map $http_upgrade $connection_upgrade { server { listen 80; listen [::]:80; - server_name {{ domain }}{% if xmpp_enabled != "True" %} xmpp-upload.{{ domain }} muc.{{ domain }}{% endif %}; + server_name {{ domain }}{% if xmpp_enabled == "True" %} xmpp-upload.{{ domain }} muc.{{ domain }}{% endif %}; access_by_lua_file /usr/share/ssowat/access.lua; From b29ee31c7aa601a7aefe5965b0d07521d538a8a8 Mon Sep 17 00:00:00 2001 From: ppr Date: Tue, 10 Jan 2023 16:58:43 +0000 Subject: [PATCH 047/319] Translated using Weblate (French) Currently translated at 99.0% (743 of 750 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 9c5b9a9e3..b26f67215 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -747,5 +747,6 @@ "app_not_enough_disk": "Cette application nécessite {required} d'espace libre.", "app_not_enough_ram": "Cette application nécessite {required} de mémoire vive (RAM) pour être installée/mise à niveau mais seule {current} de mémoire est disponible actuellement.", "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}", - "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des éléments d'information importants à connaître. [{answers}]" -} \ No newline at end of file + "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des éléments d'information importants à connaître. [{answers}]", + "global_settings_setting_passwordless_sudo_help": "RÉPAREZ-MOI" +} From 12d4c16309bb2ddb0ee1c05b8438dbbd4b11ddf1 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Wed, 11 Jan 2023 17:39:43 +0000 Subject: [PATCH 048/319] Translated using Weblate (Arabic) Currently translated at 19.0% (143 of 750 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index aa40f2420..570f036ec 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -11,7 +11,7 @@ "app_not_properly_removed": "لم يتم حذف تطبيق {app} بشكلٍ جيّد", "app_removed": "تمت إزالة تطبيق {app}", "app_requirements_checking": "جار فحص الحزم اللازمة لـ {app}…", - "app_sources_fetch_failed": "تعذرت عملية جلب مصادر الملفات", + "app_sources_fetch_failed": "تعذر جلب ملفات المصدر ، هل عنوان URL صحيح؟", "app_unknown": "برنامج مجهول", "app_upgrade_app_name": "جارٍ تحديث {app}…", "app_upgrade_failed": "تعذرت عملية ترقية {app}", @@ -39,7 +39,7 @@ "done": "تم", "downloading": "عملية التنزيل جارية …", "dyndns_ip_updated": "لقد تم تحديث عنوان الإيبي الخاص بك على نظام أسماء النطاقات الديناميكي", - "dyndns_key_generating": "عملية توليد مفتاح نظام أسماء النطاقات جارية. يمكن للعملية أن تستغرق بعضا من الوقت…", + "dyndns_key_generating": "جارٍ إنشاء مفتاح DNS ... قد يستغرق الأمر بعض الوقت.", "dyndns_key_not_found": "لم يتم العثور على مفتاح DNS الخاص باسم النطاق هذا", "extracting": "عملية فك الضغط جارية…", "installation_complete": "إكتملت عملية التنصيب", @@ -57,7 +57,7 @@ "service_add_failed": "تعذرت إضافة خدمة '{service}'", "service_already_stopped": "إنّ خدمة '{service}' متوقفة مِن قبلُ", "service_disabled": "لن يتم إطلاق خدمة '{service}' أثناء بداية تشغيل النظام.", - "service_enabled": "تم تنشيط خدمة '{service}'", + "service_enabled": "سيتم الآن بدء تشغيل الخدمة '{service}' تلقائيًا أثناء تمهيد النظام.", "service_removed": "تمت إزالة خدمة '{service}'", "service_started": "تم إطلاق تشغيل خدمة '{service}'", "service_stopped": "تمّ إيقاف خدمة '{service}'", @@ -119,7 +119,7 @@ "already_up_to_date": "كل شيء على ما يرام. ليس هناك ما يتطلّب تحديثًا.", "service_description_slapd": "يخزّن المستخدمين والنطاقات والمعلومات المتعلقة بها", "service_reloaded": "تم إعادة تشغيل خدمة '{service}'", - "service_restarted": "تم إعادة تشغيل خدمة '{service}'", + "service_restarted": "تم إعادة تشغيل خدمة '{service}'", "group_unknown": "الفريق '{group}' مجهول", "group_deletion_failed": "فشلت عملية حذف الفريق '{group}': {error}", "group_deleted": "تم حذف الفريق '{group}'", @@ -193,5 +193,16 @@ "diagnosis_ports_ok": "المنفذ {port} مفتوح ومتاح الوصول إليه مِن الخارج.", "global_settings_setting_smtp_allow_ipv6": "سماح IPv6", "disk_space_not_sufficient_update": "ليس هناك مساحة كافية لتحديث هذا التطبيق", - "domain_cert_gen_failed": "لا يمكن إعادة توليد الشهادة" -} \ No newline at end of file + "domain_cert_gen_failed": "لا يمكن إعادة توليد الشهادة", + "diagnosis_apps_issue": "تم العثور على مشكلة في تطبيق {app}", + "tools_upgrade": "تحديث حُزم النظام", + "service_description_yunomdns": "يسمح لك بالوصول إلى خادمك الخاص باستخدام 'yunohost.local' في شبكتك المحلية", + "good_practices_about_user_password": "أنت الآن على وشك تحديد كلمة مرور مستخدم جديدة. يجب أن تتكون كلمة المرور من 8 أحرف على الأقل - على الرغم من أنه من الممارسات الجيدة استخدام كلمة مرور أطول (أي عبارة مرور) و / أو مجموعة متنوعة من الأحرف (الأحرف الكبيرة والصغيرة والأرقام والأحرف الخاصة).", + "root_password_changed": "تم تغيير كلمة مرور الجذر", + "root_password_desynchronized": "تم تغيير كلمة مرور المسؤول ، لكن لم يتمكن YunoHost من نشرها على كلمة مرور الجذر!", + "user_import_bad_line": "سطر غير صحيح {line}: {details}", + "user_import_success": "تم استيراد المستخدمين بنجاح", + "visitors": "الزوار", + "password_too_simple_3": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل وأن تحتوي على أرقام وأرقام علوية وسفلية وأحرف خاصة", + "password_too_simple_4": "يجب أن تتكون كلمة المرور من 12 حرفًا على الأقل وأن تحتوي على أرقام وأرقام علوية وسفلية وأحرف خاصة" +} From ec22c2ad1f0b471b84568f02160280db6d9fdd11 Mon Sep 17 00:00:00 2001 From: ppr Date: Wed, 11 Jan 2023 17:06:27 +0000 Subject: [PATCH 049/319] Translated using Weblate (French) Currently translated at 99.0% (743 of 750 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 b26f67215..2045be8ac 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -748,5 +748,5 @@ "app_not_enough_ram": "Cette application nécessite {required} de mémoire vive (RAM) pour être installée/mise à niveau mais seule {current} de mémoire est disponible actuellement.", "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}", "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des éléments d'information importants à connaître. [{answers}]", - "global_settings_setting_passwordless_sudo_help": "RÉPAREZ-MOI" + "global_settings_setting_passwordless_sudo_help": "RÉPAREZ-MOI / AIDEZ-MOI" } From 087030ac7f1b024b134a7176000c23685314611f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Thu, 12 Jan 2023 05:24:34 +0000 Subject: [PATCH 050/319] Translated using Weblate (Galician) Currently translated at 99.7% (748 of 750 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/locales/gl.json b/locales/gl.json index a4f9d9772..af3d49165 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -739,5 +739,12 @@ "domain_cannot_add_muc_upload": "Non podes engadir dominios que comecen por 'muc.'. Este tipo de dominio está reservado para as salas de conversa de XMPP integradas en YunoHost.", "global_settings_setting_passwordless_sudo": "Permitir a Admins usar 'sudo' sen ter que volver a escribir o contrasinal", "global_settings_setting_portal_theme": "Decorado do Portal", - "global_settings_setting_portal_theme_help": "Tes máis info acerca da creación de decorados para o portal de acceso en https://yunohost.org/theming" -} \ No newline at end of file + "global_settings_setting_portal_theme_help": "Tes máis info acerca da creación de decorados para o portal de acceso en https://yunohost.org/theming", + "app_arch_not_supported": "Esta app só pode ser instalada e arquitecturas {', '.join(required)} pero a arquitectura do teu servidor é {current}", + "app_not_enough_disk": "Esta app precisa {required} de espazo libre.", + "app_yunohost_version_not_supported": "Esta app require YunoHost >= {required} pero a versión actual instalada é {current}", + "confirm_app_insufficient_ram": "PERIGO! Esta app precisa {required} de RAM para instalar/actualizar pero só hai {current} dispoñibles. Incluso se a app funcionase, o seu proceso de instalación/actualización require gran cantidade de RAM e o teu servidor podería colgarse e fallar. Se queres asumir o risco, escribe '{answers}'", + "confirm_notifications_read": "AVISO: Deberías comprobar as notificacións da app antes de continuar, poderías ter información importante que revisar. [{answers}]", + "global_settings_setting_passwordless_sudo_help": "ARRÁNXAME", + "app_not_enough_ram": "Esta app require {required} de RAM para instalar/actualizar pero só hai {current} dispoñible." +} From 0d279baa2c48cecf49b9ddbb22f4cb794852f612 Mon Sep 17 00:00:00 2001 From: Weblate Date: Sat, 14 Jan 2023 15:42:30 +0100 Subject: [PATCH 051/319] Added translation using Weblate (Lithuanian) --- locales/lt.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/lt.json diff --git a/locales/lt.json b/locales/lt.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/lt.json @@ -0,0 +1 @@ +{} From 908fa1035716447fff15852d84594521c2077da6 Mon Sep 17 00:00:00 2001 From: cristian amoyao Date: Tue, 17 Jan 2023 20:26:15 +0000 Subject: [PATCH 052/319] Translated using Weblate (Spanish) Currently translated at 96.9% (727 of 750 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/es/ --- locales/es.json | 63 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/locales/es.json b/locales/es.json index 9a92908a4..3d14ab781 100644 --- a/locales/es.json +++ b/locales/es.json @@ -677,5 +677,64 @@ "config_action_failed": "Error al ejecutar la acción '{action}': {error}", "config_forbidden_readonly_type": "El tipo '{type}' no puede establecerse como solo lectura, utilice otro tipo para representar este valor (arg id relevante: '{id}').", "diagnosis_using_stable_codename": "apt (el gestor de paquetes del sistema) está configurado actualmente para instalar paquetes de nombre en clave 'estable', en lugar del nombre en clave de la versión actual de Debian (bullseye).", - "diagnosis_using_stable_codename_details": "Esto suele deberse a una configuración incorrecta de su proveedor de alojamiento. Esto es peligroso, porque tan pronto como la siguiente versión de Debian se convierta en la nueva 'estable', apt querrá actualizar todos los paquetes del sistema sin pasar por un procedimiento de migración adecuado. Se recomienda arreglar esto editando la fuente de apt para el repositorio base de Debian, y reemplazar la palabra clave stable por bullseye. El fichero de configuración correspondiente debería ser /etc/apt/sources.list, o un fichero en /etc/apt/sources.list.d/." -} \ No newline at end of file + "diagnosis_using_stable_codename_details": "Esto suele deberse a una configuración incorrecta de su proveedor de alojamiento. Esto es peligroso, porque tan pronto como la siguiente versión de Debian se convierta en la nueva 'estable', apt querrá actualizar todos los paquetes del sistema sin pasar por un procedimiento de migración adecuado. Se recomienda arreglar esto editando la fuente de apt para el repositorio base de Debian, y reemplazar la palabra clave stable por bullseye. El fichero de configuración correspondiente debería ser /etc/apt/sources.list, o un fichero en /etc/apt/sources.list.d/.", + "domain_config_cert_install": "Instalar el certificado Let's Encrypt", + "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.", + "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_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!.", + "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.", + "global_settings_reset_success": "Restablecer la configuración global", + "global_settings_setting_nginx_compatibility": "Compatibilidad con NGINX", + "global_settings_setting_nginx_redirect_to_https": "Forzar HTTPS", + "global_settings_setting_user_strength_help": "Estos requisitos sólo se aplican al inicializar o cambiar la contraseña", + "log_resource_snippet": "Aprovisionar/desaprovisionar/actualizar un recurso", + "global_settings_setting_pop3_enabled": "Habilitar POP3", + "global_settings_setting_smtp_allow_ipv6": "Permitir IPv6", + "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 {', '.join(required)} pero la arquitectura de su 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.", + "app_yunohost_version_not_supported": "Esta aplicación requiere YunoHost >= {required} pero la versión actualmente instalada es {current}", + "global_settings_setting_ssh_compatibility": "Compatibilidad con SSH", + "root_password_changed": "la contraseña de root fue cambiada", + "domain_config_acme_eligible_explain": "Este dominio no parece estar preparado para un certificado Let's Encrypt. Compruebe la configuración DNS y la accesibilidad del servidor HTTP. Las secciones \"Registros DNS\" y \"Web\" de la página de diagnóstico pueden ayudarte a entender qué está mal configurado.", + "domain_config_cert_no_checks": "Ignorar las comprobaciones de diagnóstico", + "domain_config_cert_renew": "Renovar el certificado Let's Encrypt", + "domain_config_cert_summary": "Estado del certificado", + "domain_config_cert_summary_abouttoexpire": "El certificado actual está a punto de caducar. Pronto debería renovarse automáticamente.", + "domain_config_cert_summary_ok": "Muy bien, ¡el certificado actual tiene buena pinta!", + "domain_config_cert_validity": "Validez", + "global_settings_setting_admin_strength_help": "Estos requisitos sólo se aplican al inicializar o cambiar la contraseña", + "global_settings_setting_pop3_enabled_help": "Habilitar el protocolo POP3 para el servidor de correo", + "log_settings_reset_all": "Restablecer todos los ajustes", + "log_settings_set": "Aplicar ajustes", + "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_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.", + "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", + "global_settings_setting_portal_theme_help": "Más información sobre la creación de temas de portal personalizados en https://yunohost.org/theming", + "invalid_credentials": "Contraseña o nombre de usuario no válidos", + "global_settings_setting_root_password": "Nueva contraseña de root", + "global_settings_setting_webadmin_allowlist": "Lista de IPs permitidas para Webadmin", + "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" +} From 3c6ab69ae62edf5f95e0978de35a60cc8df7f026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= Date: Thu, 19 Jan 2023 15:58:35 +0000 Subject: [PATCH 053/319] Translated using Weblate (French) Currently translated at 100.0% (751 of 751 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 2045be8ac..3bb666bd3 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -743,10 +743,11 @@ "global_settings_setting_passwordless_sudo": "Permettre aux administrateurs d'utiliser 'sudo' sans retaper leur mot de passe", "app_arch_not_supported": "Cette application ne peut être installée que sur les architectures {', '.join(required)}. L'architecture de votre serveur est {current}", "app_resource_failed": "L'allocation automatique des ressources (provisioning), la suppression d'accès à ces ressources (déprovisioning) ou la mise à jour des ressources pour {app} a échoué : {error}", - "confirm_app_insufficient_ram": "ATTENTION ! Cette application requiert {required} de RAM pour l'installation/mise à niveau mais il n'y a que {current} de disponible actuellement. Même si cette application pouvait fonctionner, son processus d'installation/mise à niveau nécessite une grande quantité de RAM. Votre serveur pourrait donc geler (freezer) et planter lamentablement. Si vous êtes prêt à prendre ce risque, tapez '{answers}'", + "confirm_app_insufficient_ram": "ATTENTION ! Cette application requiert {required} de RAM pour l'installation/mise à niveau mais il n'y a que {current} de disponible actuellement. Même si cette application pouvait fonctionner, son processus d'installation/mise à niveau nécessite une grande quantité de RAM. Votre serveur pourrait donc geler et planter lamentablement. Si vous êtes prêt à prendre ce risque, tapez '{answers}'", "app_not_enough_disk": "Cette application nécessite {required} d'espace libre.", "app_not_enough_ram": "Cette application nécessite {required} de mémoire vive (RAM) pour être installée/mise à niveau mais seule {current} de mémoire est disponible actuellement.", "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}", - "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des éléments d'information importants à connaître. [{answers}]", - "global_settings_setting_passwordless_sudo_help": "RÉPAREZ-MOI / AIDEZ-MOI" + "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des informations importantes à connaître. [{answers}]", + "global_settings_setting_passwordless_sudo_help": "RÉPAREZ-MOI / AIDEZ-MOI", + "invalid_shell": "Shell invalide : {shell}" } From 94b1338dc668b23b72df0ae1c0a0e46ef9966535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= Date: Mon, 23 Jan 2023 11:48:41 +0000 Subject: [PATCH 054/319] Translated using Weblate (French) Currently translated at 100.0% (750 of 750 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 3bb666bd3..426b84fcf 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -655,7 +655,7 @@ "global_settings_setting_backup_compress_tar_archives_help": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar). N.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.", "global_settings_setting_security_experimental_enabled": "Fonctionnalités de sécurité expérimentales", "global_settings_setting_security_experimental_enabled_help": "Activer les fonctionnalités de sécurité expérimentales (ne l'activez pas si vous ne savez pas ce que vous faites !)", - "global_settings_setting_nginx_compatibility_help": "Compromis 'compatibilité versus sécurité' pour le serveur web Nginx. Affecte les cryptogrammes utilisés (et d'autres aspects liés à la sécurité)", + "global_settings_setting_nginx_compatibility_help": "Compromis 'compatibilité versus sécurité' pour le serveur web NGINX. Affecte les cryptogrammes utilisés (et d'autres aspects liés à la sécurité)", "global_settings_setting_nginx_redirect_to_https_help": "Rediriger les requêtes HTTP vers HTTPS par défaut (NE PAS DÉSACTIVER à moins de savoir vraiment ce que vous faites !)", "global_settings_setting_admin_strength": "Critères pour les mots de passe administrateur", "global_settings_setting_user_strength": "Critères pour les mots de passe utilisateurs", From cba36d2cf53d441f71cf4bd91df6d034fb52f69d Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Mon, 23 Jan 2023 23:29:11 +0000 Subject: [PATCH 055/319] Translated using Weblate (Basque) Currently translated at 98.2% (737 of 750 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/eu/ --- locales/eu.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/locales/eu.json b/locales/eu.json index 4d212bf58..1754343c4 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -735,5 +735,17 @@ "password_too_long": "Aukeratu 127 karaktere baino laburragoa den pasahitz bat", "diagnosis_using_stable_codename_details": "Ostatatzaileak zerbait oker ezarri duenean gertatu ohi da hau. Arriskutsua da, Debianen datorren bertsioa 'estable' (egonkorra) bilakatzen denean, apt-ek sistemaren pakete guztiak bertsio-berritzen saiatuko da, beharrezko migrazio-prozedurarik burutu gabe. Debianen repositorioan apt iturria editatzen konpontzea da gomendioa, stable gakoa bullseye gakoarekin ordezkatuz. Ezarpen-fitxategia /etc/apt/sources.list izan beharko litzateke, edo /etc/apt/sources.list.d/ direktorioko fitxategiren bat.", "group_update_aliases": "'{group}' taldearen aliasak eguneratzen", - "group_no_change": "Ez da ezer aldatu behar '{group}' talderako" -} \ No newline at end of file + "group_no_change": "Ez da ezer aldatu behar '{group}' talderako", + "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 {current} bakarrik daude erabilgarri. Aplikazioa ibiliko balitz ere, instalazioak edo bertsio-berritzeak RAM koporu handia eskatzen du eta zure zerbitzaria izoztu 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}]", + "app_arch_not_supported": "Aplikazio hau {', '.join(required)} arkitekturan instala daiteke bakarrik, baina zure zerbitzariaren arkitektura {current} da", + "app_resource_failed": "Huts egin du {app} aplikaziorako baliabideak", + "app_not_enough_disk": "Aplikazio honek {required} espazio libre behar ditu.", + "app_yunohost_version_not_supported": "Aplikazio honek YunoHost >= {required} behar du baina unean instalatutako bertsioa {current} da", + "global_settings_setting_passwordless_sudo": "Baimendu administrariek 'sudo' erabiltzea pasahitzak berriro idatzi beharrik gabe", + "global_settings_setting_portal_theme": "Atariko gaia", + "global_settings_setting_portal_theme_help": "Atariko gai propioak sortzeari buruzko informazio gehiago: https://yunohost.org/theming", + "invalid_shell": "Shell baliogabea: {shell}" +} From dafdf1c4ba48ea880b39b1afab28b3303c03ebf3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 24 Jan 2023 17:47:04 +0100 Subject: [PATCH 056/319] i18n: typo in fr string --- locales/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 9c5b9a9e3..4551f294d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -708,7 +708,7 @@ "visitors": "Visiteurs", "global_settings_reset_success": "Réinitialisation des paramètres généraux", "domain_config_acme_eligible": "Éligibilité au protocole ACME (Automatic Certificate Management Environment, littéralement : environnement de gestion automatique de certificat)", - "domain_config_acme_eligible_explain": "Ce domaine ne semble pas près pour installer un certificat Let's Encrypt. Veuillez vérifier votre configuration DNS mais aussi que votre serveur est bien joignable en HTTP. Les sections 'Enregistrements DNS' et 'Web' de la page Diagnostic peuvent vous aider à comprendre ce qui est mal configuré.", + "domain_config_acme_eligible_explain": "Ce domaine ne semble pas prêt pour installer un certificat Let's Encrypt. Veuillez vérifier votre configuration DNS mais aussi que votre serveur est bien joignable en HTTP. Les sections 'Enregistrements DNS' et 'Web' de la page Diagnostic peuvent vous aider à comprendre ce qui est mal configuré.", "domain_config_cert_install": "Installer un certificat Let's Encrypt", "domain_config_cert_issuer": "Autorité de certification", "domain_config_cert_no_checks": "Ignorer les tests et autres vérifications du diagnostic", @@ -748,4 +748,4 @@ "app_not_enough_ram": "Cette application nécessite {required} de mémoire vive (RAM) pour être installée/mise à niveau mais seule {current} de mémoire est disponible actuellement.", "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}", "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des éléments d'information importants à connaître. [{answers}]" -} \ No newline at end of file +} From e28d8a9fe5e5c2b3fe160d438881469456ce2661 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 24 Jan 2023 17:54:51 +0100 Subject: [PATCH 057/319] i18n: funky fr translation --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 9ee8d421a..db268bb56 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -627,7 +627,7 @@ "diagnosis_http_special_use_tld": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et n'est donc pas censé être exposé en dehors du réseau local.", "domain_dns_conf_special_use_tld": "Ce domaine est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.", "other_available_options": "... et {n} autres options disponibles non affichées", - "domain_config_auth_consumer_key": "La clé utilisateur", + "domain_config_auth_consumer_key": "Clé utilisateur", "domain_unknown": "Domaine '{domain}' inconnu", "migration_0021_start": "Démarrage de la migration vers Bullseye", "migration_0021_patching_sources_list": "Mise à jour du fichier sources.lists...", From c2998411944d05b5dfcae523370a9f774ad3056a Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Tue, 24 Jan 2023 17:19:38 +0000 Subject: [PATCH 058/319] [CI] Reformat / remove stale translated strings --- locales/ar.json | 2 +- locales/es.json | 2 +- locales/eu.json | 2 +- locales/fr.json | 2 +- locales/gl.json | 3 +-- locales/lt.json | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 570f036ec..dd254096b 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -205,4 +205,4 @@ "visitors": "الزوار", "password_too_simple_3": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل وأن تحتوي على أرقام وأرقام علوية وسفلية وأحرف خاصة", "password_too_simple_4": "يجب أن تتكون كلمة المرور من 12 حرفًا على الأقل وأن تحتوي على أرقام وأرقام علوية وسفلية وأحرف خاصة" -} +} \ No newline at end of file diff --git a/locales/es.json b/locales/es.json index 3d14ab781..63100e04c 100644 --- a/locales/es.json +++ b/locales/es.json @@ -737,4 +737,4 @@ "global_settings_setting_webadmin_allowlist": "Lista de IPs permitidas para Webadmin", "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" -} +} \ No newline at end of file diff --git a/locales/eu.json b/locales/eu.json index 1754343c4..cf6b0abea 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -748,4 +748,4 @@ "global_settings_setting_portal_theme": "Atariko gaia", "global_settings_setting_portal_theme_help": "Atariko gai propioak sortzeari buruzko informazio gehiago: https://yunohost.org/theming", "invalid_shell": "Shell baliogabea: {shell}" -} +} \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index db268bb56..41e58a1c5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -749,4 +749,4 @@ "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}", "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des informations importantes à connaître. [{answers}]", "invalid_shell": "Shell invalide : {shell}" -} +} \ No newline at end of file diff --git a/locales/gl.json b/locales/gl.json index af3d49165..c592c650f 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -745,6 +745,5 @@ "app_yunohost_version_not_supported": "Esta app require YunoHost >= {required} pero a versión actual instalada é {current}", "confirm_app_insufficient_ram": "PERIGO! Esta app precisa {required} de RAM para instalar/actualizar pero só hai {current} dispoñibles. Incluso se a app funcionase, o seu proceso de instalación/actualización require gran cantidade de RAM e o teu servidor podería colgarse e fallar. Se queres asumir o risco, escribe '{answers}'", "confirm_notifications_read": "AVISO: Deberías comprobar as notificacións da app antes de continuar, poderías ter información importante que revisar. [{answers}]", - "global_settings_setting_passwordless_sudo_help": "ARRÁNXAME", "app_not_enough_ram": "Esta app require {required} de RAM para instalar/actualizar pero só hai {current} dispoñible." -} +} \ No newline at end of file diff --git a/locales/lt.json b/locales/lt.json index 0967ef424..9e26dfeeb 100644 --- a/locales/lt.json +++ b/locales/lt.json @@ -1 +1 @@ -{} +{} \ No newline at end of file From 36205a7b4c3968029de12d2b8021f31d3ed64bee Mon Sep 17 00:00:00 2001 From: quiwy Date: Wed, 25 Jan 2023 15:14:56 +0000 Subject: [PATCH 059/319] Translated using Weblate (Spanish) Currently translated at 100.0% (750 of 750 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/es/ --- locales/es.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/locales/es.json b/locales/es.json index 3d14ab781..942c8dd13 100644 --- a/locales/es.json +++ b/locales/es.json @@ -78,8 +78,8 @@ "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", - "pattern_lastname": "Debe ser un apellido válido", + "pattern_firstname": "Debe ser un nombre válido (al menos 3 caracteres)", + "pattern_lastname": "Debe ser un apellido válido (al menos 3 caracteres)", "pattern_mailbox_quota": "Debe ser un tamaño con el sufijo «b/k/M/G/T» o «0» para no tener una cuota", "pattern_password": "Debe contener al menos 3 caracteres", "pattern_port_or_range": "Debe ser un número de puerto válido (es decir entre 0-65535) o un intervalo de puertos (por ejemplo 100:200)", @@ -266,7 +266,7 @@ "migrations_failed_to_load_migration": "No se pudo cargar la migración {id}: {error}", "migrations_dependencies_not_satisfied": "Ejecutar estas migraciones: «{dependencies_id}» antes de migrar {id}.", "migrations_already_ran": "Esas migraciones ya se han realizado: {ids}", - "mail_unavailable": "Esta dirección de correo está reservada y será asignada automáticamente al primer usuario", + "mail_unavailable": "Esta dirección de correo electrónico está reservada para el grupo de administradores", "mailbox_disabled": "Correo desactivado para usuario {user}", "log_tools_reboot": "Reiniciar el servidor", "log_tools_shutdown": "Apagar el servidor", @@ -316,7 +316,7 @@ "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.", "dpkg_lock_not_available": "Esta orden no se puede ejecutar en este momento ,parece que programa está usando el bloqueo de dpkg (el gestor de paquetes del sistema)", - "dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/APT (los gestores de paquetes del sistema) parecen estar mal configurados... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo apt install --fix-broken` y/o `sudo dpkg --configure -a`.", + "dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/APT (los gestores de paquetes del sistema) parecen estar mal configurados... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo apt install --fix-broken` y/o `sudo dpkg --configure -a` y/o `sudo dpkg --audit`.", "confirm_app_install_thirdparty": "¡PELIGRO! Esta aplicación no forma parte del catálogo de aplicaciones de YunoHost. La instalación de aplicaciones de terceros puede comprometer la integridad y seguridad de tu sistema. Probablemente NO deberías instalarla a menos que sepas lo que estás haciendo. NO se proporcionará NINGÚN SOPORTE si esta aplicación no funciona o rompe su sistema… Si de todos modos quieres correr ese riesgo, escribe '{answers}'", "confirm_app_install_danger": "¡PELIGRO! ¡Esta aplicación sigue siendo experimental (si no es expresamente no funcional)! Probablemente NO deberías instalarla a menos que sepas lo que estás haciendo. NO se proporcionará NINGÚN SOPORTE si esta aplicación no funciona o rompe tu sistema… Si de todos modos quieres correr ese riesgo, escribe '{answers}'", "confirm_app_install_warning": "Aviso: esta aplicación puede funcionar pero no está bien integrada en YunoHost. Algunas herramientas como la autentificación única y respaldo/restauración podrían no estar disponibles. ¿Instalar de todos modos? [{answers}] ", @@ -454,7 +454,7 @@ "diagnosis_ports_forwarding_tip": "Para solucionar este incidente, lo más seguro deberías configurar la redirección de los puertos en el router como se especifica en https://yunohost.org/isp_box_config", "certmanager_warning_subdomain_dns_record": "El subdominio '{subdomain}' no se resuelve en la misma dirección IP que '{domain}'. Algunas funciones no estarán disponibles hasta que solucione esto y regenere el certificado.", "domain_cannot_add_xmpp_upload": "No puede agregar dominios que comiencen con 'xmpp-upload'. Este tipo de nombre está reservado para la función de carga XMPP integrada en YunoHost.", - "yunohost_postinstall_end_tip": "¡La post-instalación completada! Para finalizar su configuración, por favor considere:\n - agregar un primer usuario a través de la sección 'Usuarios' del administrador web (o 'yunohost user create ' en la línea de comandos);\n - diagnosticar problemas potenciales a través de la sección 'Diagnóstico' del administrador web (o 'yunohost diagnosis run' en la línea de comandos);\n - leyendo las partes 'Finalizando su configuración' y 'Conociendo YunoHost' en la documentación del administrador: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "¡La post-instalación completada! Para finalizar su configuración, por favor considere:\n - diagnosticar problemas potenciales a través de la sección 'Diagnóstico' del administrador web (o 'yunohost diagnosis run' en la línea de comandos);\n - leyendo las partes 'Finalizando su configuración' y 'Conociendo YunoHost' en la documentación del administrador: https://yunohost.org/admindoc.", "diagnosis_dns_point_to_doc": "Por favor, consulta la documentación en https://yunohost.org/dns_config si necesitas ayuda para configurar los registros DNS.", "diagnosis_ip_global": "IP Global: {global}", "diagnosis_mail_outgoing_port_25_ok": "El servidor de email SMTP puede mandar emails (puerto saliente 25 no está bloqueado).", @@ -598,7 +598,7 @@ "postinstall_low_rootfsspace": "El sistema de archivos raíz tiene un espacio total inferior a 10 GB, ¡lo cual es bastante preocupante! ¡Es probable que se quede sin espacio en disco muy rápidamente! Se recomienda tener al menos 16 GB para el sistema de archivos raíz. Si desea instalar YunoHost a pesar de esta advertencia, vuelva a ejecutar la instalación posterior con --force-diskspace", "migration_ldap_rollback_success": "Sistema revertido.", "permission_protected": "Permiso {permission} está protegido. No puede agregar o quitar el grupo de visitantes a/desde este permiso.", - "global_settings_setting_ssowat_panel_overlay_enabled": "Habilitar la superposición del panel SSOwat", + "global_settings_setting_ssowat_panel_overlay_enabled": "Habilitar el pequeño cuadrado de acceso directo al portal \"YunoHost\" en las aplicaciones", "migration_0021_start": "Iniciando migración a Bullseye", "migration_0021_patching_sources_list": "Parcheando los sources.lists...", "migration_0021_main_upgrade": "Iniciando actualización principal...", @@ -736,5 +736,10 @@ "global_settings_setting_root_password": "Nueva contraseña de root", "global_settings_setting_webadmin_allowlist": "Lista de IPs permitidas para Webadmin", "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" + "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", + "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 fd4ab9620c176a2b77cc69c6f0f3e206618d339a Mon Sep 17 00:00:00 2001 From: cristian amoyao Date: Wed, 25 Jan 2023 15:18:35 +0000 Subject: [PATCH 060/319] Translated using Weblate (Spanish) Currently translated at 100.0% (750 of 750 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/es/ --- locales/es.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index 942c8dd13..51bacba7a 100644 --- a/locales/es.json +++ b/locales/es.json @@ -741,5 +741,12 @@ "domain_config_acme_eligible": "Elegibilidad 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}" + "invalid_shell": "Shell inválido: {shell}", + "log_settings_reset": "Restablecer ajuste", + "migration_description_0026_new_admins_group": "Migrar al nuevo sistema de 'varios administradores'", + "visitors": "Visitantes", + "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" } From 31bc4d4f43dd19d5d18592af25a8fd94d02e2876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alperen=20=C4=B0sa=20Nalbant?= Date: Mon, 30 Jan 2023 13:11:18 +0000 Subject: [PATCH 061/319] Translated using Weblate (Turkish) Currently translated at 2.2% (17 of 750 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/tr/ --- locales/tr.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/locales/tr.json b/locales/tr.json index 3ba829b95..6768f95e4 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -5,5 +5,15 @@ "already_up_to_date": "Yapılacak yeni bir şey yok. Her şey zaten güncel.", "app_action_broke_system": "Bu işlem bazı hizmetleri bozmuş olabilir: {services}", "good_practices_about_user_password": "Şimdi yeni bir kullanıcı şifresi tanımlamak üzeresiniz. Parola en az 8 karakter uzunluğunda olmalıdır - ancak daha uzun bir parola (yani bir parola) ve/veya çeşitli karakterler (büyük harf, küçük harf, rakamlar ve özel karakterler) daha iyidir.", - "aborting": "İptal ediliyor." -} \ No newline at end of file + "aborting": "İptal ediliyor.", + "app_action_failed": "{app} uygulaması için {action} eylemini çalıştırma başarısız", + "admins": "Yöneticiler", + "all_users": "Tüm YunoHost kullanıcıları", + "app_already_up_to_date": "{app} zaten güncel", + "app_already_installed": "{app} zaten kurulu", + "app_already_installed_cant_change_url": "Bu uygulama zaten kurulu. URL yalnızca bu işlev kullanarak değiştirilemez. Eğer varsa `app changeurl`'i kontrol edin.", + "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 {', '.join(required)} işlemci mimarisi üzerine kurulabilir ancak sunucunuzun işlemci mimarisi {current}." +} From 78036b555eb291562ae8af2130e0cedff17fc334 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Jan 2023 15:48:10 +0100 Subject: [PATCH 062/319] Update changelog for 11.1.3.1 --- debian/changelog | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debian/changelog b/debian/changelog index 10172fa9b..fd3bcd742 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +yunohost (11.1.3.1) testing; urgency=low + + - nginx: add xmpp-upload. and muc. server_name only if xmpp_enabled is enabled (c444dee4) + - [i18n] Translations updated for Arabic, Basque, French, Galician, Spanish, Turkish + + Thanks to all contributors <3 ! (Alperen İsa Nalbant, ButterflyOfFire, cristian amoyao, Éric Gaspar, José M, Kayou, ppr, quiwy, xabirequejo) + + -- Alexandre Aubin Mon, 30 Jan 2023 15:44:30 +0100 + yunohost (11.1.3) testing; urgency=low - helpers: Include procedures in MySQL database backup ([#1570](https://github.com/yunohost/yunohost/pull/1570)) From b8f87e372d86b7f80485a60349a9023ad5826d2b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Jan 2023 16:14:49 +0100 Subject: [PATCH 063/319] dns_exposure setting: we don't want to regenconf nginx/postfix when values change --- src/settings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/settings.py b/src/settings.py index 96f11caeb..d9ea600a4 100644 --- a/src/settings.py +++ b/src/settings.py @@ -310,7 +310,6 @@ def regen_ssowatconf(setting_name, old_value, new_value): @post_change_hook("nginx_compatibility") @post_change_hook("webadmin_allowlist_enabled") @post_change_hook("webadmin_allowlist") -@post_change_hook("dns_exposure") def reconfigure_nginx(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["nginx"]) @@ -342,7 +341,6 @@ def reconfigure_ssh_and_fail2ban(setting_name, old_value, new_value): @post_change_hook("smtp_relay_user") @post_change_hook("smtp_relay_password") @post_change_hook("postfix_compatibility") -@post_change_hook("dns_exposure") def reconfigure_postfix(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["postfix"]) From 56d3b4762b27eece9dfd37dce7dbbc03a6d1e497 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Jan 2023 16:18:15 +0100 Subject: [PATCH 064/319] dns_exposure setting: add setting description + help --- locales/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/locales/en.json b/locales/en.json index 789ec5a4b..98abb9812 100644 --- a/locales/en.json +++ b/locales/en.json @@ -401,6 +401,8 @@ "firewall_rules_cmd_failed": "Some firewall rule commands have failed. More info in log.", "global_settings_reset_success": "Reset global settings", "global_settings_setting_passwordless_sudo": "Allow admins to use 'sudo' without re-typing their passwords", + "global_settings_setting_dns_exposure": "IP versions to consider for DNS configuration and diagnosis", + "global_settings_setting_dns_exposure_help": "NB: This only affects the recommended DNS configuration and diagnosis checks. This does not affect system configurations.", "global_settings_setting_admin_strength": "Admin password strength requirements", "global_settings_setting_admin_strength_help": "These requirements are only enforced when initializing or changing the password", "global_settings_setting_backup_compress_tar_archives": "Compress backups", From 26e539fea63f784955e52b567a4a8bdf68e2a547 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Jan 2023 16:31:42 +0100 Subject: [PATCH 065/319] Update changelog for 11.1.4 --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index fd3bcd742..a6a30947a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +yunohost (11.1.4) testing; urgency=low + + - settings: Add DNS exposure setting given the IP version ([#1451](https://github.com/yunohost/yunohost/pull/1451)) + + Thanks to all contributors <3 ! (Tagada) + + -- Alexandre Aubin Mon, 30 Jan 2023 16:28:56 +0100 + yunohost (11.1.3.1) testing; urgency=low - nginx: add xmpp-upload. and muc. server_name only if xmpp_enabled is enabled (c444dee4) From 82d30f02e208f36433e5689d205ddd174555df98 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Jan 2023 17:46:29 +0100 Subject: [PATCH 066/319] debian: don't dump upgradable apps during postinst's catalog update --- debian/postinst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/postinst b/debian/postinst index 353317488..238817cd7 100644 --- a/debian/postinst +++ b/debian/postinst @@ -30,7 +30,7 @@ do_configure() { yunohost diagnosis run --force echo "Refreshing app catalog..." - yunohost tools update apps || true + yunohost tools update apps --output-as none || true fi # Trick to let yunohost handle the restart of the API, From 2d024557a5784de37534cbca1461dbdc90ec68ca Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Mon, 30 Jan 2023 17:34:24 +0000 Subject: [PATCH 067/319] [CI] Format code with Black --- src/diagnosers/10-ip.py | 20 ++++++++++++++++---- src/diagnosers/14-ports.py | 10 ++++++++-- src/diagnosers/21-web.py | 18 ++++++++++++++---- src/diagnosers/24-mail.py | 8 ++++++-- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index 6b35731a0..a4df11dde 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -120,12 +120,16 @@ class MyDiagnoser(Diagnoser): return local_ip def is_ipvx_important(x): - return settings_get("misc.network.dns_exposure") in ["both", "ipv"+str(x)] + return settings_get("misc.network.dns_exposure") in ["both", "ipv" + str(x)] yield dict( meta={"test": "ipv4"}, data={"global": ipv4, "local": get_local_ip("ipv4")}, - status="SUCCESS" if ipv4 else "ERROR" if is_ipvx_important(4) else "WARNING", + status="SUCCESS" + if ipv4 + else "ERROR" + if is_ipvx_important(4) + else "WARNING", summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None, ) @@ -133,11 +137,19 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv6"}, data={"global": ipv6, "local": get_local_ip("ipv6")}, - status="SUCCESS" if ipv6 else "ERROR" if is_ipvx_important(6) else "WARNING", + status="SUCCESS" + if ipv6 + else "ERROR" + if is_ipvx_important(6) + else "WARNING", summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv6 - else ["diagnosis_ip_no_ipv6_tip_important" if is_ipvx_important(6) else "diagnosis_ip_no_ipv6_tip"], + else [ + "diagnosis_ip_no_ipv6_tip_important" + if is_ipvx_important(6) + else "diagnosis_ip_no_ipv6_tip" + ], ) # TODO / FIXME : add some attempt to detect ISP (using whois ?) ? diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index 57fb7cd98..b3ea3d48d 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -47,7 +47,10 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" or settings_get("misc.network.dns_exposure") != "ipv6": + if ( + ipv4.get("status") == "SUCCESS" + or settings_get("misc.network.dns_exposure") != "ipv6" + ): ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -121,7 +124,10 @@ class MyDiagnoser(Diagnoser): for record in dnsrecords.get("items", []) ) - if (failed == 4 and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]) or (failed == 6 and ipv6_is_important()): + if ( + failed == 4 + and settings_get("misc.network.dns_exposure") in ["both", "ipv4"] + ) or (failed == 6 and ipv6_is_important()): yield dict( meta={"port": port}, data={ diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index 25554fe9d..64775180c 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -77,7 +77,9 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: + if ipv4.get("status") == "SUCCESS" and settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv4"]: ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -97,7 +99,10 @@ class MyDiagnoser(Diagnoser): # "curl --head the.global.ip" will simply timeout... if self.do_hairpinning_test: global_ipv4 = ipv4.get("data", {}).get("global", None) - if global_ipv4 and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: + if global_ipv4 and settings_get("misc.network.dns_exposure") in [ + "both", + "ipv4", + ]: try: requests.head("http://" + global_ipv4, timeout=5) except requests.exceptions.Timeout: @@ -148,7 +153,10 @@ class MyDiagnoser(Diagnoser): if all( results[ipversion][domain]["status"] == "ok" for ipversion in ipversions ): - if 4 in ipversions and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: + if 4 in ipversions and settings_get("misc.network.dns_exposure") in [ + "both", + "ipv4", + ]: self.do_hairpinning_test = True yield dict( meta={"domain": domain}, @@ -186,7 +194,9 @@ class MyDiagnoser(Diagnoser): ) AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") - return AAAA_status in ["OK", "WRONG"] or settings_get("misc.network.dns_exposure") in ["both", "ipv6"] + return AAAA_status in ["OK", "WRONG"] or settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv6"] if failed == 4 or ipv6_is_important_for_this_domain(): yield dict( diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 1ae1da885..785f33703 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -302,13 +302,17 @@ class MyDiagnoser(Diagnoser): outgoing_ipversions = [] outgoing_ips = [] ipv4 = Diagnoser.get_cached_report("ip", {"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS" and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: + if ipv4.get("status") == "SUCCESS" and settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv4"]: outgoing_ipversions.append(4) global_ipv4 = ipv4.get("data", {}).get("global", {}) if global_ipv4: outgoing_ips.append(global_ipv4) - if settings_get("email.smtp.smtp_allow_ipv6") or settings_get("misc.network.dns_exposure") in ["both", "ipv6"]: + if settings_get("email.smtp.smtp_allow_ipv6") or settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv6"]: ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": outgoing_ipversions.append(6) From 90aa55599d8b35b5f06e54ebc5f754000af1a0c3 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 31 Jan 2023 17:56:32 +0100 Subject: [PATCH 068/319] Output checksums if ynh_setup_source fails during their verification. --- helpers/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/utils b/helpers/utils index 344493ff3..52ff245c8 100644 --- a/helpers/utils +++ b/helpers/utils @@ -167,7 +167,7 @@ ynh_setup_source() { || ynh_die --message="$out" # Check the control sum echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status \ - || ynh_die --message="Corrupt source" + || ynh_die --message="Corrupt source for ${src_filename}: Expected ${src_sum} but got $(${src_sumprg} ${src_filename} | cut --delimiter=' ' --fields=1)." fi # Keep files to be backup/restored at the end of the helper From 7dd2b41eeff69cff92c3ad01d4de76b32d210611 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 31 Jan 2023 18:07:25 +0100 Subject: [PATCH 069/319] Print size in error message if ynh_setup_source checksum fails Co-authored-by: Alexandre Aubin --- helpers/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/utils b/helpers/utils index 52ff245c8..bc83888e9 100644 --- a/helpers/utils +++ b/helpers/utils @@ -167,7 +167,7 @@ ynh_setup_source() { || ynh_die --message="$out" # Check the control sum echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status \ - || ynh_die --message="Corrupt source for ${src_filename}: Expected ${src_sum} but got $(${src_sumprg} ${src_filename} | cut --delimiter=' ' --fields=1)." + || ynh_die --message="Corrupt source for ${src_filename}: Expected ${src_sum} but got $(${src_sumprg} ${src_filename} | cut --delimiter=' ' --fields=1) (size: $(du -hs ${src_filename} | cut --delimiter=' ' --fields=1))." fi # Keep files to be backup/restored at the end of the helper From c990cee63027f1c8669e72b4a65ab697c0279155 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 31 Jan 2023 18:17:08 +0100 Subject: [PATCH 070/319] metronome: Auto-enable/disable metronome if there's no/at least one domain configured for XMPP --- hooks/conf_regen/12-metronome | 18 ++++++++++++++++-- src/service.py | 4 ++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/hooks/conf_regen/12-metronome b/hooks/conf_regen/12-metronome index cad8d3805..b039ace31 100755 --- a/hooks/conf_regen/12-metronome +++ b/hooks/conf_regen/12-metronome @@ -74,8 +74,22 @@ do_post_regen() { chown -R metronome: /var/lib/metronome/ chown -R metronome: /etc/metronome/conf.d/ - [[ -z "$regen_conf_files" ]] \ - || systemctl restart metronome + if [[ -z "$(ls /etc/metronome/conf.d/*.cfg.lua 2>/dev/null)" ]] + then + if systemctl is-enabled metronome &>/dev/null + then + systemctl disable metronome --now 2>/dev/null + fi + else + if ! systemctl is-enabled metronome &>/dev/null + then + systemctl enable metronome --now 2>/dev/null + sleep 3 + fi + + [[ -z "$regen_conf_files" ]] \ + || systemctl restart metronome + fi } do_$1_regen ${@:2} diff --git a/src/service.py b/src/service.py index 1f1c35c44..e11c2b609 100644 --- a/src/service.py +++ b/src/service.py @@ -712,6 +712,10 @@ def _get_services(): "category": "web", } + # Ignore metronome entirely if XMPP was disabled on all domains + if "metronome" in services and not glob("/etc/metronome/conf.d/*.cfg.lua"): + del services["metronome"] + # Remove legacy /var/log/daemon.log and /var/log/syslog from log entries # because they are too general. Instead, now the journalctl log is # returned by default which is more relevant. From 971b1b044ef32b9abe034859f802e8c81023f4a2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 1 Feb 2023 17:11:31 +0100 Subject: [PATCH 071/319] Update changelog for 11.1.4.1 --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index a6a30947a..637a74bfd 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (11.1.4.1) testing; urgency=low + + - debian: don't dump upgradable apps during postinst's catalog update (82d30f02) + - ynh_setup_source: Output checksums when source is 'corrupt' ([#1578](https://github.com/yunohost/yunohost/pull/1578)) + - metronome: Auto-enable/disable metronome if there's no/at least one domain configured for XMPP (c990cee6) + + Thanks to all contributors <3 ! (tituspijean) + + -- Alexandre Aubin Wed, 01 Feb 2023 17:10:32 +0100 + yunohost (11.1.4) testing; urgency=low - settings: Add DNS exposure setting given the IP version ([#1451](https://github.com/yunohost/yunohost/pull/1451)) From ade92e431d7f95a297cddd8ac37311bcc3f65336 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 1 Feb 2023 17:55:32 +0100 Subject: [PATCH 072/319] diagnosis: we can't yield an ERROR if there's no IPv6, otherwise that blocks all subsequent network-related diagnoser because of the dependency system ... --- src/diagnosers/10-ip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index a4df11dde..255b1165f 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -140,7 +140,7 @@ class MyDiagnoser(Diagnoser): status="SUCCESS" if ipv6 else "ERROR" - if is_ipvx_important(6) + if settings_get("misc.network.dns_exposure") == "ipv6" else "WARNING", summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6", details=["diagnosis_ip_global", "diagnosis_ip_local"] From b943c69c8be3b49626a84b074f9e61e3d925fbcd Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Wed, 1 Feb 2023 17:10:08 +0000 Subject: [PATCH 073/319] [CI] Format code with Black --- doc/generate_bash_completion.py | 2 -- doc/generate_helper_doc.py | 8 ----- doc/generate_manpages.py | 1 - doc/generate_resource_doc.py | 1 - maintenance/autofix_locale_format.py | 4 --- maintenance/missing_i18n_keys.py | 2 -- src/__init__.py | 4 --- src/app.py | 36 ------------------- src/app_catalog.py | 3 -- src/authenticators/ldap_admin.py | 5 --- src/backup.py | 9 ----- src/certificate.py | 12 ------- src/diagnosers/00-basesystem.py | 5 --- src/diagnosers/10-ip.py | 4 --- src/diagnosers/12-dnsrecords.py | 6 ---- src/diagnosers/14-ports.py | 2 -- src/diagnosers/21-web.py | 5 --- src/diagnosers/24-mail.py | 2 -- src/diagnosers/30-services.py | 3 -- src/diagnosers/50-systemresources.py | 3 -- src/diagnosers/70-regenconf.py | 3 -- src/diagnosers/80-apps.py | 4 --- src/diagnosis.py | 16 --------- src/dns.py | 12 ------- src/domain.py | 9 ----- src/dyndns.py | 2 -- src/firewall.py | 2 -- src/hook.py | 4 --- src/log.py | 6 ---- src/migrations/0021_migrate_to_bullseye.py | 6 ---- src/migrations/0022_php73_to_php74_pools.py | 2 -- src/migrations/0023_postgresql_11_to_13.py | 4 --- src/migrations/0024_rebuild_python_venv.py | 3 -- .../0025_global_settings_to_configpanel.py | 1 - src/migrations/0026_new_admins_group.py | 1 - src/permission.py | 4 --- src/regenconf.py | 4 --- src/service.py | 7 ---- src/settings.py | 6 ---- src/tests/conftest.py | 3 -- src/tests/test_app_catalog.py | 21 ----------- src/tests/test_app_config.py | 13 ------- src/tests/test_app_resources.py | 20 ----------- src/tests/test_apps.py | 36 ------------------- src/tests/test_appurl.py | 6 ---- src/tests/test_backuprestore.py | 33 ----------------- src/tests/test_dns.py | 3 -- src/tests/test_domains.py | 3 -- src/tests/test_ldapauth.py | 5 --- src/tests/test_permission.py | 3 -- src/tests/test_questions.py | 11 ------ src/tests/test_regenconf.py | 8 ----- src/tests/test_service.py | 15 -------- src/tests/test_settings.py | 2 -- src/tests/test_user-group.py | 4 --- src/tools.py | 17 --------- src/user.py | 11 ------ src/utils/config.py | 15 -------- src/utils/dns.py | 3 -- src/utils/error.py | 5 --- src/utils/ldap.py | 1 - src/utils/legacy.py | 7 ---- src/utils/network.py | 3 -- src/utils/password.py | 3 -- src/utils/resources.py | 28 --------------- src/utils/system.py | 7 ---- src/utils/yunopaste.py | 1 - 67 files changed, 500 deletions(-) diff --git a/doc/generate_bash_completion.py b/doc/generate_bash_completion.py index d55973010..88aa273fd 100644 --- a/doc/generate_bash_completion.py +++ b/doc/generate_bash_completion.py @@ -31,7 +31,6 @@ def get_dict_actions(OPTION_SUBTREE, category): with open(ACTIONSMAP_FILE, "r") as stream: - # Getting the dictionary containning what actions are possible per category OPTION_TREE = yaml.safe_load(stream) @@ -65,7 +64,6 @@ with open(ACTIONSMAP_FILE, "r") as stream: os.makedirs(BASH_COMPLETION_FOLDER, exist_ok=True) with open(BASH_COMPLETION_FILE, "w") as generated_file: - # header of the file generated_file.write("#\n") generated_file.write("# completion for yunohost\n") diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index 525482596..63fa109e6 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -20,7 +20,6 @@ def get_current_commit(): def render(helpers): - current_commit = get_current_commit() data = { @@ -56,20 +55,17 @@ def render(helpers): class Parser: def __init__(self, filename): - self.filename = filename self.file = open(filename, "r").readlines() self.blocks = None def parse_blocks(self): - self.blocks = [] current_reading = "void" current_block = {"name": None, "line": -1, "comments": [], "code": []} for i, line in enumerate(self.file): - if line.startswith("#!/bin/bash"): continue @@ -117,7 +113,6 @@ class Parser: current_reading = "code" elif current_reading == "code": - if line == "}": # We're getting out of the function current_reading = "void" @@ -138,7 +133,6 @@ class Parser: continue def parse_block(self, b): - b["brief"] = "" b["details"] = "" b["usage"] = "" @@ -164,7 +158,6 @@ class Parser: elif subblock.startswith("usage"): for line in subblock.split("\n"): - if line.startswith("| arg"): linesplit = line.split() argname = linesplit[2] @@ -216,7 +209,6 @@ def malformed_error(line_number): def main(): - helper_files = sorted(glob.glob("../helpers/*")) helpers = [] diff --git a/doc/generate_manpages.py b/doc/generate_manpages.py index bdb1fcaee..782dd8a90 100644 --- a/doc/generate_manpages.py +++ b/doc/generate_manpages.py @@ -60,7 +60,6 @@ def main(): # man pages of "yunohost *" with open(ACTIONSMAP_FILE, "r") as actionsmap: - # Getting the dictionary containning what actions are possible per domain actionsmap = ordered_yaml_load(actionsmap) diff --git a/doc/generate_resource_doc.py b/doc/generate_resource_doc.py index 1e16a76d9..2063c4ab9 100644 --- a/doc/generate_resource_doc.py +++ b/doc/generate_resource_doc.py @@ -3,7 +3,6 @@ from yunohost.utils.resources import AppResourceClassesByType resources = sorted(AppResourceClassesByType.values(), key=lambda r: r.priority) for klass in resources: - doc = klass.__doc__.replace("\n ", "\n") print("") diff --git a/maintenance/autofix_locale_format.py b/maintenance/autofix_locale_format.py index 1c56ea386..caa36f9f2 100644 --- a/maintenance/autofix_locale_format.py +++ b/maintenance/autofix_locale_format.py @@ -32,7 +32,6 @@ def autofix_i18n_placeholders(): # We iterate over all keys/string in en.json for key, string in reference.items(): - # Ignore check if there's no translation yet for this key if key not in this_locale: continue @@ -89,7 +88,6 @@ Please fix it manually ! def autofix_orthotypography_and_standardized_words(): def reformat(lang, transformations): - locale = open(f"{LOCALE_FOLDER}{lang}.json").read() for pattern, replace in transformations.items(): locale = re.compile(pattern).sub(replace, locale) @@ -146,11 +144,9 @@ def autofix_orthotypography_and_standardized_words(): def remove_stale_translated_strings(): - reference = json.loads(open(LOCALE_FOLDER + "en.json").read()) for locale_file in TRANSLATION_FILES: - print(locale_file) this_locale = json.loads( open(LOCALE_FOLDER + locale_file).read(), object_pairs_hook=OrderedDict diff --git a/maintenance/missing_i18n_keys.py b/maintenance/missing_i18n_keys.py index f49fc923e..2ed7fd141 100644 --- a/maintenance/missing_i18n_keys.py +++ b/maintenance/missing_i18n_keys.py @@ -19,7 +19,6 @@ REFERENCE_FILE = LOCALE_FOLDER + "en.json" def find_expected_string_keys(): - # Try to find : # m18n.n( "foo" # YunohostError("foo" @@ -197,7 +196,6 @@ undefined_keys = sorted(undefined_keys) mode = sys.argv[1].strip("-") if mode == "check": - # Unused keys are not too problematic, will be automatically # removed by the other autoreformat script, # but still informative to display them diff --git a/src/__init__.py b/src/__init__.py index af18e1fe4..4d4026fdf 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -32,7 +32,6 @@ def is_installed(): def cli(debug, quiet, output_as, timeout, args, parser): - init_logging(interface="cli", debug=debug, quiet=quiet) # Check that YunoHost is installed @@ -51,7 +50,6 @@ def cli(debug, quiet, output_as, timeout, args, parser): def api(debug, host, port): - init_logging(interface="api", debug=debug) def is_installed_api(): @@ -71,7 +69,6 @@ def api(debug, host, port): def check_command_is_valid_before_postinstall(args): - allowed_if_not_postinstalled = [ "tools postinstall", "tools versions", @@ -109,7 +106,6 @@ def init_i18n(): def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yunohost"): - logfile = os.path.join(logdir, "yunohost-%s.log" % interface) if not os.path.isdir(logdir): diff --git a/src/app.py b/src/app.py index 5b2e63e44..205dec505 100644 --- a/src/app.py +++ b/src/app.py @@ -238,7 +238,6 @@ def app_info(app, full=False, upgradable=False): def _app_upgradable(app_infos): - # Determine upgradability app_in_catalog = app_infos.get("from_catalog") @@ -374,7 +373,6 @@ def app_map(app=None, raw=False, user=None): ) for url in perm_all_urls: - # Here, we decide to completely ignore regex-type urls ... # Because : # - displaying them in regular "yunohost app map" output creates @@ -716,7 +714,6 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False ), ) finally: - # If upgrade failed, try to restore the safety backup if ( upgrade_failed @@ -762,7 +759,6 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False # If upgrade failed or broke the system, # raise an error and interrupt all other pending upgrades if upgrade_failed or broke_the_system: - # display this if there are remaining apps if apps[number + 1 :]: not_upgraded_apps = apps[number:] @@ -843,7 +839,6 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False def app_manifest(app, with_screenshot=False): - manifest, extracted_app_folder = _extract_app(app) raw_questions = manifest.get("install", {}).values() @@ -886,7 +881,6 @@ def app_manifest(app, with_screenshot=False): def _confirm_app_install(app, force=False): - # Ignore if there's nothing for confirm (good quality app), if --force is used # or if request on the API (confirm already implemented on the API side) if force or Moulinette.interface.type == "api": @@ -1036,7 +1030,6 @@ def app_install( # If packaging_format v2+, save all install questions as settings if packaging_format >= 2: for question in questions: - # Except user-provider passwords if question.type == "password": continue @@ -1135,7 +1128,6 @@ def app_install( # If the install failed or broke the system, we remove it if install_failed or broke_the_system: - # This option is meant for packagers to debug their apps more easily if no_remove_on_failure: raise YunohostError( @@ -1390,7 +1382,6 @@ def app_setting(app, key, value=None, delete=False): ) if is_legacy_permission_setting: - from yunohost.permission import ( user_permission_list, user_permission_update, @@ -1433,7 +1424,6 @@ def app_setting(app, key, value=None, delete=False): # 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 @@ -1445,7 +1435,6 @@ def app_setting(app, key, value=None, delete=False): else: user_permission_update(app + ".main", remove="visitors") else: - urls = urls.split(",") if key.endswith("_regex"): urls = ["re:" + url for url in urls] @@ -1604,7 +1593,6 @@ def app_ssowatconf(): ) for app in _installed_apps(): - app_settings = read_yaml(APPS_SETTING_PATH + app + "/settings.yml") or {} # Redirected @@ -1630,7 +1618,6 @@ def app_ssowatconf(): # New permission system for perm_name, perm_info in all_permissions.items(): - uris = ( [] + ([perm_info["url"]] if perm_info["url"] else []) @@ -1694,13 +1681,11 @@ def app_change_label(app, new_label): def app_action_list(app): - return AppConfigPanel(app).list_actions() @is_unit_operation() def app_action_run(operation_logger, app, action, args=None, args_file=None): - return AppConfigPanel(app).run_action( action, args=args, args_file=args_file, operation_logger=operation_logger ) @@ -2036,12 +2021,10 @@ def _get_manifest_of_app(path): def _parse_app_doc_and_notifications(path): - doc = {} notification_names = ["PRE_INSTALL", "POST_INSTALL", "PRE_UPGRADE", "POST_UPGRADE"] for filepath in glob.glob(os.path.join(path, "doc") + "/*.md"): - # to be improved : [a-z]{2,3} is a clumsy way of parsing the # lang code ... some lang code are more complex that this é_è m = re.match("([A-Z]*)(_[a-z]{2,3})?.md", filepath.split("/")[-1]) @@ -2091,11 +2074,9 @@ def _parse_app_doc_and_notifications(path): def _hydrate_app_template(template, data): - stuff_to_replace = set(re.findall(r"__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__", template)) for stuff in stuff_to_replace: - varname = stuff.strip("_").lower() if varname in data: @@ -2105,7 +2086,6 @@ def _hydrate_app_template(template, data): def _convert_v1_manifest_to_v2(manifest): - manifest = copy.deepcopy(manifest) if "upstream" not in manifest: @@ -2186,7 +2166,6 @@ def _convert_v1_manifest_to_v2(manifest): def _set_default_ask_questions(questions, script_name="install"): - # arguments is something like # { "domain": # { @@ -2244,7 +2223,6 @@ def _set_default_ask_questions(questions, script_name="install"): def _is_app_repo_url(string: str) -> bool: - string = string.strip() # Dummy test for ssh-based stuff ... should probably be improved somehow @@ -2261,7 +2239,6 @@ def _app_quality(src: str) -> str: raw_app_catalog = _load_apps_catalog()["apps"] if src in raw_app_catalog or _is_app_repo_url(src): - # If we got an app name directly (e.g. just "wordpress"), we gonna test this name if src in raw_app_catalog: app_name_to_test = src @@ -2274,7 +2251,6 @@ def _app_quality(src: str) -> str: return "thirdparty" if app_name_to_test in raw_app_catalog: - state = raw_app_catalog[app_name_to_test].get("state", "notworking") level = raw_app_catalog[app_name_to_test].get("level", None) if state in ["working", "validated"]: @@ -2385,7 +2361,6 @@ def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: def _extract_app_from_gitrepo( url: str, branch: Optional[str] = None, revision: str = "HEAD", app_info: Dict = {} ) -> Tuple[Dict, str]: - logger.debug("Checking default branch") try: @@ -2635,7 +2610,6 @@ def _check_manifest_requirements( def _guess_webapp_path_requirement(app_folder: str) -> str: - # If there's only one "domain" and "path", validate that domain/path # is an available url and normalize the path. @@ -2681,7 +2655,6 @@ def _guess_webapp_path_requirement(app_folder: str) -> str: def _validate_webpath_requirement( args: Dict[str, Any], path_requirement: str, ignore_app=None ) -> None: - domain = args.get("domain") path = args.get("path") @@ -2729,7 +2702,6 @@ def _get_conflicting_apps(domain, path, ignore_app=None): def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False): - conflicts = _get_conflicting_apps(domain, path, ignore_app) if conflicts: @@ -2748,7 +2720,6 @@ def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False def _make_environment_for_app_script( app, args={}, args_prefix="APP_ARG_", workdir=None, action=None ): - app_setting_path = os.path.join(APPS_SETTING_PATH, app) manifest = _get_manifest_of_app(app_setting_path) @@ -2777,7 +2748,6 @@ def _make_environment_for_app_script( if manifest["packaging_format"] >= 2: env_dict["app"] = app for setting_name, setting_value in _get_app_settings(app).items(): - # Ignore special internal settings like checksum__ # (not a huge deal to load them but idk...) if setting_name.startswith("checksum__"): @@ -2822,7 +2792,6 @@ def _parse_app_instance_name(app_instance_name: str) -> Tuple[str, int]: def _next_instance_number_for_app(app): - # Get list of sibling apps, such as {app}, {app}__2, {app}__4 apps = _installed_apps() sibling_app_ids = [a for a in apps if a == app or a.startswith(f"{app}__")] @@ -2840,7 +2809,6 @@ def _next_instance_number_for_app(app): def _make_tmp_workdir_for_app(app=None): - # Create parent dir if it doesn't exists yet if not os.path.exists(APP_TMP_WORKDIRS): os.makedirs(APP_TMP_WORKDIRS) @@ -2870,12 +2838,10 @@ def _make_tmp_workdir_for_app(app=None): def unstable_apps(): - output = [] deprecated_apps = ["mailman", "ffsync"] for infos in app_list(full=True)["apps"]: - if ( not infos.get("from_catalog") or infos.get("from_catalog").get("state") @@ -2891,7 +2857,6 @@ def unstable_apps(): def _assert_system_is_sane_for_app(manifest, when): - from yunohost.service import service_status logger.debug("Checking that required services are up and running...") @@ -2954,7 +2919,6 @@ def _assert_system_is_sane_for_app(manifest, when): def app_dismiss_notification(app, name): - assert isinstance(name, str) name = name.lower() assert name in ["post_install", "post_upgrade"] diff --git a/src/app_catalog.py b/src/app_catalog.py index 5d4378544..59d2ebdc1 100644 --- a/src/app_catalog.py +++ b/src/app_catalog.py @@ -157,7 +157,6 @@ def _read_apps_catalog_list(): def _actual_apps_catalog_api_url(base_url): - return f"{base_url}/v{APPS_CATALOG_API_VERSION}/apps.json" @@ -269,7 +268,6 @@ def _load_apps_catalog(): merged_catalog = {"apps": {}, "categories": [], "antifeatures": []} for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]: - # Let's load the json from cache for this catalog cache_file = f"{APPS_CATALOG_CACHE}/{apps_catalog_id}.json" @@ -298,7 +296,6 @@ def _load_apps_catalog(): # Add apps from this catalog to the output for app, info in apps_catalog_content["apps"].items(): - # (N.B. : there's a small edge case where multiple apps catalog could be listing the same apps ... # in which case we keep only the first one found) if app in merged_catalog["apps"]: diff --git a/src/authenticators/ldap_admin.py b/src/authenticators/ldap_admin.py index 22b796e23..8637b3833 100644 --- a/src/authenticators/ldap_admin.py +++ b/src/authenticators/ldap_admin.py @@ -38,14 +38,12 @@ AUTH_DN = "uid={uid},ou=users,dc=yunohost,dc=org" class Authenticator(BaseAuthenticator): - name = "ldap_admin" def __init__(self, *args, **kwargs): pass def _authenticate_credentials(self, credentials=None): - try: admins = ( _get_ldap_interface() @@ -125,7 +123,6 @@ class Authenticator(BaseAuthenticator): con.unbind_s() def set_session_cookie(self, infos): - from bottle import response assert isinstance(infos, dict) @@ -145,7 +142,6 @@ class Authenticator(BaseAuthenticator): ) def get_session_cookie(self, raise_if_no_session_exists=True): - from bottle import request try: @@ -174,7 +170,6 @@ class Authenticator(BaseAuthenticator): return infos def delete_session_cookie(self): - from bottle import response response.set_cookie("yunohost.admin", "", max_age=-1) diff --git a/src/backup.py b/src/backup.py index c3e47bddc..0783996b9 100644 --- a/src/backup.py +++ b/src/backup.py @@ -93,7 +93,6 @@ class BackupRestoreTargetsManager: """ def __init__(self): - self.targets = {} self.results = {"system": {}, "apps": {}} @@ -349,7 +348,6 @@ class BackupManager: if not os.path.isdir(self.work_dir): mkdir(self.work_dir, 0o750, parents=True) elif self.is_tmp_work_dir: - logger.debug( "temporary directory for backup '%s' already exists... attempting to clean it", self.work_dir, @@ -887,7 +885,6 @@ class RestoreManager: @property def success(self): - successful_apps = self.targets.list("apps", include=["Success", "Warning"]) successful_system = self.targets.list("system", include=["Success", "Warning"]) @@ -1443,7 +1440,6 @@ class RestoreManager: existing_groups = user_group_list()["groups"] for permission_name, permission_infos in permissions.items(): - if "allowed" not in permission_infos: logger.warning( f"'allowed' key corresponding to allowed groups for permission {permission_name} not found when restoring app {app_instance_name} … You might have to reconfigure permissions yourself." @@ -1547,7 +1543,6 @@ class RestoreManager: self.targets.set_result("apps", app_instance_name, "Success") operation_logger.success() else: - self.targets.set_result("apps", app_instance_name, "Error") remove_script = os.path.join(app_scripts_in_archive, "remove") @@ -1938,12 +1933,10 @@ class CopyBackupMethod(BackupMethod): class TarBackupMethod(BackupMethod): - method_name = "tar" @property def _archive_file(self): - if isinstance(self.manager, BackupManager) and settings_get( "misc.backup.backup_compress_tar_archives" ): @@ -2430,7 +2423,6 @@ def backup_list(with_info=False, human_readable=False): def backup_download(name): - if Moulinette.interface.type != "api": logger.error( "This option is only meant for the API/webadmin and doesn't make sense for the command line." @@ -2571,7 +2563,6 @@ def backup_info(name, with_details=False, human_readable=False): if "size_details" in info.keys(): for category in ["apps", "system"]: for name, key_info in info[category].items(): - if category == "system": # Stupid legacy fix for weird format between 3.5 and 3.6 if isinstance(key_info, dict): diff --git a/src/certificate.py b/src/certificate.py index 928bea499..0addca858 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -124,10 +124,8 @@ def certificate_install(domain_list, force=False, no_checks=False, self_signed=F def _certificate_install_selfsigned(domain_list, force=False): - failed_cert_install = [] for domain in domain_list: - operation_logger = OperationLogger( "selfsigned_cert_install", [("domain", domain)], args={"force": force} ) @@ -238,7 +236,6 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): # certificates if domains == []: for domain in domain_list()["domains"]: - status = _get_status(domain) if status["CA_type"] != "selfsigned": continue @@ -260,7 +257,6 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): # Actual install steps failed_cert_install = [] for domain in domains: - if not no_checks: try: _check_domain_is_ready_for_ACME(domain) @@ -317,7 +313,6 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): # certificates if domains == []: for domain in domain_list()["domains"]: - # Does it have a Let's Encrypt cert? status = _get_status(domain) if status["CA_type"] != "letsencrypt": @@ -342,7 +337,6 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): # Else, validate the domain list given else: for domain in domains: - # Is it in Yunohost domain list? _assert_domain_exists(domain) @@ -369,7 +363,6 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): # Actual renew steps failed_cert_install = [] for domain in domains: - if not no_checks: try: _check_domain_is_ready_for_ACME(domain) @@ -468,13 +461,11 @@ investigate : def _check_acme_challenge_configuration(domain): - domain_conf = f"/etc/nginx/conf.d/{domain}.conf" return "include /etc/nginx/conf.d/acme-challenge.conf.inc" in read_file(domain_conf) def _fetch_and_enable_new_certificate(domain, no_checks=False): - if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() @@ -628,7 +619,6 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): - cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): @@ -777,7 +767,6 @@ def _backup_current_cert(domain): def _check_domain_is_ready_for_ACME(domain): - from yunohost.domain import _get_parent_domain_of from yunohost.dns import _get_dns_zone_for_domain from yunohost.utils.dns import is_yunohost_dyndns_domain @@ -866,7 +855,6 @@ def _regen_dnsmasq_if_needed(): # For all domain files in DNSmasq conf... domainsconf = glob.glob("/etc/dnsmasq.d/*.*") for domainconf in domainsconf: - # Look for the IP, it's in the lines with this format : # host-record=the.domain.tld,11.22.33.44 for line in open(domainconf).readlines(): diff --git a/src/diagnosers/00-basesystem.py b/src/diagnosers/00-basesystem.py index 5793a00aa..8be334406 100644 --- a/src/diagnosers/00-basesystem.py +++ b/src/diagnosers/00-basesystem.py @@ -35,13 +35,11 @@ logger = log.getActionLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = [] def run(self): - virt = system_virt() if virt.lower() == "none": virt = "bare-metal" @@ -193,7 +191,6 @@ class MyDiagnoser(Diagnoser): ) def bad_sury_packages(self): - packages_to_check = ["openssl", "libssl1.1", "libssl-dev"] for package in packages_to_check: cmd = "dpkg --list | grep '^ii' | grep gbp | grep -q -w %s" % package @@ -209,12 +206,10 @@ class MyDiagnoser(Diagnoser): yield (package, version_to_downgrade_to) def backports_in_sources_list(self): - cmd = "grep -q -nr '^ *deb .*-backports' /etc/apt/sources.list*" return os.system(cmd) == 0 def number_of_recent_auth_failure(self): - # Those syslog facilities correspond to auth and authpriv # c.f. https://unix.stackexchange.com/a/401398 # and https://wiki.archlinux.org/title/Systemd/Journal#Facility diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index 255b1165f..ea68fc7bb 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -34,13 +34,11 @@ logger = log.getActionLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = [] def run(self): - # ############################################################ # # PING : Check that we can ping outside at least in ipv4 or v6 # # ############################################################ # @@ -155,7 +153,6 @@ class MyDiagnoser(Diagnoser): # TODO / FIXME : add some attempt to detect ISP (using whois ?) ? def can_ping_outside(self, protocol=4): - assert protocol in [ 4, 6, @@ -234,7 +231,6 @@ class MyDiagnoser(Diagnoser): return len(content) == 1 and content[0].split() == ["nameserver", "127.0.0.1"] def get_public_ip(self, protocol=4): - # FIXME - TODO : here we assume that DNS resolution for ip.yunohost.org is working # but if we want to be able to diagnose DNS resolution issues independently from # internet connectivity, we gotta rely on fixed IPs first.... diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py index 92d795ea9..58bd04d39 100644 --- a/src/diagnosers/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -43,13 +43,11 @@ logger = log.getActionLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = ["ip"] def run(self): - main_domain = _get_maindomain() major_domains = domain_list(exclude_subdomains=True)["domains"] @@ -77,7 +75,6 @@ class MyDiagnoser(Diagnoser): yield report def check_domain(self, domain, is_main_domain): - if is_special_use_tld(domain): yield dict( meta={"domain": domain}, @@ -97,13 +94,11 @@ class MyDiagnoser(Diagnoser): categories = ["basic", "mail", "xmpp", "extra"] for category in categories: - records = expected_configuration[category] discrepancies = [] results = {} for r in records: - id_ = r["type"] + ":" + r["name"] fqdn = r["name"] + "." + base_dns_zone if r["name"] != "@" else domain @@ -182,7 +177,6 @@ class MyDiagnoser(Diagnoser): yield output def get_current_record(self, fqdn, type_): - success, answers = dig(fqdn, type_, resolvers="force_external") if success != "ok": diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index b3ea3d48d..12f2481f7 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -25,13 +25,11 @@ from yunohost.settings import settings_get class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = ["ip"] def run(self): - # TODO: report a warning if port 53 or 5353 is exposed to the outside world... # This dict is something like : diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index 64775180c..a12a83f94 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -32,17 +32,14 @@ DIAGNOSIS_SERVER = "diagnosis.yunohost.org" class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = ["ip"] def run(self): - all_domains = domain_list()["domains"] domains_to_check = [] for domain in all_domains: - # If the diagnosis location ain't defined, can't do diagnosis, # probably because nginx conf manually modified... nginx_conf = "/etc/nginx/conf.d/%s.conf" % domain @@ -119,7 +116,6 @@ class MyDiagnoser(Diagnoser): pass def test_http(self, domains, ipversions): - results = {} for ipversion in ipversions: try: @@ -144,7 +140,6 @@ class MyDiagnoser(Diagnoser): return for domain in domains: - # i18n: diagnosis_http_bad_status_code # i18n: diagnosis_http_connection_error # i18n: diagnosis_http_timeout diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 785f33703..d48b1959e 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -39,13 +39,11 @@ logger = log.getActionLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = ["ip"] def run(self): - self.ehlo_domain = _get_maindomain() self.mail_domains = domain_list()["domains"] self.ipversions, self.ips = self.get_ips_checked() diff --git a/src/diagnosers/30-services.py b/src/diagnosers/30-services.py index 7adfd7c01..44bbf1745 100644 --- a/src/diagnosers/30-services.py +++ b/src/diagnosers/30-services.py @@ -24,17 +24,14 @@ from yunohost.service import service_status class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 dependencies: List[str] = [] def run(self): - all_result = service_status() for service, result in sorted(all_result.items()): - item = dict( meta={"service": service}, data={ diff --git a/src/diagnosers/50-systemresources.py b/src/diagnosers/50-systemresources.py index 50933b9f9..10a153c61 100644 --- a/src/diagnosers/50-systemresources.py +++ b/src/diagnosers/50-systemresources.py @@ -28,13 +28,11 @@ from yunohost.diagnosis import Diagnoser class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 dependencies: List[str] = [] def run(self): - MB = 1024**2 GB = MB * 1024 @@ -189,7 +187,6 @@ class MyDiagnoser(Diagnoser): return [] def analyzed_kern_log(): - cmd = 'tail -n 10000 /var/log/kern.log | grep "oom_reaper: reaped process" || true' out = check_output(cmd) lines = out.split("\n") if out else [] diff --git a/src/diagnosers/70-regenconf.py b/src/diagnosers/70-regenconf.py index 8c0bf74cc..7d11b9174 100644 --- a/src/diagnosers/70-regenconf.py +++ b/src/diagnosers/70-regenconf.py @@ -27,13 +27,11 @@ from moulinette.utils.filesystem import read_file class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 dependencies: List[str] = [] def run(self): - regenconf_modified_files = list(self.manually_modified_files()) if not regenconf_modified_files: @@ -82,7 +80,6 @@ class MyDiagnoser(Diagnoser): ) def manually_modified_files(self): - for category, infos in _get_regenconf_infos().items(): for path, hash_ in infos["conffiles"].items(): if hash_ != _calculate_hash(path): diff --git a/src/diagnosers/80-apps.py b/src/diagnosers/80-apps.py index faff925e6..ae89f26d3 100644 --- a/src/diagnosers/80-apps.py +++ b/src/diagnosers/80-apps.py @@ -25,13 +25,11 @@ from yunohost.diagnosis import Diagnoser class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 dependencies: List[str] = [] def run(self): - apps = app_list(full=True)["apps"] for app in apps: app["issues"] = list(self.issues(app)) @@ -44,7 +42,6 @@ class MyDiagnoser(Diagnoser): ) else: for app in apps: - if not app["issues"]: continue @@ -62,7 +59,6 @@ class MyDiagnoser(Diagnoser): ) def issues(self, app): - # Check quality level in catalog if not app.get("from_catalog") or app["from_catalog"].get("state") != "working": diff --git a/src/diagnosis.py b/src/diagnosis.py index 2dff6a40d..6b9f8fa92 100644 --- a/src/diagnosis.py +++ b/src/diagnosis.py @@ -45,7 +45,6 @@ def diagnosis_list(): def diagnosis_get(category, item): - # Get all the categories all_categories_names = _list_diagnosis_categories() @@ -69,7 +68,6 @@ def diagnosis_get(category, item): def diagnosis_show( categories=[], issues=False, full=False, share=False, human_readable=False ): - if not os.path.exists(DIAGNOSIS_CACHE): logger.warning(m18n.n("diagnosis_never_ran_yet")) return @@ -90,7 +88,6 @@ def diagnosis_show( # Fetch all reports all_reports = [] for category in categories: - try: report = Diagnoser.get_cached_report(category) except Exception as e: @@ -139,7 +136,6 @@ def diagnosis_show( def _dump_human_readable_reports(reports): - output = "" for report in reports: @@ -159,7 +155,6 @@ def _dump_human_readable_reports(reports): def diagnosis_run( categories=[], force=False, except_if_never_ran_yet=False, email=False ): - if (email or except_if_never_ran_yet) and not os.path.exists(DIAGNOSIS_CACHE): return @@ -263,7 +258,6 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): return {"ignore_filters": configuration.get("ignore_filters", {})} def validate_filter_criterias(filter_): - # Get all the categories all_categories_names = _list_diagnosis_categories() @@ -286,7 +280,6 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): return category, criterias if add_filter: - category, criterias = validate_filter_criterias(add_filter) # Fetch current issues for the requested category @@ -320,7 +313,6 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): return if remove_filter: - category, criterias = validate_filter_criterias(remove_filter) # Make sure the subdicts/lists exists @@ -394,12 +386,10 @@ def add_ignore_flag_to_issues(report): class Diagnoser: def __init__(self): - self.cache_file = Diagnoser.cache_file(self.id_) self.description = Diagnoser.get_description(self.id_) def cached_time_ago(self): - if not os.path.exists(self.cache_file): return 99999999 return time.time() - os.path.getmtime(self.cache_file) @@ -410,7 +400,6 @@ class Diagnoser: return write_to_json(self.cache_file, report) def diagnose(self, force=False): - if not force and self.cached_time_ago() < self.cache_duration: logger.debug(f"Cache still valid : {self.cache_file}") logger.info( @@ -548,7 +537,6 @@ class Diagnoser: @staticmethod def i18n(report, force_remove_html_tags=False): - # "Render" the strings with m18n.n # N.B. : we do those m18n.n right now instead of saving the already-translated report # because we can't be sure we'll redisplay the infos with the same locale as it @@ -558,7 +546,6 @@ class Diagnoser: report["description"] = Diagnoser.get_description(report["id"]) for item in report["items"]: - # For the summary and each details, we want to call # m18n() on the string, with the appropriate data for string # formatting which can come from : @@ -597,7 +584,6 @@ class Diagnoser: @staticmethod def remote_diagnosis(uri, data, ipversion, timeout=30): - # Lazy loading for performance import requests import socket @@ -646,7 +632,6 @@ class Diagnoser: def _list_diagnosis_categories(): - paths = glob.glob(os.path.dirname(__file__) + "/diagnosers/??-*.py") names = [ name.split("-")[-1] @@ -657,7 +642,6 @@ def _list_diagnosis_categories(): def _load_diagnoser(diagnoser_name): - logger.debug(f"Loading diagnoser {diagnoser_name}") paths = glob.glob(os.path.dirname(__file__) + f"/diagnosers/??-{diagnoser_name}.py") diff --git a/src/dns.py b/src/dns.py index d56e8e625..e697e6324 100644 --- a/src/dns.py +++ b/src/dns.py @@ -169,7 +169,6 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): base_dns_zone = _get_dns_zone_for_domain(base_domain) for domain, settings in domains_settings.items(): - # Domain # Base DNS zone # Basename # Suffix # # ------------------ # ----------------- # --------- # -------- # # domain.tld # domain.tld # @ # # @@ -462,7 +461,6 @@ def _get_dns_zone_for_domain(domain): # We don't wan't to do A NS request on the tld for parent in parent_list[0:-1]: - # Check if there's a NS record for that domain answer = dig(parent, rdtype="NS", full_answers=True, resolvers="force_external") @@ -503,7 +501,6 @@ 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 = { @@ -517,7 +514,6 @@ def _get_registrar_config_section(domain): # If parent domain exists in yunohost parent_domain = _get_parent_domain_of(domain, topest=True) if parent_domain: - # Dirty hack to have a link on the webadmin if Moulinette.interface.type == "api": parent_domain_link = f"[{parent_domain}](#/domains/{parent_domain}/dns)" @@ -572,7 +568,6 @@ def _get_registrar_config_section(domain): } ) else: - registrar_infos["registrar"] = OrderedDict( { "type": "alert", @@ -606,7 +601,6 @@ def _get_registrar_config_section(domain): def _get_registar_settings(domain): - _assert_domain_exists(domain) settings = domain_config_get(domain, key="dns.registrar", export=True) @@ -670,7 +664,6 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= wanted_records = [] for records in _build_dns_conf(domain).values(): for record in records: - # Make sure the name is a FQDN name = ( f"{record['name']}.{base_dns_zone}" @@ -745,7 +738,6 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= ] for record in current_records: - # Try to get rid of weird stuff like ".domain.tld" or "@.domain.tld" record["name"] = record["name"].strip("@").strip(".") @@ -795,7 +787,6 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= comparison[(record["type"], record["name"])]["wanted"].append(record) for type_and_name, records in comparison.items(): - # # Step 1 : compute a first "diff" where we remove records which are the same on both sides # @@ -939,9 +930,7 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= results = {"warnings": [], "errors": []} for action in ["delete", "create", "update"]: - for record in changes[action]: - relative_name = _get_relative_name_for_dns_zone( record["name"], base_dns_zone ) @@ -1026,7 +1015,6 @@ def _set_managed_dns_records_hashes(domain: str, hashes: list) -> None: def _hash_dns_record(record: dict) -> int: - fields = ["name", "type", "content"] record_ = {f: record.get(f) for f in fields} diff --git a/src/domain.py b/src/domain.py index fbe147fce..5728c6884 100644 --- a/src/domain.py +++ b/src/domain.py @@ -187,7 +187,6 @@ def _assert_domain_exists(domain): def _list_subdomains_of(parent_domain): - _assert_domain_exists(parent_domain) out = [] @@ -199,7 +198,6 @@ def _list_subdomains_of(parent_domain): def _get_parent_domain_of(domain, return_self=False, topest=False): - domains = _get_domains(exclude_subdomains=topest) domain_ = domain @@ -248,7 +246,6 @@ def domain_add(operation_logger, domain, dyndns=False): # DynDNS domain if dyndns: - from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.dyndns import _guess_current_dyndns_domain @@ -589,7 +586,6 @@ class DomainConfigPanel(ConfigPanel): regen_conf(names=stuff_to_regen_conf) def _get_toml(self): - toml = super()._get_toml() toml["feature"]["xmpp"]["xmpp"]["default"] = ( @@ -611,7 +607,6 @@ class DomainConfigPanel(ConfigPanel): # Cert stuff if not filter_key or filter_key[0] == "cert": - from yunohost.certificate import certificate_status status = certificate_status([self.entity], full=True)["certificates"][ @@ -638,7 +633,6 @@ class DomainConfigPanel(ConfigPanel): return toml def _load_current_values(self): - # TODO add mechanism to share some settings with other domains on the same zone super()._load_current_values() @@ -656,7 +650,6 @@ class DomainConfigPanel(ConfigPanel): def domain_action_run(domain, action, args=None): - import urllib.parse if action == "cert.cert.cert_install": @@ -671,7 +664,6 @@ def domain_action_run(domain, action, args=None): def _get_domain_settings(domain: str) -> dict: - _assert_domain_exists(domain) if os.path.exists(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml"): @@ -681,7 +673,6 @@ def _get_domain_settings(domain: str) -> dict: def _set_domain_settings(domain: str, settings: dict) -> None: - _assert_domain_exists(domain) write_to_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", settings) diff --git a/src/dyndns.py b/src/dyndns.py index 217cf2e15..9cba360ab 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -227,7 +227,6 @@ def dyndns_update( for dns_auth in DYNDNS_DNS_AUTH: for type_ in ["A", "AAAA"]: - ok, result = dig(dns_auth, type_) if ok == "ok" and len(result) and result[0]: auth_resolvers.append(result[0]) @@ -238,7 +237,6 @@ def dyndns_update( ) def resolve_domain(domain, rdtype): - ok, result = dig(domain, rdtype, resolvers=auth_resolvers) if ok == "ok": return result[0] if len(result) else None diff --git a/src/firewall.py b/src/firewall.py index 6cf68f1f7..f4d7f77fe 100644 --- a/src/firewall.py +++ b/src/firewall.py @@ -415,7 +415,6 @@ def firewall_upnp(action="status", no_refresh=False): for protocol in ["TCP", "UDP"]: if protocol + "_TO_CLOSE" in firewall["uPnP"]: for port in firewall["uPnP"][protocol + "_TO_CLOSE"]: - if not isinstance(port, int): # FIXME : how should we handle port ranges ? logger.warning("Can't use UPnP to close '%s'" % port) @@ -430,7 +429,6 @@ def firewall_upnp(action="status", no_refresh=False): firewall["uPnP"][protocol + "_TO_CLOSE"] = [] for port in firewall["uPnP"][protocol]: - if not isinstance(port, int): # FIXME : how should we handle port ranges ? logger.warning("Can't use UPnP to open '%s'" % port) diff --git a/src/hook.py b/src/hook.py index d985f5184..eb5a7c035 100644 --- a/src/hook.py +++ b/src/hook.py @@ -339,7 +339,6 @@ def hook_exec( raise YunohostError("file_does_not_exist", path=path) def is_relevant_warning(msg): - # Ignore empty warning messages... if not msg: return False @@ -389,7 +388,6 @@ def hook_exec( def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers): - from moulinette.utils.process import call_async_output # Construct command variables @@ -477,7 +475,6 @@ def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers): def _hook_exec_python(path, args, env, loggers): - dir_ = os.path.dirname(path) name = os.path.splitext(os.path.basename(path))[0] @@ -497,7 +494,6 @@ def _hook_exec_python(path, args, env, loggers): def hook_exec_with_script_debug_if_failure(*args, **kwargs): - operation_logger = kwargs.pop("operation_logger") error_message_if_failed = kwargs.pop("error_message_if_failed") error_message_if_script_failed = kwargs.pop("error_message_if_script_failed") diff --git a/src/log.py b/src/log.py index 6525b904d..f8eb65f8f 100644 --- a/src/log.py +++ b/src/log.py @@ -95,7 +95,6 @@ def log_list(limit=None, with_details=False, with_suboperations=False): logs = logs[: limit * 5] for log in logs: - base_filename = log[: -len(METADATA_FILE_EXT)] md_path = os.path.join(OPERATIONS_PATH, log) @@ -264,7 +263,6 @@ def log_show( return for filename in os.listdir(OPERATIONS_PATH): - if not filename.endswith(METADATA_FILE_EXT): continue @@ -438,7 +436,6 @@ class RedactingFormatter(Formatter): return msg def identify_data_to_redact(self, record): - # Wrapping this in a try/except because we don't want this to # break everything in case it fails miserably for some reason :s try: @@ -497,7 +494,6 @@ class OperationLogger: os.makedirs(self.path) def parent_logger(self): - # If there are other operation logger instances for instance in reversed(self._instances): # Is one of these operation logger started but not yet done ? @@ -732,7 +728,6 @@ class OperationLogger: self.error(m18n.n("log_operation_unit_unclosed_properly")) def dump_script_log_extract_for_debugging(self): - with open(self.log_path, "r") as f: lines = f.readlines() @@ -774,7 +769,6 @@ class OperationLogger: def _get_datetime_from_name(name): - # Filenames are expected to follow the format: # 20200831-170740-short_description-and-stuff diff --git a/src/migrations/0021_migrate_to_bullseye.py b/src/migrations/0021_migrate_to_bullseye.py index 54917cf95..f320577e1 100644 --- a/src/migrations/0021_migrate_to_bullseye.py +++ b/src/migrations/0021_migrate_to_bullseye.py @@ -72,13 +72,11 @@ def _backup_pip_freeze_for_python_app_venvs(): class MyMigration(Migration): - "Upgrade the system to Debian Bullseye and Yunohost 11.x" mode = "manual" def run(self): - self.check_assertions() logger.info(m18n.n("migration_0021_start")) @@ -389,7 +387,6 @@ class MyMigration(Migration): return int(get_ynh_package_version("yunohost")["version"].split(".")[0]) def check_assertions(self): - # Be on buster (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 @@ -453,7 +450,6 @@ class MyMigration(Migration): @property def disclaimer(self): - # Avoid having a super long disclaimer + uncessary check if we ain't # on buster / yunohost 4.x anymore # NB : we do both check to cover situations where the upgrade crashed @@ -494,7 +490,6 @@ class MyMigration(Migration): 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") @@ -516,7 +511,6 @@ class MyMigration(Migration): os.system(command) def get_apps_equivs_packages(self): - command = ( "dpkg --get-selections" " | grep -v deinstall" diff --git a/src/migrations/0022_php73_to_php74_pools.py b/src/migrations/0022_php73_to_php74_pools.py index a2e5eae54..dc428e504 100644 --- a/src/migrations/0022_php73_to_php74_pools.py +++ b/src/migrations/0022_php73_to_php74_pools.py @@ -27,7 +27,6 @@ MIGRATION_COMMENT = ( class MyMigration(Migration): - "Migrate php7.3-fpm 'pool' conf files to php7.4" dependencies = ["migrate_to_bullseye"] @@ -43,7 +42,6 @@ class MyMigration(Migration): oldphp_pool_files = [f for f in oldphp_pool_files if f != "www.conf"] for pf in oldphp_pool_files: - # Copy the files to the php7.3 pool src = "{}/{}".format(OLDPHP_POOLS, pf) dest = "{}/{}".format(NEWPHP_POOLS, pf) diff --git a/src/migrations/0023_postgresql_11_to_13.py b/src/migrations/0023_postgresql_11_to_13.py index f0128da0b..6d37ffa74 100644 --- a/src/migrations/0023_postgresql_11_to_13.py +++ b/src/migrations/0023_postgresql_11_to_13.py @@ -13,13 +13,11 @@ logger = getActionLogger("yunohost.migration") class MyMigration(Migration): - "Migrate DBs from Postgresql 11 to 13 after migrating to Bullseye" dependencies = ["migrate_to_bullseye"] def run(self): - if ( os.system( 'grep -A10 "ynh-deps" /var/lib/dpkg/status | grep -E "Package:|Depends:" | grep -B1 postgresql' @@ -63,7 +61,6 @@ class MyMigration(Migration): self.runcmd("systemctl start postgresql") def package_is_installed(self, package_name): - (returncode, out, err) = self.runcmd( "dpkg --list | grep '^ii ' | grep -q -w {}".format(package_name), raise_on_errors=False, @@ -71,7 +68,6 @@ class MyMigration(Migration): return returncode == 0 def runcmd(self, cmd, raise_on_errors=True): - logger.debug("Running command: " + cmd) p = subprocess.Popen( diff --git a/src/migrations/0024_rebuild_python_venv.py b/src/migrations/0024_rebuild_python_venv.py index d5aa7fc10..01a229b87 100644 --- a/src/migrations/0024_rebuild_python_venv.py +++ b/src/migrations/0024_rebuild_python_venv.py @@ -14,7 +14,6 @@ VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bullseye_upgrade.txt" def extract_app_from_venv_path(venv_path): - venv_path = venv_path.replace("/var/www/", "") venv_path = venv_path.replace("/opt/yunohost/", "") venv_path = venv_path.replace("/opt/", "") @@ -137,13 +136,11 @@ class MyMigration(Migration): return msg def run(self): - if self.mode == "auto": return venvs = _get_all_venvs("/opt/") + _get_all_venvs("/var/www/") for venv in venvs: - app_corresponding_to_venv = extract_app_from_venv_path(venv) # Search for ignore apps diff --git a/src/migrations/0025_global_settings_to_configpanel.py b/src/migrations/0025_global_settings_to_configpanel.py index 3a43ccb13..3a8818461 100644 --- a/src/migrations/0025_global_settings_to_configpanel.py +++ b/src/migrations/0025_global_settings_to_configpanel.py @@ -14,7 +14,6 @@ OLD_SETTINGS_PATH = "/etc/yunohost/settings.json" class MyMigration(Migration): - "Migrate old global settings to the new ConfigPanel global settings" dependencies = ["migrate_to_bullseye"] diff --git a/src/migrations/0026_new_admins_group.py b/src/migrations/0026_new_admins_group.py index 5d9167ae7..98f2a54be 100644 --- a/src/migrations/0026_new_admins_group.py +++ b/src/migrations/0026_new_admins_group.py @@ -21,7 +21,6 @@ class MyMigration(Migration): @Migration.ldap_migration def run(self, *args): - from yunohost.user import ( user_list, user_info, diff --git a/src/permission.py b/src/permission.py index e451bb74c..7f5a65f2e 100644 --- a/src/permission.py +++ b/src/permission.py @@ -79,7 +79,6 @@ def user_permission_list( permissions = {} for infos in permissions_infos: - name = infos["cn"][0] app = name.split(".")[0] @@ -654,7 +653,6 @@ def permission_sync_to_user(): permissions = user_permission_list(full=True)["permissions"] for permission_name, permission_infos in permissions.items(): - # These are the users currently allowed because there's an 'inheritPermission' object corresponding to it currently_allowed_users = set(permission_infos["corresponding_users"]) @@ -740,7 +738,6 @@ def _update_ldap_group_permission( update["isProtected"] = [str(protected).upper()] if show_tile is not None: - if show_tile is True: if not existing_permission["url"]: logger.warning( @@ -876,7 +873,6 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): raise YunohostValidationError("invalid_regex", regex=regex) if url.startswith("re:"): - # regex without domain # we check for the first char after 're:' if url[3] in ["/", "^", "\\"]: diff --git a/src/regenconf.py b/src/regenconf.py index f1163e66a..7acc6f58f 100644 --- a/src/regenconf.py +++ b/src/regenconf.py @@ -77,7 +77,6 @@ def regen_conf( for category, conf_files in pending_conf.items(): for system_path, pending_path in conf_files.items(): - pending_conf[category][system_path] = { "pending_conf": pending_path, "diff": _get_files_diff(system_path, pending_path, True), @@ -595,7 +594,6 @@ def _update_conf_hashes(category, hashes): def _force_clear_hashes(paths): - categories = _get_regenconf_infos() for path in paths: for category in categories.keys(): @@ -675,7 +673,6 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): def manually_modified_files(): - output = [] regenconf_categories = _get_regenconf_infos() for category, infos in regenconf_categories.items(): @@ -690,7 +687,6 @@ def manually_modified_files(): def manually_modified_files_compared_to_debian_default( ignore_handled_by_regenconf=False, ): - # from https://serverfault.com/a/90401 files = check_output( "dpkg-query -W -f='${Conffiles}\n' '*' \ diff --git a/src/service.py b/src/service.py index e11c2b609..935e87339 100644 --- a/src/service.py +++ b/src/service.py @@ -249,12 +249,10 @@ def service_reload_or_restart(names, test_conf=True): services = _get_services() for name in names: - logger.debug(f"Reloading service {name}") test_conf_cmd = services.get(name, {}).get("test_conf") if test_conf and test_conf_cmd: - p = subprocess.Popen( test_conf_cmd, shell=True, @@ -393,7 +391,6 @@ def _get_service_information_from_systemd(service): def _get_and_format_service_status(service, infos): - systemd_service = infos.get("actual_systemd_service", service) raw_status, raw_service = _get_service_information_from_systemd(systemd_service) @@ -414,7 +411,6 @@ def _get_and_format_service_status(service, infos): # If no description was there, try to get it from the .json locales if not description: - translation_key = f"service_description_{service}" if m18n.key_exists(translation_key): description = m18n.n(translation_key) @@ -521,7 +517,6 @@ def service_log(name, number=50): result["journalctl"] = _get_journalctl_logs(name, number).splitlines() for log_path in log_list: - if not os.path.exists(log_path): continue @@ -620,7 +615,6 @@ def _run_service_command(action, service): def _give_lock(action, service, p): - # Depending of the action, systemctl calls the PID differently :/ if action == "start" or action == "restart": systemctl_PID_name = "MainPID" @@ -744,7 +738,6 @@ def _save_services(services): diff = {} for service_name, service_infos in services.items(): - # Ignore php-fpm services, they are to be added dynamically by the core, # but not actually saved if service_name.startswith("php") and service_name.endswith("-fpm"): diff --git a/src/settings.py b/src/settings.py index d9ea600a4..d1203930d 100644 --- a/src/settings.py +++ b/src/settings.py @@ -59,7 +59,6 @@ def settings_get(key="", full=False, export=False): def settings_list(full=False): - settings = settings_get(full=full) if full: @@ -126,7 +125,6 @@ class SettingsConfigPanel(ConfigPanel): super().__init__("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) @@ -141,7 +139,6 @@ class SettingsConfigPanel(ConfigPanel): assert all(v not in self.future_values for v in self.virtual_settings) if root_password and root_password.strip(): - if root_password != root_password_confirm: raise YunohostValidationError("password_confirmation_not_the_same") @@ -173,7 +170,6 @@ class SettingsConfigPanel(ConfigPanel): raise def _get_toml(self): - toml = super()._get_toml() # Dynamic choice list for portal themes @@ -187,7 +183,6 @@ class SettingsConfigPanel(ConfigPanel): return toml def _load_current_values(self): - super()._load_current_values() # Specific logic for those settings who are "virtual" settings @@ -207,7 +202,6 @@ class SettingsConfigPanel(ConfigPanel): self.values["passwordless_sudo"] = False def get(self, key="", mode="classic"): - result = super().get(key=key, mode=mode) if mode == "full": diff --git a/src/tests/conftest.py b/src/tests/conftest.py index cd5cb307e..393c33564 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -51,7 +51,6 @@ old_translate = moulinette.core.Translator.translate def new_translate(self, key, *args, **kwargs): - if key not in self._translations[self.default_locale].keys(): raise KeyError("Unable to retrieve key %s for default locale !" % key) @@ -67,7 +66,6 @@ moulinette.core.Translator.translate = new_translate def pytest_cmdline_main(config): - import sys sys.path.insert(0, "/usr/lib/moulinette/") @@ -76,7 +74,6 @@ def pytest_cmdline_main(config): yunohost.init(debug=config.option.yunodebug) class DummyInterface: - type = "cli" def prompt(self, *args, **kwargs): diff --git a/src/tests/test_app_catalog.py b/src/tests/test_app_catalog.py index e9ecb1c12..f7363dabe 100644 --- a/src/tests/test_app_catalog.py +++ b/src/tests/test_app_catalog.py @@ -44,7 +44,6 @@ class AnyStringWith(str): def setup_function(function): - # Clear apps catalog cache shutil.rmtree(APPS_CATALOG_CACHE, ignore_errors=True) @@ -54,7 +53,6 @@ def setup_function(function): def teardown_function(function): - # Clear apps catalog cache # Otherwise when using apps stuff after running the test, # we'll still have the dummy unusable list @@ -67,7 +65,6 @@ def teardown_function(function): def test_apps_catalog_init(mocker): - # Cache is empty assert not glob.glob(APPS_CATALOG_CACHE + "/*") # Conf doesn't exist yet @@ -91,7 +88,6 @@ def test_apps_catalog_init(mocker): def test_apps_catalog_emptylist(): - # Initialize ... _initialize_apps_catalog_system() @@ -104,7 +100,6 @@ def test_apps_catalog_emptylist(): def test_apps_catalog_update_nominal(mocker): - # Initialize ... _initialize_apps_catalog_system() @@ -113,7 +108,6 @@ def test_apps_catalog_update_nominal(mocker): # Update with requests_mock.Mocker() as m: - _actual_apps_catalog_api_url, # Mock the server response with a dummy apps catalog m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) @@ -139,12 +133,10 @@ def test_apps_catalog_update_nominal(mocker): def test_apps_catalog_update_404(mocker): - # Initialize ... _initialize_apps_catalog_system() with requests_mock.Mocker() as m: - # 404 error m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, status_code=404) @@ -155,12 +147,10 @@ def test_apps_catalog_update_404(mocker): def test_apps_catalog_update_timeout(mocker): - # Initialize ... _initialize_apps_catalog_system() with requests_mock.Mocker() as m: - # Timeout m.register_uri( "GET", APPS_CATALOG_DEFAULT_URL_FULL, exc=requests.exceptions.ConnectTimeout @@ -173,12 +163,10 @@ def test_apps_catalog_update_timeout(mocker): def test_apps_catalog_update_sslerror(mocker): - # Initialize ... _initialize_apps_catalog_system() with requests_mock.Mocker() as m: - # SSL error m.register_uri( "GET", APPS_CATALOG_DEFAULT_URL_FULL, exc=requests.exceptions.SSLError @@ -191,12 +179,10 @@ def test_apps_catalog_update_sslerror(mocker): def test_apps_catalog_update_corrupted(mocker): - # Initialize ... _initialize_apps_catalog_system() with requests_mock.Mocker() as m: - # Corrupted json m.register_uri( "GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG[:-2] @@ -209,7 +195,6 @@ def test_apps_catalog_update_corrupted(mocker): def test_apps_catalog_load_with_empty_cache(mocker): - # Initialize ... _initialize_apps_catalog_system() @@ -218,7 +203,6 @@ def test_apps_catalog_load_with_empty_cache(mocker): # Update with requests_mock.Mocker() as m: - # Mock the server response with a dummy apps catalog m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) @@ -237,7 +221,6 @@ def test_apps_catalog_load_with_empty_cache(mocker): def test_apps_catalog_load_with_conflicts_between_lists(mocker): - # Initialize ... _initialize_apps_catalog_system() @@ -253,7 +236,6 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker): # Update with requests_mock.Mocker() as m: - # Mock the server response with a dummy apps catalog # + the same apps catalog for the second list m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) @@ -277,13 +259,11 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker): def test_apps_catalog_load_with_oudated_api_version(mocker): - # Initialize ... _initialize_apps_catalog_system() # Update with requests_mock.Mocker() as m: - mocker.spy(m18n, "n") m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) _update_apps_catalog() @@ -300,7 +280,6 @@ def test_apps_catalog_load_with_oudated_api_version(mocker): # Update with requests_mock.Mocker() as m: - # Mock the server response with a dummy apps catalog m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) diff --git a/src/tests/test_app_config.py b/src/tests/test_app_config.py index b524a7a51..4a74cbc0d 100644 --- a/src/tests/test_app_config.py +++ b/src/tests/test_app_config.py @@ -25,17 +25,14 @@ from yunohost.utils.error import YunohostError, YunohostValidationError def setup_function(function): - clean() def teardown_function(function): - clean() def clean(): - # Make sure we have a ssowat os.system("mkdir -p /etc/ssowat/") app_ssowatconf() @@ -43,7 +40,6 @@ def clean(): test_apps = ["config_app", "legacy_app"] for test_app in test_apps: - if _is_installed(test_app): app_remove(test_app) @@ -66,7 +62,6 @@ def clean(): @pytest.fixture() def legacy_app(request): - main_domain = _get_maindomain() app_install( @@ -85,7 +80,6 @@ def legacy_app(request): @pytest.fixture() def config_app(request): - app_install( os.path.join(get_test_apps_dir(), "config_app_ynh"), args="", @@ -101,7 +95,6 @@ def config_app(request): def test_app_config_get(config_app): - user_create("alice", _get_maindomain(), "test123Ynh", fullname="Alice White") assert isinstance(app_config_get(config_app), dict) @@ -115,13 +108,11 @@ def test_app_config_get(config_app): def test_app_config_nopanel(legacy_app): - with pytest.raises(YunohostValidationError): app_config_get(legacy_app) def test_app_config_get_nonexistentstuff(config_app): - with pytest.raises(YunohostValidationError): app_config_get("nonexistent") @@ -140,7 +131,6 @@ def test_app_config_get_nonexistentstuff(config_app): def test_app_config_regular_setting(config_app): - assert app_config_get(config_app, "main.components.boolean") == 0 app_config_set(config_app, "main.components.boolean", "no") @@ -160,7 +150,6 @@ def test_app_config_regular_setting(config_app): def test_app_config_bind_on_file(config_app): - # c.f. conf/test.php in the config app assert '$arg5= "Arg5 value";' in read_file("/var/www/config_app/test.php") assert app_config_get(config_app, "bind.variable.arg5") == "Arg5 value" @@ -184,7 +173,6 @@ def test_app_config_bind_on_file(config_app): def test_app_config_custom_validator(config_app): - # c.f. the config script # arg8 is a password that must be at least 8 chars assert not os.path.exists("/var/www/config_app/password") @@ -198,7 +186,6 @@ def test_app_config_custom_validator(config_app): def test_app_config_custom_set(config_app): - assert not os.path.exists("/var/www/config_app/password") assert app_setting(config_app, "arg8") is None diff --git a/src/tests/test_app_resources.py b/src/tests/test_app_resources.py index 879f6e29a..d2df647a3 100644 --- a/src/tests/test_app_resources.py +++ b/src/tests/test_app_resources.py @@ -17,7 +17,6 @@ dummyfile = "/tmp/dummyappresource-testapp" class DummyAppResource(AppResource): - type = "dummy" default_properties = { @@ -26,14 +25,12 @@ class DummyAppResource(AppResource): } def provision_or_update(self, context): - open(self.file, "w").write(self.content) if self.content == "forbiddenvalue": raise Exception("Emeged you used the forbidden value!1!£&") def deprovision(self, context): - os.system(f"rm -f {self.file}") @@ -41,7 +38,6 @@ AppResourceClassesByType["dummy"] = DummyAppResource def setup_function(function): - clean() os.system("mkdir /etc/yunohost/apps/testapp") @@ -51,12 +47,10 @@ def setup_function(function): def teardown_function(function): - clean() def clean(): - os.system(f"rm -f {dummyfile}") os.system("rm -rf /etc/yunohost/apps/testapp") os.system("rm -rf /var/www/testapp") @@ -70,7 +64,6 @@ def clean(): def test_provision_dummy(): - current = {"resources": {}} wanted = {"resources": {"dummy": {}}} @@ -82,7 +75,6 @@ def test_provision_dummy(): def test_deprovision_dummy(): - current = {"resources": {"dummy": {}}} wanted = {"resources": {}} @@ -96,7 +88,6 @@ def test_deprovision_dummy(): def test_provision_dummy_nondefaultvalue(): - current = {"resources": {}} wanted = {"resources": {"dummy": {"content": "bar"}}} @@ -108,7 +99,6 @@ def test_provision_dummy_nondefaultvalue(): def test_update_dummy(): - current = {"resources": {"dummy": {}}} wanted = {"resources": {"dummy": {"content": "bar"}}} @@ -122,7 +112,6 @@ def test_update_dummy(): def test_update_dummy_failwithrollback(): - current = {"resources": {"dummy": {}}} wanted = {"resources": {"dummy": {"content": "forbiddenvalue"}}} @@ -137,7 +126,6 @@ def test_update_dummy_failwithrollback(): def test_resource_system_user(): - r = AppResourceClassesByType["system_user"] conf = {} @@ -161,7 +149,6 @@ def test_resource_system_user(): def test_resource_install_dir(): - r = AppResourceClassesByType["install_dir"] conf = {"owner": "nobody:rx", "group": "nogroup:rx"} @@ -196,7 +183,6 @@ def test_resource_install_dir(): def test_resource_data_dir(): - r = AppResourceClassesByType["data_dir"] conf = {"owner": "nobody:rx", "group": "nogroup:rx"} @@ -228,7 +214,6 @@ def test_resource_data_dir(): def test_resource_ports(): - r = AppResourceClassesByType["ports"] conf = {} @@ -244,7 +229,6 @@ def test_resource_ports(): def test_resource_ports_several(): - r = AppResourceClassesByType["ports"] conf = {"main": {"default": 12345}, "foobar": {"default": 23456}} @@ -263,7 +247,6 @@ def test_resource_ports_several(): def test_resource_ports_firewall(): - r = AppResourceClassesByType["ports"] conf = {"main": {"default": 12345}} @@ -283,7 +266,6 @@ def test_resource_ports_firewall(): def test_resource_database(): - r = AppResourceClassesByType["database"] conf = {"type": "mysql"} @@ -308,7 +290,6 @@ def test_resource_database(): def test_resource_apt(): - r = AppResourceClassesByType["apt"] conf = { "packages": "nyancat, sl", @@ -356,7 +337,6 @@ def test_resource_apt(): def test_resource_permissions(): - maindomain = _get_maindomain() os.system(f"echo 'domain: {maindomain}' >> /etc/yunohost/apps/testapp/settings.yml") os.system("echo 'path: /testapp' >> /etc/yunohost/apps/testapp/settings.yml") diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index 6efdaa0b0..965ce5892 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -28,17 +28,14 @@ from yunohost.permission import user_permission_list, permission_delete def setup_function(function): - clean() def teardown_function(function): - clean() def clean(): - # Make sure we have a ssowat os.system("mkdir -p /etc/ssowat/") app_ssowatconf() @@ -53,7 +50,6 @@ def clean(): ] for test_app in test_apps: - if _is_installed(test_app): app_remove(test_app) @@ -95,7 +91,6 @@ def check_permission_for_apps_call(): @pytest.fixture(scope="module") def secondary_domain(request): - if "example.test" not in domain_list()["domains"]: domain_add("example.test") @@ -113,7 +108,6 @@ def secondary_domain(request): def app_expected_files(domain, app): - yield "/etc/nginx/conf.d/{}.d/{}.conf".format(domain, app) if app.startswith("legacy_app"): yield "/var/www/%s/index.html" % app @@ -127,21 +121,18 @@ def app_expected_files(domain, app): def app_is_installed(domain, app): - return _is_installed(app) and all( os.path.exists(f) for f in app_expected_files(domain, app) ) def app_is_not_installed(domain, app): - return not _is_installed(app) and not all( os.path.exists(f) for f in app_expected_files(domain, app) ) def app_is_exposed_on_http(domain, path, message_in_page): - try: r = requests.get( "https://127.0.0.1" + path + "/", @@ -155,7 +146,6 @@ def app_is_exposed_on_http(domain, path, message_in_page): def install_legacy_app(domain, path, public=True): - app_install( os.path.join(get_test_apps_dir(), "legacy_app_ynh"), args="domain={}&path={}&is_public={}".format(domain, path, 1 if public else 0), @@ -164,7 +154,6 @@ def install_legacy_app(domain, path, public=True): def install_manifestv2_app(domain, path, public=True): - app_install( os.path.join(get_test_apps_dir(), "manifestv2_app_ynh"), args="domain={}&path={}&init_main_permission={}".format( @@ -175,7 +164,6 @@ def install_manifestv2_app(domain, path, public=True): def install_full_domain_app(domain): - app_install( os.path.join(get_test_apps_dir(), "full_domain_app_ynh"), args="domain=%s" % domain, @@ -184,7 +172,6 @@ def install_full_domain_app(domain): def install_break_yo_system(domain, breakwhat): - app_install( os.path.join(get_test_apps_dir(), "break_yo_system_ynh"), args="domain={}&breakwhat={}".format(domain, breakwhat), @@ -193,7 +180,6 @@ def install_break_yo_system(domain, breakwhat): def test_legacy_app_install_main_domain(): - main_domain = _get_maindomain() install_legacy_app(main_domain, "/legacy") @@ -213,7 +199,6 @@ def test_legacy_app_install_main_domain(): def test_legacy_app_manifest_preinstall(): - m = app_manifest(os.path.join(get_test_apps_dir(), "legacy_app_ynh")) # v1 manifesto are expected to have been autoconverted to v2 @@ -231,7 +216,6 @@ def test_legacy_app_manifest_preinstall(): def test_manifestv2_app_manifest_preinstall(): - m = app_manifest(os.path.join(get_test_apps_dir(), "manifestv2_app_ynh")) assert "id" in m @@ -258,7 +242,6 @@ def test_manifestv2_app_manifest_preinstall(): def test_manifestv2_app_install_main_domain(): - main_domain = _get_maindomain() install_manifestv2_app(main_domain, "/manifestv2") @@ -278,7 +261,6 @@ def test_manifestv2_app_install_main_domain(): def test_manifestv2_app_info_postinstall(): - main_domain = _get_maindomain() install_manifestv2_app(main_domain, "/manifestv2") m = app_info("manifestv2_app", full=True)["manifest"] @@ -308,13 +290,11 @@ def test_manifestv2_app_info_postinstall(): def test_manifestv2_app_info_preupgrade(monkeypatch): - manifest = app_manifest(os.path.join(get_test_apps_dir(), "manifestv2_app_ynh")) from yunohost.app_catalog import _load_apps_catalog as original_load_apps_catalog def custom_load_apps_catalog(*args, **kwargs): - res = original_load_apps_catalog(*args, **kwargs) res["apps"]["manifestv2_app"] = { "id": "manifestv2_app", @@ -372,7 +352,6 @@ def test_app_from_catalog(): def test_legacy_app_install_secondary_domain(secondary_domain): - install_legacy_app(secondary_domain, "/legacy") assert app_is_installed(secondary_domain, "legacy_app") @@ -384,7 +363,6 @@ def test_legacy_app_install_secondary_domain(secondary_domain): def test_legacy_app_install_secondary_domain_on_root(secondary_domain): - install_legacy_app(secondary_domain, "/") app_map_ = app_map(raw=True) @@ -402,7 +380,6 @@ def test_legacy_app_install_secondary_domain_on_root(secondary_domain): def test_legacy_app_install_private(secondary_domain): - install_legacy_app(secondary_domain, "/legacy", public=False) assert app_is_installed(secondary_domain, "legacy_app") @@ -416,7 +393,6 @@ def test_legacy_app_install_private(secondary_domain): def test_legacy_app_install_unknown_domain(mocker): - with pytest.raises(YunohostError): with message(mocker, "app_argument_invalid"): install_legacy_app("whatever.nope", "/legacy") @@ -425,7 +401,6 @@ def test_legacy_app_install_unknown_domain(mocker): def test_legacy_app_install_multiple_instances(secondary_domain): - install_legacy_app(secondary_domain, "/foo") install_legacy_app(secondary_domain, "/bar") @@ -447,7 +422,6 @@ def test_legacy_app_install_multiple_instances(secondary_domain): def test_legacy_app_install_path_unavailable(mocker, secondary_domain): - # These will be removed in teardown install_legacy_app(secondary_domain, "/legacy") @@ -460,7 +434,6 @@ def test_legacy_app_install_path_unavailable(mocker, secondary_domain): def test_legacy_app_install_with_nginx_down(mocker, secondary_domain): - os.system("systemctl stop nginx") with raiseYunohostError( @@ -470,7 +443,6 @@ def test_legacy_app_install_with_nginx_down(mocker, secondary_domain): def test_legacy_app_failed_install(mocker, secondary_domain): - # This will conflict with the folder that the app # attempts to create, making the install fail mkdir("/var/www/legacy_app/", 0o750) @@ -483,7 +455,6 @@ def test_legacy_app_failed_install(mocker, secondary_domain): def test_legacy_app_failed_remove(mocker, secondary_domain): - install_legacy_app(secondary_domain, "/legacy") # The remove script runs with set -eu and attempt to remove this @@ -503,14 +474,12 @@ def test_legacy_app_failed_remove(mocker, secondary_domain): def test_full_domain_app(secondary_domain): - install_full_domain_app(secondary_domain) assert app_is_exposed_on_http(secondary_domain, "/", "This is a dummy app") def test_full_domain_app_with_conflicts(mocker, secondary_domain): - install_legacy_app(secondary_domain, "/legacy") with raiseYunohostError(mocker, "app_full_domain_unavailable"): @@ -518,7 +487,6 @@ def test_full_domain_app_with_conflicts(mocker, secondary_domain): def test_systemfuckedup_during_app_install(mocker, secondary_domain): - with pytest.raises(YunohostError): with message(mocker, "app_install_failed"): with message(mocker, "app_action_broke_system"): @@ -528,7 +496,6 @@ def test_systemfuckedup_during_app_install(mocker, secondary_domain): def test_systemfuckedup_during_app_remove(mocker, secondary_domain): - install_break_yo_system(secondary_domain, breakwhat="remove") with pytest.raises(YunohostError): @@ -540,7 +507,6 @@ def test_systemfuckedup_during_app_remove(mocker, secondary_domain): def test_systemfuckedup_during_app_install_and_remove(mocker, secondary_domain): - with pytest.raises(YunohostError): with message(mocker, "app_install_failed"): with message(mocker, "app_action_broke_system"): @@ -550,7 +516,6 @@ def test_systemfuckedup_during_app_install_and_remove(mocker, secondary_domain): def test_systemfuckedup_during_app_upgrade(mocker, secondary_domain): - install_break_yo_system(secondary_domain, breakwhat="upgrade") with pytest.raises(YunohostError): @@ -562,7 +527,6 @@ def test_systemfuckedup_during_app_upgrade(mocker, secondary_domain): def test_failed_multiple_app_upgrade(mocker, secondary_domain): - install_legacy_app(secondary_domain, "/legacy") install_break_yo_system(secondary_domain, breakwhat="upgrade") diff --git a/src/tests/test_appurl.py b/src/tests/test_appurl.py index c036ae28a..351bb4e83 100644 --- a/src/tests/test_appurl.py +++ b/src/tests/test_appurl.py @@ -18,7 +18,6 @@ maindomain = _get_maindomain() def setup_function(function): - try: app_remove("register_url_app") except Exception: @@ -26,7 +25,6 @@ def setup_function(function): def teardown_function(function): - try: app_remove("register_url_app") except Exception: @@ -34,7 +32,6 @@ def teardown_function(function): def test_parse_app_instance_name(): - assert _parse_app_instance_name("yolo") == ("yolo", 1) assert _parse_app_instance_name("yolo1") == ("yolo1", 1) assert _parse_app_instance_name("yolo__0") == ("yolo__0", 1) @@ -86,7 +83,6 @@ def test_repo_url_definition(): def test_urlavailable(): - # Except the maindomain/macnuggets to be available assert domain_url_available(maindomain, "/macnuggets") @@ -96,7 +92,6 @@ def test_urlavailable(): def test_registerurl(): - app_install( os.path.join(get_test_apps_dir(), "register_url_app_ynh"), args="domain={}&path={}".format(maindomain, "/urlregisterapp"), @@ -115,7 +110,6 @@ def test_registerurl(): def test_registerurl_baddomain(): - with pytest.raises(YunohostError): app_install( os.path.join(get_test_apps_dir(), "register_url_app_ynh"), diff --git a/src/tests/test_backuprestore.py b/src/tests/test_backuprestore.py index dc37d3497..28646960c 100644 --- a/src/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -30,7 +30,6 @@ maindomain = "" def setup_function(function): - global maindomain maindomain = _get_maindomain() @@ -89,7 +88,6 @@ def setup_function(function): def teardown_function(function): - assert tmp_backup_directory_is_empty() reset_ssowat_conf() @@ -133,7 +131,6 @@ def check_permission_for_apps_call(): def app_is_installed(app): - if app == "permissions_app": return _is_installed(app) @@ -147,7 +144,6 @@ 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") @@ -161,7 +157,6 @@ def backup_test_dependencies_are_met(): def tmp_backup_directory_is_empty(): - if not os.path.exists("/home/yunohost.backup/tmp/"): return True else: @@ -169,7 +164,6 @@ def tmp_backup_directory_is_empty(): def clean_tmp_backup_directory(): - if tmp_backup_directory_is_empty(): return @@ -191,27 +185,23 @@ def clean_tmp_backup_directory(): def reset_ssowat_conf(): - # Make sure we have a ssowat os.system("mkdir -p /etc/ssowat/") app_ssowatconf() def delete_all_backups(): - for archive in backup_list()["archives"]: backup_delete(archive) def uninstall_test_apps_if_needed(): - for app in ["legacy_app", "backup_recommended_app", "wordpress", "permissions_app"]: if _is_installed(app): app_remove(app) def install_app(app, path, additionnal_args=""): - app_install( os.path.join(get_test_apps_dir(), app), args="domain={}&path={}{}".format(maindomain, path, additionnal_args), @@ -220,7 +210,6 @@ def install_app(app, path, additionnal_args=""): def add_archive_wordpress_from_4p2(): - os.system("mkdir -p /home/yunohost.backup/archives") os.system( @@ -231,7 +220,6 @@ def add_archive_wordpress_from_4p2(): def add_archive_system_from_4p2(): - os.system("mkdir -p /home/yunohost.backup/archives") os.system( @@ -247,7 +235,6 @@ def add_archive_system_from_4p2(): def test_backup_only_ldap(mocker): - # Create the backup with message(mocker, "backup_created"): backup_create(system=["conf_ldap"], apps=None) @@ -262,7 +249,6 @@ def test_backup_only_ldap(mocker): def test_backup_system_part_that_does_not_exists(mocker): - # Create the backup with message(mocker, "backup_hook_unknown", hook="doesnt_exist"): with raiseYunohostError(mocker, "backup_nothings_done"): @@ -275,7 +261,6 @@ def test_backup_system_part_that_does_not_exists(mocker): def test_backup_and_restore_all_sys(mocker): - # Create the backup with message(mocker, "backup_created"): backup_create(system=[], apps=None) @@ -309,7 +294,6 @@ def test_backup_and_restore_all_sys(mocker): @pytest.mark.with_system_archive_from_4p2 def test_restore_system_from_Ynh4p2(monkeypatch, mocker): - # Backup current system with message(mocker, "backup_created"): backup_create(system=[], apps=None) @@ -337,7 +321,6 @@ def test_restore_system_from_Ynh4p2(monkeypatch, mocker): @pytest.mark.with_backup_recommended_app_installed def test_backup_script_failure_handling(monkeypatch, mocker): def custom_hook_exec(name, *args, **kwargs): - if os.path.basename(name).startswith("backup_"): raise Exception else: @@ -373,7 +356,6 @@ def test_backup_not_enough_free_space(monkeypatch, mocker): def test_backup_app_not_installed(mocker): - assert not _is_installed("wordpress") with message(mocker, "unbackup_app", app="wordpress"): @@ -383,7 +365,6 @@ def test_backup_app_not_installed(mocker): @pytest.mark.with_backup_recommended_app_installed def test_backup_app_with_no_backup_script(mocker): - backup_script = "/etc/yunohost/apps/backup_recommended_app/scripts/backup" os.system("rm %s" % backup_script) assert not os.path.exists(backup_script) @@ -397,7 +378,6 @@ def test_backup_app_with_no_backup_script(mocker): @pytest.mark.with_backup_recommended_app_installed def test_backup_app_with_no_restore_script(mocker): - restore_script = "/etc/yunohost/apps/backup_recommended_app/scripts/restore" os.system("rm %s" % restore_script) assert not os.path.exists(restore_script) @@ -413,7 +393,6 @@ def test_backup_app_with_no_restore_script(mocker): @pytest.mark.clean_opt_dir def test_backup_with_different_output_directory(mocker): - # Create the backup with message(mocker, "backup_created"): backup_create( @@ -436,7 +415,6 @@ def test_backup_with_different_output_directory(mocker): @pytest.mark.clean_opt_dir def test_backup_using_copy_method(mocker): - # Create the backup with message(mocker, "backup_created"): backup_create( @@ -458,7 +436,6 @@ def test_backup_using_copy_method(mocker): @pytest.mark.with_wordpress_archive_from_4p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_wordpress_from_Ynh4p2(mocker): - with message(mocker, "restore_complete"): backup_restore( system=None, name=backup_list()["archives"][0], apps=["wordpress"] @@ -507,7 +484,6 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker): @pytest.mark.with_wordpress_archive_from_4p2 def test_restore_app_not_in_backup(mocker): - assert not _is_installed("wordpress") assert not _is_installed("yoloswag") @@ -524,7 +500,6 @@ def test_restore_app_not_in_backup(mocker): @pytest.mark.with_wordpress_archive_from_4p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_already_installed(mocker): - assert not _is_installed("wordpress") with message(mocker, "restore_complete"): @@ -544,25 +519,21 @@ def test_restore_app_already_installed(mocker): @pytest.mark.with_legacy_app_installed def test_backup_and_restore_legacy_app(mocker): - _test_backup_and_restore_app(mocker, "legacy_app") @pytest.mark.with_backup_recommended_app_installed def test_backup_and_restore_recommended_app(mocker): - _test_backup_and_restore_app(mocker, "backup_recommended_app") @pytest.mark.with_backup_recommended_app_installed_with_ynh_restore def test_backup_and_restore_with_ynh_restore(mocker): - _test_backup_and_restore_app(mocker, "backup_recommended_app") @pytest.mark.with_permission_app_installed def test_backup_and_restore_permission_app(mocker): - res = user_permission_list(full=True)["permissions"] assert "permissions_app.main" in res assert "permissions_app.admin" in res @@ -593,7 +564,6 @@ def test_backup_and_restore_permission_app(mocker): def _test_backup_and_restore_app(mocker, app): - # Create a backup of this app with message(mocker, "backup_created"): backup_create(system=None, apps=[app]) @@ -628,7 +598,6 @@ def _test_backup_and_restore_app(mocker, app): def test_restore_archive_with_no_json(mocker): - # Create a backup with no info.json associated os.system("touch /tmp/afile") os.system("tar -cvf /home/yunohost.backup/archives/badbackup.tar /tmp/afile") @@ -641,7 +610,6 @@ def test_restore_archive_with_no_json(mocker): @pytest.mark.with_wordpress_archive_from_4p2 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" @@ -656,7 +624,6 @@ def test_restore_archive_with_bad_archive(mocker): def test_restore_archive_with_custom_hook(mocker): - custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, "restore") os.system("touch %s/99-yolo" % custom_restore_hook_folder) diff --git a/src/tests/test_dns.py b/src/tests/test_dns.py index a23ac7982..e896d9c9f 100644 --- a/src/tests/test_dns.py +++ b/src/tests/test_dns.py @@ -12,12 +12,10 @@ from yunohost.dns import ( def setup_function(function): - clean() def teardown_function(function): - clean() @@ -76,7 +74,6 @@ def example_domain(): def test_domain_dns_suggest(example_domain): - assert _build_dns_conf(example_domain) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 95a33e0ba..b414c21d8 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -19,7 +19,6 @@ TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"] def setup_function(function): - # Save domain list in variable to avoid multiple calls to domain_list() domains = domain_list()["domains"] @@ -52,7 +51,6 @@ def setup_function(function): def teardown_function(function): - clean() @@ -102,7 +100,6 @@ def test_domain_config_get_default(): def test_domain_config_get_export(): - assert domain_config_get(TEST_DOMAINS[0], export=True)["xmpp"] == 1 assert domain_config_get(TEST_DOMAINS[1], export=True)["xmpp"] == 0 diff --git a/src/tests/test_ldapauth.py b/src/tests/test_ldapauth.py index e8a48aa6d..9e3ae36cc 100644 --- a/src/tests/test_ldapauth.py +++ b/src/tests/test_ldapauth.py @@ -10,7 +10,6 @@ from moulinette.core import MoulinetteError def setup_function(function): - for u in user_list()["users"]: user_delete(u, purge=True) @@ -24,7 +23,6 @@ def setup_function(function): def teardown_function(): - os.system("systemctl is-active slapd >/dev/null || systemctl start slapd; sleep 5") for u in user_list()["users"]: @@ -36,7 +34,6 @@ def test_authenticate(): def test_authenticate_with_no_user(): - with pytest.raises(MoulinetteError): LDAPAuth().authenticate_credentials(credentials="Yunohost") @@ -45,7 +42,6 @@ def test_authenticate_with_no_user(): def test_authenticate_with_user_who_is_not_admin(): - with pytest.raises(MoulinetteError) as exception: LDAPAuth().authenticate_credentials(credentials="bob:test123Ynh") @@ -70,7 +66,6 @@ def test_authenticate_server_down(mocker): def test_authenticate_change_password(): - LDAPAuth().authenticate_credentials(credentials="alice:Yunohost") user_update("alice", change_password="plopette") diff --git a/src/tests/test_permission.py b/src/tests/test_permission.py index acb3419c9..10bd018d2 100644 --- a/src/tests/test_permission.py +++ b/src/tests/test_permission.py @@ -354,7 +354,6 @@ def check_permission_for_apps(): def can_access_webpage(webpath, logged_as=None): - webpath = webpath.rstrip("/") sso_url = "https://" + maindomain + "/yunohost/sso/" @@ -1094,7 +1093,6 @@ def test_permission_protection_management_by_helper(): @pytest.mark.other_domains(number=1) def test_permission_app_propagation_on_ssowat(): - app_install( os.path.join(get_test_apps_dir(), "permissions_app_ynh"), args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s" @@ -1131,7 +1129,6 @@ def test_permission_app_propagation_on_ssowat(): @pytest.mark.other_domains(number=1) 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" diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index e49047469..cf7c3c6e6 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -49,7 +49,6 @@ def test_question_empty(): def test_question_string(): - questions = { "some_string": { "type": "string", @@ -65,7 +64,6 @@ def test_question_string(): def test_question_string_from_query_string(): - questions = { "some_string": { "type": "string", @@ -1539,7 +1537,6 @@ def test_question_user_two_users_default_input(): os, "isatty", return_value=True ): with patch.object(user, "user_info", return_value={}): - with patch.object(Moulinette, "prompt", return_value=username): out = ask_questions_and_parse_answers(questions, answers)[0] @@ -1843,7 +1840,6 @@ def test_question_display_text(): def test_question_file_from_cli(): - FileQuestion.clean_upload_dirs() filename = "/tmp/ynh_test_question_file" @@ -1874,7 +1870,6 @@ def test_question_file_from_cli(): def test_question_file_from_api(): - FileQuestion.clean_upload_dirs() from base64 import b64encode @@ -1907,7 +1902,6 @@ def test_question_file_from_api(): def test_normalize_boolean_nominal(): - assert BooleanQuestion.normalize("yes") == 1 assert BooleanQuestion.normalize("Yes") == 1 assert BooleanQuestion.normalize(" yes ") == 1 @@ -1937,7 +1931,6 @@ def test_normalize_boolean_nominal(): def test_normalize_boolean_humanize(): - assert BooleanQuestion.humanize("yes") == "yes" assert BooleanQuestion.humanize("true") == "yes" assert BooleanQuestion.humanize("on") == "yes" @@ -1948,7 +1941,6 @@ def test_normalize_boolean_humanize(): def test_normalize_boolean_invalid(): - with pytest.raises(YunohostValidationError): BooleanQuestion.normalize("yesno") with pytest.raises(YunohostValidationError): @@ -1958,7 +1950,6 @@ def test_normalize_boolean_invalid(): def test_normalize_boolean_special_yesno(): - customyesno = {"yes": "enabled", "no": "disabled"} assert BooleanQuestion.normalize("yes", customyesno) == "enabled" @@ -1977,14 +1968,12 @@ def test_normalize_boolean_special_yesno(): def test_normalize_domain(): - assert DomainQuestion.normalize("https://yolo.swag/") == "yolo.swag" assert DomainQuestion.normalize("http://yolo.swag") == "yolo.swag" assert DomainQuestion.normalize("yolo.swag/") == "yolo.swag" def test_normalize_path(): - assert PathQuestion.normalize("") == "/" assert PathQuestion.normalize("") == "/" assert PathQuestion.normalize("macnuggets") == "/macnuggets" diff --git a/src/tests/test_regenconf.py b/src/tests/test_regenconf.py index f454f33e3..8dda1a7f2 100644 --- a/src/tests/test_regenconf.py +++ b/src/tests/test_regenconf.py @@ -16,19 +16,16 @@ SSHD_CONFIG = "/etc/ssh/sshd_config" def setup_function(function): - _force_clear_hashes([TEST_DOMAIN_NGINX_CONFIG]) clean() def teardown_function(function): - clean() _force_clear_hashes([TEST_DOMAIN_NGINX_CONFIG]) def clean(): - assert os.system("pgrep slapd >/dev/null") == 0 assert os.system("pgrep nginx >/dev/null") == 0 @@ -48,7 +45,6 @@ def clean(): def test_add_domain(): - domain_add(TEST_DOMAIN) assert TEST_DOMAIN in domain_list()["domains"] @@ -60,7 +56,6 @@ def test_add_domain(): def test_add_and_edit_domain_conf(): - domain_add(TEST_DOMAIN) assert os.path.exists(TEST_DOMAIN_NGINX_CONFIG) @@ -73,7 +68,6 @@ def test_add_and_edit_domain_conf(): def test_add_domain_conf_already_exists(): - os.system("echo ' ' >> %s" % TEST_DOMAIN_NGINX_CONFIG) domain_add(TEST_DOMAIN) @@ -84,7 +78,6 @@ def test_add_domain_conf_already_exists(): def test_ssh_conf_unmanaged(): - _force_clear_hashes([SSHD_CONFIG]) assert SSHD_CONFIG not in _get_conf_hashes("ssh") @@ -95,7 +88,6 @@ def test_ssh_conf_unmanaged(): def test_ssh_conf_unmanaged_and_manually_modified(mocker): - _force_clear_hashes([SSHD_CONFIG]) os.system("echo ' ' >> %s" % SSHD_CONFIG) diff --git a/src/tests/test_service.py b/src/tests/test_service.py index 88013a3fe..84573fd89 100644 --- a/src/tests/test_service.py +++ b/src/tests/test_service.py @@ -14,17 +14,14 @@ from yunohost.service import ( def setup_function(function): - clean() def teardown_function(function): - clean() def clean(): - # To run these tests, we assume ssh(d) service exists and is running assert os.system("pgrep sshd >/dev/null") == 0 @@ -45,46 +42,39 @@ def clean(): def test_service_status_all(): - status = service_status() assert "ssh" in status.keys() assert status["ssh"]["status"] == "running" def test_service_status_single(): - status = service_status("ssh") assert "status" in status.keys() assert status["status"] == "running" def test_service_log(): - logs = service_log("ssh") assert "journalctl" in logs.keys() assert "/var/log/auth.log" in logs.keys() def test_service_status_unknown_service(mocker): - with raiseYunohostError(mocker, "service_unknown"): service_status(["ssh", "doesnotexists"]) def test_service_add(): - service_add("dummyservice", description="A dummy service to run tests") assert "dummyservice" in service_status().keys() def test_service_add_real_service(): - service_add("networking") assert "networking" in service_status().keys() def test_service_remove(): - service_add("dummyservice", description="A dummy service to run tests") assert "dummyservice" in service_status().keys() service_remove("dummyservice") @@ -92,7 +82,6 @@ def test_service_remove(): def test_service_remove_service_that_doesnt_exists(mocker): - assert "dummyservice" not in service_status().keys() with raiseYunohostError(mocker, "service_unknown"): @@ -102,7 +91,6 @@ def test_service_remove_service_that_doesnt_exists(mocker): def test_service_update_to_add_properties(): - service_add("dummyservice", description="dummy") assert not _get_services()["dummyservice"].get("test_status") service_add("dummyservice", description="dummy", test_status="true") @@ -110,7 +98,6 @@ def test_service_update_to_add_properties(): def test_service_update_to_change_properties(): - service_add("dummyservice", description="dummy", test_status="false") assert _get_services()["dummyservice"].get("test_status") == "false" service_add("dummyservice", description="dummy", test_status="true") @@ -118,7 +105,6 @@ def test_service_update_to_change_properties(): def test_service_update_to_remove_properties(): - service_add("dummyservice", description="dummy", test_status="false") assert _get_services()["dummyservice"].get("test_status") == "false" service_add("dummyservice", description="dummy", test_status="") @@ -126,7 +112,6 @@ def test_service_update_to_remove_properties(): def test_service_conf_broken(): - os.system("echo pwet > /etc/nginx/conf.d/broken.conf") status = service_status("nginx") diff --git a/src/tests/test_settings.py b/src/tests/test_settings.py index 2eaebba55..20f959a80 100644 --- a/src/tests/test_settings.py +++ b/src/tests/test_settings.py @@ -65,7 +65,6 @@ old_translate = moulinette.core.Translator.translate def _monkeypatch_translator(self, key, *args, **kwargs): - if key.startswith("global_settings_setting_"): return f"Dummy translation for {key}" @@ -175,7 +174,6 @@ def test_settings_set_doesexit(): def test_settings_set_bad_type_bool(): - with patch.object(os, "isatty", return_value=False): with pytest.raises(YunohostError): settings_set("example.example.boolean", 42) diff --git a/src/tests/test_user-group.py b/src/tests/test_user-group.py index 343431b69..eececb827 100644 --- a/src/tests/test_user-group.py +++ b/src/tests/test_user-group.py @@ -92,7 +92,6 @@ def test_list_groups(): def test_create_user(mocker): - with message(mocker, "user_created"): user_create("albert", maindomain, "test123Ynh", fullname="Albert Good") @@ -104,7 +103,6 @@ def test_create_user(mocker): def test_del_user(mocker): - with message(mocker, "user_deleted"): user_delete("alice") @@ -185,7 +183,6 @@ def test_export_user(mocker): def test_create_group(mocker): - with message(mocker, "group_created", group="adminsys"): user_group_create("adminsys") @@ -196,7 +193,6 @@ def test_create_group(mocker): def test_del_group(mocker): - with message(mocker, "group_deleted", group="dev"): user_group_delete("dev") diff --git a/src/tools.py b/src/tools.py index eb385f4a8..777d8fc8f 100644 --- a/src/tools.py +++ b/src/tools.py @@ -62,7 +62,6 @@ def tools_versions(): def tools_rootpw(new_password, check_strength=True): - from yunohost.user import _hash_user_password from yunohost.utils.password import ( assert_password_is_strong_enough, @@ -154,7 +153,6 @@ def tools_postinstall( ignore_dyndns=False, force_diskspace=False, ): - from yunohost.dyndns import _dyndns_available from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.utils.password import ( @@ -193,7 +191,6 @@ def tools_postinstall( # If this is a nohost.me/noho.st, actually check for availability if not ignore_dyndns and is_yunohost_dyndns_domain(domain): - available = None # Check if the domain is available... @@ -281,7 +278,6 @@ def tools_postinstall( def tools_regen_conf( names=[], with_diff=False, force=False, dry_run=False, list_pending=False ): - # Make sure the settings are migrated before running the migration, # which may otherwise fuck things up such as the ssh config ... # We do this here because the regen-conf is called before the migration in debian/postinst @@ -312,7 +308,6 @@ def tools_update(target=None): upgradable_system_packages = [] if target in ["system", "all"]: - # Update APT cache # LC_ALL=C is here to make sure the results are in english command = ( @@ -426,7 +421,6 @@ def tools_upgrade(operation_logger, target=None): # if target == "apps": - # Make sure there's actually something to upgrade upgradable_apps = [app["id"] for app in app_list(upgradable=True)["apps"]] @@ -450,7 +444,6 @@ def tools_upgrade(operation_logger, target=None): # if target == "system": - # Check that there's indeed some packages to upgrade upgradables = list(_list_upgradable_apt_packages()) if not upgradables: @@ -498,7 +491,6 @@ def tools_upgrade(operation_logger, target=None): any(p["name"] == "yunohost" for p in upgradables) and Moulinette.interface.type == "api" ): - # Restart the API after 10 sec (at now doesn't support sub-minute times...) # We do this so that the API / webadmin still gets the proper HTTP response # It's then up to the webadmin to implement a proper UX process to wait 10 sec and then auto-fresh the webadmin @@ -722,7 +714,6 @@ def tools_migrations_run( # Actually run selected migrations for migration in targets: - # If we are migrating in "automatic mode" (i.e. from debian configure # during an upgrade of the package) but we are asked for running # migrations to be ran manually by the user, stop there and ask the @@ -778,7 +769,6 @@ def tools_migrations_run( _write_migration_state(migration.id, "skipped") operation_logger.success() else: - try: migration.operation_logger = operation_logger logger.info(m18n.n("migrations_running_forward", id=migration.id)) @@ -810,14 +800,12 @@ def tools_migrations_state(): def _write_migration_state(migration_id, state): - current_states = tools_migrations_state() current_states["migrations"][migration_id] = state write_to_yaml(MIGRATIONS_STATE_PATH, current_states) def _get_migrations_list(): - # states is a datastructure that represents the last run migration # it has this form: # { @@ -868,7 +856,6 @@ def _get_migration_by_name(migration_name): def _load_migration(migration_file): - migration_id = migration_file[: -len(".py")] logger.debug(m18n.n("migrations_loading_migration", id=migration_id)) @@ -903,7 +890,6 @@ def _skip_all_migrations(): def _tools_migrations_run_after_system_restore(backup_version): - all_migrations = _get_migrations_list() current_version = version.parse(ynh_packages_version()["yunohost"]["version"]) @@ -930,7 +916,6 @@ def _tools_migrations_run_after_system_restore(backup_version): def _tools_migrations_run_before_app_restore(backup_version, app_id): - all_migrations = _get_migrations_list() current_version = version.parse(ynh_packages_version()["yunohost"]["version"]) @@ -957,7 +942,6 @@ def _tools_migrations_run_before_app_restore(backup_version, app_id): class Migration: - # Those are to be implemented by daughter classes mode = "auto" @@ -985,7 +969,6 @@ class Migration: def ldap_migration(run): def func(self): - # Backup LDAP before the migration logger.info(m18n.n("migration_ldap_backup_before_migration")) try: diff --git a/src/user.py b/src/user.py index ee0aebae6..797c3252f 100644 --- a/src/user.py +++ b/src/user.py @@ -53,7 +53,6 @@ ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] def user_list(fields=None): - from yunohost.utils.ldap import _get_ldap_interface ldap_attrs = { @@ -149,7 +148,6 @@ def user_create( 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." @@ -319,7 +317,6 @@ def user_create( @is_unit_operation([("username", "user")]) def user_delete(operation_logger, username, purge=False, from_import=False): - from yunohost.hook import hook_callback from yunohost.utils.ldap import _get_ldap_interface @@ -380,7 +377,6 @@ 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." @@ -735,7 +731,6 @@ def user_import(operation_logger, csvfile, update=False, delete=False): ) for user in reader: - # Validate column values against regexes format_errors = [ f"{key}: '{user[key]}' doesn't match the expected format" @@ -991,7 +986,6 @@ def user_group_list(short=False, full=False, include_primary_groups=True): users = user_list()["users"] groups = {} for infos in groups_infos: - name = infos["cn"][0] if not include_primary_groups and name in users: @@ -1141,7 +1135,6 @@ def user_group_update( sync_perm=True, from_import=False, ): - from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract @@ -1184,7 +1177,6 @@ def user_group_update( new_attr_dict = {} if add: - users_to_add = [add] if not isinstance(add, list) else add for user in users_to_add: @@ -1225,7 +1217,6 @@ def user_group_update( # Check the whole alias situation if add_mailalias: - from yunohost.domain import domain_list domains = domain_list()["domains"] @@ -1269,7 +1260,6 @@ def user_group_update( raise YunohostValidationError("mail_alias_remove_failed", mail=mail) if set(new_group_mail) != set(current_group_mail): - logger.info(m18n.n("group_update_aliases", group=groupname)) new_attr_dict["mail"] = set(new_group_mail) @@ -1477,7 +1467,6 @@ def _hash_user_password(password): def _update_admins_group_aliases(old_main_domain, new_main_domain): - current_admin_aliases = user_group_info("admins")["mail-aliases"] aliases_to_remove = [ diff --git a/src/utils/config.py b/src/utils/config.py index bd3a6b6a9..5dce4070d 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -264,7 +264,6 @@ class ConfigPanel: # In 'classic' mode, we display the current value if key refer to an option if self.filter_key.count(".") == 2 and mode == "classic": - option = self.filter_key.split(".")[-1] value = self.values.get(option, None) @@ -280,7 +279,6 @@ class ConfigPanel: logger.debug(f"Formating result in '{mode}' mode") result = {} for panel, section, option in self._iterate(): - if section["is_action_section"] and mode != "full": continue @@ -323,7 +321,6 @@ class ConfigPanel: return result def list_actions(self): - actions = {} # FIXME : meh, loading the entire config panel is again going to cause @@ -462,7 +459,6 @@ class ConfigPanel: 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: @@ -639,7 +635,6 @@ 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", @@ -701,7 +696,6 @@ class ConfigPanel: Moulinette.display(colorize(message, "purple")) for panel, section, obj in self._iterate(["panel", "section"]): - if ( section and section.get("visible") @@ -814,7 +808,6 @@ class ConfigPanel: write_to_yaml(self.save_path, values_to_save) def _reload_services(self): - from yunohost.service import service_reload_or_restart services_to_reload = set() @@ -905,7 +898,6 @@ class Question: ) def ask_if_needed(self): - if self.visible and not evaluate_simple_js_expression( self.visible, context=self.context ): @@ -980,7 +972,6 @@ class Question: ) def _format_text_for_user_input_in_cli(self): - text_for_user_input_in_cli = _value_for_locale(self.ask) if self.readonly: @@ -991,7 +982,6 @@ class Question: ) return text_for_user_input_in_cli + f" {self.humanize(self.current_value)}" elif self.choices: - # Prevent displaying a shitload of choices # (e.g. 100+ available users when choosing an app admin...) choices = ( @@ -1160,7 +1150,6 @@ class PathQuestion(Question): @staticmethod def normalize(value, option={}): - option = option.__dict__ if isinstance(option, Question) else option if not value.strip(): @@ -1187,7 +1176,6 @@ class BooleanQuestion(Question): @staticmethod def humanize(value, option={}): - option = option.__dict__ if isinstance(option, Question) else option yes = option.get("yes", 1) @@ -1211,7 +1199,6 @@ class BooleanQuestion(Question): @staticmethod def normalize(value, option={}): - option = option.__dict__ if isinstance(option, Question) else option if isinstance(value, str): @@ -1368,7 +1355,6 @@ class GroupQuestion(Question): def __init__( self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} ): - from yunohost.user import user_group_list super().__init__(question, context) @@ -1401,7 +1387,6 @@ class NumberQuestion(Question): @staticmethod def normalize(value, option={}): - if isinstance(value, int): return value diff --git a/src/utils/dns.py b/src/utils/dns.py index 091168615..225a0e98f 100644 --- a/src/utils/dns.py +++ b/src/utils/dns.py @@ -31,19 +31,16 @@ external_resolvers_: List[str] = [] def is_yunohost_dyndns_domain(domain): - return any( domain.endswith(f".{dyndns_domain}") for dyndns_domain in YNH_DYNDNS_DOMAINS ) def is_special_use_tld(domain): - return any(domain.endswith(f".{tld}") for tld in SPECIAL_USE_TLDS) def external_resolvers(): - global external_resolvers_ if not external_resolvers_: diff --git a/src/utils/error.py b/src/utils/error.py index e7046540d..cdf2a3d09 100644 --- a/src/utils/error.py +++ b/src/utils/error.py @@ -21,7 +21,6 @@ from moulinette import m18n class YunohostError(MoulinetteError): - http_code = 500 """ @@ -43,7 +42,6 @@ class YunohostError(MoulinetteError): super(YunohostError, self).__init__(msg, raw_msg=True) def content(self): - if not self.log_ref: return super().content() else: @@ -51,14 +49,11 @@ class YunohostError(MoulinetteError): class YunohostValidationError(YunohostError): - http_code = 400 def content(self): - return {"error": self.strerror, "error_key": self.key, **self.kwargs} class YunohostAuthenticationError(MoulinetteAuthenticationError): - pass diff --git a/src/utils/ldap.py b/src/utils/ldap.py index ee50d0b98..5a0e3ba35 100644 --- a/src/utils/ldap.py +++ b/src/utils/ldap.py @@ -36,7 +36,6 @@ _ldap_interface = None def _get_ldap_interface(): - global _ldap_interface if _ldap_interface is None: diff --git a/src/utils/legacy.py b/src/utils/legacy.py index 3334632c2..fa0b68137 100644 --- a/src/utils/legacy.py +++ b/src/utils/legacy.py @@ -193,7 +193,6 @@ LEGACY_PHP_VERSION_REPLACEMENTS = [ def _patch_legacy_php_versions(app_folder): - files_to_patch = [] files_to_patch.extend(glob.glob("%s/conf/*" % app_folder)) files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder)) @@ -203,7 +202,6 @@ def _patch_legacy_php_versions(app_folder): files_to_patch.append("%s/manifest.toml" % app_folder) for filename in files_to_patch: - # Ignore non-regular files if not os.path.isfile(filename): continue @@ -217,7 +215,6 @@ def _patch_legacy_php_versions(app_folder): def _patch_legacy_php_versions_in_settings(app_folder): - settings = read_yaml(os.path.join(app_folder, "settings.yml")) if settings.get("fpm_config_dir") in ["/etc/php/7.0/fpm", "/etc/php/7.3/fpm"]: @@ -243,7 +240,6 @@ def _patch_legacy_php_versions_in_settings(app_folder): def _patch_legacy_helpers(app_folder): - files_to_patch = [] files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder)) files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder)) @@ -291,7 +287,6 @@ def _patch_legacy_helpers(app_folder): infos["replace"] = infos.get("replace") for filename in files_to_patch: - # Ignore non-regular files if not os.path.isfile(filename): continue @@ -305,7 +300,6 @@ def _patch_legacy_helpers(app_folder): show_warning = False for helper, infos in stuff_to_replace.items(): - # Ignore if not relevant for this file if infos.get("only_for") and not any( filename.endswith(f) for f in infos["only_for"] @@ -329,7 +323,6 @@ def _patch_legacy_helpers(app_folder): ) if replaced_stuff: - # Check the app do load the helper # If it doesn't, add the instruction ourselve (making sure it's after the #!/bin/bash if it's there... if filename.split("/")[-1] in [ diff --git a/src/utils/network.py b/src/utils/network.py index 06dd3493d..e9892333e 100644 --- a/src/utils/network.py +++ b/src/utils/network.py @@ -29,7 +29,6 @@ logger = logging.getLogger("yunohost.utils.network") def get_public_ip(protocol=4): - assert protocol in [4, 6], ( "Invalid protocol version for get_public_ip: %s, expected 4 or 6" % protocol ) @@ -90,7 +89,6 @@ def get_public_ip_from_remote_server(protocol=4): def get_network_interfaces(): - # Get network devices and their addresses (raw infos from 'ip addr') devices_raw = {} output = check_output("ip addr show") @@ -111,7 +109,6 @@ def get_network_interfaces(): def get_gateway(): - output = check_output("ip route show") m = re.search(r"default via (.*) dev ([a-z]+[0-9]?)", output) if not m: diff --git a/src/utils/password.py b/src/utils/password.py index 3202e8055..569833a7d 100644 --- a/src/utils/password.py +++ b/src/utils/password.py @@ -58,7 +58,6 @@ def assert_password_is_compatible(password): """ if len(password) >= 127: - # Note that those imports are made here and can't be put # on top (at least not the moulinette ones) # because the moulinette needs to be correctly initialized @@ -69,7 +68,6 @@ def assert_password_is_compatible(password): def assert_password_is_strong_enough(profile, password): - PasswordValidator(profile).validate(password) @@ -197,7 +195,6 @@ class PasswordValidator: return strength_level def is_in_most_used_list(self, password): - # Decompress file if compressed if os.path.exists("%s.gz" % MOST_USED_PASSWORDS): os.system("gzip -fd %s.gz" % MOST_USED_PASSWORDS) diff --git a/src/utils/resources.py b/src/utils/resources.py index 7b500ad3f..569512006 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -37,7 +37,6 @@ logger = getActionLogger("yunohost.app_resources") class AppResourceManager: def __init__(self, app: str, current: Dict, wanted: Dict): - self.app = app self.current = current self.wanted = wanted @@ -50,7 +49,6 @@ class AppResourceManager: def apply( self, rollback_and_raise_exception_if_failure, operation_logger=None, **context ): - todos = list(self.compute_todos()) completed = [] rollback = False @@ -121,7 +119,6 @@ class AppResourceManager: logger.error(exception) def compute_todos(self): - for name, infos in reversed(self.current["resources"].items()): if name not in self.wanted["resources"].keys(): resource = AppResourceClassesByType[name](infos, self.app, self) @@ -140,12 +137,10 @@ class AppResourceManager: class AppResource: - type: str = "" default_properties: Dict[str, Any] = {} def __init__(self, properties: Dict[str, Any], app: str, manager=None): - self.app = app self.manager = manager @@ -175,7 +170,6 @@ class AppResource: app_setting(self.app, key, delete=True) def _run_script(self, action, script, env={}, user="root"): - from yunohost.app import ( _make_tmp_workdir_for_app, _make_environment_for_app_script, @@ -295,7 +289,6 @@ class PermissionsResource(AppResource): permissions: Dict[str, Dict[str, Any]] = {} def __init__(self, properties: Dict[str, Any], *args, **kwargs): - # FIXME : if url != None, we should check that there's indeed a domain/path defined ? ie that app is a webapp for perm, infos in properties.items(): @@ -315,7 +308,6 @@ class PermissionsResource(AppResource): super().__init__({"permissions": properties}, *args, **kwargs) def provision_or_update(self, context: Dict = {}): - from yunohost.permission import ( permission_create, permission_url, @@ -375,7 +367,6 @@ class PermissionsResource(AppResource): permission_sync_to_user() def deprovision(self, context: Dict = {}): - from yunohost.permission import ( permission_delete, user_permission_list, @@ -432,7 +423,6 @@ class SystemuserAppResource(AppResource): allow_sftp: bool = False def provision_or_update(self, context: Dict = {}): - # FIXME : validate that no yunohost user exists with that name? # and/or that no system user exists during install ? @@ -462,7 +452,6 @@ class SystemuserAppResource(AppResource): os.system(f"usermod -G {','.join(groups)} {self.app}") def deprovision(self, context: Dict = {}): - if check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): os.system(f"deluser {self.app} >/dev/null") if check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): @@ -528,7 +517,6 @@ class InstalldirAppResource(AppResource): # FIXME: change default dir to /opt/stuff if app ain't a webapp ... def provision_or_update(self, context: Dict = {}): - assert self.dir.strip() # Be paranoid about self.dir being empty... assert self.owner.strip() assert self.group.strip() @@ -582,7 +570,6 @@ class InstalldirAppResource(AppResource): self.delete_setting("final_path") # Legacy def deprovision(self, context: Dict = {}): - assert self.dir.strip() # Be paranoid about self.dir being empty... assert self.owner.strip() assert self.group.strip() @@ -643,7 +630,6 @@ class DatadirAppResource(AppResource): group: str = "" def provision_or_update(self, context: Dict = {}): - assert self.dir.strip() # Be paranoid about self.dir being empty... assert self.owner.strip() assert self.group.strip() @@ -686,7 +672,6 @@ class DatadirAppResource(AppResource): self.delete_setting("datadir") # Legacy def deprovision(self, context: Dict = {}): - assert self.dir.strip() # Be paranoid about self.dir being empty... assert self.owner.strip() assert self.group.strip() @@ -737,7 +722,6 @@ 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"] @@ -749,7 +733,6 @@ class AptDependenciesAppResource(AppResource): super().__init__(properties, *args, **kwargs) def provision_or_update(self, context: Dict = {}): - script = [f"ynh_install_app_dependencies {self.packages}"] for repo, values in self.extras.items(): script += [ @@ -760,7 +743,6 @@ class AptDependenciesAppResource(AppResource): self._run_script("provision_or_update", "\n".join(script)) def deprovision(self, context: Dict = {}): - self._run_script("deprovision", "ynh_remove_app_dependencies") @@ -818,7 +800,6 @@ class PortsResource(AppResource): ports: Dict[str, Dict[str, Any]] def __init__(self, properties: Dict[str, Any], *args, **kwargs): - if "main" not in properties: properties["main"] = {} @@ -832,7 +813,6 @@ class PortsResource(AppResource): super().__init__({"ports": properties}, *args, **kwargs) def _port_is_used(self, port): - # FIXME : this could be less brutal than two os.system ... cmd1 = ( "ss --numeric --listening --tcp --udp | awk '{print$5}' | grep --quiet --extended-regexp ':%s$'" @@ -843,11 +823,9 @@ class PortsResource(AppResource): return os.system(cmd1) == 0 and os.system(cmd2) == 0 def provision_or_update(self, context: Dict = {}): - from yunohost.firewall import firewall_allow, firewall_disallow for name, infos in self.ports.items(): - setting_name = f"port_{name}" if name != "main" else "port" port_value = self.get_setting(setting_name) if not port_value and name != "main": @@ -881,7 +859,6 @@ class PortsResource(AppResource): ) def deprovision(self, context: Dict = {}): - from yunohost.firewall import firewall_disallow for name, infos in self.ports.items(): @@ -938,7 +915,6 @@ class DatabaseAppResource(AppResource): } def __init__(self, properties: Dict[str, Any], *args, **kwargs): - if "type" not in properties or properties["type"] not in [ "mysql", "postgresql", @@ -956,7 +932,6 @@ class DatabaseAppResource(AppResource): super().__init__(properties, *args, **kwargs) def db_exists(self, db_name): - if self.dbtype == "mysql": return os.system(f"mysqlshow '{db_name}' >/dev/null 2>/dev/null") == 0 elif self.dbtype == "postgresql": @@ -970,7 +945,6 @@ class DatabaseAppResource(AppResource): return False def provision_or_update(self, context: Dict = {}): - # This is equivalent to ynh_sanitize_dbid db_name = self.app.replace("-", "_").replace(".", "_") db_user = db_name @@ -997,7 +971,6 @@ class DatabaseAppResource(AppResource): self.set_setting("db_pwd", db_pwd) if not self.db_exists(db_name): - if self.dbtype == "mysql": self._run_script( "provision", @@ -1010,7 +983,6 @@ class DatabaseAppResource(AppResource): ) def deprovision(self, context: Dict = {}): - db_name = self.app.replace("-", "_").replace(".", "_") db_user = db_name diff --git a/src/utils/system.py b/src/utils/system.py index 8b0ed7092..c55023e52 100644 --- a/src/utils/system.py +++ b/src/utils/system.py @@ -49,7 +49,6 @@ def free_space_in_directory(dirpath): def space_used_by_directory(dirpath, follow_symlinks=True): - if not follow_symlinks: du_output = check_output(["du", "-sb", dirpath], shell=False) return int(du_output.split()[0]) @@ -61,7 +60,6 @@ def space_used_by_directory(dirpath, follow_symlinks=True): def human_to_binary(size: str) -> int: - symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") factor = {} for i, s in enumerate(symbols): @@ -99,14 +97,12 @@ def binary_to_human(n: int) -> str: def ram_available(): - import psutil return (psutil.virtual_memory().available, psutil.swap_memory().free) def get_ynh_package_version(package): - # Returns the installed version and release version ('stable' or 'testing' # or 'unstable') @@ -152,7 +148,6 @@ def dpkg_lock_available(): def _list_upgradable_apt_packages(): - # List upgradable packages # LC_ALL=C is here to make sure the results are in english upgradable_raw = check_output("LC_ALL=C apt list --upgradable") @@ -162,7 +157,6 @@ def _list_upgradable_apt_packages(): line.strip() for line in upgradable_raw.split("\n") if line.strip() ] for line in upgradable_raw: - # Remove stupid warning and verbose messages >.> if "apt does not have a stable CLI interface" in line or "Listing..." in line: continue @@ -182,7 +176,6 @@ def _list_upgradable_apt_packages(): def _dump_sources_list(): - from glob import glob filenames = glob("/etc/apt/sources.list") + glob("/etc/apt/sources.list.d/*") diff --git a/src/utils/yunopaste.py b/src/utils/yunopaste.py index 0edcc721b..806f8a34f 100644 --- a/src/utils/yunopaste.py +++ b/src/utils/yunopaste.py @@ -28,7 +28,6 @@ logger = logging.getLogger("yunohost.utils.yunopaste") def yunopaste(data): - paste_server = "https://paste.yunohost.org" try: From 480f7a43ef1e571fe6800afd547da0c41cd3ec4e Mon Sep 17 00:00:00 2001 From: Axolotle Date: Wed, 1 Feb 2023 18:13:07 +0100 Subject: [PATCH 074/319] fix domain_config.toml typos in conditions --- share/config_domain.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/share/config_domain.toml b/share/config_domain.toml index b1ec436c5..c67996d13 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -64,24 +64,24 @@ name = "Certificate" [cert.cert.acme_eligible_explain] type = "alert" style = "warning" - visible = "acme_eligible == false || acme_elligible == null" + visible = "acme_eligible == false || acme_eligible == null" [cert.cert.cert_no_checks] ask = "Ignore diagnosis checks" type = "boolean" default = false - visible = "acme_eligible == false || acme_elligible == null" + visible = "acme_eligible == false || acme_eligible == null" [cert.cert.cert_install] type = "button" icon = "star" style = "success" - visible = "issuer != 'letsencrypt'" + visible = "cert_issuer != 'letsencrypt'" enabled = "acme_eligible || cert_no_checks" [cert.cert.cert_renew] type = "button" icon = "refresh" style = "warning" - visible = "issuer == 'letsencrypt'" + visible = "cert_issuer == 'letsencrypt'" enabled = "acme_eligible || cert_no_checks" From daa9eb1cab7eb6da81e808d19766e0ebeefa60a3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 1 Feb 2023 18:30:51 +0100 Subject: [PATCH 075/319] Duplicate import --- src/diagnosers/24-mail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index d48b1959e..9a4cd1fc3 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -31,7 +31,6 @@ from yunohost.diagnosis import Diagnoser from yunohost.domain import _get_maindomain, domain_list from yunohost.settings import settings_get from yunohost.utils.dns import dig -from yunohost.settings import settings_get DEFAULT_DNS_BLACKLIST = "/usr/share/yunohost/dnsbl_list.yml" From 452ba8bb9a8da0ed32a0e5f57e74134c37893f0c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 1 Feb 2023 18:39:05 +0100 Subject: [PATCH 076/319] Don't try restarting metronome if no domain configured for it --- src/certificate.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/certificate.py b/src/certificate.py index 0addca858..c9165fa6d 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -20,7 +20,7 @@ import os import sys import shutil import subprocess -import glob +from glob import glob from datetime import datetime @@ -734,10 +734,11 @@ def _enable_certificate(domain, new_cert_folder): logger.debug("Restarting services...") for service in ("dovecot", "metronome"): - # Ugly trick to not restart metronome if it's not installed + # Ugly trick to not restart metronome if it's not installed or no domain configured for XMPP if ( service == "metronome" - and os.system("dpkg --list | grep -q 'ii *metronome'") != 0 + and (os.system("dpkg --list | grep -q 'ii *metronome'") != 0 + or not glob("/etc/metronome/conf.d/*.cfg.lua")) ): continue _run_service_command("restart", service) @@ -853,7 +854,7 @@ def _regen_dnsmasq_if_needed(): do_regen = False # For all domain files in DNSmasq conf... - domainsconf = glob.glob("/etc/dnsmasq.d/*.*") + domainsconf = glob("/etc/dnsmasq.d/*.*") for domainconf in domainsconf: # Look for the IP, it's in the lines with this format : # host-record=the.domain.tld,11.22.33.44 From 314d27bec1fc96a63690309753b1b81e2872f028 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 1 Feb 2023 20:21:19 +0100 Subject: [PATCH 077/319] Fix flake8 complains --- doc/generate_api_doc.py | 13 +++++++------ src/settings.py | 2 +- tox.ini | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/generate_api_doc.py b/doc/generate_api_doc.py index fc44ffbcd..514415eef 100644 --- a/doc/generate_api_doc.py +++ b/doc/generate_api_doc.py @@ -26,15 +26,16 @@ import requests def main(): - """ """ + with open("../share/actionsmap.yml") as f: action_map = yaml.safe_load(f) - try: - with open("/etc/yunohost/current_host", "r") as f: - domain = f.readline().rstrip() - except IOError: - domain = requests.get("http://ip.yunohost.org").text + #try: + # with open("/etc/yunohost/current_host", "r") as f: + # domain = f.readline().rstrip() + #except IOError: + # domain = requests.get("http://ip.yunohost.org").text + with open("../debian/changelog") as f: top_changelog = f.readline() api_version = top_changelog[top_changelog.find("(") + 1 : top_changelog.find(")")] diff --git a/src/settings.py b/src/settings.py index d1203930d..a06377176 100644 --- a/src/settings.py +++ b/src/settings.py @@ -198,7 +198,7 @@ class SettingsConfigPanel(ConfigPanel): self.values["passwordless_sudo"] = "!authenticate" in ldap.search( "ou=sudo", "cn=admins", ["sudoOption"] )[0].get("sudoOption", []) - except: + except Exception: self.values["passwordless_sudo"] = False def get(self, key="", mode="classic"): diff --git a/tox.ini b/tox.ini index dc2c52074..49c78959d 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 --exclude src/vendor + py39-lint: flake8 src doc maintenance tests --ignore E402,E501,E203,W503,E741 --exclude 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 c2c0a66cdf43abccf83fc2bdea66f04eecd5c44e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 1 Feb 2023 20:22:56 +0100 Subject: [PATCH 078/319] Upate changelog for 11.1.5 --- debian/changelog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/debian/changelog b/debian/changelog index 637a74bfd..0d13b3c92 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +yunohost (11.1.5) stable; urgency=low + + - Release as stable ! + + - diagnosis: we can't yield an ERROR if there's no IPv6, otherwise that blocks all subsequent network-related diagnoser because of the dependency system ... (ade92e43) + - domains: fix domain_config.toml typos in conditions (480f7a43) + - certs: Don't try restarting metronome if no domain configured for it (452ba8bb) + + Thanks to all contributors <3 ! (Axolotle) + + -- Alexandre Aubin Wed, 01 Feb 2023 20:21:56 +0100 + yunohost (11.1.4.1) testing; urgency=low - debian: don't dump upgradable apps during postinst's catalog update (82d30f02) From 0826a54189f6ac3424c3b59dace1f550a28acf8f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 14:14:35 +0100 Subject: [PATCH 079/319] debian: Bump moulinette/ssowat requirement to 11.1 --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index facedbff2..0258eaac7 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Package: yunohost Essential: yes Architecture: all Depends: ${python3:Depends}, ${misc:Depends} - , moulinette (>= 11.0), ssowat (>= 11.0) + , 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 From 9004cc76152b23b577f09f44574d230466fe71dc Mon Sep 17 00:00:00 2001 From: ppr Date: Mon, 30 Jan 2023 17:21:33 +0000 Subject: [PATCH 080/319] Translated using Weblate (French) Currently translated at 99.6% (750 of 753 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 41e58a1c5..9e0a24578 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -748,5 +748,8 @@ "app_not_enough_ram": "Cette application nécessite {required} de mémoire vive (RAM) pour être installée/mise à niveau mais seule {current} de mémoire est disponible actuellement.", "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}", "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des informations importantes à connaître. [{answers}]", - "invalid_shell": "Shell invalide : {shell}" -} \ No newline at end of file + "invalid_shell": "Shell invalide : {shell}", + "global_settings_setting_dns_exposure": "Versions/Suites d'IP à prendre en compte pour la configuration et le diagnostic du DNS", + "global_settings_setting_dns_exposure_help": "NB : Ceci n'affecte que la configuration DNS recommandée et les vérifications de diagnostic. Cela n'affecte pas les configurations du système.", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 devrait généralement être configuré automatiquement par le système ou par votre fournisseur d'accès à internet (FAI) s'il est disponible. Sinon, vous devrez peut-être configurer quelques éléments manuellement, comme expliqué dans la documentation ici : https://yunohost.org/#/ipv6." +} From 7ac6471b00165440e4c824ffeb0c67751058698c Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Tue, 31 Jan 2023 09:53:24 +0000 Subject: [PATCH 081/319] Translated using Weblate (Arabic) Currently translated at 22.0% (166 of 753 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index dd254096b..c1d6cbcfb 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -10,7 +10,7 @@ "app_not_installed": "إنّ التطبيق {app} غير مُنصَّب", "app_not_properly_removed": "لم يتم حذف تطبيق {app} بشكلٍ جيّد", "app_removed": "تمت إزالة تطبيق {app}", - "app_requirements_checking": "جار فحص الحزم اللازمة لـ {app}…", + "app_requirements_checking": "جار فحص متطلبات تطبيق {app}…", "app_sources_fetch_failed": "تعذر جلب ملفات المصدر ، هل عنوان URL صحيح؟", "app_unknown": "برنامج مجهول", "app_upgrade_app_name": "جارٍ تحديث {app}…", @@ -49,14 +49,14 @@ "pattern_domain": "يتوجب أن يكون إسم نطاق صالح (مثل my-domain.org)", "pattern_email": "يتوجب أن يكون عنوان بريد إلكتروني صالح (مثل someone@domain.org)", "pattern_password": "يتوجب أن تكون مكونة من 3 حروف على الأقل", - "restore_extracting": "جارٍ فك الضغط عن الملفات التي نحتاجها من النسخة الاحتياطية…", + "restore_extracting": "جارٍ فك الضغط عن الملفات اللازمة من النسخة الاحتياطية…", "server_shutdown": "سوف ينطفئ الخادوم", "server_shutdown_confirm": "سوف ينطفئ الخادوم حالا. متأكد ؟ [{answers}]", "server_reboot": "سيعاد تشغيل الخادوم", "server_reboot_confirm": "سيعاد تشغيل الخادوم في الحين. هل أنت متأكد ؟ [{answers}]", "service_add_failed": "تعذرت إضافة خدمة '{service}'", "service_already_stopped": "إنّ خدمة '{service}' متوقفة مِن قبلُ", - "service_disabled": "لن يتم إطلاق خدمة '{service}' أثناء بداية تشغيل النظام.", + "service_disabled": "لن يتم إطلاق خدمة '{service}' أثناء بداية تشغيل النظام بتاتا.", "service_enabled": "سيتم الآن بدء تشغيل الخدمة '{service}' تلقائيًا أثناء تمهيد النظام.", "service_removed": "تمت إزالة خدمة '{service}'", "service_started": "تم إطلاق تشغيل خدمة '{service}'", @@ -71,10 +71,10 @@ "user_deleted": "تم حذف المستخدم", "user_deletion_failed": "لا يمكن حذف المستخدم", "user_unknown": "المستخدم {user} مجهول", - "user_update_failed": "لا يمكن تحديث المستخدم", - "user_updated": "تم تحديث المستخدم", + "user_update_failed": "لا يمكن تحديث المستخدم {user}: {error}", + "user_updated": "تم تحديث معلومات المستخدم", "yunohost_installing": "عملية تنصيب واي يونوهوست جارية …", - "yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب أو هو مثبت حاليا بشكل خاطئ. قم بتنفيذ الأمر 'yunohost tools postinstall'", + "yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب بشكل جيد. فضلًا قم بتنفيذ الأمر 'yunohost tools postinstall'", "migrations_list_conflict_pending_done": "لا يمكنك استخدام --previous و --done معًا على نفس سطر الأوامر.", "service_description_metronome": "يُدير حسابات الدردشة الفورية XMPP", "service_description_nginx": "يقوم بتوفير النفاذ و السماح بالوصول إلى كافة مواقع الويب المستضافة على خادومك", @@ -199,10 +199,15 @@ "service_description_yunomdns": "يسمح لك بالوصول إلى خادمك الخاص باستخدام 'yunohost.local' في شبكتك المحلية", "good_practices_about_user_password": "أنت الآن على وشك تحديد كلمة مرور مستخدم جديدة. يجب أن تتكون كلمة المرور من 8 أحرف على الأقل - على الرغم من أنه من الممارسات الجيدة استخدام كلمة مرور أطول (أي عبارة مرور) و / أو مجموعة متنوعة من الأحرف (الأحرف الكبيرة والصغيرة والأرقام والأحرف الخاصة).", "root_password_changed": "تم تغيير كلمة مرور الجذر", - "root_password_desynchronized": "تم تغيير كلمة مرور المسؤول ، لكن لم يتمكن YunoHost من نشرها على كلمة مرور الجذر!", + "root_password_desynchronized": "تم تغيير كلمة مرور المدير ، لكن لم يتمكن YunoHost من نشرها على كلمة مرور الجذر!", "user_import_bad_line": "سطر غير صحيح {line}: {details}", "user_import_success": "تم استيراد المستخدمين بنجاح", "visitors": "الزوار", "password_too_simple_3": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل وأن تحتوي على أرقام وأرقام علوية وسفلية وأحرف خاصة", - "password_too_simple_4": "يجب أن تتكون كلمة المرور من 12 حرفًا على الأقل وأن تحتوي على أرقام وأرقام علوية وسفلية وأحرف خاصة" -} \ No newline at end of file + "password_too_simple_4": "يجب أن تتكون كلمة المرور من 12 حرفًا على الأقل وأن تحتوي على أرقام وأرقام علوية وسفلية وأحرف خاصة", + "service_unknown": "الخدمة '{service}' غير معروفة", + "unbackup_app": "لن يتم حفظ التطبيق '{app}'", + "unrestore_app": "لن يتم استعادة التطبيق '{app}'", + "yunohost_already_installed": "إنّ YunoHost مُنصّب مِن قَبل", + "hook_name_unknown": "إسم الإجراء '{name}' غير معروف" +} From dabf86be7767a1cedfb79aff0921868ffe19998f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= Date: Tue, 31 Jan 2023 11:31:41 +0000 Subject: [PATCH 082/319] Translated using Weblate (French) Currently translated at 100.0% (753 of 753 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 9e0a24578..c1647334a 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -749,7 +749,7 @@ "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}", "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des informations importantes à connaître. [{answers}]", "invalid_shell": "Shell invalide : {shell}", - "global_settings_setting_dns_exposure": "Versions/Suites d'IP à prendre en compte pour la configuration et le diagnostic du DNS", + "global_settings_setting_dns_exposure": "Suites d'IP à prendre en compte pour la configuration et le diagnostic du DNS", "global_settings_setting_dns_exposure_help": "NB : Ceci n'affecte que la configuration DNS recommandée et les vérifications de diagnostic. Cela n'affecte pas les configurations du système.", "diagnosis_ip_no_ipv6_tip_important": "IPv6 devrait généralement être configuré automatiquement par le système ou par votre fournisseur d'accès à internet (FAI) s'il est disponible. Sinon, vous devrez peut-être configurer quelques éléments manuellement, comme expliqué dans la documentation ici : https://yunohost.org/#/ipv6." } From 7e9678622a0131e1586c367cc6984bc0dccdec87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Thu, 2 Feb 2023 05:27:30 +0000 Subject: [PATCH 083/319] Translated using Weblate (Galician) Currently translated at 99.6% (750 of 753 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/locales/gl.json b/locales/gl.json index c592c650f..7ba5c181f 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -745,5 +745,8 @@ "app_yunohost_version_not_supported": "Esta app require YunoHost >= {required} pero a versión actual instalada é {current}", "confirm_app_insufficient_ram": "PERIGO! Esta app precisa {required} de RAM para instalar/actualizar pero só hai {current} dispoñibles. Incluso se a app funcionase, o seu proceso de instalación/actualización require gran cantidade de RAM e o teu servidor podería colgarse e fallar. Se queres asumir o risco, escribe '{answers}'", "confirm_notifications_read": "AVISO: Deberías comprobar as notificacións da app antes de continuar, poderías ter información importante que revisar. [{answers}]", - "app_not_enough_ram": "Esta app require {required} de RAM para instalar/actualizar pero só hai {current} dispoñible." -} \ No newline at end of file + "app_not_enough_ram": "Esta app require {required} de RAM para instalar/actualizar pero só hai {current} dispoñible.", + "global_settings_setting_dns_exposure": "Versións de IP a ter en conta para a configuración DNS e diagnóstico", + "global_settings_setting_dns_exposure_help": "Nota: Esto só lle afecta á configuración DNS recomendada e diagnóstico do sistema. Non lle afecta aos axustes do sistema.", + "diagnosis_ip_no_ipv6_tip_important": "Se está dispoñible, IPv6 debería estar automáticamente configurado polo sistema ou o teu provedor. Se non, pode que teñas que facer algúns axustes manualmente tal como se explica na documentación: https://yunohost.org/#/ipv6." +} From 3577956c06c6c52b3876d46462b84f0c97e831c1 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Thu, 2 Feb 2023 13:35:03 +0000 Subject: [PATCH 084/319] [CI] Format code with Black --- doc/generate_api_doc.py | 5 ++--- src/certificate.py | 7 +++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/doc/generate_api_doc.py b/doc/generate_api_doc.py index 514415eef..6b75f8a73 100644 --- a/doc/generate_api_doc.py +++ b/doc/generate_api_doc.py @@ -26,14 +26,13 @@ import requests def main(): - with open("../share/actionsmap.yml") as f: action_map = yaml.safe_load(f) - #try: + # try: # with open("/etc/yunohost/current_host", "r") as f: # domain = f.readline().rstrip() - #except IOError: + # except IOError: # domain = requests.get("http://ip.yunohost.org").text with open("../debian/changelog") as f: diff --git a/src/certificate.py b/src/certificate.py index c9165fa6d..a0eba212a 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -735,10 +735,9 @@ def _enable_certificate(domain, new_cert_folder): for service in ("dovecot", "metronome"): # Ugly trick to not restart metronome if it's not installed or no domain configured for XMPP - if ( - service == "metronome" - and (os.system("dpkg --list | grep -q 'ii *metronome'") != 0 - or not glob("/etc/metronome/conf.d/*.cfg.lua")) + if service == "metronome" and ( + os.system("dpkg --list | grep -q 'ii *metronome'") != 0 + or not glob("/etc/metronome/conf.d/*.cfg.lua") ): continue _run_service_command("restart", service) From 7c4c3188e497900741a38772185045b6318589ef Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 14:37:17 +0100 Subject: [PATCH 085/319] Unused import --- doc/generate_api_doc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/generate_api_doc.py b/doc/generate_api_doc.py index 6b75f8a73..bb5f1df29 100644 --- a/doc/generate_api_doc.py +++ b/doc/generate_api_doc.py @@ -22,7 +22,6 @@ import os import sys import yaml import json -import requests def main(): From 6372bd3d4ef2c8746fa538ca5c0cabf474e51ade Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Thu, 2 Feb 2023 14:03:54 +0000 Subject: [PATCH 086/319] [CI] Reformat / remove stale translated strings --- locales/ar.json | 2 +- locales/en.json | 4 ++-- locales/fr.json | 2 +- locales/gl.json | 2 +- locales/tr.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index c1d6cbcfb..c9f35dab3 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -210,4 +210,4 @@ "unrestore_app": "لن يتم استعادة التطبيق '{app}'", "yunohost_already_installed": "إنّ YunoHost مُنصّب مِن قَبل", "hook_name_unknown": "إسم الإجراء '{name}' غير معروف" -} +} \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index 9f56aacd5..f2ba48af4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -400,12 +400,12 @@ "firewall_reloaded": "Firewall reloaded", "firewall_rules_cmd_failed": "Some firewall rule commands have failed. More info in log.", "global_settings_reset_success": "Reset global settings", - "global_settings_setting_dns_exposure": "IP versions to consider for DNS configuration and diagnosis", - "global_settings_setting_dns_exposure_help": "NB: This only affects the recommended DNS configuration and diagnosis checks. This does not affect system configurations.", "global_settings_setting_admin_strength": "Admin password strength requirements", "global_settings_setting_admin_strength_help": "These requirements are only enforced when initializing or changing the password", "global_settings_setting_backup_compress_tar_archives": "Compress backups", "global_settings_setting_backup_compress_tar_archives_help": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", + "global_settings_setting_dns_exposure": "IP versions to consider for DNS configuration and diagnosis", + "global_settings_setting_dns_exposure_help": "NB: This only affects the recommended DNS configuration and diagnosis checks. This does not affect system configurations.", "global_settings_setting_nginx_compatibility": "NGINX Compatibility", "global_settings_setting_nginx_compatibility_help": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", "global_settings_setting_nginx_redirect_to_https": "Force HTTPS", diff --git a/locales/fr.json b/locales/fr.json index c1647334a..70d7a2683 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -752,4 +752,4 @@ "global_settings_setting_dns_exposure": "Suites d'IP à prendre en compte pour la configuration et le diagnostic du DNS", "global_settings_setting_dns_exposure_help": "NB : Ceci n'affecte que la configuration DNS recommandée et les vérifications de diagnostic. Cela n'affecte pas les configurations du système.", "diagnosis_ip_no_ipv6_tip_important": "IPv6 devrait généralement être configuré automatiquement par le système ou par votre fournisseur d'accès à internet (FAI) s'il est disponible. Sinon, vous devrez peut-être configurer quelques éléments manuellement, comme expliqué dans la documentation ici : https://yunohost.org/#/ipv6." -} +} \ No newline at end of file diff --git a/locales/gl.json b/locales/gl.json index 7ba5c181f..1b5147ac6 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -749,4 +749,4 @@ "global_settings_setting_dns_exposure": "Versións de IP a ter en conta para a configuración DNS e diagnóstico", "global_settings_setting_dns_exposure_help": "Nota: Esto só lle afecta á configuración DNS recomendada e diagnóstico do sistema. Non lle afecta aos axustes do sistema.", "diagnosis_ip_no_ipv6_tip_important": "Se está dispoñible, IPv6 debería estar automáticamente configurado polo sistema ou o teu provedor. Se non, pode que teñas que facer algúns axustes manualmente tal como se explica na documentación: https://yunohost.org/#/ipv6." -} +} \ No newline at end of file diff --git a/locales/tr.json b/locales/tr.json index 6768f95e4..c219e997b 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -16,4 +16,4 @@ "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 {', '.join(required)} işlemci mimarisi üzerine kurulabilir ancak sunucunuzun işlemci mimarisi {current}." -} +} \ No newline at end of file From b9dc371a1c87ab0504e25a8a0cab87109190197a Mon Sep 17 00:00:00 2001 From: Florent Date: Thu, 2 Feb 2023 16:33:01 +0100 Subject: [PATCH 087/319] Fixes $app unbound when running ynh_secure_remove Fixes this issue: https://github.com/YunoHost/issues/issues/2138 --- helpers/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/utils b/helpers/utils index bc83888e9..cd3b1b8d2 100644 --- a/helpers/utils +++ b/helpers/utils @@ -718,7 +718,7 @@ _acceptable_path_to_delete() { local forbidden_paths=$(ls -d / /* /{var,home,usr}/* /etc/{default,sudoers.d,yunohost,cron*}) # Legacy : A couple apps still have data in /home/$app ... - if [[ -n "$app" ]] + if [[ -n "${app:-}" ]] then forbidden_paths=$(echo "$forbidden_paths" | grep -v "/home/$app") fi From a9ac55e4a5e7bfb3e22594f4d54d1073a06cd816 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 22:54:39 +0100 Subject: [PATCH 088/319] log/appv2: don't dump all settings in log metadata --- src/log.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/log.py b/src/log.py index f8eb65f8f..dc4ba3dbf 100644 --- a/src/log.py +++ b/src/log.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 copy import os import re import yaml @@ -594,6 +595,14 @@ class OperationLogger: Write or rewrite the metadata file with all metadata known """ + metadata = copy.copy(self.metadata) + + # Remove lower-case keys ... this is because with the new v2 app packaging, + # all settings are included in the env but we probably don't want to dump all of these + # which may contain various secret/private data ... + if "env" in metadata: + metadata["env"] = {k: v for k, v in metadata["env"].items() if k == k.upper()} + dump = yaml.safe_dump(self.metadata, default_flow_style=False) for data in self.data_to_redact: # N.B. : we need quotes here, otherwise yaml isn't happy about loading the yml later From 3110460a40981f5fba6d7d3ccdad31ba9046b700 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 22:59:43 +0100 Subject: [PATCH 089/319] appv2: resource upgrade will tweak settings, we have to re-update the env_dict after upgrading resources --- src/app.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/app.py b/src/app.py index 205dec505..2b3496655 100644 --- a/src/app.py +++ b/src/app.py @@ -677,11 +677,17 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False env_dict = _make_environment_for_app_script( app_instance_name, workdir=extracted_app_folder, action="upgrade" ) - env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type - env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version) - env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version) + + 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), + } + if manifest["packaging_format"] < 2: - env_dict["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0" + env_dict_more["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0" + + env_dict.update(env_dict_more) # Start register change on system related_to = [("app", app_instance_name)] @@ -698,6 +704,13 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False operation_logger=operation_logger, ) + # Boring stuff : the resource upgrade may have added/remove/updated setting + # so we need to reflect this in the env_dict used to call the actual upgrade script x_x + env_dict = _make_environment_for_app_script( + app_instance_name, workdir=extracted_app_folder, action="upgrade" + ) + env_dict.update(env_dict_more) + # Execute the app upgrade script upgrade_failed = True try: From 8090acb158042687486d556b41790ac6833ecc9c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 23:09:58 +0100 Subject: [PATCH 090/319] backup: add name of the backup in create/delete message, otherwise that creates some spooky messages with 'Backup created' directly followed by 'Backup deleted' during safety-backup-before-upgrade in v2 apps --- locales/en.json | 6 +++--- src/backup.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index f2ba48af4..b8ca0c229 100644 --- a/locales/en.json +++ b/locales/en.json @@ -102,14 +102,14 @@ "backup_copying_to_organize_the_archive": "Copying {size}MB to organize the archive", "backup_couldnt_bind": "Could not bind {src} to {dest}.", "backup_create_size_estimation": "The archive will contain about {size} of data.", - "backup_created": "Backup created", + "backup_created": "Backup created: {name}", "backup_creation_failed": "Could not create the backup archive", "backup_csv_addition_failed": "Could not add files to backup into the CSV file", "backup_csv_creation_failed": "Could not create the CSV file needed for restoration", "backup_custom_backup_error": "Custom backup method could not get past the 'backup' step", "backup_custom_mount_error": "Custom backup method could not get past the 'mount' step", "backup_delete_error": "Could not delete '{path}'", - "backup_deleted": "Backup deleted", + "backup_deleted": "Backup deleted: {name}", "backup_hook_unknown": "The backup hook '{hook}' is unknown", "backup_method_copy_finished": "Backup copy finalized", "backup_method_custom_finished": "Custom backup method '{method}' finished", @@ -752,4 +752,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/src/backup.py b/src/backup.py index 0783996b9..bb4f8af0e 100644 --- a/src/backup.py +++ b/src/backup.py @@ -2295,7 +2295,7 @@ def backup_create( ) backup_manager.backup() - logger.success(m18n.n("backup_created")) + logger.success(m18n.n("backup_created", name=name)) operation_logger.success() return { @@ -2622,7 +2622,7 @@ def backup_delete(name): hook_callback("post_backup_delete", args=[name]) - logger.success(m18n.n("backup_deleted")) + logger.success(m18n.n("backup_deleted", name=name)) # From 1c95bcff094231edf495a54967a2ac27697bffd1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 23:13:00 +0100 Subject: [PATCH 091/319] appv2: safety-backup-before-upgrade should only contain the app --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 2b3496655..ab0c5d720 100644 --- a/src/app.py +++ b/src/app.py @@ -647,7 +647,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False 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]) + backup_create(name=safety_backup_name, apps=[app_instance_name], system=None) if safety_backup_name in backup_list()["archives"]: # if the backup suceeded, delete old safety backup to save space From 2b2d49a504372c7147018c91feb63451036b6136 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 23:20:29 +0100 Subject: [PATCH 092/319] appv2: fix env not including vars for v1->v2 upgrade --- src/app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app.py b/src/app.py index ab0c5d720..e0e08a215 100644 --- a/src/app.py +++ b/src/app.py @@ -706,8 +706,9 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False # Boring stuff : the resource upgrade may have added/remove/updated setting # so we need to reflect this in the env_dict used to call the actual upgrade script x_x + # Or: the old manifest may be in v1 and the new in v2, so force to add the setting in env env_dict = _make_environment_for_app_script( - app_instance_name, workdir=extracted_app_folder, action="upgrade" + app_instance_name, workdir=extracted_app_folder, action="upgrade", include_app_settings=True, ) env_dict.update(env_dict_more) @@ -2731,7 +2732,7 @@ def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False def _make_environment_for_app_script( - app, args={}, args_prefix="APP_ARG_", workdir=None, action=None + app, args={}, args_prefix="APP_ARG_", workdir=None, action=None, include_app_settings=False, ): app_setting_path = os.path.join(APPS_SETTING_PATH, app) @@ -2758,7 +2759,7 @@ def _make_environment_for_app_script( env_dict[f"YNH_{args_prefix}{arg_name_upper}"] = str(arg_value) # If packaging format v2, load all settings - if manifest["packaging_format"] >= 2: + if manifest["packaging_format"] >= 2 or include_app_settings: env_dict["app"] = app for setting_name, setting_value in _get_app_settings(app).items(): # Ignore special internal settings like checksum__ From 80b38d0e8a8f99cd1cf7154a7216c21a6132efff Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Thu, 2 Feb 2023 18:35:49 +0000 Subject: [PATCH 093/319] Translated using Weblate (Arabic) Currently translated at 22.7% (171 of 753 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index c9f35dab3..04fd27001 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -209,5 +209,10 @@ "unbackup_app": "لن يتم حفظ التطبيق '{app}'", "unrestore_app": "لن يتم استعادة التطبيق '{app}'", "yunohost_already_installed": "إنّ YunoHost مُنصّب مِن قَبل", - "hook_name_unknown": "إسم الإجراء '{name}' غير معروف" -} \ No newline at end of file + "hook_name_unknown": "إسم الإجراء '{name}' غير معروف", + "app_manifest_install_ask_admin": "اختر مستخدمًا إداريًا لهذا التطبيق", + "domain_config_cert_summary_abouttoexpire": "مدة صلاحية الشهادة الحالية على وشك الإنتهاء ومِن المفتَرض أن يتم تجديدها تلقائيا قريبا.", + "app_manifest_install_ask_path": "اختر مسار URL (بعد النطاق) حيث ينبغي تنصيب هذا التطبيق", + "app_manifest_install_ask_domain": "اختر اسم النطاق الذي ينبغي فيه تنصيب هذا التطبيق", + "app_manifest_install_ask_is_public": "هل يجب أن يكون هذا التطبيق ظاهرًا للزوار المجهولين؟" +} From 98fe846886f0dd7c0c8d3973aeab6472efda1e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= Date: Thu, 2 Feb 2023 14:44:47 +0000 Subject: [PATCH 094/319] Translated using Weblate (French) Currently translated at 100.0% (753 of 753 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 70d7a2683..e8302fb82 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -652,7 +652,7 @@ "tools_upgrade_failed": "Impossible de mettre à jour les paquets : {packages_list}", "migration_0023_not_enough_space": "Prévoyez suffisamment d'espace disponible dans {path} pour exécuter la migration.", "migration_0023_postgresql_11_not_installed": "PostgreSQL n'a pas été installé sur votre système. Il n'y a rien à faire.", - "global_settings_setting_backup_compress_tar_archives_help": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar). N.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.", + "global_settings_setting_backup_compress_tar_archives_help": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar).\nN.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.", "global_settings_setting_security_experimental_enabled": "Fonctionnalités de sécurité expérimentales", "global_settings_setting_security_experimental_enabled_help": "Activer les fonctionnalités de sécurité expérimentales (ne l'activez pas si vous ne savez pas ce que vous faites !)", "global_settings_setting_nginx_compatibility_help": "Compromis 'compatibilité versus sécurité' pour le serveur web NGINX. Affecte les cryptogrammes utilisés (et d'autres aspects liés à la sécurité)", @@ -752,4 +752,4 @@ "global_settings_setting_dns_exposure": "Suites d'IP à prendre en compte pour la configuration et le diagnostic du DNS", "global_settings_setting_dns_exposure_help": "NB : Ceci n'affecte que la configuration DNS recommandée et les vérifications de diagnostic. Cela n'affecte pas les configurations du système.", "diagnosis_ip_no_ipv6_tip_important": "IPv6 devrait généralement être configuré automatiquement par le système ou par votre fournisseur d'accès à internet (FAI) s'il est disponible. Sinon, vous devrez peut-être configurer quelques éléments manuellement, comme expliqué dans la documentation ici : https://yunohost.org/#/ipv6." -} \ No newline at end of file +} From dd49ed2154f39f78fa11cf7ccced8eccd9ac0d63 Mon Sep 17 00:00:00 2001 From: Eryk Michalak Date: Thu, 2 Feb 2023 16:59:22 +0000 Subject: [PATCH 095/319] Translated using Weblate (Polish) Currently translated at 9.1% (69 of 753 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 66 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index 6734e6558..c73de7314 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -5,5 +5,67 @@ "already_up_to_date": "Nic do zrobienia. Wszystko jest obecnie aktualne.", "admin_password": "Hasło administratora", "action_invalid": "Nieprawidłowe działanie '{action:s}'", - "aborting": "Przerywanie." -} \ No newline at end of file + "aborting": "Przerywanie.", + "domain_config_auth_consumer_key": "Klucz konsumenta", + "domain_config_cert_validity": "Ważność", + "visitors": "Odwiedzający", + "app_start_install": "Instalowanie {app}...", + "app_unknown": "Nieznana aplikacja", + "ask_main_domain": "Domena główna", + "backup_created": "Utworzono kopię zapasową", + "firewall_reloaded": "Przeładowano zaporę sieciową", + "user_created": "Utworzono użytkownika", + "yunohost_installing": "Instalowanie YunoHost...", + "global_settings_setting_smtp_allow_ipv6": "Zezwól na IPv6", + "user_deleted": "Usunięto użytkownika", + "domain_config_default_app": "Domyślna aplikacja", + "restore_complete": "Przywracanie zakończone", + "domain_deleted": "Usunięto domenę", + "domains_available": "Dostępne domeny:", + "domain_config_api_protocol": "API protokołu", + "domain_config_auth_application_key": "Klucz aplikacji", + "diagnosis_description_systemresources": "Zasoby systemu", + "log_user_import": "Importuj użytkowników", + "system_upgraded": "Zaktualizowano system", + "diagnosis_description_regenconf": "Konfiguracja systemu", + "diagnosis_description_apps": "Aplikacje", + "diagnosis_description_basesystem": "Podstawowy system", + "unlimit": "Brak limitu", + "global_settings_setting_pop3_enabled": "Włącz POP3", + "domain_created": "Utworzono domenę", + "ask_new_admin_password": "Nowe hasło administracyjne", + "ask_new_domain": "Nowa domena", + "ask_new_path": "Nowa ścieżka", + "downloading": "Pobieranie...", + "ask_password": "Hasło", + "backup_deleted": "Usunięto kopię zapasową", + "done": "Gotowe", + "diagnosis_description_dnsrecords": "Rekordy DNS", + "diagnosis_description_ip": "Połączenie z internetem", + "diagnosis_description_mail": "Email", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Błąd: {error}", + "diagnosis_mail_queue_unavailable_details": "Błąd: {error}", + "diagnosis_http_could_not_diagnose_details": "Błąd: {error}", + "installation_complete": "Instalacja zakończona", + "app_start_remove": "Usuwanie {app}...", + "app_start_restore": "Przywracanie {app}...", + "app_upgraded": "Zaktualizowano {app}", + "extracting": "Rozpakowywanie...", + "app_removed": "Odinstalowano {app}", + "upgrade_complete": "Aktualizacja zakończona", + "global_settings_setting_backup_compress_tar_archives": "Kompresuj kopie zapasowe", + "global_settings_setting_nginx_compatibility": "Kompatybilność z NGINX", + "global_settings_setting_nginx_redirect_to_https": "Wymuszaj HTTPS", + "ask_admin_username": "Nazwa użytkownika administratora", + "ask_fullname": "Pełne imię i nazwisko", + "upgrading_packages": "Aktualizowanie paczek...", + "admins": "Administratorzy", + "diagnosis_ports_could_not_diagnose_details": "Błąd: {error}", + "log_settings_set": "Zastosuj ustawienia", + "domain_config_cert_issuer": "Organ certyfikacji", + "domain_config_cert_summary": "Status certyfikatu", + "global_settings_setting_ssh_compatibility": "Kompatybilność z SSH", + "global_settings_setting_ssh_port": "Port SSH", + "log_settings_reset": "Resetuj ustawienia", + "log_tools_migrations_migrate_forward": "Uruchom migracje" +} From d32fd89aeaae96ab9adf1c446a6c9d339047bf71 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 23:38:55 +0100 Subject: [PATCH 096/319] Update changelog for 11.1.5.1 --- debian/changelog | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/debian/changelog b/debian/changelog index 0d13b3c92..c6dc7c92d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +yunohost (11.1.5.1) stable; urgency=low + + - debian: Bump moulinette/ssowat requirement to 11.1 (0826a541) + - helpers: Fixes $app unbound when running ynh_secure_remove ([#1582](https://github.com/yunohost/yunohost/pull/1582)) + - log/appv2: don't dump all settings in log metadata (a9ac55e4) + - appv2: resource upgrade will tweak settings, we have to re-update the env_dict after upgrading resources (3110460a) + - appv2: safety-backup-before-upgrade should only contain the app (1c95bcff) + - appv2: fix env not including vars for v1->v2 upgrade (2b2d49a5) + - backup: add name of the backup in create/delete message, otherwise that creates some spooky messages with 'Backup created' directly followed by 'Backup deleted' during safety-backup-before-upgrade in v2 apps (8090acb1) + - [i18n] Translations updated for Arabic, French, Galician, Polish + + Thanks to all contributors <3 ! (ButterflyOfFire, Éric Gaspar, Eryk Michalak, Florent, José M, ppr) + + -- Alexandre Aubin Thu, 02 Feb 2023 23:37:46 +0100 + yunohost (11.1.5) stable; urgency=low - Release as stable ! From ca3fb8528652acbcfe5ec43f134768e0011870b7 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Thu, 2 Feb 2023 22:43:47 +0000 Subject: [PATCH 097/319] [CI] Format code with Black --- src/app.py | 16 +++++++++++++--- src/log.py | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/app.py b/src/app.py index e0e08a215..b8bed3c47 100644 --- a/src/app.py +++ b/src/app.py @@ -647,7 +647,9 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False 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) + backup_create( + name=safety_backup_name, apps=[app_instance_name], system=None + ) if safety_backup_name in backup_list()["archives"]: # if the backup suceeded, delete old safety backup to save space @@ -708,7 +710,10 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False # so we need to reflect this in the env_dict used to call the actual upgrade script x_x # Or: the old manifest may be in v1 and the new in v2, so force to add the setting in env env_dict = _make_environment_for_app_script( - app_instance_name, workdir=extracted_app_folder, action="upgrade", include_app_settings=True, + app_instance_name, + workdir=extracted_app_folder, + action="upgrade", + include_app_settings=True, ) env_dict.update(env_dict_more) @@ -2732,7 +2737,12 @@ def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False def _make_environment_for_app_script( - app, args={}, args_prefix="APP_ARG_", workdir=None, action=None, include_app_settings=False, + app, + args={}, + args_prefix="APP_ARG_", + workdir=None, + action=None, + include_app_settings=False, ): app_setting_path = os.path.join(APPS_SETTING_PATH, app) diff --git a/src/log.py b/src/log.py index dc4ba3dbf..cc344a936 100644 --- a/src/log.py +++ b/src/log.py @@ -601,7 +601,9 @@ class OperationLogger: # all settings are included in the env but we probably don't want to dump all of these # which may contain various secret/private data ... if "env" in metadata: - metadata["env"] = {k: v for k, v in metadata["env"].items() if k == k.upper()} + metadata["env"] = { + k: v for k, v in metadata["env"].items() if k == k.upper() + } dump = yaml.safe_dump(self.metadata, default_flow_style=False) for data in self.data_to_redact: From f2e01e7a4a650f8faa6b08559b8e170c09fafee6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 23:50:09 +0100 Subject: [PATCH 098/319] ci: tweak rules for Black and translation fixes because that's really too much flood x_x --- .gitlab/ci/lint.gitlab-ci.yml | 3 +-- .gitlab/ci/translation.gitlab-ci.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index 2c2bdcc1d..69e87b6ca 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -44,5 +44,4 @@ black: - git push -f origin "ci-format-${CI_COMMIT_REF_NAME}":"ci-format-${CI_COMMIT_REF_NAME}" - hub pull-request -m "[CI] Format code with Black" -b Yunohost:$CI_COMMIT_REF_NAME -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd only: - variables: - - $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH + - tags diff --git a/.gitlab/ci/translation.gitlab-ci.yml b/.gitlab/ci/translation.gitlab-ci.yml index b6c683f57..83db2b5a4 100644 --- a/.gitlab/ci/translation.gitlab-ci.yml +++ b/.gitlab/ci/translation.gitlab-ci.yml @@ -26,7 +26,7 @@ autofix-translated-strings: - git checkout -b "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}" --no-track - python3 maintenance/missing_i18n_keys.py --fix - python3 maintenance/autofix_locale_format.py - - '[ $(git diff | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit + - '[ $(git diff --ignore-blank-lines --ignore-all-space --ignore-space-at-eol --ignore-cr-at-eol | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit - git commit -am "[CI] Reformat / remove stale translated strings" || true - git push -f origin "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}":"ci-remove-stale-translated-strings-${CI_COMMIT_REF_NAME}" - hub pull-request -m "[CI] Reformat / remove stale translated strings" -b Yunohost:$CI_COMMIT_REF_NAME -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd From ba4f192557fe7e0c41158104ae1cc5a3d429f396 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 23:51:04 +0100 Subject: [PATCH 099/319] maintenance: new year, update copyright header --- src/__init__.py | 2 +- src/app.py | 2 +- src/app_catalog.py | 2 +- src/authenticators/ldap_admin.py | 2 +- src/backup.py | 2 +- src/certificate.py | 2 +- src/diagnosers/00-basesystem.py | 2 +- src/diagnosers/10-ip.py | 2 +- src/diagnosers/12-dnsrecords.py | 2 +- src/diagnosers/14-ports.py | 2 +- src/diagnosers/21-web.py | 2 +- src/diagnosers/24-mail.py | 2 +- src/diagnosers/30-services.py | 2 +- src/diagnosers/50-systemresources.py | 2 +- src/diagnosers/70-regenconf.py | 2 +- src/diagnosers/80-apps.py | 2 +- src/diagnosers/__init__.py | 2 +- src/diagnosis.py | 2 +- src/dns.py | 2 +- src/domain.py | 2 +- src/dyndns.py | 2 +- src/firewall.py | 2 +- src/hook.py | 2 +- src/log.py | 2 +- src/permission.py | 2 +- src/regenconf.py | 2 +- src/service.py | 2 +- src/settings.py | 2 +- src/ssh.py | 2 +- src/tools.py | 2 +- src/user.py | 2 +- src/utils/__init__.py | 2 +- src/utils/config.py | 2 +- src/utils/dns.py | 2 +- src/utils/error.py | 2 +- src/utils/i18n.py | 2 +- src/utils/ldap.py | 2 +- src/utils/legacy.py | 2 +- src/utils/network.py | 2 +- src/utils/password.py | 2 +- src/utils/resources.py | 2 +- src/utils/system.py | 2 +- src/utils/yunopaste.py | 2 +- 43 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 4d4026fdf..d13d61089 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,6 +1,6 @@ #! /usr/bin/python # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/app.py b/src/app.py index b8bed3c47..6d754ab25 100644 --- a/src/app.py +++ b/src/app.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/app_catalog.py b/src/app_catalog.py index 59d2ebdc1..9fb662845 100644 --- a/src/app_catalog.py +++ b/src/app_catalog.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/authenticators/ldap_admin.py b/src/authenticators/ldap_admin.py index 8637b3833..b1b550bc0 100644 --- a/src/authenticators/ldap_admin.py +++ b/src/authenticators/ldap_admin.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/backup.py b/src/backup.py index bb4f8af0e..0727ad295 100644 --- a/src/backup.py +++ b/src/backup.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/certificate.py b/src/certificate.py index a0eba212a..52e0d8c1b 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/00-basesystem.py b/src/diagnosers/00-basesystem.py index 8be334406..336271bd1 100644 --- a/src/diagnosers/00-basesystem.py +++ b/src/diagnosers/00-basesystem.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index ea68fc7bb..4f9cd9708 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py index 58bd04d39..2d46f979c 100644 --- a/src/diagnosers/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index 12f2481f7..34c512f14 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index a12a83f94..2050cd658 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 9a4cd1fc3..857de687d 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/30-services.py b/src/diagnosers/30-services.py index 44bbf1745..42ea9d18f 100644 --- a/src/diagnosers/30-services.py +++ b/src/diagnosers/30-services.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/50-systemresources.py b/src/diagnosers/50-systemresources.py index 10a153c61..096c3483f 100644 --- a/src/diagnosers/50-systemresources.py +++ b/src/diagnosers/50-systemresources.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/70-regenconf.py b/src/diagnosers/70-regenconf.py index 7d11b9174..65195aac5 100644 --- a/src/diagnosers/70-regenconf.py +++ b/src/diagnosers/70-regenconf.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/80-apps.py b/src/diagnosers/80-apps.py index ae89f26d3..44ce86bcc 100644 --- a/src/diagnosers/80-apps.py +++ b/src/diagnosers/80-apps.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/__init__.py b/src/diagnosers/__init__.py index 5cad500fa..7c1e7b0cd 100644 --- a/src/diagnosers/__init__.py +++ b/src/diagnosers/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosis.py b/src/diagnosis.py index 6b9f8fa92..02047c001 100644 --- a/src/diagnosis.py +++ b/src/diagnosis.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/dns.py b/src/dns.py index e697e6324..d4c9b1380 100644 --- a/src/dns.py +++ b/src/dns.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/domain.py b/src/domain.py index 5728c6884..e83b5e3e8 100644 --- a/src/domain.py +++ b/src/domain.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/dyndns.py b/src/dyndns.py index 9cba360ab..2594abe8f 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/firewall.py b/src/firewall.py index f4d7f77fe..073e48c88 100644 --- a/src/firewall.py +++ b/src/firewall.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/hook.py b/src/hook.py index eb5a7c035..42d9d3eac 100644 --- a/src/hook.py +++ b/src/hook.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/log.py b/src/log.py index cc344a936..e7ea18857 100644 --- a/src/log.py +++ b/src/log.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/permission.py b/src/permission.py index 7f5a65f2e..c7446b7ad 100644 --- a/src/permission.py +++ b/src/permission.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/regenconf.py b/src/regenconf.py index 7acc6f58f..69bedb262 100644 --- a/src/regenconf.py +++ b/src/regenconf.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/service.py b/src/service.py index 935e87339..a3bcc5561 100644 --- a/src/service.py +++ b/src/service.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/settings.py b/src/settings.py index a06377176..fbe4db7d0 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/ssh.py b/src/ssh.py index d5951cba5..2ae5ffe46 100644 --- a/src/ssh.py +++ b/src/ssh.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/tools.py b/src/tools.py index 777d8fc8f..dee4c8486 100644 --- a/src/tools.py +++ b/src/tools.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/user.py b/src/user.py index 797c3252f..12f13f75c 100644 --- a/src/user.py +++ b/src/user.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 5cad500fa..7c1e7b0cd 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/config.py b/src/utils/config.py index 5dce4070d..534cddcb3 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/dns.py b/src/utils/dns.py index 225a0e98f..b3ca4b564 100644 --- a/src/utils/dns.py +++ b/src/utils/dns.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/error.py b/src/utils/error.py index cdf2a3d09..9be48c5df 100644 --- a/src/utils/error.py +++ b/src/utils/error.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/i18n.py b/src/utils/i18n.py index ecbfe36e8..2aafafbdd 100644 --- a/src/utils/i18n.py +++ b/src/utils/i18n.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/ldap.py b/src/utils/ldap.py index 5a0e3ba35..6b41cdb22 100644 --- a/src/utils/ldap.py +++ b/src/utils/ldap.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/legacy.py b/src/utils/legacy.py index fa0b68137..82507d64d 100644 --- a/src/utils/legacy.py +++ b/src/utils/legacy.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/network.py b/src/utils/network.py index e9892333e..2a13f966e 100644 --- a/src/utils/network.py +++ b/src/utils/network.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/password.py b/src/utils/password.py index 569833a7d..833933d33 100644 --- a/src/utils/password.py +++ b/src/utils/password.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/resources.py b/src/utils/resources.py index 569512006..96e1b349d 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/system.py b/src/utils/system.py index c55023e52..2538f74fb 100644 --- a/src/utils/system.py +++ b/src/utils/system.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/yunopaste.py b/src/utils/yunopaste.py index 806f8a34f..46131846d 100644 --- a/src/utils/yunopaste.py +++ b/src/utils/yunopaste.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 YunoHost Contributors +# Copyright (c) 2023 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # From 9b7668dab0b446a5b37d5d059106ec91f9a9b69f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 23:57:24 +0100 Subject: [PATCH 100/319] helpers: fix remaining __FINALPATH__ in php template (note that this is backward compatible because ynh_add_config will replace __INSTALL_DIR__ by $finalpath if $finalpath exists... --- helpers/php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/php b/helpers/php index 6119c4870..0149bc243 100644 --- a/helpers/php +++ b/helpers/php @@ -156,7 +156,7 @@ ynh_add_fpm_config() { user = __APP__ group = __APP__ -chdir = __FINALPATH__ +chdir = __INSTALL_DIR__ listen = /var/run/php/php__PHPVERSION__-fpm-__APP__.sock listen.owner = www-data From 3f2dbe87543968d55a29db293f197a11228da0a1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Feb 2023 23:59:08 +0100 Subject: [PATCH 101/319] Update changelog for 11.1.5.2 --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index c6dc7c92d..4cc1b745f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (11.1.5.2) testing; urgency=low + + - maintenance: new year, update copyright header (ba4f1925) + - helpers: fix remaining __FINALPATH__ in php template (note that this is backward compatible because ynh_add_config will replace __INSTALL_DIR__ by $finalpath if $finalpath exists... (9b7668da) + + -- Alexandre Aubin Thu, 02 Feb 2023 23:58:29 +0100 + yunohost (11.1.5.1) stable; urgency=low - debian: Bump moulinette/ssowat requirement to 11.1 (0826a541) From 13d4e16e7de7deab1d4a9e606f5b4ea7b8ebbc0b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Feb 2023 02:25:36 +0100 Subject: [PATCH 102/319] helpers/appsv2: replacement of __PHPVERSION__ should use the phpversion setting, not YNH_PHP_VERSION --- helpers/utils | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/helpers/utils b/helpers/utils index cd3b1b8d2..a0efa2c45 100644 --- a/helpers/utils +++ b/helpers/utils @@ -348,7 +348,7 @@ ynh_local_curl() { # __NAMETOCHANGE__ by $app # __USER__ by $app # __FINALPATH__ by $final_path -# __PHPVERSION__ by $YNH_PHP_VERSION +# __PHPVERSION__ by $YNH_PHP_VERSION (packaging v1 only, packaging v2 uses phpversion setting implicitly set by apt resource) # __YNH_NODE_LOAD_PATH__ by $ynh_node_load_PATH # ``` # And any dynamic variables that should be defined before calling this helper like: @@ -417,7 +417,7 @@ ynh_add_config() { # __NAMETOCHANGE__ by $app # __USER__ by $app # __FINALPATH__ by $final_path -# __PHPVERSION__ by $YNH_PHP_VERSION +# __PHPVERSION__ by $YNH_PHP_VERSION (packaging v1 only, packaging v2 uses phpversion setting implicitly set by apt resource) # __YNH_NODE_LOAD_PATH__ by $ynh_node_load_PATH # # And any dynamic variables that should be defined before calling this helper like: @@ -452,7 +452,8 @@ ynh_replace_vars() { ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$file" ynh_replace_string --match_string="__INSTALL_DIR__" --replace_string="$final_path" --target_file="$file" fi - if test -n "${YNH_PHP_VERSION:-}"; then + # Legacy / Packaging v1 only + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2 && test -n "${YNH_PHP_VERSION:-}"; then ynh_replace_string --match_string="__PHPVERSION__" --replace_string="$YNH_PHP_VERSION" --target_file="$file" fi if test -n "${ynh_node_load_PATH:-}"; then From 2107a84852b0c5be2c64d1095c7928a33674e693 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Feb 2023 02:41:28 +0100 Subject: [PATCH 103/319] appv2 resources: document the fact that the apt resource may create a phpversion setting when the dependencies contain php packages --- src/utils/resources.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/resources.py b/src/utils/resources.py index 96e1b349d..65c8ee8cd 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -703,6 +703,7 @@ class AptDependenciesAppResource(AppResource): ##### Provision/Update: - The code literally calls the bash helpers `ynh_install_app_dependencies` and `ynh_install_extra_app_dependencies`, similar to what happens in v1. + - Note that when `packages` contains some phpX.Y-foobar dependencies, this will automagically define a `phpversion` setting equal to `X.Y` which can therefore be used in app scripts ($phpversion) or templates (`__PHPVERSION__`) ##### Deprovision: - The code literally calls the bash helper `ynh_remove_app_dependencies` From c255fe24955e00c2041d93766624c06b39c5b984 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Feb 2023 03:05:40 +0100 Subject: [PATCH 104/319] Update changelog for 11.1.5.3 --- debian/changelog | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 4cc1b745f..db74b6d6a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,11 @@ -yunohost (11.1.5.2) testing; urgency=low +yunohost (11.1.5.3) stable; urgency=low + + - helpers/appsv2: replacement of __PHPVERSION__ should use the phpversion setting, not YNH_PHP_VERSION (13d4e16e) + - appv2 resources: document the fact that the apt resource may create a phpversion setting when the dependencies contain php packages (2107a848) + + -- Alexandre Aubin Fri, 03 Feb 2023 03:05:11 +0100 + +yunohost (11.1.5.2) stable; urgency=low - maintenance: new year, update copyright header (ba4f1925) - helpers: fix remaining __FINALPATH__ in php template (note that this is backward compatible because ynh_add_config will replace __INSTALL_DIR__ by $finalpath if $finalpath exists... (9b7668da) From 634fd6e7fc1515c037b2918acb4802126629f628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= <46165813+ericgaspar@users.noreply.github.com> Date: Fri, 3 Feb 2023 09:45:20 +0100 Subject: [PATCH 105/319] Fix workdir variable for package v.2 --- helpers/php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helpers/php b/helpers/php index 0149bc243..8fff8d78b 100644 --- a/helpers/php +++ b/helpers/php @@ -499,9 +499,9 @@ ynh_composer_exec() { # Install and initialize Composer in the given directory # -# usage: ynh_install_composer [--phpversion=phpversion] [--workdir=$final_path] [--install_args="--optimize-autoloader"] [--composerversion=composerversion] +# usage: ynh_install_composer [--phpversion=phpversion] [--workdir=$install_dir] [--install_args="--optimize-autoloader"] [--composerversion=composerversion] # | arg: -v, --phpversion - PHP version to use with composer -# | arg: -w, --workdir - The directory from where the command will be executed. Default $final_path. +# | arg: -w, --workdir - The directory from where the command will be executed. Default $install_dir. # | arg: -a, --install_args - Additional arguments provided to the composer install. Argument --no-dev already include # | arg: -c, --composerversion - Composer version to install # @@ -516,7 +516,7 @@ ynh_install_composer() { local composerversion # Manage arguments with getopts ynh_handle_getopts_args "$@" - workdir="${workdir:-$final_path}" + workdir="${workdir:-$install_dir}" phpversion="${phpversion:-$YNH_PHP_VERSION}" install_args="${install_args:-}" composerversion="${composerversion:-$YNH_COMPOSER_VERSION}" From b06a3053f6cd645a87fb3c5b6bd219d26e5051db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= <46165813+ericgaspar@users.noreply.github.com> Date: Fri, 3 Feb 2023 10:21:09 +0100 Subject: [PATCH 106/319] Fix spacing --- src/utils/resources.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 65c8ee8cd..3b6f5d45e 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -58,13 +58,13 @@ class AppResourceManager: try: if todo == "deprovision": # FIXME : i18n, better info strings - logger.info(f"Deprovisionning {name} ...") + logger.info(f"Deprovisionning {name}...") old.deprovision(context=context) elif todo == "provision": - logger.info(f"Provisionning {name} ...") + logger.info(f"Provisionning {name}...") new.provision_or_update(context=context) elif todo == "update": - logger.info(f"Updating {name} ...") + logger.info(f"Updating {name}...") new.provision_or_update(context=context) except (KeyboardInterrupt, Exception) as e: exception = e @@ -87,13 +87,13 @@ 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"Reprovisionning {name}...") old.provision_or_update(context=context) elif todo == "provision": - logger.info(f"Deprovisionning {name} ...") + logger.info(f"Deprovisionning {name}...") new.deprovision(context=context) elif todo == "update": - logger.info(f"Reverting {name} ...") + logger.info(f"Reverting {name}...") old.provision_or_update(context=context) except (KeyboardInterrupt, Exception) as e: if isinstance(e, KeyboardInterrupt): @@ -222,7 +222,7 @@ ynh_abort_if_errors ) else: # FIXME: currently in app install code, we have - # more sophisticated code checking if this broke something on the system etc ... + # more sophisticated code checking if this broke something on the system etc. # dunno if we want to do this here or manage it elsewhere pass @@ -514,7 +514,7 @@ class InstalldirAppResource(AppResource): owner: str = "" group: str = "" - # FIXME: change default dir to /opt/stuff if app ain't a webapp ... + # FIXME: change default dir to /opt/stuff if app ain't a webapp... def provision_or_update(self, context: Dict = {}): assert self.dir.strip() # Be paranoid about self.dir being empty... @@ -539,7 +539,7 @@ class InstalldirAppResource(AppResource): # and check for available space on the destination if current_install_dir and os.path.isdir(current_install_dir): logger.warning( - f"Moving {current_install_dir} to {self.dir} ... (this may take a while)" + f"Moving {current_install_dir} to {self.dir}... (this may take a while)" ) shutil.move(current_install_dir, self.dir) else: @@ -643,7 +643,7 @@ class DatadirAppResource(AppResource): # FIXME: same as install_dir, is this what we want ? if current_data_dir and os.path.isdir(current_data_dir): logger.warning( - f"Moving {current_data_dir} to {self.dir} ... (this may take a while)" + f"Moving {current_data_dir} to {self.dir}... (this may take a while)" ) shutil.move(current_data_dir, self.dir) else: @@ -756,7 +756,7 @@ class PortsResource(AppResource): ##### Example: ```toml [resources.port] - # (empty should be fine for most apps ... though you can customize stuff if absolutely needed) + # (empty should be fine for most apps... though you can customize stuff if absolutely needed) main.default = 12345 # if you really want to specify a prefered value .. but shouldnt matter in the majority of cases @@ -814,7 +814,7 @@ class PortsResource(AppResource): super().__init__({"ports": properties}, *args, **kwargs) def _port_is_used(self, port): - # FIXME : this could be less brutal than two os.system ... + # FIXME : this could be less brutal than two os.system... cmd1 = ( "ss --numeric --listening --tcp --udp | awk '{print$5}' | grep --quiet --extended-regexp ':%s$'" % port @@ -903,7 +903,7 @@ class DatabaseAppResource(AppResource): """ # Notes for future? - # deep_clean -> ... idk look into any db name that would not be related to any app ... + # deep_clean -> ... idk look into any db name that would not be related to any app... # backup -> dump db # restore -> setup + inject db dump @@ -926,7 +926,7 @@ class DatabaseAppResource(AppResource): ) # Hack so that people can write type = "mysql/postgresql" in toml but it's loaded as dbtype - # to avoid conflicting with the generic self.type of the resource object ... + # to avoid conflicting with the generic self.type of the resource object... # dunno if that's really a good idea :| properties = {"dbtype": properties["type"]} From 0e787acb5db3a03902ace597705fd1f0e76309ff Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Feb 2023 15:32:09 +0100 Subject: [PATCH 107/319] appv2: typo in ports resource doc x_x --- 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 65c8ee8cd..586cb1583 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -755,7 +755,7 @@ class PortsResource(AppResource): ##### Example: ```toml - [resources.port] + [resources.ports] # (empty should be fine for most apps ... though you can customize stuff if absolutely needed) main.default = 12345 # if you really want to specify a prefered value .. but shouldnt matter in the majority of cases From 476908bdc2a02ea96a2e53dc647306a07fde1616 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Feb 2023 20:27:52 +0100 Subject: [PATCH 108/319] appsv2: fix permission provisioning for fulldomain apps + fix apps not properly getting removed after failed resources init --- src/app.py | 47 +++++++++++++++++++++++++++--------------- src/utils/resources.py | 10 +++++++++ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/app.py b/src/app.py index 6d754ab25..4e04c035a 100644 --- a/src/app.py +++ b/src/app.py @@ -1074,10 +1074,14 @@ def app_install( if packaging_format >= 2: from yunohost.utils.resources import AppResourceManager - AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( - rollback_and_raise_exception_if_failure=True, - operation_logger=operation_logger, - ) + try: + AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( + rollback_and_raise_exception_if_failure=True, + operation_logger=operation_logger, + ) + except (KeyboardInterrupt, EOFError, Exception) as e: + shutil.rmtree(app_setting_path) + raise e else: # Initialize the main permission for the app # The permission is initialized with no url associated, and with tile disabled @@ -2651,22 +2655,31 @@ def _guess_webapp_path_requirement(app_folder: str) -> str: if len(domain_questions) == 1 and len(path_questions) == 1: return "domain_and_path" if len(domain_questions) == 1 and len(path_questions) == 0: - # This is likely to be a full-domain app... - # Confirm that this is a full-domain app This should cover most cases - # ... though anyway the proper solution is to implement some mechanism - # in the manifest for app to declare that they require a full domain - # (among other thing) so that we can dynamically check/display this - # requirement on the webadmin form and not miserably fail at submit time + if manifest.get("packaging_format", 0) < 2: - # Full-domain apps typically declare something like path_url="/" or path=/ - # and use ynh_webpath_register or yunohost_app_checkurl inside the install script - install_script_content = read_file(os.path.join(app_folder, "scripts/install")) + # This is likely to be a full-domain app... - if re.search( - r"\npath(_url)?=[\"']?/[\"']?", install_script_content - ) and re.search(r"ynh_webpath_register", install_script_content): - return "full_domain" + # Confirm that this is a full-domain app This should cover most cases + # ... though anyway the proper solution is to implement some mechanism + # in the manifest for app to declare that they require a full domain + # (among other thing) so that we can dynamically check/display this + # requirement on the webadmin form and not miserably fail at submit time + + # Full-domain apps typically declare something like path_url="/" or path=/ + # and use ynh_webpath_register or yunohost_app_checkurl inside the install script + install_script_content = read_file(os.path.join(app_folder, "scripts/install")) + + if re.search( + r"\npath(_url)?=[\"']?/[\"']?", install_script_content + ) and re.search(r"ynh_webpath_register", install_script_content): + return "full_domain" + + else: + # For packaging v2 apps, check if there's a permission with url being a string + perm_resource = manifest.get("resources", {}).get("permissions") + if perm_resource is not None and isinstance(perm_resource.get("main", {}).get("url"), str): + return "full_domain" return "?" diff --git a/src/utils/resources.py b/src/utils/resources.py index 586cb1583..b1a31324c 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -320,6 +320,16 @@ class PermissionsResource(AppResource): # Delete legacy is_public setting if not already done self.delete_setting("is_public") + # Detect that we're using a full-domain app, + # in which case we probably need to automagically + # define the "path" setting with "/" + if ( + isinstance(self.permissions["main"]["url"], str) + and self.get_setting("domain") + and not self.get_setting("path") + ): + self.set_setting("path", "/") + existing_perms = user_permission_list(short=True, apps=[self.app])[ "permissions" ] From 9459aed65ebcce850a02895b53ca6781450f5614 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Feb 2023 20:43:42 +0100 Subject: [PATCH 109/319] Update changelog for 11.1.5.4 --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index db74b6d6a..85924f4e9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (11.1.5.4) stable; urgency=low + + - appsv2: typo in ports resource doc x_x (0e787acb) + - appsv2: fix permission provisioning for fulldomain apps + fix apps not properly getting removed after failed resources init (476908bd) + + -- Alexandre Aubin Fri, 03 Feb 2023 20:43:04 +0100 + yunohost (11.1.5.3) stable; urgency=low - helpers/appsv2: replacement of __PHPVERSION__ should use the phpversion setting, not YNH_PHP_VERSION (13d4e16e) From 3bbba640e9384f54fbeca6133114d44fd462744e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Feb 2023 21:05:23 +0100 Subject: [PATCH 110/319] appsv2: ignore the old/ugly/legacy removal of apt deps when removing the php conf, because that's handled by the apt resource --- helpers/php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/php b/helpers/php index 0149bc243..4522e65a9 100644 --- a/helpers/php +++ b/helpers/php @@ -283,7 +283,7 @@ ynh_remove_fpm_config() { # 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:-}" ]; then + 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 From 8485ebc75a9425ae90d44a466dbf81140112e21c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Feb 2023 21:13:17 +0100 Subject: [PATCH 111/319] admin->admins migration: try to handle boring case where the 'first' user cant be identified because it doesnt have the root@ alias --- src/migrations/0026_new_admins_group.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/migrations/0026_new_admins_group.py b/src/migrations/0026_new_admins_group.py index 98f2a54be..a260074fd 100644 --- a/src/migrations/0026_new_admins_group.py +++ b/src/migrations/0026_new_admins_group.py @@ -46,6 +46,19 @@ class MyMigration(Migration): new_admin_user = user break + # For some reason some system have no user with root@ alias, + # but the user does has admin / postmaster / ... alias + # ... try to find it instead otherwise this creashes the migration + # later because the admin@, postmaster@, .. aliases will already exist + if not new_admin_user: + for user in all_users: + aliases = user_info(user).get("mail-aliases", []) + if any(alias.startswith(f"admin@{main_domain}") for alias in aliases) \ + and any(alias.startswith(f"postmaster@{main_domain}") for alias in aliases): + new_admin_user = user + break + + self.ldap_migration_started = True if new_admin_user: From fb54da2e35f88e182ffc877c6e29a3209b022c5a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 4 Feb 2023 18:46:33 +0100 Subject: [PATCH 112/319] appsv2: moar fixes for v1->v2 upgrade not getting the proper env context --- 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 b1a31324c..e76a6cc92 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -179,7 +179,7 @@ class AppResource: tmpdir = _make_tmp_workdir_for_app(app=self.app) env_ = _make_environment_for_app_script( - self.app, workdir=tmpdir, action=f"{action}_{self.type}" + self.app, workdir=tmpdir, action=f"{action}_{self.type}", include_app_settings=True, ) env_.update(env) From a0350246665f4dd1f5756fb4780e83c3c675cc8b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 4 Feb 2023 18:51:35 +0100 Subject: [PATCH 113/319] Update changelog for 11.1.5.5 --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index 85924f4e9..42b3d9f1c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +yunohost (11.1.5.5) stable; urgency=low + + - admin->admins migration: try to handle boring case where the 'first' user cant be identified because it doesnt have the root@ alias (8485ebc7) + - appsv2: ignore the old/ugly/legacy removal of apt deps when removing the php conf, because that's handled by the apt resource (3bbba640) + - appsv2: moar fixes for v1->v2 upgrade not getting the proper env context (fb54da2e) + + -- Alexandre Aubin Sat, 04 Feb 2023 18:51:03 +0100 + yunohost (11.1.5.4) stable; urgency=low - appsv2: typo in ports resource doc x_x (0e787acb) From 4dee434e71f79d93eebc5ad98a5fde0fc37da665 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 5 Feb 2023 18:31:36 +0100 Subject: [PATCH 114/319] domains: add missing logic to inject translated 'help' keys in config panel like we do for global settings --- locales/en.json | 2 ++ share/config_domain.toml | 5 ----- src/domain.py | 20 +++++++++++++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/locales/en.json b/locales/en.json index b8ca0c229..3832cb6c0 100644 --- a/locales/en.json +++ b/locales/en.json @@ -345,9 +345,11 @@ "domain_config_cert_summary_selfsigned": "WARNING: Current certificate is self-signed. Browsers will display a spooky warning to new visitors!", "domain_config_cert_validity": "Validity", "domain_config_default_app": "Default app", + "domain_config_default_app_help": "People will automatically be redirected to this app when opening this domain. If no app is specified, people are redirected to the user portal login form.", "domain_config_mail_in": "Incoming emails", "domain_config_mail_out": "Outgoing emails", "domain_config_xmpp": "Instant messaging (XMPP)", + "domain_config_xmpp_help": "NB: some XMPP features will require that you update your DNS records and regenerate your Lets Encrypt certificate to be enabled", "domain_created": "Domain created", "domain_creation_failed": "Unable to create domain {domain}: {error}", "domain_deleted": "Domain deleted", diff --git a/share/config_domain.toml b/share/config_domain.toml index c67996d13..82ef90c32 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -9,8 +9,6 @@ name = "Features" type = "app" filter = "is_webapp" default = "_none" - # FIXME: i18n - help = "People will automatically be redirected to this app when opening this domain. If no app is specified, people are redirected to the user portal login form." [feature.mail] @@ -27,8 +25,6 @@ name = "Features" [feature.xmpp.xmpp] type = "boolean" default = 0 - # FIXME: i18n - help = "NB: some XMPP features will require that you update your DNS records and regenerate your Lets Encrypt certificate to be enabled" [dns] name = "DNS" @@ -67,7 +63,6 @@ name = "Certificate" visible = "acme_eligible == false || acme_eligible == null" [cert.cert.cert_no_checks] - ask = "Ignore diagnosis checks" type = "boolean" default = false visible = "acme_eligible == false || acme_eligible == null" diff --git a/src/domain.py b/src/domain.py index e83b5e3e8..7839b988d 100644 --- a/src/domain.py +++ b/src/domain.py @@ -624,14 +624,28 @@ class DomainConfigPanel(ConfigPanel): f"domain_config_cert_summary_{status['summary']}" ) - # Other specific strings used in config panels - # i18n: domain_config_cert_renew_help - # FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ... self.cert_status = status return toml + def 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 + def _load_current_values(self): # TODO add mechanism to share some settings with other domains on the same zone super()._load_current_values() From f742bdf83217519339a4cd56710c2fabe2eeee9d Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Mon, 6 Feb 2023 13:26:36 +0100 Subject: [PATCH 115/319] [fix] Allow to do replace string with @ --- helpers/string | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/string b/helpers/string index 4dd5c0b4b..dc1658e3d 100644 --- a/helpers/string +++ b/helpers/string @@ -48,7 +48,7 @@ ynh_replace_string() { ynh_handle_getopts_args "$@" set +o xtrace # set +x - local delimit=@ + local delimit=$'\001' # Escape the delimiter if it's in the string. match_string=${match_string//${delimit}/"\\${delimit}"} replace_string=${replace_string//${delimit}/"\\${delimit}"} From 8241e26fc29978298d8251f60ac5330aabed9089 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Mon, 6 Feb 2023 13:51:32 +0100 Subject: [PATCH 116/319] [fix] Write var in files with redundants keys --- helpers/utils | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/utils b/helpers/utils index a0efa2c45..4cd23da00 100644 --- a/helpers/utils +++ b/helpers/utils @@ -568,7 +568,7 @@ ynh_read_var_in_file() { var_part+='\s*' # Extract the part after assignation sign - local expression_with_comment="$(tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL | head -n1)" + local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" if [[ "$expression_with_comment" == "YNH_NULL" ]]; then set -o xtrace # set -x echo YNH_NULL @@ -647,7 +647,7 @@ ynh_write_var_in_file() { var_part+='\s*' # Extract the part after assignation sign - local expression_with_comment="$(tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL | head -n1)" + local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" if [[ "$expression_with_comment" == "YNH_NULL" ]]; then set -o xtrace # set -x return 1 From 9f686a115f1a15668940332d876cad306f7625d7 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Mon, 6 Feb 2023 14:28:06 +0100 Subject: [PATCH 117/319] [fix] Put a & into a config var --- helpers/utils | 1 + 1 file changed, 1 insertion(+) diff --git a/helpers/utils b/helpers/utils index 4cd23da00..04a3d1373 100644 --- a/helpers/utils +++ b/helpers/utils @@ -658,6 +658,7 @@ ynh_write_var_in_file() { endline=${expression_with_comment#"$expression"} endline="$(echo "$endline" | sed 's/\\/\\\\/g')" value="$(echo "$value" | sed 's/\\/\\\\/g')" + value=${value//&/"\&"} local first_char="${expression:0:1}" delimiter=$'\001' if [[ "$first_char" == '"' ]]; then From c179d4b88f4e756fa48693b09776e540cde01129 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Feb 2023 13:44:54 +0100 Subject: [PATCH 118/319] appsv2/group question: don't include primary groups in choices --- src/utils/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/config.py b/src/utils/config.py index 534cddcb3..5704686c0 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1359,7 +1359,7 @@ class GroupQuestion(Question): super().__init__(question, context) - self.choices = list(user_group_list(short=True)["groups"]) + self.choices = list(user_group_list(short=True, include_primary_groups=False)["groups"]) def _human_readable_group(g): # i18n: visitors From 71042f086065aec63de911c82f1177d9306c3dbf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Feb 2023 14:31:57 +0100 Subject: [PATCH 119/319] appsv2: when initalizing permission, make sure to add 'all_users' when visitors is chosen --- src/utils/resources.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/resources.py b/src/utils/resources.py index e76a6cc92..771a0e1e1 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -348,6 +348,11 @@ class PermissionsResource(AppResource): or self.get_setting(f"init_{perm}_permission") or [] ) + + # If we're choosing 'visitors' from the init_{perm}_permission question, add all_users too + if not infos["allowed"] and init_allowed == "visitors": + init_allowed = ["visitors", "all_users"] + permission_create( perm_id, allowed=init_allowed, From ca0db0ec58ca6969b6454d4b94573f042f90bac2 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 6 Feb 2023 15:44:50 +0100 Subject: [PATCH 120/319] [fix] ynh_write_var_in_file should replace one occurencies --- helpers/utils | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/helpers/utils b/helpers/utils index 04a3d1373..f80c22901 100644 --- a/helpers/utils +++ b/helpers/utils @@ -614,15 +614,14 @@ ynh_write_var_in_file() { set +o xtrace # set +x # Get the line number after which we search for the variable - local line_number=1 + local after_line_number=1 if [[ -n "$after" ]]; then - line_number=$(grep -m1 -n $after $file | cut -d: -f1) - if [[ -z "$line_number" ]]; then + after_line_number=$(grep -m1 -n $after $file | cut -d: -f1) + if [[ -z "$after_line_number" ]]; then set -o xtrace # set -x return 1 fi fi - local range="${line_number},\$ " local filename="$(basename -- "$file")" local ext="${filename##*.}" @@ -647,11 +646,14 @@ ynh_write_var_in_file() { var_part+='\s*' # Extract the part after assignation sign - local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" + local expression_with_comment="$((tail +$after_line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" if [[ "$expression_with_comment" == "YNH_NULL" ]]; then set -o xtrace # set -x return 1 fi + local value_line_number="$(tail +$after_line_number ${file} | grep -m1 -n -i -P $var_part'\K.*$' | cut -d: -f1)" + value_line_number=$((after_line_number + value_line_number)) + local range="${after_line_number},${value_line_number} " # Remove comments if needed local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" From 170eaf5d742e369645805b6ef725d21a1b818afb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Feb 2023 15:46:04 +0100 Subject: [PATCH 121/319] backup/multimedia: test that /home/yunohots.multimedia does exists to avoid boring warning later --- hooks/backup/18-data_multimedia | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/backup/18-data_multimedia b/hooks/backup/18-data_multimedia index f80cff0b3..94162d517 100644 --- a/hooks/backup/18-data_multimedia +++ b/hooks/backup/18-data_multimedia @@ -9,7 +9,7 @@ source /usr/share/yunohost/helpers # Backup destination backup_dir="${1}/data/multimedia" -if [ -e "/home/yunohost.multimedia/.nobackup" ]; then +if [ ! -e "/home/yunohost.multimedia" ] || [ -e "/home/yunohost.multimedia/.nobackup" ]; then exit 0 fi From 0f24846e0b91d471062eb84e3b4c06b1ff152b8f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Feb 2023 16:15:22 +0100 Subject: [PATCH 122/319] backup: fix previou fix /o\, name is sometimes None --- src/backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backup.py b/src/backup.py index 0727ad295..a3a24aa52 100644 --- a/src/backup.py +++ b/src/backup.py @@ -2295,7 +2295,7 @@ def backup_create( ) backup_manager.backup() - logger.success(m18n.n("backup_created", name=name)) + logger.success(m18n.n("backup_created", name=backup_manager.name)) operation_logger.success() return { From 29c6564f0900c3bb452e962b9e958adb4b743630 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Feb 2023 16:15:55 +0100 Subject: [PATCH 123/319] =?UTF-8?q?ci:=20fix=20backup=20test=20/=20gotta?= =?UTF-8?q?=20tell=20the=20mocker=20about=20the=20new=20'name'=20arg=20for?= =?UTF-8?q?=20backup=20create=20messages=20=C3=A9=5F=C3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tests/test_backuprestore.py | 44 ++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/tests/test_backuprestore.py b/src/tests/test_backuprestore.py index 28646960c..413d44470 100644 --- a/src/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -6,6 +6,8 @@ from mock import patch from .conftest import message, raiseYunohostError, get_test_apps_dir +from moulinette.utils.text import random_ascii + from yunohost.app import app_install, app_remove, app_ssowatconf from yunohost.app import _is_installed from yunohost.backup import ( @@ -236,8 +238,9 @@ def add_archive_system_from_4p2(): def test_backup_only_ldap(mocker): # Create the backup - with message(mocker, "backup_created"): - backup_create(system=["conf_ldap"], apps=None) + name = random_ascii(8) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=["conf_ldap"], apps=None) archives = backup_list()["archives"] assert len(archives) == 1 @@ -261,9 +264,10 @@ def test_backup_system_part_that_does_not_exists(mocker): def test_backup_and_restore_all_sys(mocker): + name = random_ascii(8) # Create the backup - with message(mocker, "backup_created"): - backup_create(system=[], apps=None) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=[], apps=None) archives = backup_list()["archives"] assert len(archives) == 1 @@ -294,9 +298,10 @@ def test_backup_and_restore_all_sys(mocker): @pytest.mark.with_system_archive_from_4p2 def test_restore_system_from_Ynh4p2(monkeypatch, mocker): + name = random_ascii(8) # Backup current system - with message(mocker, "backup_created"): - backup_create(system=[], apps=None) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=[], apps=None) archives = backup_list()["archives"] assert len(archives) == 2 @@ -393,16 +398,17 @@ def test_backup_app_with_no_restore_script(mocker): @pytest.mark.clean_opt_dir def test_backup_with_different_output_directory(mocker): + name = random_ascii(8) # Create the backup - with message(mocker, "backup_created"): + with message(mocker, "backup_created", name=name): backup_create( system=["conf_ynh_settings"], apps=None, output_directory="/opt/test_backup_output_directory", - name="backup", + name=name, ) - assert os.path.exists("/opt/test_backup_output_directory/backup.tar") + assert os.path.exists(f"/opt/test_backup_output_directory/{name}.tar") archives = backup_list()["archives"] assert len(archives) == 1 @@ -416,13 +422,14 @@ def test_backup_with_different_output_directory(mocker): @pytest.mark.clean_opt_dir def test_backup_using_copy_method(mocker): # Create the backup - with message(mocker, "backup_created"): + name = random_ascii(8) + with message(mocker, "backup_created", name=name): backup_create( system=["conf_ynh_settings"], apps=None, output_directory="/opt/test_backup_output_directory", methods=["copy"], - name="backup", + name=name, ) assert os.path.exists("/opt/test_backup_output_directory/info.json") @@ -565,8 +572,9 @@ def test_backup_and_restore_permission_app(mocker): def _test_backup_and_restore_app(mocker, app): # Create a backup of this app - with message(mocker, "backup_created"): - backup_create(system=None, apps=[app]) + name = random_ascii(8) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=None, apps=[app]) archives = backup_list()["archives"] assert len(archives) == 1 @@ -628,8 +636,9 @@ def test_restore_archive_with_custom_hook(mocker): os.system("touch %s/99-yolo" % custom_restore_hook_folder) # Backup with custom hook system - with message(mocker, "backup_created"): - backup_create(system=[], apps=None) + name = random_ascii(8) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=[], apps=None) archives = backup_list()["archives"] assert len(archives) == 1 @@ -666,5 +675,6 @@ def test_backup_binds_are_readonly(mocker, monkeypatch): ) # Create the backup - with message(mocker, "backup_created"): - backup_create(system=[]) + name = random_ascii(8) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=[]) From b5b69e952d70f0209b0e0579d0ad244a6bb19a1c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Feb 2023 16:26:01 +0100 Subject: [PATCH 124/319] domain/dns: don't miserably crash when the domain is known by lexicon but not in registrar_list.toml --- src/dns.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dns.py b/src/dns.py index d4c9b1380..eb0812c97 100644 --- a/src/dns.py +++ b/src/dns.py @@ -591,7 +591,10 @@ def _get_registrar_config_section(domain): # TODO : add a help tip with the link to the registar's API doc (c.f. Lexicon's README) registrar_list = read_toml(DOMAIN_REGISTRAR_LIST_PATH) - registrar_credentials = registrar_list[registrar] + registrar_credentials = registrar_list.get(registrar) + if registrar_credentials is None: + logger.warning(f"Registrar {registrar} unknown / Should be added to YunoHost's registrar_list.toml by the development team!") + registrar_credentials = {} for credential, infos in registrar_credentials.items(): infos["default"] = infos.get("default", "") infos["optional"] = infos.get("optional", "False") From 1e5203426b1764b94265cff373c60c6bae47699c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Feb 2023 17:28:08 +0100 Subject: [PATCH 125/319] admins migration: try to losen up even more the search for first admin user x_x --- src/migrations/0026_new_admins_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/migrations/0026_new_admins_group.py b/src/migrations/0026_new_admins_group.py index a260074fd..3b2207eb8 100644 --- a/src/migrations/0026_new_admins_group.py +++ b/src/migrations/0026_new_admins_group.py @@ -54,7 +54,7 @@ class MyMigration(Migration): for user in all_users: aliases = user_info(user).get("mail-aliases", []) if any(alias.startswith(f"admin@{main_domain}") for alias in aliases) \ - and any(alias.startswith(f"postmaster@{main_domain}") for alias in aliases): + or any(alias.startswith(f"postmaster@{main_domain}") for alias in aliases): new_admin_user = user break From cb505b578bcab8060d4d9731da55923752f37e08 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Feb 2023 17:47:03 +0100 Subject: [PATCH 126/319] ynh_install_composer: use either final_path or install_dir depending on packaging format --- helpers/php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/helpers/php b/helpers/php index 8fff8d78b..eb21a6b5c 100644 --- a/helpers/php +++ b/helpers/php @@ -516,7 +516,11 @@ ynh_install_composer() { local composerversion # Manage arguments with getopts ynh_handle_getopts_args "$@" - workdir="${workdir:-$install_dir}" + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then + workdir="${workdir:-$final_path}" + else + workdir="${workdir:-$install_dir}" + fi phpversion="${phpversion:-$YNH_PHP_VERSION}" install_args="${install_args:-}" composerversion="${composerversion:-$YNH_COMPOSER_VERSION}" From 91a3e79eaf0852d417d4dce8f1ee31cb6d0e468a Mon Sep 17 00:00:00 2001 From: ppr Date: Fri, 3 Feb 2023 20:16:10 +0000 Subject: [PATCH 127/319] Translated using Weblate (French) Currently translated at 100.0% (753 of 753 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index e8302fb82..22b30ff8d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -27,10 +27,10 @@ "backup_archive_name_unknown": "L'archive locale de sauvegarde nommée '{name}' est inconnue", "backup_archive_open_failed": "Impossible d'ouvrir l'archive de la sauvegarde", "backup_cleaning_failed": "Impossible de nettoyer le dossier temporaire de sauvegarde", - "backup_created": "Sauvegarde terminée", + "backup_created": "Sauvegarde créée : {name}", "backup_creation_failed": "Impossible de créer l'archive de la sauvegarde", "backup_delete_error": "Impossible de supprimer '{path}'", - "backup_deleted": "La sauvegarde a été supprimée", + "backup_deleted": "Sauvegarde supprimée : {name}", "backup_hook_unknown": "Script de sauvegarde '{hook}' inconnu", "backup_nothings_done": "Il n'y a rien à sauvegarder", "backup_output_directory_forbidden": "Choisissez un répertoire de destination différent. Les sauvegardes ne peuvent pas être créées dans les sous-dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", From a80734d2406d1ceca17241ba8197072c371daf61 Mon Sep 17 00:00:00 2001 From: Grzegorz Cichocki Date: Fri, 3 Feb 2023 19:41:23 +0000 Subject: [PATCH 128/319] Translated using Weblate (Polish) Currently translated at 10.2% (77 of 753 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/locales/pl.json b/locales/pl.json index c73de7314..3e630f147 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -67,5 +67,14 @@ "global_settings_setting_ssh_compatibility": "Kompatybilność z SSH", "global_settings_setting_ssh_port": "Port SSH", "log_settings_reset": "Resetuj ustawienia", - "log_tools_migrations_migrate_forward": "Uruchom migracje" + "log_tools_migrations_migrate_forward": "Uruchom migracje", + "app_action_cannot_be_ran_because_required_services_down": "Następujące usługi powinny być uruchomione, aby rozpocząć to działanie: {services}. Spróbuj uruchomić je ponownie aby kontynuować (i dowiedzieć się, dlaczego były one wyłączone)", + "app_argument_choice_invalid": "Wybierz poprawną wartość dla argumentu '{name}': '{value}' nie znajduje się w liście poprawnych opcji ({choices})", + "app_action_broke_system": "Wydaje się, że ta akcja przerwała te ważne usługi: {services}", + "additional_urls_already_removed": "Dodatkowy URL '{url}' już usunięty w dodatkowym URL dla uprawnienia '{permission}'", + "additional_urls_already_added": "Dodatkowy URL '{url}' już dodany w dodatkowym URL dla uprawnienia '{permission}'", + "app_arch_not_supported": "Ta aplikacja może być zainstalowana tylko na architekturach {', '.join(required)}, a twoja architektura serwera to {current}", + "app_argument_invalid": "Wybierz poprawną wartość dla argumentu '{name}': {błąd}", + "all_users": "Wszyscy użytkownicy YunoHost", + "app_action_failed": "Nie udało się uruchomić akcji {action} dla aplikacji {app}" } From 8247a649e7b76e435e5af571e28fa4cc625c3af7 Mon Sep 17 00:00:00 2001 From: Krzysztof Nowakowski Date: Fri, 3 Feb 2023 19:41:43 +0000 Subject: [PATCH 129/319] Translated using Weblate (Polish) Currently translated at 10.2% (77 of 753 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/pl.json b/locales/pl.json index 3e630f147..08c3e1d43 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -76,5 +76,6 @@ "app_arch_not_supported": "Ta aplikacja może być zainstalowana tylko na architekturach {', '.join(required)}, a twoja architektura serwera to {current}", "app_argument_invalid": "Wybierz poprawną wartość dla argumentu '{name}': {błąd}", "all_users": "Wszyscy użytkownicy YunoHost", - "app_action_failed": "Nie udało się uruchomić akcji {action} dla aplikacji {app}" + "app_action_failed": "Nie udało się uruchomić akcji {action} dla aplikacji {app}", + "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`" } From e78b62448b185efade7529ba3d25397092788f62 Mon Sep 17 00:00:00 2001 From: ppr Date: Sun, 5 Feb 2023 18:45:36 +0000 Subject: [PATCH 130/319] Translated using Weblate (French) Currently translated at 100.0% (755 of 755 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 22b30ff8d..9939bb6cb 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -751,5 +751,7 @@ "invalid_shell": "Shell invalide : {shell}", "global_settings_setting_dns_exposure": "Suites d'IP à prendre en compte pour la configuration et le diagnostic du DNS", "global_settings_setting_dns_exposure_help": "NB : Ceci n'affecte que la configuration DNS recommandée et les vérifications de diagnostic. Cela n'affecte pas les configurations du système.", - "diagnosis_ip_no_ipv6_tip_important": "IPv6 devrait généralement être configuré automatiquement par le système ou par votre fournisseur d'accès à internet (FAI) s'il est disponible. Sinon, vous devrez peut-être configurer quelques éléments manuellement, comme expliqué dans la documentation ici : https://yunohost.org/#/ipv6." + "diagnosis_ip_no_ipv6_tip_important": "IPv6 devrait généralement être configuré automatiquement par le système ou par votre fournisseur d'accès à internet (FAI) s'il est disponible. Sinon, vous devrez peut-être configurer quelques éléments manuellement, comme expliqué dans la documentation ici : https://yunohost.org/#/ipv6.", + "domain_config_default_app_help": "Les personnes seront automatiquement redirigées vers cette application lorsqu'elles ouvriront ce domaine. Si aucune application n'est spécifiée, les personnes sont redirigées vers le formulaire de connexion du portail utilisateur.", + "domain_config_xmpp_help": "NB : certaines fonctions XMPP nécessiteront la mise à jour de vos enregistrements DNS et la régénération de votre certificat Lets Encrypt pour être activées" } From fa7f7f77b9fddc3956539d5bd0b3ef2a2fe294d5 Mon Sep 17 00:00:00 2001 From: Kayou Date: Mon, 6 Feb 2023 20:17:55 +0100 Subject: [PATCH 131/319] run code_quality jobs only for tags (new versions) --- .gitlab-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 11d920bd0..7a0bf9cd8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,6 +16,8 @@ default: code_quality: tags: - docker + only: + - tags code_quality_html: extends: code_quality @@ -23,6 +25,8 @@ code_quality_html: REPORT_FORMAT: html artifacts: paths: [gl-code-quality-report.html] + only: + - tags # see: https://docs.gitlab.com/ee/ci/yaml/#switch-between-branch-pipelines-and-merge-request-pipelines workflow: From edf8ec1944bb39a3b06e2a985f27175b72929856 Mon Sep 17 00:00:00 2001 From: Kayou Date: Mon, 6 Feb 2023 20:22:50 +0100 Subject: [PATCH 132/319] =?UTF-8?q?code=5Fquality=20only=20for=20tags?= =?UTF-8?q?=C2=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7a0bf9cd8..7746c48c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,8 +16,9 @@ default: code_quality: tags: - docker - only: - - tags +rules: + - if: $CI_COMMIT_TAG # Only for tags + code_quality_html: extends: code_quality @@ -25,8 +26,9 @@ code_quality_html: REPORT_FORMAT: html artifacts: paths: [gl-code-quality-report.html] - only: - - tags +rules: + - if: $CI_COMMIT_TAG # Only for tags + # see: https://docs.gitlab.com/ee/ci/yaml/#switch-between-branch-pipelines-and-merge-request-pipelines workflow: From 106cb0a6fd05c82e7a41e0216299c70332235886 Mon Sep 17 00:00:00 2001 From: Kayou Date: Mon, 6 Feb 2023 21:49:35 +0100 Subject: [PATCH 133/319] =?UTF-8?q?code=5Fquality=20only=20for=20tags?= =?UTF-8?q?=C2=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7746c48c8..3e030940b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,8 +16,8 @@ default: code_quality: tags: - docker -rules: - - if: $CI_COMMIT_TAG # Only for tags + rules: + - if: $CI_COMMIT_TAG # Only for tags code_quality_html: @@ -26,8 +26,8 @@ code_quality_html: REPORT_FORMAT: html artifacts: paths: [gl-code-quality-report.html] -rules: - - if: $CI_COMMIT_TAG # Only for tags + rules: + - if: $CI_COMMIT_TAG # Only for tags # see: https://docs.gitlab.com/ee/ci/yaml/#switch-between-branch-pipelines-and-merge-request-pipelines From 63a95c28d6192728f9e1ed4d06e6600b90ab5be7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 7 Feb 2023 00:19:08 +0100 Subject: [PATCH 134/319] Update changelog for 11.1.6 --- debian/changelog | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/debian/changelog b/debian/changelog index 42b3d9f1c..8b3449a5f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,21 @@ +yunohost (11.1.6) testing; urgency=low + + - helpers: allow to use ynh_replace_string with @ ([#1588](https://github.com/yunohost/yunohost/pull/1588)) + - helpers: fix behavior of ynh_write_var_in_file when key is duplicated ([#1589](https://github.com/yunohost/yunohost/pull/1589), [#1591](https://github.com/yunohost/yunohost/pull/1591)) + - helpers: fix composer workdir variable for package v2 ([#1586](https://github.com/yunohost/yunohost/pull/1586)) + - configpanels: properly escape & for values used in ynh_write_var_in_file ([#1590](https://github.com/yunohost/yunohost/pull/1590)) + - appsv2/group question: don't include primary groups in choices (c179d4b8) + - appsv2: when initalizing permission, make sure to add 'all_users' when visitors is chosen (71042f08) + - backup/multimedia: test that /home/yunohots.multimedia does exists to avoid boring warning later (170eaf5d) + - domains: add missing logic to inject translated 'help' keys in config panel like we do for global settings (4dee434e) + - domain/dns: don't miserably crash when the domain is known by lexicon but not in registrar_list.toml (b5b69e95) + - admin->admins migration: try to losen up even more the search for first admin user x_x (1e520342) + - i18n: Translations updated for French, Polish + + Thanks to all contributors <3 ! (Éric Gaspar, Grzegorz Cichocki, Kayou, Krzysztof Nowakowski, ljf, ppr) + + -- Alexandre Aubin Tue, 07 Feb 2023 00:14:17 +0100 + yunohost (11.1.5.5) stable; urgency=low - admin->admins migration: try to handle boring case where the 'first' user cant be identified because it doesnt have the root@ alias (8485ebc7) From 00b411d18df25cffe0253b384f17a7b93116b7f2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 7 Feb 2023 00:21:39 +0100 Subject: [PATCH 135/319] Update changelog for 11.1.6 --- debian/changelog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 8b3449a5f..066f65215 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -yunohost (11.1.6) testing; urgency=low +yunohost (11.1.6) stable; urgency=low - helpers: allow to use ynh_replace_string with @ ([#1588](https://github.com/yunohost/yunohost/pull/1588)) - helpers: fix behavior of ynh_write_var_in_file when key is duplicated ([#1589](https://github.com/yunohost/yunohost/pull/1589), [#1591](https://github.com/yunohost/yunohost/pull/1591)) From 2eb7da060345e95f10c01f78acbee3b3e35c5739 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 7 Feb 2023 12:17:28 +0100 Subject: [PATCH 136/319] dns: fix CAA recommended DNS conf -> 0 is apparently a more sensible value than 128... --- src/dns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dns.py b/src/dns.py index eb0812c97..2d39aa02e 100644 --- a/src/dns.py +++ b/src/dns.py @@ -138,7 +138,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): {"type": "A", "name": "*", "value": "123.123.123.123", "ttl": 3600}, # if ipv6 available {"type": "AAAA", "name": "*", "value": "valid-ipv6", "ttl": 3600}, - {"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600}, + {"type": "CAA", "name": "@", "value": "0 issue \"letsencrypt.org\"", "ttl": 3600}, ], "example_of_a_custom_rule": [ {"type": "SRV", "name": "_matrix", "value": "domain.tld.", "ttl": 3600} @@ -248,7 +248,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): elif include_empty_AAAA_if_no_ipv6: extra.append([f"*{suffix}", ttl, "AAAA", None]) - extra.append([basename, ttl, "CAA", '128 issue "letsencrypt.org"']) + extra.append([basename, ttl, "CAA", '0 issue "letsencrypt.org"']) #################### # Standard records # From 6520d82e4dbd3881606a5739d11ead0b146e1185 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 7 Feb 2023 13:43:32 +0100 Subject: [PATCH 137/319] ci: fix helpers doc regen + add auto app resource doc --- .gitlab/ci/doc.gitlab-ci.yml | 9 +++++--- doc/generate_resource_doc.py | 40 +++++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 528d8f5aa..4f6ea6ba1 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -13,15 +13,18 @@ generate-helpers-doc: script: - cd doc - python3 generate_helper_doc.py + - python3 generate_resource_doc.py > resources.md - hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo - - cp helpers.md doc_repo/pages/06.contribute/10.packaging_apps/11.helpers/packaging_apps_helpers.md + - cp helpers.md doc_repo/pages/06.contribute/10.packaging_apps/80.resources/11.helpers/packaging_apps_helpers.md + - cp resources.md doc_repo/pages/06.contribute/10.packaging_apps/80.resources/15.appresources/packaging_apps_resources.md - cd doc_repo # replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ? - hub checkout -b "${CI_COMMIT_REF_NAME}" - - hub commit -am "[CI] Helper for ${CI_COMMIT_REF_NAME}" - - hub pull-request -m "[CI] Helper for ${CI_COMMIT_REF_NAME}" -p # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd + - hub commit -am "[CI] Update app helpers/resources for ${CI_COMMIT_REF_NAME}" + - hub pull-request -m "[CI] Update app helpers/resources for ${CI_COMMIT_REF_NAME}" -p # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd artifacts: paths: - doc/helpers.md + - doc/resources.md only: - tags diff --git a/doc/generate_resource_doc.py b/doc/generate_resource_doc.py index 2063c4ab9..20a9a994d 100644 --- a/doc/generate_resource_doc.py +++ b/doc/generate_resource_doc.py @@ -1,11 +1,41 @@ -from yunohost.utils.resources import AppResourceClassesByType +import ast -resources = sorted(AppResourceClassesByType.values(), key=lambda r: r.priority) +print("""--- +title: App resources +template: docs +taxonomy: + category: docs +routes: + default: '/packaging_apps_resources' +--- -for klass in resources: - doc = klass.__doc__.replace("\n ", "\n") +""") + + +fname = "../src/utils/resources.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) + +ResourceClasses = [c for c in tree.body if isinstance(c, ast.ClassDef) and c.bases and c.bases[0].id == 'AppResource'] + +ResourceDocString = {} + +for c in ResourceClasses: + + assert c.body[1].targets[0].id == "type" + resource_id = c.body[1].value.value + docstring = ast.get_docstring(c) + + ResourceDocString[resource_id] = docstring + + +for resource_id, doc in sorted(ResourceDocString.items()): + doc = doc.replace("\n ", "\n") print("") - print(f"## {klass.type.replace('_', ' ').title()}") + print(f"## {resource_id.replace('_', ' ').title()}") print("") print(doc) From 024db62a1dfd92a2f894a034a1ba9f5e35074ffc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 7 Feb 2023 19:22:54 +0100 Subject: [PATCH 138/319] users: Allow digits in user fullname --- share/actionsmap.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/actionsmap.yml b/share/actionsmap.yml index ece32e1ca..7f0fdabe9 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -72,7 +72,7 @@ user: ask: ask_fullname required: False pattern: &pattern_fullname - - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ + - !!str ^([^\W_]{1,30}[ ,.'-]{0,3})+$ - "pattern_fullname" -f: full: --firstname From 48e488f89ec39fa7df46d3c22c410890c3676b20 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 8 Feb 2023 22:46:03 +0100 Subject: [PATCH 139/319] backup: fix full backup restore postinstall calls that now need first username+fullname+password --- src/backup.py | 6 +++++- src/tools.py | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/backup.py b/src/backup.py index a3a24aa52..ee961a7bf 100644 --- a/src/backup.py +++ b/src/backup.py @@ -32,6 +32,7 @@ from functools import reduce from packaging import version 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, @@ -936,7 +937,10 @@ class RestoreManager: ) logger.debug("executing the post-install...") - tools_postinstall(domain, "Yunohost", True) + + # Use a dummy password which is not gonna be saved anywhere + # because the next thing to happen should be that a full restore of the LDAP db will happen + tools_postinstall(domain, "admin", "Admin", password=random_ascii(70), ignore_dyndns=True, overwrite_root_password=False) def clean(self): """ diff --git a/src/tools.py b/src/tools.py index dee4c8486..f5a89a22a 100644 --- a/src/tools.py +++ b/src/tools.py @@ -152,6 +152,7 @@ def tools_postinstall( password, ignore_dyndns=False, force_diskspace=False, + overwrite_root_password=True, ): from yunohost.dyndns import _dyndns_available from yunohost.utils.dns import is_yunohost_dyndns_domain @@ -225,10 +226,11 @@ def tools_postinstall( domain_add(domain, dyndns) domain_main_domain(domain) + # First user user_create(username, domain, password, admin=True, fullname=fullname) - # Update LDAP admin and create home dir - tools_rootpw(password) + if overwrite_root_password: + tools_rootpw(password) # Enable UPnP silently and reload firewall firewall_upnp("enable", no_refresh=True) From ea2b0d0c516e9011cfbb204f6f4579b5e5f73787 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Mon, 6 Feb 2023 17:37:56 +0000 Subject: [PATCH 140/319] Translated using Weblate (Basque) Currently translated at 98.1% (741 of 755 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/eu/ --- locales/eu.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/locales/eu.json b/locales/eu.json index cf6b0abea..63ecf7231 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -8,7 +8,7 @@ "diagnosis_ip_global": "IP orokorra: {global}", "app_argument_password_no_default": "Errorea egon da '{name}' pasahitzaren argumentua ikuskatzean: pasahitzak ezin du balio hori izan segurtasun arrazoiengatik", "app_extraction_failed": "Ezinezkoa izan da instalazio fitxategiak ateratzea", - "backup_deleted": "Babeskopia ezabatuta", + "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.)", @@ -199,7 +199,7 @@ "backup_archive_writing_error": "Ezinezkoa izan da '{source}' ('{dest}' fitxategiak eskatu dituenak) fitxategia '{archive}' konprimatutako babeskopian sartzea", "backup_ask_for_copying_if_needed": "Behin-behinean {size}MB erabili nahi dituzu babeskopia gauzatu ahal izateko? (Horrela egiten da fitxategi batzuk ezin direlako modu eraginkorragoan prestatu.)", "backup_cant_mount_uncompress_archive": "Ezinezkoa izan da deskonprimatutako fitxategia muntatzea idazketa-babesa duelako", - "backup_created": "Babeskopia sortu da", + "backup_created": "Babeskopia sortu da: {name}", "backup_copying_to_organize_the_archive": "{size}MB kopiatzen fitxategia antolatzeko", "backup_couldnt_bind": "Ezin izan da {src} {dest}-ra lotu.", "backup_output_directory_forbidden": "Aukeratu beste katalogo bat emaitza gordetzeko. Babeskopiak ezin dira sortu /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var edo /home/yunohost.backup/archives azpi-katalogoetan", @@ -738,14 +738,20 @@ "group_no_change": "Ez da ezer aldatu behar '{group}' talderako", "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 {current} bakarrik daude erabilgarri. Aplikazioa ibiliko balitz ere, instalazioak edo bertsio-berritzeak RAM koporu handia eskatzen du eta zure zerbitzaria izoztu eta huts egin lezake. Hala ere arriskatu nahi baduzu idatzi '{answers}'", + "confirm_app_insufficient_ram": "KONTUZ! Aplikazio honek {required} RAM behar ditu instalatu edo bertsio-berritzeko baina unean {current} bakarrik daude erabilgarri. Aplikazioa ibiliko balitz ere, instalazioak edo bertsio-berritzeak RAM kopuru handia eskatzen du eta zure zerbitzaria izoztu 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}]", "app_arch_not_supported": "Aplikazio hau {', '.join(required)} arkitekturan instala daiteke bakarrik, baina zure zerbitzariaren arkitektura {current} da", - "app_resource_failed": "Huts egin du {app} aplikaziorako baliabideak", + "app_resource_failed": "Huts egin du {app} aplikaziorako baliabideen eguneraketak / prestaketak / askapenak: {error}", "app_not_enough_disk": "Aplikazio honek {required} espazio libre behar ditu.", "app_yunohost_version_not_supported": "Aplikazio honek YunoHost >= {required} behar du baina unean instalatutako bertsioa {current} da", "global_settings_setting_passwordless_sudo": "Baimendu administrariek 'sudo' erabiltzea pasahitzak berriro idatzi beharrik gabe", "global_settings_setting_portal_theme": "Atariko gaia", "global_settings_setting_portal_theme_help": "Atariko gai propioak sortzeari buruzko informazio gehiago: https://yunohost.org/theming", - "invalid_shell": "Shell baliogabea: {shell}" -} \ No newline at end of file + "invalid_shell": "Shell baliogabea: {shell}", + "domain_config_default_app_help": "Jendea automatikoki birbideratuko da aplikazio honetara domeinu hau bisitatzerakoan. Aplikaziorik ezarri ezean, jendea saioa hasteko erabiltzaileen atarira birbideratuko da.", + "domain_config_xmpp_help": "Ohart ongi: XMPP ezaugarri batzuk gaitzeko DNS erregistroak eguneratu eta Lets Encrypt ziurtagiria birsortu beharko dira", + "global_settings_setting_dns_exposure": "DNS ezarpenetan eta diagnostikoan kontuan hartzeko IP bertsioak", + "global_settings_setting_dns_exposure_help": "Ohart ongi: honek gomendatutako DNS ezarpenei eta diagnostikoari eragiten die soilik. Ez du eraginik sistemaren ezarpenetan.", + "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/#/ipv62.", + "pattern_fullname": "Baliozko izen oso bat izan behar da (gutxienez hiru karaktere)" +} From eb396fdb13b78534dc2a92e1135278530b3c7860 Mon Sep 17 00:00:00 2001 From: Poesty Li Date: Mon, 6 Feb 2023 17:15:56 +0000 Subject: [PATCH 141/319] Translated using Weblate (Chinese (Simplified)) Currently translated at 70.0% (529 of 755 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/zh_Hans/ --- locales/zh_Hans.json | 102 +++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index 687064de6..f73b16757 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -28,9 +28,9 @@ "diagnosis_basesystem_hardware_model": "服务器型号为 {model}", "diagnosis_basesystem_hardware": "服务器硬件架构为{virt} {arch}", "custom_app_url_required": "您必须提供URL才能升级自定义应用 {app}", - "confirm_app_install_thirdparty": "危险! 该应用程序不是YunoHost的应用程序目录的一部分。 安装第三方应用程序可能会损害系统的完整性和安全性。 除非您知道自己在做什么,否则可能不应该安装它, 如果此应用无法运行或无法正常使用系统,将不会提供任何支持。如果您仍然愿意承担此风险,请输入'{answers}'", + "confirm_app_install_thirdparty": "危险! 该应用不是YunoHost的应用目录的一部分。 安装第三方应用可能会损害系统的完整性和安全性。 除非您知道自己在做什么,否则可能不应该安装它, 如果此应用无法运行或无法正常使用系统,将不会提供任何支持。如果您仍然愿意承担此风险,请输入'{answers}'", "confirm_app_install_danger": "危险! 已知此应用仍处于实验阶段(如果未明确无法正常运行)! 除非您知道自己在做什么,否则可能不应该安装它。 如果此应用无法运行或无法正常使用系统,将不会提供任何支持。如果您仍然愿意承担此风险,请输入'{answers}'", - "confirm_app_install_warning": "警告:此应用程序可能可以运行,但未与YunoHost很好地集成。某些功能(例如单点登录和备份/还原)可能不可用, 仍要安装吗? [{answers}] ", + "confirm_app_install_warning": "警告:此应用可能可以运行,但未与YunoHost很好地集成。某些功能(例如单点登录和备份/还原)可能不可用, 仍要安装吗? [{answers}] ", "certmanager_unable_to_parse_self_CA_name": "无法解析自签名授权的名称 (file: {file})", "certmanager_self_ca_conf_file_not_found": "找不到用于自签名授权的配置文件(file: {file})", "certmanager_no_cert_file": "无法读取域{domain}的证书文件(file: {file})", @@ -50,7 +50,7 @@ "certmanager_attempt_to_renew_valid_cert": "域'{domain}'的证书不会过期!(如果知道自己在做什么,则可以使用--force)", "certmanager_attempt_to_renew_nonLE_cert": "“Let's Encrypt”未颁发域'{domain}'的证书,无法自动续订!", "certmanager_acme_not_configured_for_domain": "目前无法针对{domain}运行ACME挑战,因为其nginx conf缺少相应的代码段...请使用“yunohost tools regen-conf nginx --dry-run --with-diff”确保您的nginx配置是最新的。", - "backup_with_no_restore_script_for_app": "{app} 没有还原脚本,您将无法自动还原该应用程序的备份。", + "backup_with_no_restore_script_for_app": "{app} 没有还原脚本,您将无法自动还原该应用的备份。", "backup_with_no_backup_script_for_app": "应用'{app}'没有备份脚本。无视。", "backup_unable_to_organize_files": "无法使用快速方法来组织档案中的文件", "backup_system_part_failed": "无法备份'{part}'系统部分", @@ -101,36 +101,36 @@ "ask_new_admin_password": "新的管理密码", "ask_main_domain": "主域", "ask_user_domain": "用户的电子邮件地址和XMPP帐户要使用的域", - "apps_catalog_update_success": "应用程序目录已更新!", - "apps_catalog_obsolete_cache": "应用程序目录缓存为空或已过时。", + "apps_catalog_update_success": "应用目录已更新!", + "apps_catalog_obsolete_cache": "应用目录缓存为空或已过时。", "apps_catalog_failed_to_download": "无法下载{apps_catalog} 应用目录: {error}", - "apps_catalog_updating": "正在更新应用程序目录…", + "apps_catalog_updating": "正在更新应用目录…", "apps_catalog_init_success": "应用目录系统已初始化!", - "apps_already_up_to_date": "所有应用程序都是最新的", + "apps_already_up_to_date": "所有应用都是最新的", "app_packaging_format_not_supported": "无法安装此应用,因为您的YunoHost版本不支持其打包格式。 您应该考虑升级系统。", "app_upgraded": "{app}upgraded", "app_upgrade_some_app_failed": "某些应用无法升级", "app_upgrade_script_failed": "应用升级脚本内部发生错误", "app_upgrade_app_name": "现在升级{app} ...", "app_upgrade_several_apps": "以下应用将被升级: {apps}", - "app_unsupported_remote_type": "应用程序使用的远程类型不受支持", + "app_unsupported_remote_type": "应用使用的远程类型不受支持", "app_start_backup": "正在收集要备份的文件,用于{app} ...", "app_start_install": "{app}安装中...", "app_sources_fetch_failed": "无法获取源文件,URL是否正确?", "app_restore_script_failed": "应用还原脚本内部发生错误", "app_restore_failed": "无法还原 {app}: {error}", - "app_remove_after_failed_install": "安装失败后删除应用程序...", + "app_remove_after_failed_install": "安装失败后删除应用...", "app_requirements_checking": "正在检查{app}所需的软件包...", "app_removed": "{app} 已卸载", "app_not_properly_removed": "{app} 未正确删除", "app_not_correctly_installed": "{app} 似乎安装不正确", - "app_not_upgraded": "应用程序'{failed_app}'升级失败,因此以下应用程序的升级已被取消: {apps}", + "app_not_upgraded": "应用'{failed_app}'升级失败,因此以下应用的升级已被取消: {apps}", "app_manifest_install_ask_is_public": "该应用是否应该向匿名访问者公开?", "app_manifest_install_ask_admin": "选择此应用的管理员用户", "app_manifest_install_ask_password": "选择此应用的管理密码", "additional_urls_already_removed": "权限'{permission}'的其他URL中已经删除了附加URL'{url}'", "app_manifest_install_ask_path": "选择安装此应用的路径(在域名之后)", - "app_manifest_install_ask_domain": "选择应安装此应用程序的域", + "app_manifest_install_ask_domain": "选择应安装此应用的域", "app_location_unavailable": "该URL不可用,或与已安装的应用冲突:\n{apps}", "app_label_deprecated": "不推荐使用此命令!请使用新命令 'yunohost user permission update'来管理应用标签。", "app_make_default_location_already_used": "无法将'{app}' 设置为域上的默认应用,'{other_app}'已在使用'{domain}'", @@ -138,10 +138,10 @@ "app_install_failed": "无法安装 {app}: {error}", "app_install_files_invalid": "这些文件无法安装", "additional_urls_already_added": "附加URL '{url}' 已添加到权限'{permission}'的附加URL中", - "app_full_domain_unavailable": "抱歉,此应用必须安装在其自己的域中,但其他应用已安装在域“ {domain}”上。 您可以改用专用于此应用程序的子域。", + "app_full_domain_unavailable": "抱歉,此应用必须安装在其自己的域中,但其他应用已安装在域“ {domain}”上。 您可以改用专用于此应用的子域。", "app_extraction_failed": "无法解压缩安装文件", "app_change_url_success": "{app} URL现在为 {domain}{path}", - "app_change_url_no_script": "应用程序'{app_name}'尚不支持URL修改. 也许您应该升级它。", + "app_change_url_no_script": "应用'{app_name}'尚不支持URL修改. 也许您应该升级它。", "app_change_url_identical_domains": "新旧domain / url_path是相同的('{domain}{path}'),无需执行任何操作。", "app_argument_required": "参数'{name}'为必填项", "app_argument_password_no_default": "解析密码参数'{name}'时出错:出于安全原因,密码参数不能具有默认值", @@ -156,7 +156,7 @@ "port_already_opened": "{ip_version}个连接的端口 {port} 已打开", "port_already_closed": "{ip_version}个连接的端口 {port} 已关闭", "permission_require_account": "权限{permission}只对有账户的用户有意义,因此不能对访客启用。", - "permission_protected": "权限{permission}是受保护的。你不能向/从这个权限添加或删除访问者组。", + "permission_protected": "权限{permission}是受保护的。您不能向/从这个权限添加或删除访问者组。", "permission_updated": "权限 '{permission}' 已更新", "permission_update_failed": "无法更新权限 '{permission}': {error}", "permission_not_found": "找不到权限'{permission}'", @@ -210,8 +210,8 @@ "service_description_rspamd": "过滤垃圾邮件和其他与电子邮件相关的功能", "service_description_redis-server": "用于快速数据访问,任务队列和程序之间通信的专用数据库", "service_description_postfix": "用于发送和接收电子邮件", - "service_description_nginx": "为你的服务器上托管的所有网站提供服务或访问", - "service_description_mysql": "存储应用程序数据(SQL数据库)", + "service_description_nginx": "为您的服务器上托管的所有网站提供服务或访问", + "service_description_mysql": "存储应用数据(SQL数据库)", "service_description_metronome": "管理XMPP即时消息传递帐户", "service_description_fail2ban": "防止来自互联网的暴力攻击和其他类型的攻击", "service_description_dovecot": "允许电子邮件客户端访问/获取电子邮件(通过IMAP和POP3)", @@ -234,7 +234,7 @@ "system_username_exists": "用户名已存在于系统用户列表中", "system_upgraded": "系统升级", "ssowat_conf_generated": "SSOwat配置已重新生成", - "show_tile_cant_be_enabled_for_regex": "你不能启用'show_tile',因为权限'{permission}'的URL是一个重合词", + "show_tile_cant_be_enabled_for_regex": "您不能启用'show_tile',因为权限'{permission}'的URL是一个重合词", "show_tile_cant_be_enabled_for_url_not_defined": "您现在无法启用 'show_tile' ,因为您必须先为权限'{permission}'定义一个URL", "service_unknown": "未知服务 '{service}'", "service_stopped": "服务'{service}' 已停止", @@ -266,7 +266,7 @@ "upnp_enabled": "UPnP已启用", "upnp_disabled": "UPnP已禁用", "yunohost_not_installed": "YunoHost没有正确安装,请运行 'yunohost tools postinstall'", - "yunohost_postinstall_end_tip": "后期安装完成! 为了最终完成你的设置,请考虑:\n -通过webadmin的“用户”部分添加第一个用户(或在命令行中'yunohost user create ' );\n -通过网络管理员的“诊断”部分(或命令行中的'yunohost diagnosis run')诊断潜在问题;\n -阅读管理文档中的“完成安装设置”和“了解YunoHost”部分: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "后期安装完成! 为了最终完成您的设置,请考虑:\n -通过webadmin的“用户”部分添加第一个用户(或在命令行中'yunohost user create ' );\n -通过网络管理员的“诊断”部分(或命令行中的'yunohost diagnosis run')诊断潜在问题;\n -阅读管理文档中的“完成安装设置”和“了解YunoHost”部分: https://yunohost.org/admindoc.", "operation_interrupted": "该操作是否被手动中断?", "invalid_regex": "无效的正则表达式:'{regex}'", "installation_complete": "安装完成", @@ -318,13 +318,13 @@ "downloading": "下载中…", "done": "完成", "domains_available": "可用域:", - "domain_uninstall_app_first": "这些应用程序仍安装在您的域中:\n{apps}\n\n请先使用 'yunohost app remove the_app_id' 将其卸载,或使用 'yunohost app change-url the_app_id'将其移至另一个域,然后再继续删除域", - "domain_remove_confirm_apps_removal": "删除该域将删除这些应用程序:\n{apps}\n\n您确定要这样做吗? [{answers}]", + "domain_uninstall_app_first": "这些应用仍安装在您的域中:\n{apps}\n\n请先使用 'yunohost app remove the_app_id' 将其卸载,或使用 'yunohost app change-url the_app_id'将其移至另一个域,然后再继续删除域", + "domain_remove_confirm_apps_removal": "删除该域将删除这些应用:\n{apps}\n\n您确定要这样做吗? [{answers}]", "domain_hostname_failed": "无法设置新的主机名。稍后可能会引起问题(可能没问题)。", "domain_exists": "该域已存在", "domain_dyndns_root_unknown": "未知的DynDNS根域", "domain_dyndns_already_subscribed": "您已经订阅了DynDNS域", - "domain_dns_conf_is_just_a_recommendation": "本页向你展示了*推荐的*配置。它并*不*为你配置DNS。你有责任根据该建议在你的DNS注册商处配置你的DNS区域。", + "domain_dns_conf_is_just_a_recommendation": "本页向您展示了*推荐的*配置。它并*不*为您配置DNS。您有责任根据该建议在您的DNS注册商处配置您的DNS区域。", "domain_deletion_failed": "无法删除域 {domain}: {error}", "domain_deleted": "域已删除", "domain_creation_failed": "无法创建域 {domain}: {error}", @@ -369,7 +369,7 @@ "diagnosis_description_ip": "互联网连接", "diagnosis_description_basesystem": "基本系统", "diagnosis_security_vulnerable_to_meltdown_details": "要解决此问题,您应该升级系统并重新启动以加载新的Linux内核(如果无法使用,请与您的服务器提供商联系)。有关更多信息,请参见https://meltdownattack.com/。", - "diagnosis_security_vulnerable_to_meltdown": "你似乎容易受到Meltdown关键安全漏洞的影响", + "diagnosis_security_vulnerable_to_meltdown": "您似乎容易受到Meltdown关键安全漏洞的影响", "diagnosis_regenconf_manually_modified": "配置文件 {file} 似乎已被手动修改。", "diagnosis_regenconf_allgood": "所有配置文件均符合建议的配置!", "diagnosis_mail_queue_too_big": "邮件队列中的待处理电子邮件过多({nb_pending} emails)", @@ -420,35 +420,35 @@ "diagnosis_ip_connected_ipv4": "服务器通过IPv4连接到Internet!", "diagnosis_no_cache": "尚无类别 '{category}'的诊断缓存", "diagnosis_failed": "无法获取类别 '{category}'的诊断结果: {error}", - "diagnosis_package_installed_from_sury_details": "一些软件包被无意中从一个名为Sury的第三方仓库安装。YunoHost团队改进了处理这些软件包的策略,但预计一些安装了PHP7.3应用程序的设置在仍然使用Stretch的情况下还有一些不一致的地方。为了解决这种情况,你应该尝试运行以下命令:{cmd_to_fix}", + "diagnosis_package_installed_from_sury_details": "一些软件包被无意中从一个名为Sury的第三方仓库安装。YunoHost团队改进了处理这些软件包的策略,但预计一些安装了PHP7.3应用的设置在仍然使用Stretch的情况下还有一些不一致的地方。为了解决这种情况,您应该尝试运行以下命令:{cmd_to_fix}", "app_not_installed": "在已安装的应用列表中找不到 {app}:{all_apps}", - "app_already_installed_cant_change_url": "这个应用程序已经被安装。URL不能仅仅通过这个函数来改变。在`app changeurl`中检查是否可用。", + "app_already_installed_cant_change_url": "这个应用已经被安装。URL不能仅仅通过这个函数来改变。在`app changeurl`中检查是否可用。", "restore_not_enough_disk_space": "没有足够的空间(空间: {free_space} B,需要的空间: {needed_space} B,安全系数: {margin} B)", "regenconf_pending_applying": "正在为类别'{category}'应用挂起的配置..", "regenconf_up_to_date": "类别'{category}'的配置已经是最新的", "regenconf_file_kept_back": "配置文件'{conf}'预计将被regen-conf(类别{category})删除,但被保留了下来。", "good_practices_about_user_password": "现在,您将设置一个新的管理员密码。 密码至少应包含8个字符。并且出于安全考虑建议使用较长的密码同时尽可能使用各种字符(大写,小写,数字和特殊字符)", - "domain_cannot_remove_main_add_new_one": "你不能删除'{domain}',因为它是主域和你唯一的域,你需要先用'yunohost domain add '添加另一个域,然后用'yunohost domain main-domain -n '设置为主域,然后你可以用'yunohost domain remove {domain}'删除域", - "domain_cannot_add_xmpp_upload": "你不能添加以'xmpp-upload.'开头的域名。这种名称是为YunoHost中集成的XMPP上传功能保留的。", - "domain_cannot_remove_main": "你不能删除'{domain}',因为它是主域,你首先需要用'yunohost domain main-domain -n '设置另一个域作为主域;这里是候选域的列表: {other_domains}", + "domain_cannot_remove_main_add_new_one": "您不能删除'{domain}',因为它是主域和您唯一的域,您需要先用'yunohost domain add '添加另一个域,然后用'yunohost domain main-domain -n '设置为主域,然后您可以用'yunohost domain remove {domain}'删除域", + "domain_cannot_add_xmpp_upload": "您不能添加以'xmpp-upload.'开头的域名。这种名称是为YunoHost中集成的XMPP上传功能保留的。", + "domain_cannot_remove_main": "您不能删除'{domain}',因为它是主域,您首先需要用'yunohost domain main-domain -n '设置另一个域作为主域;这里是候选域的列表: {other_domains}", "diagnosis_sshd_config_inconsistent_details": "请运行yunohost settings set security.ssh.port -v YOUR_SSH_PORT来定义SSH端口,并检查yunohost tools regen-conf ssh --dry-run --with-diffyunohost tools regen-conf ssh --force将您的配置重置为YunoHost建议。", - "diagnosis_http_bad_status_code": "它看起来像另一台机器(也许是你的互联网路由器)回答,而不是你的服务器。
1。这个问题最常见的原因是80端口(和443端口)没有正确转发到您的服务器
2.在更复杂的设置中:确保没有防火墙或反向代理的干扰。", - "diagnosis_http_timeout": "当试图从外部联系你的服务器时,出现了超时。它似乎是不可达的。
1. 这个问题最常见的原因是80端口(和443端口)没有正确转发到你的服务器
2.你还应该确保nginx服务正在运行
3.对于更复杂的设置:确保没有防火墙或反向代理的干扰。", + "diagnosis_http_bad_status_code": "它看起来像另一台机器(也许是您的互联网路由器)回答,而不是您的服务器。
1。这个问题最常见的原因是80端口(和443端口)没有正确转发到您的服务器
2.在更复杂的设置中:确保没有防火墙或反向代理的干扰。", + "diagnosis_http_timeout": "当试图从外部联系您的服务器时,出现了超时。它似乎是不可达的。
1. 这个问题最常见的原因是80端口(和443端口)没有正确转发到您的服务器
2.您还应该确保nginx服务正在运行
3.对于更复杂的设置:确保没有防火墙或反向代理的干扰。", "diagnosis_rootfstotalspace_critical": "根文件系统总共只有{space},这很令人担忧!您可能很快就会用完磁盘空间!建议根文件系统至少有16 GB。", "diagnosis_rootfstotalspace_warning": "根文件系统总共只有{space}。这可能没问题,但要小心,因为最终您可能很快会用完磁盘空间...建议根文件系统至少有16 GB。", - "diagnosis_regenconf_manually_modified_details": "如果你知道自己在做什么的话,这可能是可以的! YunoHost会自动停止更新这个文件... 但是请注意,YunoHost的升级可能包含重要的推荐变化。如果你想,你可以用yunohost tools regen-conf {category} --dry-run --with-diff检查差异,然后用yunohost tools regen-conf {category} --force强制设置为推荐配置", - "diagnosis_mail_fcrdns_nok_alternatives_6": "有些供应商不会让你配置你的反向DNS(或者他们的功能可能被破坏......)。如果你的反向DNS正确配置为IPv4,你可以尝试在发送邮件时禁用IPv6,方法是运yunohost settings set smtp.allow_ipv6 -v off。注意:这应视为最后一个解决方案因为这意味着你将无法从少数只使用IPv6的服务器发送或接收电子邮件。", - "diagnosis_mail_fcrdns_nok_alternatives_4": "有些供应商不会让你配置你的反向DNS(或者他们的功能可能被破坏......)。如果您因此而遇到问题,请考虑以下解决方案:
- 一些ISP提供了使用邮件服务器中转的选择,尽管这意味着中转将能够监视您的电子邮件流量。
- 一个有利于隐私的选择是使用VPN*与专用公共IP*来绕过这类限制。见https://yunohost.org/#/vpn_advantage
- 或者可以切换到另一个供应商", - "diagnosis_mail_ehlo_wrong_details": "远程诊断器在IPv{ipversion}中收到的EHLO与你的服务器的域名不同。
收到的EHLO: {wrong_ehlo}
预期的: {right_ehlo}
这个问题最常见的原因是端口25没有正确转发到你的服务器。另外,请确保没有防火墙或反向代理的干扰。", - "diagnosis_mail_ehlo_unreachable_details": "在IPv{ipversion}中无法打开与您服务器的25端口连接。它似乎是不可达的。
1. 这个问题最常见的原因是端口25没有正确转发到你的服务器
2.你还应该确保postfix服务正在运行。
3.在更复杂的设置中:确保没有防火墙或反向代理的干扰。", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "一些供应商不会让你解除对出站端口25的封锁,因为他们不关心网络中立性。
- 其中一些供应商提供了使用邮件服务器中继的替代方案,尽管这意味着中继将能够监视你的电子邮件流量。
- 一个有利于隐私的替代方案是使用VPN*,用一个专用的公共IP*绕过这种限制。见https://yunohost.org/#/vpn_advantage
- 你也可以考虑切换到一个更有利于网络中立的供应商", + "diagnosis_regenconf_manually_modified_details": "如果您知道自己在做什么的话,这可能是可以的! YunoHost会自动停止更新这个文件... 但是请注意,YunoHost的升级可能包含重要的推荐变化。如果您想,您可以用yunohost tools regen-conf {category} --dry-run --with-diff检查差异,然后用yunohost tools regen-conf {category} --force强制设置为推荐配置", + "diagnosis_mail_fcrdns_nok_alternatives_6": "有些供应商不会让您配置您的反向DNS(或者他们的功能可能被破坏......)。如果您的反向DNS正确配置为IPv4,您可以尝试在发送邮件时禁用IPv6,方法是运yunohost settings set smtp.allow_ipv6 -v off。注意:这应视为最后一个解决方案因为这意味着您将无法从少数只使用IPv6的服务器发送或接收电子邮件。", + "diagnosis_mail_fcrdns_nok_alternatives_4": "有些供应商不会让您配置您的反向DNS(或者他们的功能可能被破坏......)。如果您因此而遇到问题,请考虑以下解决方案:
- 一些ISP提供了使用邮件服务器中转的选择,尽管这意味着中转将能够监视您的电子邮件流量。
- 一个有利于隐私的选择是使用VPN*与专用公共IP*来绕过这类限制。见https://yunohost.org/#/vpn_advantage
- 或者可以切换到另一个供应商", + "diagnosis_mail_ehlo_wrong_details": "远程诊断器在IPv{ipversion}中收到的EHLO与您的服务器的域名不同。
收到的EHLO: {wrong_ehlo}
预期的: {right_ehlo}
这个问题最常见的原因是端口25没有正确转发到您的服务器。另外,请确保没有防火墙或反向代理的干扰。", + "diagnosis_mail_ehlo_unreachable_details": "在IPv{ipversion}中无法打开与您服务器的25端口连接。它似乎是不可达的。
1. 这个问题最常见的原因是端口25没有正确转发到您的服务器
2.您还应该确保postfix服务正在运行。
3.在更复杂的设置中:确保没有防火墙或反向代理的干扰。", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "一些供应商不会让您解除对出站端口25的封锁,因为他们不关心网络中立性。
- 其中一些供应商提供了使用邮件服务器中继的替代方案,尽管这意味着中继将能够监视您的电子邮件流量。
- 一个有利于隐私的替代方案是使用VPN*,用一个专用的公共IP*绕过这种限制。见https://yunohost.org/#/vpn_advantage
- 您也可以考虑切换到一个更有利于网络中立的供应商", "diagnosis_ram_ok": "系统在{total}中仍然有 {available} ({available_percent}%) RAM可用。", "diagnosis_ram_low": "系统有 {available} ({available_percent}%) RAM可用(共{total}个)可用。小心。", "diagnosis_ram_verylow": "系统只有 {available} ({available_percent}%) 内存可用! (在{total}中)", "diagnosis_diskusage_ok": "存储器{mountpoint}(在设备{device}上)仍有 {free} ({free_percent}%) 空间(在{total}中)!", "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_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_point_to_doc": "如果您需要有关配置DNS记录的帮助,请查看 https://yunohost.org/dns_config 上的文档。", "diagnosis_dns_discrepancy": "以下DNS记录似乎未遵循建议的配置:
类型: {type}
名称: {name}
代码> 当前值: {current}期望值: {value}", @@ -467,8 +467,8 @@ "log_help_to_get_log": "要查看操作'{desc}'的日志,请使用命令'yunohost log show {name}'", "log_link_to_log": "此操作的完整日志: '{desc}'", "log_corrupted_md_file": "与日志关联的YAML元数据文件已损坏: '{md_file}\n错误: {error}'", - "iptables_unavailable": "你不能在这里使用iptables。你要么在一个容器中,要么你的内核不支持它", - "ip6tables_unavailable": "你不能在这里使用ip6tables。你要么在一个容器中,要么你的内核不支持它", + "iptables_unavailable": "您不能在这里使用iptables。您要么在一个容器中,要么您的内核不支持它", + "ip6tables_unavailable": "您不能在这里使用ip6tables。您要么在一个容器中,要么您的内核不支持它", "log_regen_conf": "重新生成系统配置'{}'", "log_letsencrypt_cert_renew": "续订'{}'的“Let's Encrypt”证书", "log_selfsigned_cert_install": "在 '{}'域上安装自签名证书", @@ -484,7 +484,7 @@ "log_remove_on_failed_restore": "从备份存档还原失败后,删除 '{}'", "log_backup_restore_app": "从备份存档还原 '{}'", "log_backup_restore_system": "从备份档案还原系统", - "permission_currently_allowed_for_all_users": "这个权限目前除了授予其他组以外,还授予所有用户。你可能想删除'all_users'权限或删除目前授予它的其他组。", + "permission_currently_allowed_for_all_users": "这个权限目前除了授予其他组以外,还授予所有用户。您可能想删除'all_users'权限或删除目前授予它的其他组。", "permission_creation_failed": "无法创建权限'{permission}': {error}", "permission_created": "权限'{permission}'已创建", "permission_cannot_remove_main": "不允许删除主要权限", @@ -529,7 +529,7 @@ "migration_ldap_rollback_success": "系统回滚。", "migration_ldap_migration_failed_trying_to_rollback": "无法迁移...试图回滚系统。", "migration_ldap_can_not_backup_before_migration": "迁移失败之前,无法完成系统的备份。错误: {error}", - "migration_ldap_backup_before_migration": "在实际迁移之前,请创建LDAP数据库和应用程序设置的备份。", + "migration_ldap_backup_before_migration": "在实际迁移之前,请创建LDAP数据库和应用设置的备份。", "main_domain_changed": "主域已更改", "main_domain_change_failed": "无法更改主域", "mail_unavailable": "该电子邮件地址是保留的,并且将自动分配给第一个用户", @@ -541,7 +541,7 @@ "log_tools_reboot": "重新启动服务器", "log_tools_shutdown": "关闭服务器", "log_tools_upgrade": "升级系统软件包", - "log_tools_postinstall": "安装好你的YunoHost服务器后", + "log_tools_postinstall": "安装好您的YunoHost服务器后", "log_tools_migrations_migrate_forward": "运行迁移", "log_domain_main_domain": "将 '{}' 设为主要域", "log_user_permission_reset": "重置权限'{}'", @@ -554,16 +554,16 @@ "log_user_create": "添加用户'{}'", "domain_registrar_is_not_configured": "尚未为域 {domain} 配置注册商。", "domain_dns_push_not_applicable": "的自动DNS配置的特征是不适用域{domain}。您应该按照 https://yunohost.org/dns_config 上的文档手动配置DNS 记录。", - "disk_space_not_sufficient_update": "没有足够的磁盘空间来更新此应用程序", + "disk_space_not_sufficient_update": "没有足够的磁盘空间来更新此应用", "diagnosis_high_number_auth_failures": "最近出现了大量可疑的失败身份验证。您的fail2ban正在运行且配置正确,或使用自定义端口的SSH作为https://yunohost.org/解释的安全性。", - "diagnosis_apps_not_in_app_catalog": "此应用程序不在 YunoHost 的应用程序目录中。如果它过去有被删除过,您应该考虑卸载此应用程,因为它不会更新,并且可能会损害您系统的完整和安全性。", + "diagnosis_apps_not_in_app_catalog": "此应用不在 YunoHost 的应用目录中。如果它过去有被删除过,您应该考虑卸载此应用程,因为它不会更新,并且可能会损害您系统的完整和安全性。", "app_config_unable_to_apply": "无法应用配置面板值。", "app_config_unable_to_read": "无法读取配置面板值。", "config_forbidden_keyword": "关键字“{keyword}”是保留的,您不能创建或使用带有此 ID 的问题的配置面板。", "config_no_panel": "未找到配置面板。", "config_unknown_filter_key": "该过滤器钥匙“{filter_key}”有误。", - "diagnosis_apps_outdated_ynh_requirement": "此应用程序的安装 版本只需要 yunohost >= 2.x,这往往表明它与推荐的打包实践和帮助程序不是最新的。你真的应该考虑更新它。", - "disk_space_not_sufficient_install": "没有足够的磁盘空间来安装此应用程序", + "diagnosis_apps_outdated_ynh_requirement": "此应用的安装 版本只需要 yunohost >= 2.x,这往往表明它与推荐的打包实践和帮助程序不是最新的。您真的应该考虑更新它。", + "disk_space_not_sufficient_install": "没有足够的磁盘空间来安装此应用", "config_apply_failed": "应用新配置 失败:{error}", "config_cant_set_value_on_section": "无法在整个配置部分设置单个值 。", "config_validate_color": "是有效的 RGB 十六进制颜色", @@ -572,8 +572,8 @@ "config_validate_time": "应该是像 HH:MM 这样的有效时间", "config_validate_url": "应该是有效的URL", "danger": "警告:", - "diagnosis_apps_allgood": "所有已安装的应用程序都遵守基本的打包原则", - "diagnosis_apps_deprecated_practices": "此应用程序的安装 版本仍然使用一些超旧的弃用打包原则。推荐您升级它。", + "diagnosis_apps_allgood": "所有已安装的应用都遵守基本的打包原则", + "diagnosis_apps_deprecated_practices": "此应用的安装 版本仍然使用一些超旧的弃用打包原则。推荐您升级它。", "diagnosis_apps_issue": "发现应用{ app } 存在问题", "diagnosis_description_apps": "应用", "global_settings_setting_backup_compress_tar_archives_help": "创建新备份时,请压缩档案(.tar.gz) ,而不要压缩未压缩的档案(.tar)。注意:启用此选项意味着创建较小的备份存档,但是初始备份过程将明显更长且占用大量CPU。", @@ -584,12 +584,12 @@ "global_settings_setting_ssh_compatibility_help": "SSH服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)", "global_settings_setting_ssh_port": "SSH端口", "global_settings_setting_smtp_allow_ipv6_help": "允许使用IPv6接收和发送邮件", - "global_settings_setting_smtp_relay_enabled_help": "使用SMTP中继主机来代替这个YunoHost实例发送邮件。如果你有以下情况,就很有用:你的25端口被你的ISP或VPS提供商封锁,你有一个住宅IP列在DUHL上,你不能配置反向DNS,或者这个服务器没有直接暴露在互联网上,你想使用其他服务器来发送邮件。", + "global_settings_setting_smtp_relay_enabled_help": "使用SMTP中继主机来代替这个YunoHost实例发送邮件。如果您有以下情况,就很有用:您的25端口被您的ISP或VPS提供商封锁,您有一个住宅IP列在DUHL上,您不能配置反向DNS,或者这个服务器没有直接暴露在互联网上,您想使用其他服务器来发送邮件。", "all_users": "所有的YunoHost用户", - "app_manifest_install_ask_init_admin_permission": "谁应该有权访问此应用程序的管理功能?(此配置可以稍后更改)", + "app_manifest_install_ask_init_admin_permission": "谁应该有权访问此应用的管理功能?(此配置可以稍后更改)", "app_action_failed": "对应用{app}执行动作{action}失败", - "app_manifest_install_ask_init_main_permission": "谁应该有权访问此应用程序?(此配置稍后可以更改)", + "app_manifest_install_ask_init_main_permission": "谁应该有权访问此应用?(此配置稍后可以更改)", "ask_admin_fullname": "管理员全名", "ask_admin_username": "管理员用户名", "ask_fullname": "全名" -} \ No newline at end of file +} From a1e2237fbb4776d2f7623ab4cdbfe49273087cc6 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Tue, 7 Feb 2023 13:55:08 +0000 Subject: [PATCH 142/319] Translated using Weblate (Arabic) Currently translated at 23.5% (178 of 755 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 04fd27001..16e39c3af 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -30,7 +30,7 @@ "certmanager_cert_install_success_selfsigned": "نجحت عملية تثبيت الشهادة الموقعة ذاتيا الخاصة بالنطاق '{domain}'", "certmanager_cert_renew_success": "نجحت عملية تجديد شهادة Let's Encrypt الخاصة باسم النطاق '{domain}'", "certmanager_cert_signing_failed": "فشل إجراء توقيع الشهادة الجديدة", - "certmanager_no_cert_file": "تعذرت عملية قراءة شهادة نطاق {domain} (الملف : {file})", + "certmanager_no_cert_file": "تعذرت عملية قراءة ملف شهادة نطاق {domain} (الملف : {file})", "domain_created": "تم إنشاء النطاق", "domain_creation_failed": "تعذرت عملية إنشاء النطاق {domain}: {error}", "domain_deleted": "تم حذف النطاق", @@ -170,7 +170,7 @@ "domain_config_cert_issuer": "الهيئة الموثِّقة", "domain_config_cert_renew": "تجديد شهادة Let's Encrypt", "domain_config_cert_summary": "حالة الشهادة", - "domain_config_cert_summary_ok": "حسنًا، يبدو أنّ الشهادة جيدة!", + "domain_config_cert_summary_ok": "حسنًا، يبدو أنّ الشهادة الحالية جيدة!", "domain_config_cert_validity": "مدة الصلاحية", "domain_config_xmpp": "المراسَلة الفورية (XMPP)", "global_settings_setting_root_password": "كلمة السر الجديدة لـ root", @@ -214,5 +214,8 @@ "domain_config_cert_summary_abouttoexpire": "مدة صلاحية الشهادة الحالية على وشك الإنتهاء ومِن المفتَرض أن يتم تجديدها تلقائيا قريبا.", "app_manifest_install_ask_path": "اختر مسار URL (بعد النطاق) حيث ينبغي تنصيب هذا التطبيق", "app_manifest_install_ask_domain": "اختر اسم النطاق الذي ينبغي فيه تنصيب هذا التطبيق", - "app_manifest_install_ask_is_public": "هل يجب أن يكون هذا التطبيق ظاهرًا للزوار المجهولين؟" + "app_manifest_install_ask_is_public": "هل يجب أن يكون هذا التطبيق ظاهرًا للزوار المجهولين؟", + "domain_config_default_app_help": "سيعاد توجيه الناس تلقائيا إلى هذا التطبيق عند فتح اسم النطاق هذا. وإذا لم يُحدَّد أي تطبيق، يعاد توجيه الناس إلى استمارة تسجيل الدخولفي بوابة المستخدمين.", + "domain_config_xmpp_help": "ملاحطة: بعض ميزات الـ(إكس إم بي بي) ستتطلب أن تُحدّث سجلاتك الخاصة لـ DNS وتُعيد توليد شهادة Lets Encrypt لتفعيلها", + "certmanager_cert_install_failed": "أخفقت عملية تنصيب شهادة Let's Encrypt على {domains}" } From e974f30ba1dc6b834f4d6b30c78af9a2a7b80d90 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Wed, 8 Feb 2023 06:54:27 +0000 Subject: [PATCH 143/319] Translated using Weblate (Arabic) Currently translated at 25.8% (195 of 755 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 16e39c3af..168083625 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -5,7 +5,7 @@ "app_already_up_to_date": "{app} حديثٌ", "app_argument_required": "المُعامِل '{name}' مطلوب", "app_extraction_failed": "تعذر فك الضغط عن ملفات التنصيب", - "app_install_files_invalid": "ملفات التنصيب خاطئة", + "app_install_files_invalid": "لا يمكن تنصيب هذه الملفات", "app_not_correctly_installed": "يبدو أن التطبيق {app} لم يتم تنصيبه بشكل صحيح", "app_not_installed": "إنّ التطبيق {app} غير مُنصَّب", "app_not_properly_removed": "لم يتم حذف تطبيق {app} بشكلٍ جيّد", @@ -14,7 +14,7 @@ "app_sources_fetch_failed": "تعذر جلب ملفات المصدر ، هل عنوان URL صحيح؟", "app_unknown": "برنامج مجهول", "app_upgrade_app_name": "جارٍ تحديث {app}…", - "app_upgrade_failed": "تعذرت عملية ترقية {app}", + "app_upgrade_failed": "تعذرت عملية تحديث {app}: {error}", "app_upgrade_some_app_failed": "تعذرت عملية ترقية بعض التطبيقات", "app_upgraded": "تم تحديث التطبيق {app}", "ask_main_domain": "النطاق الرئيسي", @@ -22,7 +22,7 @@ "ask_password": "كلمة السر", "backup_applying_method_copy": "جارٍ نسخ كافة الملفات المراد نسخها احتياطيا …", "backup_applying_method_tar": "جارٍ إنشاء ملف TAR للنسخة الاحتياطية…", - "backup_created": "تم إنشاء النسخة الإحتياطية", + "backup_created": "تم إنشاء النسخة الإحتياطية: {name}", "backup_method_copy_finished": "إنتهت عملية النسخ الإحتياطي", "backup_nothings_done": "ليس هناك أي شيء للحفظ", "backup_output_directory_required": "يتوجب عليك تحديد مجلد لتلقي النسخ الإحتياطية", @@ -197,14 +197,14 @@ "diagnosis_apps_issue": "تم العثور على مشكلة في تطبيق {app}", "tools_upgrade": "تحديث حُزم النظام", "service_description_yunomdns": "يسمح لك بالوصول إلى خادمك الخاص باستخدام 'yunohost.local' في شبكتك المحلية", - "good_practices_about_user_password": "أنت الآن على وشك تحديد كلمة مرور مستخدم جديدة. يجب أن تتكون كلمة المرور من 8 أحرف على الأقل - على الرغم من أنه من الممارسات الجيدة استخدام كلمة مرور أطول (أي عبارة مرور) و / أو مجموعة متنوعة من الأحرف (الأحرف الكبيرة والصغيرة والأرقام والأحرف الخاصة).", + "good_practices_about_user_password": "أنت الآن على وشك تحديد كلمة مرور مستخدم جديدة. يجب أن تتكون كلمة المرور من 8 أحرف على الأقل - أخذا بعين الإعتبار أنه من الممارسات الجيدة استخدام كلمة مرور أطول (أي عبارة مرور) و / أو مجموعة متنوعة من الأحرف (الأحرف الكبيرة والصغيرة والأرقام والأحرف الخاصة).", "root_password_changed": "تم تغيير كلمة مرور الجذر", "root_password_desynchronized": "تم تغيير كلمة مرور المدير ، لكن لم يتمكن YunoHost من نشرها على كلمة مرور الجذر!", "user_import_bad_line": "سطر غير صحيح {line}: {details}", "user_import_success": "تم استيراد المستخدمين بنجاح", "visitors": "الزوار", - "password_too_simple_3": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل وأن تحتوي على أرقام وأرقام علوية وسفلية وأحرف خاصة", - "password_too_simple_4": "يجب أن تتكون كلمة المرور من 12 حرفًا على الأقل وأن تحتوي على أرقام وأرقام علوية وسفلية وأحرف خاصة", + "password_too_simple_3": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل وأن تحتوي على أرقام و حروف كبيرة وصغيرة وأحرف خاصة", + "password_too_simple_4": "يجب أن تتكون كلمة المرور من 12 حرفًا على الأقل وأن تحتوي على أرقام وحروف كبيرة وصغيرة وأحرف خاصة", "service_unknown": "الخدمة '{service}' غير معروفة", "unbackup_app": "لن يتم حفظ التطبيق '{app}'", "unrestore_app": "لن يتم استعادة التطبيق '{app}'", @@ -217,5 +217,13 @@ "app_manifest_install_ask_is_public": "هل يجب أن يكون هذا التطبيق ظاهرًا للزوار المجهولين؟", "domain_config_default_app_help": "سيعاد توجيه الناس تلقائيا إلى هذا التطبيق عند فتح اسم النطاق هذا. وإذا لم يُحدَّد أي تطبيق، يعاد توجيه الناس إلى استمارة تسجيل الدخولفي بوابة المستخدمين.", "domain_config_xmpp_help": "ملاحطة: بعض ميزات الـ(إكس إم بي بي) ستتطلب أن تُحدّث سجلاتك الخاصة لـ DNS وتُعيد توليد شهادة Lets Encrypt لتفعيلها", - "certmanager_cert_install_failed": "أخفقت عملية تنصيب شهادة Let's Encrypt على {domains}" + "certmanager_cert_install_failed": "أخفقت عملية تنصيب شهادة Let's Encrypt على {domains}", + "app_manifest_install_ask_password": "اختيار كلمة إدارية لهذا التطبيق", + "app_id_invalid": "مُعرّف التطبيق غير صالح", + "ask_admin_fullname": "الإسم الكامل للمدير", + "admins": "المدراء", + "all_users": "كافة مستخدمي واي يونوهوست", + "ask_user_domain": "اسم النطاق الذي سيُستخدَم لعنوان بريد المستخدِم وكذا لحساب XMPP", + "app_change_url_success": "تم تعديل الرابط التشعبي لتطبيق {app} إلى {domain}{path}", + "backup_app_failed": "لا يُمكن حِفظ {app}" } From dd6d083904763b69153ebf464b1ea2881375834a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 8 Feb 2023 22:51:22 +0100 Subject: [PATCH 144/319] Update changelog for 11.1.6.1 --- debian/changelog | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/debian/changelog b/debian/changelog index 066f65215..8f0a7d70d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,14 @@ +yunohost (11.1.6.1) stable; urgency=low + + - dns: fix CAA recommended DNS conf -> 0 is apparently a more sensible value than 128... (2eb7da06) + - users: Allow digits in user fullname (024db62a) + - backup: fix full backup restore postinstall calls that now need first username+fullname+password (48e488f8) + - i18n: Translations updated for Arabic, Basque, Chinese (Simplified) + + Thanks to all contributors <3 ! (ButterflyOfFire, Poesty Li, xabirequejo) + + -- Alexandre Aubin Wed, 08 Feb 2023 22:50:37 +0100 + yunohost (11.1.6) stable; urgency=low - helpers: allow to use ynh_replace_string with @ ([#1588](https://github.com/yunohost/yunohost/pull/1588)) From a4fa6e07d04daf091ddb186d511dd728042cf081 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Feb 2023 17:09:15 +0100 Subject: [PATCH 145/319] permissions: fix trailing-slash issue in edge case where app has additional urls related to a different domain --- src/permission.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/permission.py b/src/permission.py index c7446b7ad..72975561f 100644 --- a/src/permission.py +++ b/src/permission.py @@ -814,10 +814,12 @@ def _update_ldap_group_permission( def _get_absolute_url(url, base_path): # # For example transform: - # (/api, domain.tld/nextcloud) into domain.tld/nextcloud/api - # (/api, domain.tld/nextcloud/) into domain.tld/nextcloud/api - # (re:/foo.*, domain.tld/app) into re:domain\.tld/app/foo.* - # (domain.tld/bar, domain.tld/app) into domain.tld/bar + # (/, domain.tld/) into domain.tld (no trailing /) + # (/api, domain.tld/nextcloud) into domain.tld/nextcloud/api + # (/api, domain.tld/nextcloud/) into domain.tld/nextcloud/api + # (re:/foo.*, domain.tld/app) into re:domain\.tld/app/foo.* + # (domain.tld/bar, domain.tld/app) into domain.tld/bar + # (some.other.domain/, domain.tld/app) into some.other.domain (no trailing /) # base_path = base_path.rstrip("/") if url is None: @@ -827,7 +829,7 @@ def _get_absolute_url(url, base_path): if url.startswith("re:/"): return "re:" + base_path.replace(".", "\\.") + url[3:] else: - return url + return url.rstrip("/") def _validate_and_sanitize_permission_url(url, app_base_path, app): From 658940079d7d320b9ad90a92bd7ccf22987a8c4f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Feb 2023 18:59:55 +0100 Subject: [PATCH 146/319] backup: fix postinstall during full restore ... tmp admin user can't be named 'admin' because of conflicting alias with the admins group --- src/backup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/backup.py b/src/backup.py index ee961a7bf..64e85f97b 100644 --- a/src/backup.py +++ b/src/backup.py @@ -940,7 +940,7 @@ class RestoreManager: # Use a dummy password which is not gonna be saved anywhere # because the next thing to happen should be that a full restore of the LDAP db will happen - tools_postinstall(domain, "admin", "Admin", password=random_ascii(70), ignore_dyndns=True, overwrite_root_password=False) + tools_postinstall(domain, "tmpadmin", "Tmp Admin", password=random_ascii(70), ignore_dyndns=True, overwrite_root_password=False) def clean(self): """ @@ -1188,7 +1188,8 @@ class RestoreManager: self._restore_apps() except Exception as e: raise YunohostError( - f"The following critical error happened during restoration: {e}" + f"The following critical error happened during restoration: {e}", + raw_msg=True ) finally: self.clean() From a154e811db220233c74406a5d3f252db55e18123 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Feb 2023 19:00:17 +0100 Subject: [PATCH 147/319] doc: improve app resource doc --- doc/generate_helper_doc.py | 2 +- doc/generate_resource_doc.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index 63fa109e6..110d1d4cd 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -24,7 +24,7 @@ def render(helpers): data = { "helpers": helpers, - "date": datetime.datetime.now().strftime("%m/%d/%Y"), + "date": datetime.datetime.now().strftime("%d/%m/%Y"), "version": open("../debian/changelog").readlines()[0].split()[1].strip("()"), } diff --git a/doc/generate_resource_doc.py b/doc/generate_resource_doc.py index 20a9a994d..ef98dc810 100644 --- a/doc/generate_resource_doc.py +++ b/doc/generate_resource_doc.py @@ -1,6 +1,25 @@ import ast +import datetime +import subprocess -print("""--- +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: App resources template: docs taxonomy: @@ -9,6 +28,8 @@ routes: default: '/packaging_apps_resources' --- +Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_resource_doc.py) on {today} (YunoHost version {version}) + """) @@ -35,7 +56,9 @@ for c in ResourceClasses: for resource_id, doc in sorted(ResourceDocString.items()): doc = doc.replace("\n ", "\n") + print("----------------") print("") print(f"## {resource_id.replace('_', ' ').title()}") print("") print(doc) + print("") From 258e28f7039b885d01b913e8744574f2bacb5c28 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Feb 2023 19:01:11 +0100 Subject: [PATCH 148/319] Update changelog for 11.1.6.2 --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index 8f0a7d70d..ab38326f0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +yunohost (11.1.6.2) stable; urgency=low + + - permissions: fix trailing-slash issue in edge case where app has additional urls related to a different domain (a4fa6e07) + - backup: fix postinstall during full restore ... tmp admin user can't be named 'admin' because of conflicting alias with the admins group (65894007) + - doc: improve app resource doc (a154e811) + + -- Alexandre Aubin Thu, 09 Feb 2023 19:00:42 +0100 + yunohost (11.1.6.1) stable; urgency=low - dns: fix CAA recommended DNS conf -> 0 is apparently a more sensible value than 128... (2eb7da06) From 0da6370d627876604deb050dbcec838b8ff84485 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 10 Feb 2023 00:15:02 +0100 Subject: [PATCH 149/319] postfix complains about unused parameter: exclude_internal=yes / search_timeout=30 --- conf/postfix/plain/ldap-groups.cf | 2 -- 1 file changed, 2 deletions(-) diff --git a/conf/postfix/plain/ldap-groups.cf b/conf/postfix/plain/ldap-groups.cf index dbf768641..215081fac 100644 --- a/conf/postfix/plain/ldap-groups.cf +++ b/conf/postfix/plain/ldap-groups.cf @@ -2,8 +2,6 @@ server_host = localhost server_port = 389 search_base = dc=yunohost,dc=org query_filter = (&(objectClass=groupOfNamesYnh)(mail=%s)) -exclude_internal = yes -search_timeout = 30 scope = sub result_attribute = memberUid, mail terminal_result_attribute = memberUid From 013aff3d0c6693a24df73bf13f4aa2ed18d4172c Mon Sep 17 00:00:00 2001 From: John Hackett Date: Fri, 10 Feb 2023 00:14:57 +0000 Subject: [PATCH 150/319] Add push notification plugins This is reasonably important for the performance of clients such as Delta Chat. The plugins are bundled with dovecot by default (see https://wiki2.dovecot.org/Plugins ) so this should not be disruptive. --- conf/dovecot/dovecot.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index 72fd71c4d..e614c3796 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -10,7 +10,7 @@ mail_uid = 500 protocols = imap sieve {% if pop3_enabled == "True" %}pop3{% endif %} -mail_plugins = $mail_plugins quota +mail_plugins = $mail_plugins quota notify push_notification ############################################################################### From 9bd4344f25b079b66a329d93c1cd552e340cf9f9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 12 Feb 2023 22:25:42 +0100 Subject: [PATCH 151/319] appsv2: we don't want to store user-provided passwords by default, but they should still be set in the env for the script to use it --- src/app.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/app.py b/src/app.py index 4e04c035a..5b1ba7b3b 100644 --- a/src/app.py +++ b/src/app.py @@ -1050,6 +1050,7 @@ def app_install( if packaging_format >= 2: for question in questions: # Except user-provider passwords + # ... which we need to reinject later in the env_dict if question.type == "password": continue @@ -1101,11 +1102,22 @@ 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 >= 2: + for question in questions: + # 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 + + # 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] operation_logger.extra.update({"env": env_dict_for_logging}) From d0ca120eb049473bce2bd7fcbc18e90857304fa1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 13 Feb 2023 02:37:07 +0100 Subject: [PATCH 152/319] diagnosis: fix typo, diagnosis detail should be a list, not a string --- src/diagnosers/24-mail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 857de687d..df14222a5 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -277,7 +277,7 @@ class MyDiagnoser(Diagnoser): data={"error": str(e)}, status="ERROR", summary="diagnosis_mail_queue_unavailable", - details="diagnosis_mail_queue_unavailable_details", + details=["diagnosis_mail_queue_unavailable_details"], ) else: if pending_emails > 100: From 80d8d9b3c3bd8a631e1613f2d4de8e03ff0dcfa6 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Fri, 10 Feb 2023 08:16:59 +0000 Subject: [PATCH 153/319] Translated using Weblate (Arabic) Currently translated at 28.3% (214 of 755 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index 168083625..07717cef9 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -225,5 +225,24 @@ "all_users": "كافة مستخدمي واي يونوهوست", "ask_user_domain": "اسم النطاق الذي سيُستخدَم لعنوان بريد المستخدِم وكذا لحساب XMPP", "app_change_url_success": "تم تعديل الرابط التشعبي لتطبيق {app} إلى {domain}{path}", - "backup_app_failed": "لا يُمكن حِفظ {app}" + "backup_app_failed": "لا يُمكن حِفظ {app}", + "pattern_password_app": "آسف، كلمات السر لا يمكن أن تحتوي على الحروف التالية: {forbidden_chars}", + "diagnosis_http_could_not_diagnose_details": "خطأ: {error}", + "mail_unavailable": "عنوان البريد الإلكتروني هذا مخصص لفريق المدراء", + "mailbox_disabled": "صندوق البريد معطل للمستخدم {user}", + "migration_0021_cleaning_up": "تنظيف ذاكرة التخزين المؤقت وكذا الحُزم التي تَعُد مفيدة…", + "migration_0021_yunohost_upgrade": "بداية تحديث نواة YunoHost…", + "migration_ldap_migration_failed_trying_to_rollback": "فشِلَت الهجرة… محاولة استعادة الرجوع إلى النظام.", + "migration_ldap_rollback_success": "تمت العودة إلى حالة النظام الأصلي.", + "migrations_success_forward": "اكتملت الهجرة {id}", + "password_too_simple_2": "يجب أن يكون طول كلمة المرور 8 حروف على الأقل وأن تحتوي على أرقام وحروف علوية ودنيا", + "pattern_lastname": "يجب أن يكون لقبًا صالحًا (على الأقل 3 حروف)", + "migration_0021_start": "بداية الهجرة إلى Bullseye", + "migrations_running_forward": "جارٍ تنفيذ الهجرة {id}…", + "password_confirmation_not_the_same": "كلمة المرور وتأكيدها غير متطابقان", + "password_too_long": "فضلا قم باختيار كلمة مرور طولها أقل مِن 127 حرفًا", + "pattern_fullname": "يجب أن يكون اسماً كاملاً صالحاً (على الأقل 3 حروف)", + "migration_0021_main_upgrade": "بداية التحديث الرئيسي…", + "migration_0021_patching_sources_list": "تحديث ملف sources.lists…", + "pattern_firstname": "يجب أن يكون اسماً أولياً صالحاً (على الأقل 3 حروف)" } From 135dbec8b691fbc339652bcda92da6dd2c72cd12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Thu, 9 Feb 2023 06:15:07 +0000 Subject: [PATCH 154/319] Translated using Weblate (Galician) Currently translated at 99.6% (752 of 755 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/locales/gl.json b/locales/gl.json index 1b5147ac6..8a22b58e9 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -82,7 +82,7 @@ "app_change_url_success": "A URL de {app} agora é {domain}{path}", "app_change_url_no_script": "A app '{app_name}' non soporta o cambio de URL. Pode que debas actualizala.", "app_change_url_identical_domains": "O antigo e o novo dominio/url_path son idénticos ('{domain}{path}'), nada que facer.", - "backup_deleted": "Copia de apoio eliminada", + "backup_deleted": "Copia eliminada: {name}", "backup_delete_error": "Non se eliminou '{path}'", "backup_custom_mount_error": "O método personalizado de copia non superou o paso 'mount'", "backup_custom_backup_error": "O método personalizado da copia non superou o paso 'backup'", @@ -90,7 +90,7 @@ "backup_csv_addition_failed": "Non se engadiron os ficheiros a copiar ao ficheiro CSV", "backup_creation_failed": "Non se puido crear o arquivo de copia de apoio", "backup_create_size_estimation": "O arquivo vai conter arredor de {size} de datos.", - "backup_created": "Copia de apoio creada", + "backup_created": "Copia creada: {name}", "backup_couldnt_bind": "Non se puido ligar {src} a {dest}.", "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo", "backup_cleaning_failed": "Non se puido baleirar o cartafol temporal para a copia", @@ -748,5 +748,7 @@ "app_not_enough_ram": "Esta app require {required} de RAM para instalar/actualizar pero só hai {current} dispoñible.", "global_settings_setting_dns_exposure": "Versións de IP a ter en conta para a configuración DNS e diagnóstico", "global_settings_setting_dns_exposure_help": "Nota: Esto só lle afecta á configuración DNS recomendada e diagnóstico do sistema. Non lle afecta aos axustes do sistema.", - "diagnosis_ip_no_ipv6_tip_important": "Se está dispoñible, IPv6 debería estar automáticamente configurado polo sistema ou o teu provedor. Se non, pode que teñas que facer algúns axustes manualmente tal como se explica na documentación: https://yunohost.org/#/ipv6." -} \ No newline at end of file + "diagnosis_ip_no_ipv6_tip_important": "Se está dispoñible, IPv6 debería estar automáticamente configurado polo sistema ou o teu provedor. Se non, pode que teñas que facer algúns axustes manualmente tal como se explica na documentación: https://yunohost.org/#/ipv6.", + "domain_config_default_app_help": "As persoas serán automáticamente redirixidas a esta app ao abrir o dominio. Se non se indica ningunha, serán redirixidas ao formulario de acceso no portal de usuarias.", + "domain_config_xmpp_help": "Nota: algunhas características de XMPP para ser utilizadas precisan que teñas ao día os rexistros DNS e rexeneres os certificados Lets Encrypt" +} From aa9bc47aa6caaabe58f294e933247d7707a3aace Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 13 Feb 2023 14:47:09 +0100 Subject: [PATCH 155/319] appsv2: fix i18n for arch mismatch, can't juste join() inside string formated with .format() --- locales/en.json | 2 +- locales/es.json | 4 ++-- locales/eu.json | 2 +- locales/fr.json | 2 +- locales/gl.json | 4 ++-- locales/pl.json | 2 +- locales/tr.json | 4 ++-- src/app.py | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/locales/en.json b/locales/en.json index 3832cb6c0..75b4f203a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -13,7 +13,7 @@ "app_already_installed": "{app} is already installed", "app_already_installed_cant_change_url": "This app is already installed. The URL cannot be changed just by this function. Check in `app changeurl` if it's available.", "app_already_up_to_date": "{app} is already up-to-date", - "app_arch_not_supported": "This app can only be installed on architectures {', '.join(required)} but your server architecture is {current}", + "app_arch_not_supported": "This app can only be installed on architectures {required} but your server architecture is {current}", "app_argument_choice_invalid": "Pick a valid value for argument '{name}': '{value}' is not among the available choices ({choices})", "app_argument_invalid": "Pick a valid value for the argument '{name}': {error}", "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reasons", diff --git a/locales/es.json b/locales/es.json index 5851f44f4..d88a730bb 100644 --- a/locales/es.json +++ b/locales/es.json @@ -704,7 +704,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 {', '.join(required)} pero la arquitectura de su servidor es {current}", + "app_arch_not_supported": "Esta aplicación sólo puede instalarse en arquitecturas {required} pero la arquitectura de su 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.", @@ -749,4 +749,4 @@ "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 +} diff --git a/locales/eu.json b/locales/eu.json index 63ecf7231..74a54c435 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -740,7 +740,7 @@ "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 {current} bakarrik daude erabilgarri. Aplikazioa ibiliko balitz ere, instalazioak edo bertsio-berritzeak RAM kopuru handia eskatzen du eta zure zerbitzaria izoztu 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}]", - "app_arch_not_supported": "Aplikazio hau {', '.join(required)} arkitekturan instala daiteke bakarrik, baina zure zerbitzariaren arkitektura {current} da", + "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.", "app_yunohost_version_not_supported": "Aplikazio honek YunoHost >= {required} behar du baina unean instalatutako bertsioa {current} da", diff --git a/locales/fr.json b/locales/fr.json index 9939bb6cb..f05699656 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -741,7 +741,7 @@ "global_settings_setting_portal_theme": "Thème du portail", "global_settings_setting_portal_theme_help": "Pour plus d'informations sur la création de thèmes de portail personnalisés, voir https://yunohost.org/theming", "global_settings_setting_passwordless_sudo": "Permettre aux administrateurs d'utiliser 'sudo' sans retaper leur mot de passe", - "app_arch_not_supported": "Cette application ne peut être installée que sur les architectures {', '.join(required)}. L'architecture de votre serveur est {current}", + "app_arch_not_supported": "Cette application ne peut être installée que sur les architectures {required}. L'architecture de votre serveur est {current}", "app_resource_failed": "L'allocation automatique des ressources (provisioning), la suppression d'accès à ces ressources (déprovisioning) ou la mise à jour des ressources pour {app} a échoué : {error}", "confirm_app_insufficient_ram": "ATTENTION ! Cette application requiert {required} de RAM pour l'installation/mise à niveau mais il n'y a que {current} de disponible actuellement. Même si cette application pouvait fonctionner, son processus d'installation/mise à niveau nécessite une grande quantité de RAM. Votre serveur pourrait donc geler et planter lamentablement. Si vous êtes prêt à prendre ce risque, tapez '{answers}'", "app_not_enough_disk": "Cette application nécessite {required} d'espace libre.", diff --git a/locales/gl.json b/locales/gl.json index 1b5147ac6..852b48e2d 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -740,7 +740,7 @@ "global_settings_setting_passwordless_sudo": "Permitir a Admins usar 'sudo' sen ter que volver a escribir o contrasinal", "global_settings_setting_portal_theme": "Decorado do Portal", "global_settings_setting_portal_theme_help": "Tes máis info acerca da creación de decorados para o portal de acceso en https://yunohost.org/theming", - "app_arch_not_supported": "Esta app só pode ser instalada e arquitecturas {', '.join(required)} pero a arquitectura do teu servidor é {current}", + "app_arch_not_supported": "Esta app só pode ser instalada e arquitecturas {required} pero a arquitectura do teu servidor é {current}", "app_not_enough_disk": "Esta app precisa {required} de espazo libre.", "app_yunohost_version_not_supported": "Esta app require YunoHost >= {required} pero a versión actual instalada é {current}", "confirm_app_insufficient_ram": "PERIGO! Esta app precisa {required} de RAM para instalar/actualizar pero só hai {current} dispoñibles. Incluso se a app funcionase, o seu proceso de instalación/actualización require gran cantidade de RAM e o teu servidor podería colgarse e fallar. Se queres asumir o risco, escribe '{answers}'", @@ -749,4 +749,4 @@ "global_settings_setting_dns_exposure": "Versións de IP a ter en conta para a configuración DNS e diagnóstico", "global_settings_setting_dns_exposure_help": "Nota: Esto só lle afecta á configuración DNS recomendada e diagnóstico do sistema. Non lle afecta aos axustes do sistema.", "diagnosis_ip_no_ipv6_tip_important": "Se está dispoñible, IPv6 debería estar automáticamente configurado polo sistema ou o teu provedor. Se non, pode que teñas que facer algúns axustes manualmente tal como se explica na documentación: https://yunohost.org/#/ipv6." -} \ No newline at end of file +} diff --git a/locales/pl.json b/locales/pl.json index 08c3e1d43..d66427ac3 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -73,7 +73,7 @@ "app_action_broke_system": "Wydaje się, że ta akcja przerwała te ważne usługi: {services}", "additional_urls_already_removed": "Dodatkowy URL '{url}' już usunięty w dodatkowym URL dla uprawnienia '{permission}'", "additional_urls_already_added": "Dodatkowy URL '{url}' już dodany w dodatkowym URL dla uprawnienia '{permission}'", - "app_arch_not_supported": "Ta aplikacja może być zainstalowana tylko na architekturach {', '.join(required)}, a twoja architektura serwera to {current}", + "app_arch_not_supported": "Ta aplikacja może być zainstalowana tylko na architekturach {required}, a twoja architektura serwera to {current}", "app_argument_invalid": "Wybierz poprawną wartość dla argumentu '{name}': {błąd}", "all_users": "Wszyscy użytkownicy YunoHost", "app_action_failed": "Nie udało się uruchomić akcji {action} dla aplikacji {app}", diff --git a/locales/tr.json b/locales/tr.json index c219e997b..43a489d01 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -15,5 +15,5 @@ "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 {', '.join(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}." +} diff --git a/src/app.py b/src/app.py index 5b1ba7b3b..b35f4a33c 100644 --- a/src/app.py +++ b/src/app.py @@ -2582,7 +2582,7 @@ def _check_manifest_requirements( yield ( "arch", arch_requirement in ["all", "?"] or arch in arch_requirement, - {"current": arch, "required": arch_requirement}, + {"current": arch, "required": ', '.join(arch_requirement)}, "app_arch_not_supported", # i18n: app_arch_not_supported ) From 1d1a3756baddd9ed8f74a18c67c459d693fcaf3f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 13 Feb 2023 18:17:39 +0100 Subject: [PATCH 156/319] appsv2: missing raw_msg=True for exceptions --- src/utils/resources.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 030c73574..2d4a479de 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -302,7 +302,8 @@ class PermissionsResource(AppResource): and properties["main"]["url"] != "/" ): raise YunohostError( - "URL for the 'main' permission should be '/' for webapps (or undefined/None for non-webapps). Note that / refers to the install url of the app" + "URL for the 'main' permission should be '/' for webapps (or undefined/None for non-webapps). Note that / refers to the install url of the app", + raw_msg=True ) super().__init__({"permissions": properties}, *args, **kwargs) @@ -470,12 +471,12 @@ class SystemuserAppResource(AppResource): if check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): os.system(f"deluser {self.app} >/dev/null") if check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): - raise YunohostError(f"Failed to delete system user for {self.app}") + raise YunohostError(f"Failed to delete system user for {self.app}", raw_msg=True) if check_output(f"getent group {self.app} &>/dev/null || true").strip(): os.system(f"delgroup {self.app} >/dev/null") if check_output(f"getent group {self.app} &>/dev/null || true").strip(): - raise YunohostError(f"Failed to delete system user for {self.app}") + raise YunohostError(f"Failed to delete system user for {self.app}", raw_msg=True) # FIXME : better logging and error handling, add stdout/stderr from the deluser/delgroup commands... @@ -743,7 +744,8 @@ class AptDependenciesAppResource(AppResource): 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" + "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) @@ -860,7 +862,8 @@ class PortsResource(AppResource): if infos["fixed"]: if self._port_is_used(port_value): raise YunohostValidationError( - f"Port {port_value} is already used by another process or app." + f"Port {port_value} is already used by another process or app.", + raw_msg=True ) else: while self._port_is_used(port_value): From ab8a6b940f9c067f8e6d8ca82da55fea16f646a6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 13 Feb 2023 18:28:45 +0100 Subject: [PATCH 157/319] appsv2: fix check that main permission url is '/' --- src/utils/resources.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 2d4a479de..47191fa36 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -298,11 +298,11 @@ class PermissionsResource(AppResource): properties[perm]["show_tile"] = bool(properties[perm]["url"]) if ( - isinstance(properties["main"]["url"], str) - and properties["main"]["url"] != "/" + not isinstance(properties["main"].get("url"), str) + or properties["main"]["url"] != "/" ): raise YunohostError( - "URL for the 'main' permission should be '/' for webapps (or undefined/None for non-webapps). Note that / refers to the install url of the app", + "URL for the 'main' permission should be '/' for webapps (or undefined/None for non-webapps). Note that / refers to the install url of the app, i.e $domain.tld/$path/", raw_msg=True ) From 0ab20b733be15f242ca03316eb7e3b1a9a50d971 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 14 Feb 2023 16:09:55 +0100 Subject: [PATCH 158/319] appsv2: mysqlshow is fucking dumb and returns exit code 0 when DB doesnt exists ... --- 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 47191fa36..96559d8d2 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -953,7 +953,7 @@ class DatabaseAppResource(AppResource): def db_exists(self, db_name): if self.dbtype == "mysql": - return os.system(f"mysqlshow '{db_name}' >/dev/null 2>/dev/null") == 0 + return os.system(f"mysqlshow | grep -q -w '{db_name}' 2>/dev/null") == 0 elif self.dbtype == "postgresql": return ( os.system( From 7be7eb115497da6b0f92d618baa5fd86ca8fd261 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 14 Feb 2023 17:33:50 +0100 Subject: [PATCH 159/319] apps: fix inconsistent app removal during remove-after-failed-upgrade and remove-after-failed-backup contexts --- src/app.py | 23 +++++++++++++++++------ src/backup.py | 32 ++------------------------------ 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/app.py b/src/app.py index b35f4a33c..26102c723 100644 --- a/src/app.py +++ b/src/app.py @@ -743,7 +743,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False "Upgrade failed ... attempting to restore the satefy backup (Yunohost first need to remove the app for this) ..." ) - app_remove(app_instance_name) + app_remove(app_instance_name, force_workdir=extracted_app_folder) backup_restore( name=safety_backup_name, apps=[app_instance_name], force=True ) @@ -1270,14 +1270,14 @@ def app_install( @is_unit_operation() -def app_remove(operation_logger, app, purge=False): +def app_remove(operation_logger, app, purge=False, force_workdir=None): """ Remove app Keyword arguments: app -- App(s) to delete purge -- Remove with all app data - + force_workdir -- Special var to force the working directoy to use, in context such as remove-after-failed-upgrade or remove-after-failed-restore """ from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers from yunohost.hook import hook_exec, hook_remove, hook_callback @@ -1296,7 +1296,6 @@ def app_remove(operation_logger, app, purge=False): operation_logger.start() logger.info(m18n.n("app_start_remove", app=app)) - app_setting_path = os.path.join(APPS_SETTING_PATH, app) # Attempt to patch legacy helpers ... @@ -1306,8 +1305,20 @@ def app_remove(operation_logger, app, purge=False): # script might date back from jessie install) _patch_legacy_php_versions(app_setting_path) - manifest = _get_manifest_of_app(app_setting_path) - tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) + if force_workdir: + # This is when e.g. calling app_remove() from the upgrade-failed case + # where we want to remove using the *new* remove script and not the old one + # and also get the new manifest + # It's especially important during v1->v2 app format transition where the + # setting names change (e.g. install_dir instead of final_path) and + # running the old remove script doesnt make sense anymore ... + tmp_workdir_for_app = tempfile.mkdtemp(prefix="app_", dir=APP_TMP_WORKDIRS) + os.system(f"cp -a {force_workdir}/* {tmp_workdir_for_app}/") + else: + tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) + + manifest = _get_manifest_of_app(tmp_workdir_for_app) + remove_script = f"{tmp_workdir_for_app}/scripts/remove" env_dict = {} diff --git a/src/backup.py b/src/backup.py index 64e85f97b..ff2f63276 100644 --- a/src/backup.py +++ b/src/backup.py @@ -52,6 +52,7 @@ from yunohost.app import ( _make_environment_for_app_script, _make_tmp_workdir_for_app, _get_manifest_of_app, + app_remove, ) from yunohost.hook import ( hook_list, @@ -1550,36 +1551,7 @@ class RestoreManager: else: self.targets.set_result("apps", app_instance_name, "Error") - remove_script = os.path.join(app_scripts_in_archive, "remove") - - # Setup environment for remove script - env_dict_remove = _make_environment_for_app_script( - app_instance_name, workdir=app_workdir - ) - remove_operation_logger = OperationLogger( - "remove_on_failed_restore", - [("app", app_instance_name)], - env=env_dict_remove, - ) - remove_operation_logger.start() - - # Execute remove script - if hook_exec(remove_script, env=env_dict_remove)[0] != 0: - msg = m18n.n("app_not_properly_removed", app=app_instance_name) - logger.warning(msg) - remove_operation_logger.error(msg) - else: - remove_operation_logger.success() - - # Cleaning app directory - shutil.rmtree(app_settings_new_path, ignore_errors=True) - - # Remove all permission in LDAP for this app - for permission_name in user_permission_list()["permissions"].keys(): - if permission_name.startswith(app_instance_name + "."): - permission_delete(permission_name, force=True) - - # TODO Cleaning app hooks + app_remove(app_instance_name, force_workdir=app_workdir) logger.error(failure_message_with_debug_instructions) From 8fd75475280bc498462c91bd19ab6ec91a45edd1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 15 Feb 2023 16:57:32 +0100 Subject: [PATCH 160/319] Unused imports --- src/backup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backup.py b/src/backup.py index ff2f63276..38d4c080f 100644 --- a/src/backup.py +++ b/src/backup.py @@ -1369,8 +1369,6 @@ class RestoreManager: from yunohost.user import user_group_list from yunohost.permission import ( permission_create, - permission_delete, - user_permission_list, permission_sync_to_user, ) From e24ddd299ecf935d042d56a70c729c440561abcc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 15 Feb 2023 19:24:14 +0100 Subject: [PATCH 161/319] helpers: in apt/php stuff, don't try to upgrade-alternatives if the default PHP version ain't available anymore --- helpers/apt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helpers/apt b/helpers/apt index 8caf9f3dc..c36f4aa27 100644 --- a/helpers/apt +++ b/helpers/apt @@ -277,7 +277,10 @@ ynh_install_app_dependencies() { ynh_app_setting_set --app=$app --key=phpversion --value=$specific_php_version # Set the default php version back as the default version for php-cli. - update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION + if test -e /usr/bin/php$YNH_DEFAULT_PHP_VERSION + then + update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION + fi elif grep --quiet 'php' <<< "$dependencies"; then ynh_app_setting_set --app=$app --key=phpversion --value=$YNH_DEFAULT_PHP_VERSION fi From 0c4a006a4f8db593dac857eb68648c815dd506ff Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 15 Feb 2023 19:46:25 +0100 Subject: [PATCH 162/319] appsv2: also replace __DOMAIN__ in resource properties --- src/utils/resources.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/resources.py b/src/utils/resources.py index 96559d8d2..7a1ebb386 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -152,6 +152,9 @@ class AppResource: for key, value in properties.items(): if isinstance(value, str): value = value.replace("__APP__", self.app) + # This one is needed for custom permission urls where the domain might be used + if "__DOMAIN__" in value: + value.replace("__DOMAIN__", self.get_setting("domain")) setattr(self, key, value) def get_setting(self, key): From 60b21795b8b131471f5026384a8188d3b6026c35 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 15 Feb 2023 19:49:54 +0100 Subject: [PATCH 163/319] appsv2: in php helpers, use the global $phpversion var/setting by default instead of $YNH_PHP_VERSION --- helpers/php | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/helpers/php b/helpers/php index 407f205e7..417dbbc61 100644 --- a/helpers/php +++ b/helpers/php @@ -57,6 +57,7 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} # # Requires YunoHost version 4.1.0 or higher. 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) @@ -81,11 +82,16 @@ ynh_add_fpm_config() { dedicated_service=${dedicated_service:-0} # Set the default PHP-FPM version by default - phpversion="${phpversion:-$YNH_PHP_VERSION}" + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then + phpversion="${phpversion:-$YNH_PHP_VERSION}" + else + phpversion="${phpversion:-$_globalphpversion}" + fi local old_phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) # If the PHP version changed, remove the old fpm conf + # (NB: This stuff is also handled by the apt helper, which is usually triggered before this helper) if [ -n "$old_phpversion" ] && [ "$old_phpversion" != "$phpversion" ]; then local old_php_fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir) local old_php_finalphpconf="$old_php_fpm_config_dir/pool.d/$app.conf" @@ -100,6 +106,7 @@ ynh_add_fpm_config() { # 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 @@ -481,6 +488,7 @@ YNH_COMPOSER_VERSION=${YNH_COMPOSER_VERSION:-$YNH_DEFAULT_COMPOSER_VERSION} # # Requires YunoHost version 4.2 or higher. ynh_composer_exec() { + local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. local legacy_args=vwc declare -Ar args_array=([v]=phpversion= [w]=workdir= [c]=commands=) @@ -490,7 +498,12 @@ ynh_composer_exec() { # Manage arguments with getopts ynh_handle_getopts_args "$@" workdir="${workdir:-${install_dir:-$final_path}}" - phpversion="${phpversion:-$YNH_PHP_VERSION}" + + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then + phpversion="${phpversion:-$YNH_PHP_VERSION}" + else + phpversion="${phpversion:-$_globalphpversion}" + fi COMPOSER_HOME="$workdir/.composer" COMPOSER_MEMORY_LIMIT=-1 \ php${phpversion} "$workdir/composer.phar" $commands \ @@ -507,6 +520,7 @@ ynh_composer_exec() { # # Requires YunoHost version 4.2 or higher. ynh_install_composer() { + local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. local legacy_args=vwac declare -Ar args_array=([v]=phpversion= [w]=workdir= [a]=install_args= [c]=composerversion=) @@ -521,7 +535,13 @@ ynh_install_composer() { else workdir="${workdir:-$install_dir}" fi - phpversion="${phpversion:-$YNH_PHP_VERSION}" + + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then + phpversion="${phpversion:-$YNH_PHP_VERSION}" + else + phpversion="${phpversion:-$_globalphpversion}" + fi + install_args="${install_args:-}" composerversion="${composerversion:-$YNH_COMPOSER_VERSION}" From aa585963c02e9a9129c1a8ed2241b5cbeec41824 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 15 Feb 2023 19:59:57 +0100 Subject: [PATCH 164/319] ci: fix autoblack PR targetting tags instead of dev --- .gitlab/ci/lint.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index 69e87b6ca..7a8fbf1fb 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -42,6 +42,6 @@ black: - '[ $(git diff | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit - git commit -am "[CI] Format code with Black" || true - git push -f origin "ci-format-${CI_COMMIT_REF_NAME}":"ci-format-${CI_COMMIT_REF_NAME}" - - hub pull-request -m "[CI] Format code with Black" -b Yunohost:$CI_COMMIT_REF_NAME -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd + - hub pull-request -m "[CI] Format code with Black" -b Yunohost:dev -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd only: - tags From a06bb9ae825e3ef5d28846e26f39125deb998d89 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 15 Feb 2023 20:15:21 +0100 Subject: [PATCH 165/319] Create codeql.yml --- .github/workflows/codeql.yml | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..d9a548b3b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,40 @@ +name: "CodeQL" + +on: + push: + branches: [ "dev" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "dev" ] + schedule: + - cron: '43 12 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" From 13c4687c7b770bb3377821374ef9bf077a468760 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 15 Feb 2023 21:13:39 +0100 Subject: [PATCH 166/319] Update changelog for 11.1.7 --- debian/changelog | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/debian/changelog b/debian/changelog index ab38326f0..d891d1805 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,23 @@ +yunohost (11.1.7) stable; urgency=low + + - mail: fix complain about unused parameters in postfix: exclude_internal=yes / search_timeout=30 (0da6370d) + - mail: Add push notification plugins in dovecot ([#1594](https://github.com/yunohost/yunohost/pull/1594)) + - diagnosis: fix typo, diagnosis detail should be a list, not a string (d0ca120e) + - helpers: in apt/php stuff, don't try to upgrade-alternatives if the default PHP version ain't available anymore (e24ddd29) + - apps: fix inconsistent app removal during remove-after-failed-upgrade and remove-after-failed-backup contexts (7be7eb11) + - appsv2: we don't want to store user-provided passwords by default, but they should still be set in the env for the script to use it (9bd4344f) + - appsv2: fix i18n for arch mismatch, can't juste join() inside string formated with .format() (aa9bc47a) + - appsv2: missing raw_msg=True for exceptions (1d1a3756) + - appsv2: fix check that main permission url is '/' (ab8a6b94) + - appsv2: mysqlshow is fucking dumb and returns exit code 0 when DB doesnt exists ... (0ab20b73) + - appsv2: also replace __DOMAIN__ in resource properties (0c4a006a) + - appsv2: in php helpers, use the global $phpversion var/setting by default instead of $YNH_PHP_VERSION (60b21795) + - i18n: Translations updated for Arabic, Galician + + Thanks to all contributors <3 ! (ButterflyOfFire, John Hackett, José M) + + -- Alexandre Aubin Wed, 15 Feb 2023 21:08:04 +0100 + yunohost (11.1.6.2) stable; urgency=low - permissions: fix trailing-slash issue in edge case where app has additional urls related to a different domain (a4fa6e07) From 069b782f07838ff42e96361620700bc7749ee15d Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Wed, 15 Feb 2023 21:37:05 +0000 Subject: [PATCH 167/319] [CI] Format code with Black --- doc/generate_resource_doc.py | 18 +++++++++++++----- src/app.py | 12 +++++++----- src/backup.py | 11 +++++++++-- src/dns.py | 4 +++- src/migrations/0026_new_admins_group.py | 8 +++++--- src/utils/config.py | 4 +++- src/utils/resources.py | 19 +++++++++++++------ 7 files changed, 53 insertions(+), 23 deletions(-) diff --git a/doc/generate_resource_doc.py b/doc/generate_resource_doc.py index ef98dc810..272845104 100644 --- a/doc/generate_resource_doc.py +++ b/doc/generate_resource_doc.py @@ -2,9 +2,10 @@ import ast import datetime import subprocess -version = open("../debian/changelog").readlines()[0].split()[1].strip("()"), +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", @@ -16,10 +17,13 @@ def get_current_commit(): current_commit = stdout.strip().decode("utf-8") return current_commit + + current_commit = get_current_commit() -print(f"""--- +print( + f"""--- title: App resources template: docs taxonomy: @@ -30,7 +34,8 @@ routes: Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_resource_doc.py) on {today} (YunoHost version {version}) -""") +""" +) fname = "../src/utils/resources.py" @@ -40,12 +45,15 @@ content = open(fname).read() # in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports... tree = ast.parse(content) -ResourceClasses = [c for c in tree.body if isinstance(c, ast.ClassDef) and c.bases and c.bases[0].id == 'AppResource'] +ResourceClasses = [ + c + for c in tree.body + if isinstance(c, ast.ClassDef) and c.bases and c.bases[0].id == "AppResource" +] ResourceDocString = {} for c in ResourceClasses: - assert c.body[1].targets[0].id == "type" resource_id = c.body[1].value.value docstring = ast.get_docstring(c) diff --git a/src/app.py b/src/app.py index 26102c723..8466fa604 100644 --- a/src/app.py +++ b/src/app.py @@ -2593,7 +2593,7 @@ def _check_manifest_requirements( yield ( "arch", arch_requirement in ["all", "?"] or arch in arch_requirement, - {"current": arch, "required": ', '.join(arch_requirement)}, + {"current": arch, "required": ", ".join(arch_requirement)}, "app_arch_not_supported", # i18n: app_arch_not_supported ) @@ -2678,9 +2678,7 @@ def _guess_webapp_path_requirement(app_folder: str) -> str: if len(domain_questions) == 1 and len(path_questions) == 1: return "domain_and_path" if len(domain_questions) == 1 and len(path_questions) == 0: - if manifest.get("packaging_format", 0) < 2: - # This is likely to be a full-domain app... # Confirm that this is a full-domain app This should cover most cases @@ -2691,7 +2689,9 @@ def _guess_webapp_path_requirement(app_folder: str) -> str: # Full-domain apps typically declare something like path_url="/" or path=/ # and use ynh_webpath_register or yunohost_app_checkurl inside the install script - install_script_content = read_file(os.path.join(app_folder, "scripts/install")) + install_script_content = read_file( + os.path.join(app_folder, "scripts/install") + ) if re.search( r"\npath(_url)?=[\"']?/[\"']?", install_script_content @@ -2701,7 +2701,9 @@ def _guess_webapp_path_requirement(app_folder: str) -> str: else: # For packaging v2 apps, check if there's a permission with url being a string perm_resource = manifest.get("resources", {}).get("permissions") - if perm_resource is not None and isinstance(perm_resource.get("main", {}).get("url"), str): + if perm_resource is not None and isinstance( + perm_resource.get("main", {}).get("url"), str + ): return "full_domain" return "?" diff --git a/src/backup.py b/src/backup.py index 38d4c080f..0cf73c4ae 100644 --- a/src/backup.py +++ b/src/backup.py @@ -941,7 +941,14 @@ class RestoreManager: # Use a dummy password which is not gonna be saved anywhere # because the next thing to happen should be that a full restore of the LDAP db will happen - tools_postinstall(domain, "tmpadmin", "Tmp Admin", password=random_ascii(70), ignore_dyndns=True, overwrite_root_password=False) + tools_postinstall( + domain, + "tmpadmin", + "Tmp Admin", + password=random_ascii(70), + ignore_dyndns=True, + overwrite_root_password=False, + ) def clean(self): """ @@ -1190,7 +1197,7 @@ class RestoreManager: except Exception as e: raise YunohostError( f"The following critical error happened during restoration: {e}", - raw_msg=True + raw_msg=True, ) finally: self.clean() diff --git a/src/dns.py b/src/dns.py index 2d39aa02e..3a5e654ec 100644 --- a/src/dns.py +++ b/src/dns.py @@ -593,7 +593,9 @@ def _get_registrar_config_section(domain): registrar_list = read_toml(DOMAIN_REGISTRAR_LIST_PATH) registrar_credentials = registrar_list.get(registrar) if registrar_credentials is None: - logger.warning(f"Registrar {registrar} unknown / Should be added to YunoHost's registrar_list.toml by the development team!") + logger.warning( + f"Registrar {registrar} unknown / Should be added to YunoHost's registrar_list.toml by the development team!" + ) registrar_credentials = {} for credential, infos in registrar_credentials.items(): infos["default"] = infos.get("default", "") diff --git a/src/migrations/0026_new_admins_group.py b/src/migrations/0026_new_admins_group.py index 3b2207eb8..43f10a7b6 100644 --- a/src/migrations/0026_new_admins_group.py +++ b/src/migrations/0026_new_admins_group.py @@ -53,12 +53,14 @@ class MyMigration(Migration): if not new_admin_user: for user in all_users: aliases = user_info(user).get("mail-aliases", []) - if any(alias.startswith(f"admin@{main_domain}") for alias in aliases) \ - or any(alias.startswith(f"postmaster@{main_domain}") for alias in aliases): + if any( + alias.startswith(f"admin@{main_domain}") for alias in aliases + ) or any( + alias.startswith(f"postmaster@{main_domain}") for alias in aliases + ): new_admin_user = user break - self.ldap_migration_started = True if new_admin_user: diff --git a/src/utils/config.py b/src/utils/config.py index 5704686c0..6f06ed1fb 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1359,7 +1359,9 @@ class GroupQuestion(Question): super().__init__(question, context) - self.choices = list(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): # i18n: visitors diff --git a/src/utils/resources.py b/src/utils/resources.py index 7a1ebb386..410d3b1a5 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -182,7 +182,10 @@ class AppResource: tmpdir = _make_tmp_workdir_for_app(app=self.app) env_ = _make_environment_for_app_script( - self.app, workdir=tmpdir, action=f"{action}_{self.type}", include_app_settings=True, + self.app, + workdir=tmpdir, + action=f"{action}_{self.type}", + include_app_settings=True, ) env_.update(env) @@ -306,7 +309,7 @@ class PermissionsResource(AppResource): ): raise YunohostError( "URL for the 'main' permission should be '/' for webapps (or undefined/None for non-webapps). Note that / refers to the install url of the app, i.e $domain.tld/$path/", - raw_msg=True + raw_msg=True, ) super().__init__({"permissions": properties}, *args, **kwargs) @@ -474,12 +477,16 @@ class SystemuserAppResource(AppResource): if check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): os.system(f"deluser {self.app} >/dev/null") if check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): - raise YunohostError(f"Failed to delete system user for {self.app}", raw_msg=True) + raise YunohostError( + f"Failed to delete system user for {self.app}", raw_msg=True + ) if check_output(f"getent group {self.app} &>/dev/null || true").strip(): os.system(f"delgroup {self.app} >/dev/null") if check_output(f"getent group {self.app} &>/dev/null || true").strip(): - raise YunohostError(f"Failed to delete system user for {self.app}", raw_msg=True) + raise YunohostError( + f"Failed to delete system user for {self.app}", raw_msg=True + ) # FIXME : better logging and error handling, add stdout/stderr from the deluser/delgroup commands... @@ -748,7 +755,7 @@ class AptDependenciesAppResource(AppResource): ): 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 + raw_msg=True, ) super().__init__(properties, *args, **kwargs) @@ -866,7 +873,7 @@ class PortsResource(AppResource): if self._port_is_used(port_value): raise YunohostValidationError( f"Port {port_value} is already used by another process or app.", - raw_msg=True + raw_msg=True, ) else: while self._port_is_used(port_value): From 97b69e7c695416053a14b4db6effccdb2be17158 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 17 Feb 2023 15:18:36 +0100 Subject: [PATCH 168/319] appsv2: add check about database vs. apt consistency in resource / warn about lack of explicit dependency to mariadb-server --- src/utils/resources.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/utils/resources.py b/src/utils/resources.py index 410d3b1a5..9e9bdac98 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -46,6 +46,27 @@ class AppResourceManager: if "resources" not in self.wanted: self.wanted["resources"] = {} + if self.wanted["resources"]: + self.validate() + + def validate(self): + + resources = self.wanted["resources"] + + if "database" in list(resources.keys()): + if "apt" not in list(resources.keys()): + logger.error(" ! Packagers: having an 'apt' resource is mandatory when using a 'database' resource, to also install postgresql/mysql if needed") + else: + if list(resources.keys()).index("database") < list(resources.keys()).index("apt"): + logger.error(" ! Packagers: the 'apt' resource should be placed before the 'database' resource, to install postgresql/mysql if needed *before* provisioning the database") + + dbtype = resources["database"]["type"] + apt_packages = resources["apt"].get("packages", "").split(", ") + if dbtype == "mysql" and "mariadb-server" not in apt_packages: + logger.error(" ! Packagers : when using a mysql database, you should add mariadb-server in apt dependencies. Even though it's currently installed by default in YunoHost installations, it might not be in the future !") + if dbtype == "postgresql" and "postgresql" not in apt_packages: + logger.error(" ! Packagers : when using a postgresql database, you should add postgresql in apt dependencies.") + def apply( self, rollback_and_raise_exception_if_failure, operation_logger=None, **context ): From 58ac633d801a380c9aa9338574f37849339e84d6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 17 Feb 2023 15:27:28 +0100 Subject: [PATCH 169/319] apps: don't miserably crash when failing to read .md file such as DESCRIPTION.md --- src/app.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/app.py b/src/app.py index 8466fa604..afa0214eb 100644 --- a/src/app.py +++ b/src/app.py @@ -2088,7 +2088,12 @@ def _parse_app_doc_and_notifications(path): if pagename not in doc: doc[pagename] = {} - doc[pagename][lang] = read_file(filepath).strip() + + try: + doc[pagename][lang] = read_file(filepath).strip() + except Exception as e: + logger.error(e) + continue notifications = {} @@ -2102,7 +2107,11 @@ def _parse_app_doc_and_notifications(path): lang = m.groups()[0].strip("_") if m.groups()[0] else "en" if pagename not in notifications[step]: notifications[step][pagename] = {} - notifications[step][pagename][lang] = read_file(filepath).strip() + try: + notifications[step][pagename][lang] = read_file(filepath).strip() + except Exception as e: + logger.error(e) + continue for filepath in glob.glob(os.path.join(path, "doc", f"{step}.d") + "/*.md"): m = re.match( @@ -2114,7 +2123,12 @@ def _parse_app_doc_and_notifications(path): lang = lang.strip("_") if lang else "en" if pagename not in notifications[step]: notifications[step][pagename] = {} - notifications[step][pagename][lang] = read_file(filepath).strip() + + try: + notifications[step][pagename][lang] = read_file(filepath).strip() + except Exception as e: + logger.error(e) + continue return doc, notifications From 475c93d582c3e17cdf4afd8cb3619f8d9f0f2ecf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 17 Feb 2023 16:37:00 +0100 Subject: [PATCH 170/319] postinstall: raise a proper error when trying to use e.g. 'admin' as the first username which will conflict with the admins group mail aliases --- src/tools.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tools.py b/src/tools.py index f5a89a22a..0ff6842ea 100644 --- a/src/tools.py +++ b/src/tools.py @@ -161,7 +161,7 @@ def tools_postinstall( assert_password_is_compatible, ) from yunohost.domain import domain_main_domain - from yunohost.user import user_create + from yunohost.user import user_create, ADMIN_ALIASES import psutil # Do some checks at first @@ -174,6 +174,9 @@ def tools_postinstall( raw_msg=True, ) + if username in ADMIN_ALIASES: + raise YunohostValidationError(f"Unfortunately, {username} cannot be used as a username", raw_msg=True) + # Check there's at least 10 GB on the rootfs... disk_partitions = sorted( psutil.disk_partitions(all=True), key=lambda k: k.mountpoint From d123fd7674a936648f6117bcfd0ec538e775bc62 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 18 Feb 2023 16:08:26 +0100 Subject: [PATCH 171/319] appsv2: fix user provisionion ... Aleks was drunk ... check_output('cmd &>/dev/null') will always return empty string... --- src/utils/resources.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 9e9bdac98..331d10f11 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -469,13 +469,13 @@ class SystemuserAppResource(AppResource): # FIXME : validate that no yunohost user exists with that name? # and/or that no system user exists during install ? - if not check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): + if os.system(f"getent passwd {self.app} &>/dev/null") != 0: # FIXME: improve logging ? os.system wont log stdout / stderr cmd = f"useradd --system --user-group {self.app}" ret = os.system(cmd) assert ret == 0, f"useradd command failed with exit code {ret}" - if not check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): + if os.system(f"getent passwd {self.app} &>/dev/null") != 0: raise YunohostError( f"Failed to create system user for {self.app}", raw_msg=True ) @@ -495,16 +495,16 @@ class SystemuserAppResource(AppResource): os.system(f"usermod -G {','.join(groups)} {self.app}") def deprovision(self, context: Dict = {}): - if check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): + if os.system(f"getent passwd {self.app} &>/dev/null") == 0: os.system(f"deluser {self.app} >/dev/null") - if check_output(f"getent passwd {self.app} &>/dev/null || true").strip(): + if os.system(f"getent passwd {self.app} &>/dev/null") == 0: raise YunohostError( f"Failed to delete system user for {self.app}", raw_msg=True ) - if check_output(f"getent group {self.app} &>/dev/null || true").strip(): + if os.system(f"getent group {self.app} &>/dev/null") == 0: os.system(f"delgroup {self.app} >/dev/null") - if check_output(f"getent group {self.app} &>/dev/null || true").strip(): + if os.system(f"getent group {self.app} &>/dev/null") == 0: raise YunohostError( f"Failed to delete system user for {self.app}", raw_msg=True ) From 8a43b0461410c59b5d171a179744ec1d89f11e12 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 18 Feb 2023 16:33:50 +0100 Subject: [PATCH 172/319] postgresql: fix regenconf hook, the arg format thingy changed a bit at some point ? --- hooks/conf_regen/35-postgresql | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/hooks/conf_regen/35-postgresql b/hooks/conf_regen/35-postgresql index 0da0767cc..1cf8e6d99 100755 --- a/hooks/conf_regen/35-postgresql +++ b/hooks/conf_regen/35-postgresql @@ -47,20 +47,4 @@ do_post_regen() { ynh_systemd_action --service_name=postgresql --action=reload } -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 +do_$1_regen ${@:2} From 18e034df8a44eee88812a1923ef4c20bba249486 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 18 Feb 2023 16:34:54 +0100 Subject: [PATCH 173/319] regenconf: in apt/php stuff, don't try to upgrade-alternatives if the default PHP version ain't available anymore (similar to commit e24ddd29) --- hooks/conf_regen/10-apt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hooks/conf_regen/10-apt b/hooks/conf_regen/10-apt index bdd6d399c..a28a869ab 100755 --- a/hooks/conf_regen/10-apt +++ b/hooks/conf_regen/10-apt @@ -68,7 +68,10 @@ do_post_regen() { fi # Make sure php7.4 is the default version when using php in cli - update-alternatives --set php /usr/bin/php7.4 + if test -e /usr/bin/php$YNH_DEFAULT_PHP_VERSION + then + update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION + fi } do_$1_regen ${@:2} From 771b801eced12442ce7a2b7bb8b87d59c1edb45d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 18 Feb 2023 17:29:37 +0100 Subject: [PATCH 174/319] appsv2: zbfgblg using '&' in os.system calls is interpreted using sh and not bash i guess... --- src/utils/resources.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 331d10f11..a431b205e 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -469,13 +469,13 @@ class SystemuserAppResource(AppResource): # FIXME : validate that no yunohost user exists with that name? # and/or that no system user exists during install ? - if os.system(f"getent passwd {self.app} &>/dev/null") != 0: + if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") != 0: # FIXME: improve logging ? os.system wont log stdout / stderr cmd = f"useradd --system --user-group {self.app}" ret = os.system(cmd) assert ret == 0, f"useradd command failed with exit code {ret}" - if os.system(f"getent passwd {self.app} &>/dev/null") != 0: + if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") != 0: raise YunohostError( f"Failed to create system user for {self.app}", raw_msg=True ) @@ -495,16 +495,16 @@ class SystemuserAppResource(AppResource): os.system(f"usermod -G {','.join(groups)} {self.app}") def deprovision(self, context: Dict = {}): - if os.system(f"getent passwd {self.app} &>/dev/null") == 0: + 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") == 0: + if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: raise YunohostError( f"Failed to delete system user for {self.app}", raw_msg=True ) - if os.system(f"getent group {self.app} &>/dev/null") == 0: + if os.system(f"getent group {self.app} >/dev/null 2>/dev/null") == 0: os.system(f"delgroup {self.app} >/dev/null") - if os.system(f"getent group {self.app} &>/dev/null") == 0: + if os.system(f"getent group {self.app} >/dev/null 2>/dev/null") == 0: raise YunohostError( f"Failed to delete system user for {self.app}", raw_msg=True ) From ce7227c07885f0fc4941607cebce84693c0642d0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 19 Feb 2023 18:10:05 +0100 Subject: [PATCH 175/319] appsv2: add home dir that defaults to /var/www/__APP__ for system user resource --- src/utils/resources.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index a431b205e..d025812dc 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -437,7 +437,8 @@ class SystemuserAppResource(AppResource): ##### Properties: - `allow_ssh`: (default: False) Adds the user to the ssh.app group, allowing SSH connection via this user - - `allow_sftp`: (defalt: False) Adds the user to the sftp.app group, allowing SFTP connection via this user + - `allow_sftp`: (default: False) Adds the user to the sftp.app group, allowing SFTP connection via this user + - `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: - will create the system user if it doesn't exists yet @@ -457,13 +458,13 @@ class SystemuserAppResource(AppResource): type = "system_user" priority = 20 - default_properties: Dict[str, Any] = {"allow_ssh": False, "allow_sftp": False} + default_properties: Dict[str, Any] = {"allow_ssh": False, "allow_sftp": False, "home": "/var/www/__APP__"} - # FIXME : wat do regarding ssl-cert, multimedia - # FIXME : wat do about home dir + # FIXME : wat do regarding ssl-cert, multimedia, and other groups allow_ssh: bool = False allow_sftp: bool = False + home: str = "" def provision_or_update(self, context: Dict = {}): # FIXME : validate that no yunohost user exists with that name? @@ -471,7 +472,7 @@ class SystemuserAppResource(AppResource): if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") != 0: # FIXME: improve logging ? os.system wont log stdout / stderr - cmd = f"useradd --system --user-group {self.app}" + cmd = f"useradd --system --user-group {self.app} --home-dir {self.home} --no-create-home" ret = os.system(cmd) assert ret == 0, f"useradd command failed with exit code {ret}" @@ -492,7 +493,17 @@ class SystemuserAppResource(AppResource): elif "sftp.app" in groups: groups.remove("sftp.app") - os.system(f"usermod -G {','.join(groups)} {self.app}") + raw_user_line_in_etc_passwd = check_output(f"getent passwd {self.app}").strip() + user_infos = raw_user_line_in_etc_passwd.split(":") + current_home = user_infos[5] + if current_home != self.home: + ret = os.system(f"usermod --home {self.home} {self.app} 2>/dev/null") + # Most of the time this won't work because apparently we can't change the home dir while being logged-in -_- + # So we gotta brute force by replacing the line in /etc/passwd T_T + if ret != 0: + user_infos[5] = self.home + new_raw_user_line_in_etc_passwd = ':'.join(user_infos) + os.system(f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd") def deprovision(self, context: Dict = {}): if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: From d3ec5d055f9b748a7060eec1678a1f241f867e4d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 19 Feb 2023 18:10:35 +0100 Subject: [PATCH 176/319] apps: fix edge case when upgrading using a local folder not modified since a while --- src/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app.py b/src/app.py index afa0214eb..73fe0ebe1 100644 --- a/src/app.py +++ b/src/app.py @@ -2392,6 +2392,10 @@ def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: if path[-1] != "/": path = path + "/" cp(path, extracted_app_folder, recursive=True) + # Change the last edit time which is used in _make_tmp_workdir_for_app + # to cleanup old dir ... otherwise it may end up being incorrectly removed + # at the end of the safety-backup-before-upgrade :/ + os.system(f"touch {extracted_app_folder}") else: try: shutil.unpack_archive(path, extracted_app_folder) From 12f1b95a6f2d186a9bbc6cc991563d309578695c Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Wed, 15 Feb 2023 19:18:53 +0000 Subject: [PATCH 177/319] Translated using Weblate (Arabic) Currently translated at 28.4% (215 of 755 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index 07717cef9..62d392263 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -244,5 +244,6 @@ "pattern_fullname": "يجب أن يكون اسماً كاملاً صالحاً (على الأقل 3 حروف)", "migration_0021_main_upgrade": "بداية التحديث الرئيسي…", "migration_0021_patching_sources_list": "تحديث ملف sources.lists…", - "pattern_firstname": "يجب أن يكون اسماً أولياً صالحاً (على الأقل 3 حروف)" + "pattern_firstname": "يجب أن يكون اسماً أولياً صالحاً (على الأقل 3 حروف)", + "yunohost_configured": "تم إعداد YunoHost الآن" } From 6da884418cb3c461d24446979b173568ae291f80 Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Thu, 16 Feb 2023 19:43:24 +0000 Subject: [PATCH 178/319] Translated using Weblate (Basque) Currently translated at 98.2% (742 of 755 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/eu/ --- locales/eu.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/eu.json b/locales/eu.json index 74a54c435..6fb35e5d6 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -738,7 +738,7 @@ "group_no_change": "Ez da ezer aldatu behar '{group}' talderako", "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 {current} bakarrik daude erabilgarri. Aplikazioa ibiliko balitz ere, instalazioak edo bertsio-berritzeak RAM kopuru handia eskatzen du eta zure zerbitzaria izoztu eta huts egin lezake. Hala ere arriskatu nahi baduzu idatzi '{answers}'", + "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}]", "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}", @@ -752,6 +752,6 @@ "domain_config_xmpp_help": "Ohart ongi: XMPP ezaugarri batzuk gaitzeko DNS erregistroak eguneratu eta Lets Encrypt ziurtagiria birsortu beharko dira", "global_settings_setting_dns_exposure": "DNS ezarpenetan eta diagnostikoan kontuan hartzeko IP bertsioak", "global_settings_setting_dns_exposure_help": "Ohart ongi: honek gomendatutako DNS ezarpenei eta diagnostikoari eragiten die soilik. Ez du eraginik sistemaren ezarpenetan.", - "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/#/ipv62.", + "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)" } From 93aeee802996028aafb85dae6876dae0f9f120d2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 19 Feb 2023 18:24:49 +0100 Subject: [PATCH 179/319] Update changelog for 11.1.8 --- debian/changelog | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/debian/changelog b/debian/changelog index d891d1805..2a68f4b7f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,19 @@ +yunohost (11.1.8) stable; urgency=low + + - apps: don't miserably crash when failing to read .md file such as DESCRIPTION.md (58ac633d) + - apps: fix edge case when upgrading using a local folder not modified since a while (d3ec5d05) + - appsv2: fix system user provisioning ... (d123fd76, 771b801e) + - appsv2: add check about database vs. apt consistency in resource / warn about lack of explicit dependency to mariadb-server (97b69e7c) + - appsv2: add home dir that defaults to /var/www/__APP__ for system user resource (ce7227c0) + - postgresql: fix regenconf hook, the arg format thingy changed a bit at some point ? (8a43b046) + - regenconf: in apt/php stuff, don't try to upgrade-alternatives if the default PHP version ain't available anymore (similar to commit e24ddd29) (18e034df) + - postinstall: raise a proper error when trying to use e.g. 'admin' as the first username which will conflict with the admins group mail aliases (475c93d5) + - i18n: Translations updated for Arabic, Basque + + Thanks to all contributors <3 ! (ButterflyOfFire, xabirequejo) + + -- Alexandre Aubin Sun, 19 Feb 2023 18:22:02 +0100 + yunohost (11.1.7) stable; urgency=low - mail: fix complain about unused parameters in postfix: exclude_internal=yes / search_timeout=30 (0da6370d) From e6ae389297bbc3d5f7692db9ae7f5d147b0dc204 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 19 Feb 2023 19:38:48 +0100 Subject: [PATCH 180/319] postgresql: moar regenconf fixes --- hooks/conf_regen/35-postgresql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hooks/conf_regen/35-postgresql b/hooks/conf_regen/35-postgresql index 1cf8e6d99..3a3843d69 100755 --- a/hooks/conf_regen/35-postgresql +++ b/hooks/conf_regen/35-postgresql @@ -20,7 +20,7 @@ do_pre_regen() { } do_post_regen() { - regen_conf_files=$1 + #regen_conf_files=$1 # Make sure postgresql is started and enabled # (N.B. : to check the active state, we check the cluster state because @@ -34,6 +34,8 @@ do_post_regen() { if [ ! -f "$PSQL_ROOT_PWD_FILE" ] || [ -z "$(cat $PSQL_ROOT_PWD_FILE)" ]; then ynh_string_random >$PSQL_ROOT_PWD_FILE fi + + [ ! -e $PSQL_ROOT_PWD_FILE ] || { chown root:postgres $PSQL_ROOT_PWD_FILE; chmod 440 $PSQL_ROOT_PWD_FILE; } sudo --login --user=postgres psql -c"ALTER user postgres WITH PASSWORD '$(cat $PSQL_ROOT_PWD_FILE)'" postgres From 13d50f4f9a1f9070976259e387b9cee20b42ce6f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 19 Feb 2023 19:40:15 +0100 Subject: [PATCH 181/319] postgresql: ugly hack to hide boring warning messages when installing postgresql with apt the first time ... --- src/hook.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/hook.py b/src/hook.py index 42d9d3eac..36fb8f814 100644 --- a/src/hook.py +++ b/src/hook.py @@ -352,6 +352,30 @@ def hook_exec( r"dpkg: warning: while removing .* not empty so not removed", r"apt-key output should not be parsed", r"update-rc.d: ", + r"update-alternatives: ", + # Postgresql boring messages -_- + r"Building PostgreSQL dictionaries from .*", + r'Removing obsolete dictionary files', + r'Creating new PostgreSQL cluster', + r'/usr/lib/postgresql/13/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', + r'The default database encoding has accordingly been set to', + 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'creating subdirectories \.\.\. ok', + r'selecting dynamic .* \.\.\. ', + r'selecting default .* \.\.\. ', + r'creating configuration files \.\.\. ok', + r'running bootstrap script \.\.\. ok', + r'performing post-bootstrap initialization \.\.\. ok', + r'syncing data to disk \.\.\. ok', + r'Success. You can now start the database server using:', + r'pg_ctlcluster \d\d main start', + r'Ver\s*Cluster\s*Port\s*Status\s*Owner\s*Data\s*directory', + r'/var/lib/postgresql/\d\d/main /var/log/postgresql/postgresql-\d\d-main.log', ] return all(not re.search(w, msg) for w in irrelevant_warnings) From 50f86af51a10b25a6c31837b37daf7023c9279ec Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 19 Feb 2023 19:40:28 +0100 Subject: [PATCH 182/319] quality: unused function --- src/tools.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/tools.py b/src/tools.py index 0ff6842ea..c5cf86e4a 100644 --- a/src/tools.py +++ b/src/tools.py @@ -474,13 +474,6 @@ def tools_upgrade(operation_logger, target=None): logger.debug("Running apt command :\n{}".format(dist_upgrade)) - def is_relevant(line): - irrelevants = [ - "service sudo-ldap already provided", - "Reading database ...", - ] - return all(i not in line.rstrip() for i in irrelevants) - callbacks = ( lambda l: logger.info("+ " + l.rstrip() + "\r") if _apt_log_line_is_relevant(l) From 56c4740274e7fa067b700510a0cdf3750a65ff05 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 19 Feb 2023 19:41:45 +0100 Subject: [PATCH 183/319] Update changelog for 11.1.8.1 --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index 2a68f4b7f..03c5926b4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (11.1.8.1) stable; urgency=low + + - postgresql: moar regenconf fixes (e6ae3892) + - postgresql: ugly hack to hide boring warning messages when installing postgresql with apt the first time ... (13d50f4f) + + -- Alexandre Aubin Sun, 19 Feb 2023 19:41:05 +0100 + yunohost (11.1.8) stable; urgency=low - apps: don't miserably crash when failing to read .md file such as DESCRIPTION.md (58ac633d) From 2389884e8531974ec6036ed3aa491ea70d6b49f4 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Sun, 19 Feb 2023 19:10:51 +0000 Subject: [PATCH 184/319] [CI] Format code with Black --- src/tools.py | 4 +++- src/utils/resources.py | 33 ++++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/tools.py b/src/tools.py index 0ff6842ea..cac19c90a 100644 --- a/src/tools.py +++ b/src/tools.py @@ -175,7 +175,9 @@ def tools_postinstall( ) if username in ADMIN_ALIASES: - raise YunohostValidationError(f"Unfortunately, {username} cannot be used as a username", raw_msg=True) + raise YunohostValidationError( + f"Unfortunately, {username} cannot be used as a username", raw_msg=True + ) # Check there's at least 10 GB on the rootfs... disk_partitions = sorted( diff --git a/src/utils/resources.py b/src/utils/resources.py index d025812dc..9367fbde5 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -50,22 +50,31 @@ class AppResourceManager: self.validate() def validate(self): - resources = self.wanted["resources"] if "database" in list(resources.keys()): if "apt" not in list(resources.keys()): - logger.error(" ! Packagers: having an 'apt' resource is mandatory when using a 'database' resource, to also install postgresql/mysql if needed") + logger.error( + " ! Packagers: having an 'apt' resource is mandatory when using a 'database' resource, to also install postgresql/mysql if needed" + ) else: - if list(resources.keys()).index("database") < list(resources.keys()).index("apt"): - logger.error(" ! Packagers: the 'apt' resource should be placed before the 'database' resource, to install postgresql/mysql if needed *before* provisioning the database") + if list(resources.keys()).index("database") < list( + resources.keys() + ).index("apt"): + logger.error( + " ! Packagers: the 'apt' resource should be placed before the 'database' resource, to install postgresql/mysql if needed *before* provisioning the database" + ) dbtype = resources["database"]["type"] apt_packages = resources["apt"].get("packages", "").split(", ") if dbtype == "mysql" and "mariadb-server" not in apt_packages: - logger.error(" ! Packagers : when using a mysql database, you should add mariadb-server in apt dependencies. Even though it's currently installed by default in YunoHost installations, it might not be in the future !") + logger.error( + " ! Packagers : when using a mysql database, you should add mariadb-server in apt dependencies. Even though it's currently installed by default in YunoHost installations, it might not be in the future !" + ) if dbtype == "postgresql" and "postgresql" not in apt_packages: - logger.error(" ! Packagers : when using a postgresql database, you should add postgresql in apt dependencies.") + logger.error( + " ! Packagers : when using a postgresql database, you should add postgresql in apt dependencies." + ) def apply( self, rollback_and_raise_exception_if_failure, operation_logger=None, **context @@ -458,7 +467,11 @@ class SystemuserAppResource(AppResource): type = "system_user" priority = 20 - default_properties: Dict[str, Any] = {"allow_ssh": False, "allow_sftp": False, "home": "/var/www/__APP__"} + default_properties: Dict[str, Any] = { + "allow_ssh": False, + "allow_sftp": False, + "home": "/var/www/__APP__", + } # FIXME : wat do regarding ssl-cert, multimedia, and other groups @@ -502,8 +515,10 @@ class SystemuserAppResource(AppResource): # So we gotta brute force by replacing the line in /etc/passwd T_T if ret != 0: user_infos[5] = self.home - new_raw_user_line_in_etc_passwd = ':'.join(user_infos) - os.system(f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd") + new_raw_user_line_in_etc_passwd = ":".join(user_infos) + os.system( + f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd" + ) def deprovision(self, context: Dict = {}): if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: From 343065eb5d7e68f4115de48b645eea8385b76c43 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 19 Feb 2023 21:38:53 +0100 Subject: [PATCH 185/319] regenconf: fix undefined var in apt regenconf --- hooks/conf_regen/10-apt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hooks/conf_regen/10-apt b/hooks/conf_regen/10-apt index a28a869ab..93ff053b8 100755 --- a/hooks/conf_regen/10-apt +++ b/hooks/conf_regen/10-apt @@ -2,6 +2,8 @@ set -e +readonly YNH_DEFAULT_PHP_VERSION=7.4 + do_pre_regen() { pending_dir=$1 From 16bae924e862406a1b428c491d7e24c7825cb69d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 19 Feb 2023 21:39:36 +0100 Subject: [PATCH 186/319] Update changelog for 11.1.8.2 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 03c5926b4..8051903e8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (11.1.8.2) stable; urgency=low + + - regenconf: fix undefined var in apt regenconf (343065eb) + + -- Alexandre Aubin Sun, 19 Feb 2023 21:38:59 +0100 + yunohost (11.1.8.1) stable; urgency=low - postgresql: moar regenconf fixes (e6ae3892) From 61b5bb02f44b9561967e486060eb8edd8067863b Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Sun, 19 Feb 2023 20:58:12 +0000 Subject: [PATCH 187/319] [CI] Format code with Black --- src/hook.py | 42 +++++++++++++++++++++--------------------- src/tools.py | 4 +++- src/utils/resources.py | 33 ++++++++++++++++++++++++--------- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/hook.py b/src/hook.py index 36fb8f814..dfbcba24f 100644 --- a/src/hook.py +++ b/src/hook.py @@ -355,27 +355,27 @@ def hook_exec( r"update-alternatives: ", # Postgresql boring messages -_- r"Building PostgreSQL dictionaries from .*", - r'Removing obsolete dictionary files', - r'Creating new PostgreSQL cluster', - r'/usr/lib/postgresql/13/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', - r'The default database encoding has accordingly been set to', - 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'creating subdirectories \.\.\. ok', - r'selecting dynamic .* \.\.\. ', - r'selecting default .* \.\.\. ', - r'creating configuration files \.\.\. ok', - r'running bootstrap script \.\.\. ok', - r'performing post-bootstrap initialization \.\.\. ok', - r'syncing data to disk \.\.\. ok', - r'Success. You can now start the database server using:', - r'pg_ctlcluster \d\d main start', - r'Ver\s*Cluster\s*Port\s*Status\s*Owner\s*Data\s*directory', - r'/var/lib/postgresql/\d\d/main /var/log/postgresql/postgresql-\d\d-main.log', + r"Removing obsolete dictionary files", + r"Creating new PostgreSQL cluster", + r"/usr/lib/postgresql/13/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", + r"The default database encoding has accordingly been set to", + 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"creating subdirectories \.\.\. ok", + r"selecting dynamic .* \.\.\. ", + r"selecting default .* \.\.\. ", + r"creating configuration files \.\.\. ok", + r"running bootstrap script \.\.\. ok", + r"performing post-bootstrap initialization \.\.\. ok", + r"syncing data to disk \.\.\. ok", + r"Success. You can now start the database server using:", + r"pg_ctlcluster \d\d main start", + r"Ver\s*Cluster\s*Port\s*Status\s*Owner\s*Data\s*directory", + r"/var/lib/postgresql/\d\d/main /var/log/postgresql/postgresql-\d\d-main.log", ] return all(not re.search(w, msg) for w in irrelevant_warnings) diff --git a/src/tools.py b/src/tools.py index c5cf86e4a..740f92c9d 100644 --- a/src/tools.py +++ b/src/tools.py @@ -175,7 +175,9 @@ def tools_postinstall( ) if username in ADMIN_ALIASES: - raise YunohostValidationError(f"Unfortunately, {username} cannot be used as a username", raw_msg=True) + raise YunohostValidationError( + f"Unfortunately, {username} cannot be used as a username", raw_msg=True + ) # Check there's at least 10 GB on the rootfs... disk_partitions = sorted( diff --git a/src/utils/resources.py b/src/utils/resources.py index d025812dc..9367fbde5 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -50,22 +50,31 @@ class AppResourceManager: self.validate() def validate(self): - resources = self.wanted["resources"] if "database" in list(resources.keys()): if "apt" not in list(resources.keys()): - logger.error(" ! Packagers: having an 'apt' resource is mandatory when using a 'database' resource, to also install postgresql/mysql if needed") + logger.error( + " ! Packagers: having an 'apt' resource is mandatory when using a 'database' resource, to also install postgresql/mysql if needed" + ) else: - if list(resources.keys()).index("database") < list(resources.keys()).index("apt"): - logger.error(" ! Packagers: the 'apt' resource should be placed before the 'database' resource, to install postgresql/mysql if needed *before* provisioning the database") + if list(resources.keys()).index("database") < list( + resources.keys() + ).index("apt"): + logger.error( + " ! Packagers: the 'apt' resource should be placed before the 'database' resource, to install postgresql/mysql if needed *before* provisioning the database" + ) dbtype = resources["database"]["type"] apt_packages = resources["apt"].get("packages", "").split(", ") if dbtype == "mysql" and "mariadb-server" not in apt_packages: - logger.error(" ! Packagers : when using a mysql database, you should add mariadb-server in apt dependencies. Even though it's currently installed by default in YunoHost installations, it might not be in the future !") + logger.error( + " ! Packagers : when using a mysql database, you should add mariadb-server in apt dependencies. Even though it's currently installed by default in YunoHost installations, it might not be in the future !" + ) if dbtype == "postgresql" and "postgresql" not in apt_packages: - logger.error(" ! Packagers : when using a postgresql database, you should add postgresql in apt dependencies.") + logger.error( + " ! Packagers : when using a postgresql database, you should add postgresql in apt dependencies." + ) def apply( self, rollback_and_raise_exception_if_failure, operation_logger=None, **context @@ -458,7 +467,11 @@ class SystemuserAppResource(AppResource): type = "system_user" priority = 20 - default_properties: Dict[str, Any] = {"allow_ssh": False, "allow_sftp": False, "home": "/var/www/__APP__"} + default_properties: Dict[str, Any] = { + "allow_ssh": False, + "allow_sftp": False, + "home": "/var/www/__APP__", + } # FIXME : wat do regarding ssl-cert, multimedia, and other groups @@ -502,8 +515,10 @@ class SystemuserAppResource(AppResource): # So we gotta brute force by replacing the line in /etc/passwd T_T if ret != 0: user_infos[5] = self.home - new_raw_user_line_in_etc_passwd = ':'.join(user_infos) - os.system(f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd") + new_raw_user_line_in_etc_passwd = ":".join(user_infos) + os.system( + f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd" + ) def deprovision(self, context: Dict = {}): if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: From 16aa09174d0641cc64475ae39af03fad87a904b3 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Sun, 19 Feb 2023 23:31:08 +0000 Subject: [PATCH 188/319] [CI] Format code with Black --- src/hook.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/hook.py b/src/hook.py index 36fb8f814..dfbcba24f 100644 --- a/src/hook.py +++ b/src/hook.py @@ -355,27 +355,27 @@ def hook_exec( r"update-alternatives: ", # Postgresql boring messages -_- r"Building PostgreSQL dictionaries from .*", - r'Removing obsolete dictionary files', - r'Creating new PostgreSQL cluster', - r'/usr/lib/postgresql/13/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', - r'The default database encoding has accordingly been set to', - 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'creating subdirectories \.\.\. ok', - r'selecting dynamic .* \.\.\. ', - r'selecting default .* \.\.\. ', - r'creating configuration files \.\.\. ok', - r'running bootstrap script \.\.\. ok', - r'performing post-bootstrap initialization \.\.\. ok', - r'syncing data to disk \.\.\. ok', - r'Success. You can now start the database server using:', - r'pg_ctlcluster \d\d main start', - r'Ver\s*Cluster\s*Port\s*Status\s*Owner\s*Data\s*directory', - r'/var/lib/postgresql/\d\d/main /var/log/postgresql/postgresql-\d\d-main.log', + r"Removing obsolete dictionary files", + r"Creating new PostgreSQL cluster", + r"/usr/lib/postgresql/13/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", + r"The default database encoding has accordingly been set to", + 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"creating subdirectories \.\.\. ok", + r"selecting dynamic .* \.\.\. ", + r"selecting default .* \.\.\. ", + r"creating configuration files \.\.\. ok", + r"running bootstrap script \.\.\. ok", + r"performing post-bootstrap initialization \.\.\. ok", + r"syncing data to disk \.\.\. ok", + r"Success. You can now start the database server using:", + r"pg_ctlcluster \d\d main start", + r"Ver\s*Cluster\s*Port\s*Status\s*Owner\s*Data\s*directory", + r"/var/lib/postgresql/\d\d/main /var/log/postgresql/postgresql-\d\d-main.log", ] return all(not re.search(w, msg) for w in irrelevant_warnings) From 848adf89c8fdd3ac16bbb27990b0e12577cc3e8d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 20 Feb 2023 15:28:10 +0100 Subject: [PATCH 189/319] log: Previous trick about getting rid of setting didnt work, forgot to use metadata instead of self.metadata --- src/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/log.py b/src/log.py index e7ea18857..5ab918e76 100644 --- a/src/log.py +++ b/src/log.py @@ -605,7 +605,7 @@ class OperationLogger: k: v for k, v in metadata["env"].items() if k == k.upper() } - dump = yaml.safe_dump(self.metadata, default_flow_style=False) + dump = yaml.safe_dump(metadata, default_flow_style=False) for data in self.data_to_redact: # N.B. : we need quotes here, otherwise yaml isn't happy about loading the yml later dump = dump.replace(data, "'**********'") From 290d627fafd1204706ee590950d6786b09129c8d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 20 Feb 2023 15:39:50 +0100 Subject: [PATCH 190/319] ux: Moar boring postgresql messages displayed as warning --- src/hook.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hook.py b/src/hook.py index dfbcba24f..7f4cc28d4 100644 --- a/src/hook.py +++ b/src/hook.py @@ -354,6 +354,7 @@ def hook_exec( r"update-rc.d: ", r"update-alternatives: ", # Postgresql boring messages -_- + r"Adding user postgres to group ssl-cert", r"Building PostgreSQL dictionaries from .*", r"Removing obsolete dictionary files", r"Creating new PostgreSQL cluster", From 890b8e808292106148149eed47786adaed0e512b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 20 Feb 2023 17:50:11 +0100 Subject: [PATCH 191/319] Semantic --- src/app.py | 6 +++--- src/utils/resources.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app.py b/src/app.py index 73fe0ebe1..4697e37a0 100644 --- a/src/app.py +++ b/src/app.py @@ -713,7 +713,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False app_instance_name, workdir=extracted_app_folder, action="upgrade", - include_app_settings=True, + force_include_app_settings=True, ) env_dict.update(env_dict_more) @@ -2798,7 +2798,7 @@ def _make_environment_for_app_script( args_prefix="APP_ARG_", workdir=None, action=None, - include_app_settings=False, + force_include_app_settings=False, ): app_setting_path = os.path.join(APPS_SETTING_PATH, app) @@ -2825,7 +2825,7 @@ def _make_environment_for_app_script( env_dict[f"YNH_{args_prefix}{arg_name_upper}"] = str(arg_value) # If packaging format v2, load all settings - if manifest["packaging_format"] >= 2 or include_app_settings: + if manifest["packaging_format"] >= 2 or force_include_app_settings: env_dict["app"] = app for setting_name, setting_value in _get_app_settings(app).items(): # Ignore special internal settings like checksum__ diff --git a/src/utils/resources.py b/src/utils/resources.py index 9367fbde5..3eb99c55b 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -215,7 +215,7 @@ class AppResource: self.app, workdir=tmpdir, action=f"{action}_{self.type}", - include_app_settings=True, + force_include_app_settings=True, ) env_.update(env) From 2b70ccbf40c6143fb1bfdd7d7414864a897d9542 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 20 Feb 2023 17:51:50 +0100 Subject: [PATCH 192/319] apps: simplify the redaction of change_url scripts by adding a new ynh_change_url_nginx_config helper + predefining new/old/change domain/path variables --- helpers/nginx | 34 ++++++++++++++++++++++ locales/en.json | 4 ++- src/app.py | 76 ++++++++++++++++++++++++++++++++++++------------- 3 files changed, 94 insertions(+), 20 deletions(-) diff --git a/helpers/nginx b/helpers/nginx index 9512f8d23..bb0fe0577 100644 --- a/helpers/nginx +++ b/helpers/nginx @@ -42,3 +42,37 @@ ynh_remove_nginx_config() { ynh_secure_remove --file="/etc/nginx/conf.d/$domain.d/$app.conf" ynh_systemd_action --service_name=nginx --action=reload } + + +# Move / regen the nginx config in a change url context +# +# usage: ynh_change_url_nginx_config +# +# Requires YunoHost version 11.1.9 or higher. +ynh_change_url_nginx_config() { + local old_nginx_conf_path=/etc/nginx/conf.d/$old_domain.d/$app.conf + local new_nginx_conf_path=/etc/nginx/conf.d/$new_domain.d/$app.conf + + # Change the path in the NGINX config file + if [ $change_path -eq 1 ] + then + # Make a backup of the original NGINX config file if modified + ynh_backup_if_checksum_is_different --file="$old_nginx_conf_path" + # Set global variables for NGINX helper + domain="$old_domain" + path="$new_path" + path_url="$new_path" + # Create a dedicated NGINX config + ynh_add_nginx_config + fi + + # Change the domain for NGINX + if [ $change_domain -eq 1 ] + then + ynh_delete_file_checksum --file="$old_nginx_conf_path" + mv "$old_nginx_conf_path" "$new_nginx_conf_path" + ynh_store_file_checksum --file="$new_nginx_conf_path" + fi + ynh_systemd_action --service_name=nginx --action=reload +} + diff --git a/locales/en.json b/locales/en.json index 75b4f203a..7cc1b96b6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -18,8 +18,11 @@ "app_argument_invalid": "Pick a valid value for the argument '{name}': {error}", "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reasons", "app_argument_required": "Argument '{name}' is required", + "app_change_url_failed": "Could not change the url for {app}: {error}", "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain}{path}'), nothing to do.", "app_change_url_no_script": "The app '{app_name}' doesn't support URL modification yet. Maybe you should upgrade it.", + "app_change_url_require_full_domain": "{app} cannot be moved to this new URL because it requires a full domain (i.e. with path = /)", + "app_change_url_script_failed": "An error occured inside the change url script", "app_change_url_success": "{app} URL is now {domain}{path}", "app_config_unable_to_apply": "Failed to apply config panel values.", "app_config_unable_to_read": "Failed to read config panel values.", @@ -513,7 +516,6 @@ "log_permission_url": "Update URL related to permission '{}'", "log_regen_conf": "Regenerate system configurations '{}'", "log_remove_on_failed_install": "Remove '{}' after a failed installation", - "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", "log_resource_snippet": "Provisioning/deprovisioning/updating a resource", "log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain", "log_settings_reset": "Reset setting", diff --git a/src/app.py b/src/app.py index 4697e37a0..ece3c71b6 100644 --- a/src/app.py +++ b/src/app.py @@ -411,7 +411,7 @@ def app_change_url(operation_logger, app, domain, path): path -- New path at which the application will be move """ - from yunohost.hook import hook_exec, hook_callback + from yunohost.hook import hook_exec_with_script_debug_if_failure, hook_callback from yunohost.service import service_reload_or_restart installed = _is_installed(app) @@ -445,6 +445,10 @@ def app_change_url(operation_logger, app, domain, path): _validate_webpath_requirement( {"domain": domain, "path": path}, path_requirement, ignore_app=app ) + if path_requirement == "full_domain" and path != "/": + raise YunohostValidationError( + "app_change_url_require_full_domain", app=app + ) tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) @@ -452,43 +456,77 @@ def app_change_url(operation_logger, app, domain, path): env_dict = _make_environment_for_app_script( app, workdir=tmp_workdir_for_app, action="change_url" ) + env_dict["YNH_APP_OLD_DOMAIN"] = old_domain env_dict["YNH_APP_OLD_PATH"] = old_path env_dict["YNH_APP_NEW_DOMAIN"] = domain env_dict["YNH_APP_NEW_PATH"] = path + env_dict["old_domain"] = old_domain + env_dict["old_path"] = old_path + env_dict["new_domain"] = domain + env_dict["new_path"] = path + env_dict["change_path"] = "1" if old_path != path else "0" + env_dict["change_domain"] = "1" if old_domain != domain else "0" + if domain != old_domain: operation_logger.related_to.append(("domain", old_domain)) operation_logger.extra.update({"env": env_dict}) operation_logger.start() + old_nginx_conf_path = f"/etc/nginx/conf.d/{old_domain}.d/{app}.conf" + new_nginx_conf_path = f"/etc/nginx/conf.d/{domain}.d/{app}.conf" + old_nginx_conf_backup = None + if not os.path.exists(old_nginx_conf_path): + logger.warning(f"Current nginx config file {old_nginx_conf_path} doesn't seem to exist ... wtf ?") + else: + old_nginx_conf_backup = read_file(old_nginx_conf_path) + change_url_script = os.path.join(tmp_workdir_for_app, "scripts/change_url") # Execute App change_url script - ret = hook_exec(change_url_script, env=env_dict)[0] - if ret != 0: - msg = f"Failed to change '{app}' url." - logger.error(msg) - operation_logger.error(msg) + change_url_failed = True + try: + ( + change_url_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( + change_url_script, + env=env_dict, + operation_logger=operation_logger, + error_message_if_script_failed=m18n.n("app_change_url_script_failed"), + error_message_if_failed=lambda e: m18n.n( + "app_change_url_failed", app=app, error=e + ), + ) + finally: - # restore values modified by app_checkurl - # see begining of the function - app_setting(app, "domain", value=old_domain) - app_setting(app, "path", value=old_path) - return - shutil.rmtree(tmp_workdir_for_app) + shutil.rmtree(tmp_workdir_for_app) - # this should idealy be done in the change_url script but let's avoid common mistakes - app_setting(app, "domain", value=domain) - app_setting(app, "path", value=path) + if change_url_failed: + logger.warning("Restoring initial nginx config file") + if old_nginx_conf_path != new_nginx_conf_path and os.path.exists(new_nginx_conf_path): + rm(new_nginx_conf_path, force=True) + write_to_file(old_nginx_conf_path, old_nginx_conf_backup) + service_reload_or_restart("nginx") - app_ssowatconf() + # restore values modified by app_checkurl + # see begining of the function + app_setting(app, "domain", value=old_domain) + app_setting(app, "path", value=old_path) + raise YunohostError(failure_message_with_debug_instructions, raw_msg=True) + else: + # make sure the domain/path setting are propagated + app_setting(app, "domain", value=domain) + app_setting(app, "path", value=path) - service_reload_or_restart("nginx") + app_ssowatconf() - logger.success(m18n.n("app_change_url_success", app=app, domain=domain, path=path)) + service_reload_or_restart("nginx") - hook_callback("post_app_change_url", env=env_dict) + logger.success(m18n.n("app_change_url_success", app=app, domain=domain, path=path)) + + hook_callback("post_app_change_url", env=env_dict) def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False): From 63f0f08421254a47bdab5de266746d2aa4f35c4f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 20 Feb 2023 18:03:32 +0100 Subject: [PATCH 193/319] appsv2: revert commit that adds a bunch of warning about apt/database consistency, it's more relevant to have them in package linter instead --- src/utils/resources.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 3eb99c55b..885ee8690 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -46,36 +46,6 @@ class AppResourceManager: if "resources" not in self.wanted: self.wanted["resources"] = {} - if self.wanted["resources"]: - self.validate() - - def validate(self): - resources = self.wanted["resources"] - - if "database" in list(resources.keys()): - if "apt" not in list(resources.keys()): - logger.error( - " ! Packagers: having an 'apt' resource is mandatory when using a 'database' resource, to also install postgresql/mysql if needed" - ) - else: - if list(resources.keys()).index("database") < list( - resources.keys() - ).index("apt"): - logger.error( - " ! Packagers: the 'apt' resource should be placed before the 'database' resource, to install postgresql/mysql if needed *before* provisioning the database" - ) - - dbtype = resources["database"]["type"] - apt_packages = resources["apt"].get("packages", "").split(", ") - if dbtype == "mysql" and "mariadb-server" not in apt_packages: - logger.error( - " ! Packagers : when using a mysql database, you should add mariadb-server in apt dependencies. Even though it's currently installed by default in YunoHost installations, it might not be in the future !" - ) - if dbtype == "postgresql" and "postgresql" not in apt_packages: - logger.error( - " ! Packagers : when using a postgresql database, you should add postgresql in apt dependencies." - ) - def apply( self, rollback_and_raise_exception_if_failure, operation_logger=None, **context ): From ec4c2684f7ae00f4a3cb2da6fed5598a0da5807f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 20 Feb 2023 20:26:50 +0100 Subject: [PATCH 194/319] appsv2: zblerg I inadvertendly removed the line that update the user group x_x --- src/utils/resources.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/resources.py b/src/utils/resources.py index 885ee8690..2de9cf00e 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -464,6 +464,7 @@ class SystemuserAppResource(AppResource): f"Failed to create system user for {self.app}", raw_msg=True ) + # Update groups groups = set(check_output(f"groups {self.app}").strip().split()[2:]) if self.allow_ssh: @@ -476,6 +477,9 @@ class SystemuserAppResource(AppResource): elif "sftp.app" in groups: groups.remove("sftp.app") + os.system(f"usermod -G {','.join(groups)} {self.app}") + + # Update home dir raw_user_line_in_etc_passwd = check_output(f"getent passwd {self.app}").strip() user_infos = raw_user_line_in_etc_passwd.split(":") current_home = user_infos[5] From f436b890d6c339ad92784a0191329f0d2def4564 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 20 Feb 2023 20:33:54 +0100 Subject: [PATCH 195/319] Update changelog for 11.1.9 --- debian/changelog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/debian/changelog b/debian/changelog index 8051903e8..1ad620d11 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +yunohost (11.1.9) stable; urgency=low + + - apps: simplify the redaction of change_url scripts by adding a new ynh_change_url_nginx_config helper + predefining new/old/change domain/path variables (2b70ccbf) + - appsv2: revert commit that adds a bunch of warning about apt/database consistency, it's more relevant to have them in package linter instead (63f0f084) + - appsv2: fix system user group update, broke in commit from earlier (ec4c2684) + - log: Previous trick about getting rid of setting didnt work, forgot to use metadata instead of self.metadata (848adf89) + - ux: Moar boring postgresql messages displayed as warning (290d627f) + + Thanks to all contributors <3 ! (Bram) + + -- Alexandre Aubin Mon, 20 Feb 2023 20:32:28 +0100 + yunohost (11.1.8.2) stable; urgency=low - regenconf: fix undefined var in apt regenconf (343065eb) From 95b80b056f1e091381bf128cfd43dd901424628d Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Mon, 20 Feb 2023 19:46:58 +0000 Subject: [PATCH 196/319] [CI] Format code with Black --- src/app.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/app.py b/src/app.py index ece3c71b6..2d8bb9bbd 100644 --- a/src/app.py +++ b/src/app.py @@ -446,9 +446,7 @@ def app_change_url(operation_logger, app, domain, path): {"domain": domain, "path": path}, path_requirement, ignore_app=app ) if path_requirement == "full_domain" and path != "/": - raise YunohostValidationError( - "app_change_url_require_full_domain", app=app - ) + raise YunohostValidationError("app_change_url_require_full_domain", app=app) tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) @@ -478,7 +476,9 @@ def app_change_url(operation_logger, app, domain, path): new_nginx_conf_path = f"/etc/nginx/conf.d/{domain}.d/{app}.conf" old_nginx_conf_backup = None if not os.path.exists(old_nginx_conf_path): - logger.warning(f"Current nginx config file {old_nginx_conf_path} doesn't seem to exist ... wtf ?") + logger.warning( + f"Current nginx config file {old_nginx_conf_path} doesn't seem to exist ... wtf ?" + ) else: old_nginx_conf_backup = read_file(old_nginx_conf_path) @@ -500,12 +500,13 @@ def app_change_url(operation_logger, app, domain, path): ), ) finally: - shutil.rmtree(tmp_workdir_for_app) if change_url_failed: logger.warning("Restoring initial nginx config file") - if old_nginx_conf_path != new_nginx_conf_path and os.path.exists(new_nginx_conf_path): + if old_nginx_conf_path != new_nginx_conf_path and os.path.exists( + new_nginx_conf_path + ): rm(new_nginx_conf_path, force=True) write_to_file(old_nginx_conf_path, old_nginx_conf_backup) service_reload_or_restart("nginx") @@ -524,7 +525,9 @@ def app_change_url(operation_logger, app, domain, path): service_reload_or_restart("nginx") - logger.success(m18n.n("app_change_url_success", app=app, domain=domain, path=path)) + logger.success( + m18n.n("app_change_url_success", app=app, domain=domain, path=path) + ) hook_callback("post_app_change_url", env=env_dict) From e1d62a1910986fac61ab0a5d7bc98887b05524ab Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 20 Feb 2023 20:50:53 +0100 Subject: [PATCH 197/319] apps: Fix edge case in change_url where old_nginx_conf_backup could be None --- src/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app.py b/src/app.py index 2d8bb9bbd..c2d4d0a89 100644 --- a/src/app.py +++ b/src/app.py @@ -508,8 +508,9 @@ def app_change_url(operation_logger, app, domain, path): new_nginx_conf_path ): rm(new_nginx_conf_path, force=True) - write_to_file(old_nginx_conf_path, old_nginx_conf_backup) - service_reload_or_restart("nginx") + if old_nginx_conf_backup: + write_to_file(old_nginx_conf_path, old_nginx_conf_backup) + service_reload_or_restart("nginx") # restore values modified by app_checkurl # see begining of the function From b887545c3e2326da74834ec6e61398f9c9b053ae Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 02:51:41 +0100 Subject: [PATCH 198/319] ci: attempt to fix the "coverage: not set up" thingy --- .gitlab/ci/test.gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index 37edbda04..a89697b44 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -46,6 +46,7 @@ full-tests: artifacts: true - job: build-moulinette artifacts: true + coverage: 'TOTAL.*\s+(\d+%)' artifacts: reports: junit: report.xml From df6a2a2cd23ac36cf434e1660fefd085affdb974 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 13:03:51 +0100 Subject: [PATCH 199/319] apps: add 'YNH_DEBIAN_VERSION' variable in apps contexts --- src/app.py | 2 ++ src/utils/system.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/app.py b/src/app.py index c2d4d0a89..67e0617dd 100644 --- a/src/app.py +++ b/src/app.py @@ -62,6 +62,7 @@ from yunohost.utils.system import ( dpkg_is_broken, get_ynh_package_version, system_arch, + debian_version, human_to_binary, binary_to_human, ram_available, @@ -2854,6 +2855,7 @@ def _make_environment_for_app_script( "YNH_APP_MANIFEST_VERSION": manifest.get("version", "?"), "YNH_APP_PACKAGING_FORMAT": str(manifest["packaging_format"]), "YNH_ARCH": system_arch(), + "YNH_DEBIAN_VERSION": debian_version(), } if workdir: diff --git a/src/utils/system.py b/src/utils/system.py index 2538f74fb..a169bd62c 100644 --- a/src/utils/system.py +++ b/src/utils/system.py @@ -28,6 +28,10 @@ logger = logging.getLogger("yunohost.utils.packages") YUNOHOST_PACKAGES = ["yunohost", "yunohost-admin", "moulinette", "ssowat"] +def debian_version(): + return check_output('grep "^VERSION_CODENAME=" /etc/os-release | cut -d= -f2') + + def system_arch(): return check_output("dpkg --print-architecture") From 4dfff201404da3c933c3b5c69a8e359ecc4613b6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 14:51:51 +0100 Subject: [PATCH 200/319] appsv2: add support for a packages_from_raw_bash option in apt where one can add a multiline bash snippet to echo packages --- src/utils/resources.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 2de9cf00e..0b9cb9968 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -20,6 +20,7 @@ import os import copy import shutil import random +import tempfile from typing import Dict, Any, List from moulinette import m18n @@ -172,7 +173,30 @@ class AppResource: app_setting(self.app, key, delete=True) - def _run_script(self, action, script, env={}, user="root"): + def check_output_bash_snippet(self, snippet, env={}): + from yunohost.app import ( + _make_environment_for_app_script, + ) + + env_ = _make_environment_for_app_script( + self.app, + force_include_app_settings=True, + ) + env_.update(env) + + with tempfile.NamedTemporaryFile(prefix="ynh_") as fp: + fp.write(snippet.encode()) + fp.seek(0) + with tempfile.TemporaryFile() as stderr: + out = check_output(f"bash {fp.name}", env=env_, stderr=stderr) + + stderr.seek(0) + err = stderr.read().decode() + + return out, err + + + def _run_script(self, action, script, env={}): from yunohost.app import ( _make_tmp_workdir_for_app, _make_environment_for_app_script, @@ -746,6 +770,7 @@ class AptDependenciesAppResource(AppResource): ##### Properties: - `packages`: Comma-separated 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 ##### Provision/Update: @@ -767,6 +792,7 @@ class AptDependenciesAppResource(AppResource): default_properties: Dict[str, Any] = {"packages": [], "extras": {}} packages: List = [] + packages_from_raw_bash: str = "" extras: Dict[str, Dict[str, str]] = {} def __init__(self, properties: Dict[str, Any], *args, **kwargs): @@ -781,6 +807,14 @@ class AptDependenciesAppResource(AppResource): super().__init__(properties, *args, **kwargs) + if self.packages_from_raw_bash: + out, err = self.check_output_bash_snippet(self.packages_from_raw_bash) + if err: + logger.error("Error while running apt resource packages_from_raw_bash snippet:") + logger.error(err) + self.packages += ", " + out.replace("\n", ", ") + + def provision_or_update(self, context: Dict = {}): script = [f"ynh_install_app_dependencies {self.packages}"] for repo, values in self.extras.items(): From 888593ad223df6bac996369b06c625c8cc70c7e0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 14:57:10 +0100 Subject: [PATCH 201/319] appsv2: fix resource provisioning scripts picking up already-closed operation logger, resulting in confusing debugging output --- src/utils/resources.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 0b9cb9968..72475fae4 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -225,9 +225,10 @@ ynh_abort_if_errors from yunohost.log import OperationLogger - if OperationLogger._instances: - # FIXME ? : this is an ugly hack :( - operation_logger = OperationLogger._instances[-1] + # FIXME ? : this is an ugly hack :( + active_operation_loggers = [o for o in OperationLogger._instances if o.ended_at is None] + if active_operation_loggers: + operation_logger = active_operation_loggers[-1] else: operation_logger = OperationLogger( "resource_snippet", [("app", self.app)], env=env_ From d725b4542878a5dac8c974983d1865fdeee7def7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 15:22:50 +0100 Subject: [PATCH 202/319] appsv2: fix reload_only_if_change option not working as expected, resulting in incorrect 'Firewall reloaded' messages --- src/firewall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/firewall.py b/src/firewall.py index 073e48c88..85e89c9c2 100644 --- a/src/firewall.py +++ b/src/firewall.py @@ -101,7 +101,7 @@ def firewall_allow( # Update and reload firewall _update_firewall_file(firewall) - if not no_reload or (reload_only_if_change and changed): + if (not reload_only_if_change and not no_reload) or (reload_only_if_change and changed): return firewall_reload() @@ -180,7 +180,7 @@ def firewall_disallow( # Update and reload firewall _update_firewall_file(firewall) - if not no_reload or (reload_only_if_change and changed): + if (not reload_only_if_change and not no_reload) or (reload_only_if_change and changed): return firewall_reload() From 1dc8b75315aba0a1a1ba4b794bd10855cba9bc75 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 17:08:12 +0100 Subject: [PATCH 203/319] appsv2: fix check that postgresql db exists... --- 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 72475fae4..53c13d1e3 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -1023,7 +1023,7 @@ class DatabaseAppResource(AppResource): elif self.dbtype == "postgresql": return ( os.system( - f"sudo --login --user=postgres psql -c '' '{db_name}' >/dev/null 2>/dev/null" + f"sudo --login --user=postgres psql '{db_name}' -c ';' >/dev/null 2>/dev/null" ) == 0 ) From 4fd10b5a1d0027ff43f0e48d39b1cb05474c4714 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 17:13:53 +0100 Subject: [PATCH 204/319] ci: hmf try to understand what that 're2 syntax' gitlab is talking about is --- .gitlab/ci/test.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index a89697b44..4f69458fb 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -46,7 +46,7 @@ full-tests: artifacts: true - job: build-moulinette artifacts: true - coverage: 'TOTAL.*\s+(\d+%)' + coverage: '/TOTAL.*\s+(\d+%)/' artifacts: reports: junit: report.xml From 232d38f22187d5e0a6635f047c6c9eb161d36751 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 18:58:36 +0100 Subject: [PATCH 205/319] Update changelog for 11.1.10 --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 1ad620d11..530d3edc0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (11.1.10) stable; urgency=low + + - apps: add 'YNH_DEBIAN_VERSION' variable in apps contexts (df6a2a2c) + - appsv2: add support for a packages_from_raw_bash option in apt where one can add a multiline bash snippet to echo packages (4dfff201) + - appsv2: fix resource provisioning scripts picking up already-closed operation logger, resulting in confusing debugging output (888593ad) + - appsv2: fix reload_only_if_change option not working as expected, resulting in incorrect 'Firewall reloaded' messages (d725b454) + - appsv2: fix check that postgresql db exists... (1dc8b753) + + -- Alexandre Aubin Tue, 21 Feb 2023 18:57:33 +0100 + yunohost (11.1.9) stable; urgency=low - apps: simplify the redaction of change_url scripts by adding a new ynh_change_url_nginx_config helper + predefining new/old/change domain/path variables (2b70ccbf) From 127c241c9a42d930f5bbc4f646a5016942966738 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 19:23:35 +0100 Subject: [PATCH 206/319] swag: update README badges --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5d37b2af1..07ee04de0 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@
![Version](https://img.shields.io/github/v/tag/yunohost/yunohost?label=version&sort=semver) -[![Build status](https://shields.io/gitlab/pipeline/yunohost/yunohost/dev)](https://gitlab.com/yunohost/yunohost/-/pipelines) -![Test coverage](https://img.shields.io/gitlab/coverage/yunohost/yunohost/dev) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/YunoHost/yunohost.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/YunoHost/yunohost/context:python) -[![GitHub license](https://img.shields.io/github/license/YunoHost/yunohost)](https://github.com/YunoHost/yunohost/blob/dev/LICENSE) +[![Pipeline status](https://gitlab.com/yunohost/yunohost/badges/dev/pipeline.svg)](https://gitlab.com/yunohost/yunohost/-/pipelines) +![Test coverage](https://gitlab.com/yunohost/yunohost/badges/dev/coverage.svg) +[![Project license](https://img.shields.io/gitlab/license/yunohost/yunohost)](https://github.com/YunoHost/yunohost/blob/dev/LICENSE) +[![CodeQL](https://github.com/yunohost/yunohost/workflows/CodeQL/badge.svg)](https://github.com/YunoHost/yunohost/security/code-scanning) [![Mastodon Follow](https://img.shields.io/mastodon/follow/28084)](https://mastodon.social/@yunohost)
From 872432973854707e5630c32db8dc00b313f9e663 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 19:34:22 +0100 Subject: [PATCH 207/319] Remove .lgtm.yml, the service doesnt exists anymore :| --- .lgtm.yml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .lgtm.yml diff --git a/.lgtm.yml b/.lgtm.yml deleted file mode 100644 index 8fd57e49e..000000000 --- a/.lgtm.yml +++ /dev/null @@ -1,4 +0,0 @@ -extraction: - python: - python_setup: - version: "3" \ No newline at end of file From 90b8e78effb7315b779ca0610231438f4e84fee2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Feb 2023 19:44:51 +0100 Subject: [PATCH 208/319] ci: zblerg, try to fix the coverage thingy computing coverage on test and vendor files x_x --- .coveragerc | 2 +- .gitlab/ci/test.gitlab-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index fe22c8381..bc952e665 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,2 @@ [report] -omit=src/tests/*,src/vendor/*,/usr/lib/moulinette/yunohost/* +omit=src/tests/*,src/vendor/*,/usr/lib/moulinette/yunohost/*,/usr/lib/python3/dist-packages/yunohost/tests/*,/usr/lib/python3/dist-packages/yunohost/vendor/* diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index 4f69458fb..b0ffd3db5 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -36,7 +36,7 @@ full-tests: - *install_debs - yunohost tools postinstall -d domain.tld -u syssa -F 'Syssa Mine' -p the_password --ignore-dyndns --force-diskspace script: - - python3 -m pytest --cov=yunohost tests/ src/tests/ src/diagnosers/ --junitxml=report.xml + - python3 -m pytest --cov=yunohost tests/ src/tests/ --junitxml=report.xml - cd tests - bash test_helpers.sh needs: From aa50526ccdc3b5ffc3ef10e641015185fe32886d Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Tue, 21 Feb 2023 19:49:52 +0000 Subject: [PATCH 209/319] [CI] Format code with Black --- src/firewall.py | 8 ++++++-- src/utils/resources.py | 10 ++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/firewall.py b/src/firewall.py index 85e89c9c2..310d263c6 100644 --- a/src/firewall.py +++ b/src/firewall.py @@ -101,7 +101,9 @@ def firewall_allow( # Update and reload firewall _update_firewall_file(firewall) - if (not reload_only_if_change and not no_reload) or (reload_only_if_change and changed): + if (not reload_only_if_change and not no_reload) or ( + reload_only_if_change and changed + ): return firewall_reload() @@ -180,7 +182,9 @@ def firewall_disallow( # Update and reload firewall _update_firewall_file(firewall) - if (not reload_only_if_change and not no_reload) or (reload_only_if_change and changed): + if (not reload_only_if_change and not no_reload) or ( + reload_only_if_change and changed + ): return firewall_reload() diff --git a/src/utils/resources.py b/src/utils/resources.py index 53c13d1e3..fafbfb45b 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -195,7 +195,6 @@ class AppResource: return out, err - def _run_script(self, action, script, env={}): from yunohost.app import ( _make_tmp_workdir_for_app, @@ -226,7 +225,9 @@ ynh_abort_if_errors from yunohost.log import OperationLogger # FIXME ? : this is an ugly hack :( - active_operation_loggers = [o for o in OperationLogger._instances if o.ended_at is None] + active_operation_loggers = [ + o for o in OperationLogger._instances if o.ended_at is None + ] if active_operation_loggers: operation_logger = active_operation_loggers[-1] else: @@ -811,11 +812,12 @@ class AptDependenciesAppResource(AppResource): if self.packages_from_raw_bash: out, err = self.check_output_bash_snippet(self.packages_from_raw_bash) if err: - logger.error("Error while running apt resource packages_from_raw_bash snippet:") + logger.error( + "Error while running apt resource packages_from_raw_bash snippet:" + ) logger.error(err) self.packages += ", " + out.replace("\n", ", ") - def provision_or_update(self, context: Dict = {}): script = [f"ynh_install_app_dependencies {self.packages}"] for repo, values in self.extras.items(): From bab27014d9e93f35fdb0fd645b9f8023d1e13f15 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Feb 2023 14:53:42 +0100 Subject: [PATCH 210/319] apps: when creating the app's bash env for script, make sure to use the manifest from the workdir instead of app setting dir, which is important for consistency during edge case when upgrade from v1 to v2 fails --- src/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 67e0617dd..17ebe96ca 100644 --- a/src/app.py +++ b/src/app.py @@ -2845,7 +2845,8 @@ def _make_environment_for_app_script( ): app_setting_path = os.path.join(APPS_SETTING_PATH, app) - manifest = _get_manifest_of_app(app_setting_path) + manifest = _get_manifest_of_app(workdir if workdir else app_setting_path) + app_id, app_instance_nb = _parse_app_instance_name(app) env_dict = { From bef4809f9414ffaec4c3aae9136a0081e26c597e Mon Sep 17 00:00:00 2001 From: Eric Geldmacher Date: Thu, 23 Feb 2023 08:48:22 -0600 Subject: [PATCH 211/319] Pass errors='replace' to open command This is to handle decoding errors described in YunoHost/issues#2156 --- src/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service.py b/src/service.py index a3bcc5561..c4835263b 100644 --- a/src/service.py +++ b/src/service.py @@ -775,7 +775,7 @@ def _tail(file, n): f = gzip.open(file) lines = f.read().splitlines() else: - f = open(file) + f = open(file, errors='replace') pos = 1 lines = [] while len(lines) < to_read and pos > 0: From f91f87a1bee9bd951af6125663d09cd2fc982b38 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 23 Feb 2023 16:06:47 +0100 Subject: [PATCH 212/319] [fix] dovecot-pop3d is never installed --- src/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings.py b/src/settings.py index fbe4db7d0..dba4703ee 100644 --- a/src/settings.py +++ b/src/settings.py @@ -347,7 +347,7 @@ def reconfigure_dovecot(setting_name, old_value, new_value): environment = os.environ.copy() environment.update({"DEBIAN_FRONTEND": "noninteractive"}) - if new_value == "True": + if new_value == True: command = [ "apt-get", "-y", From 139e54a2e52f7a8fa38df8b1c48d52204173051e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Feb 2023 21:53:59 +0100 Subject: [PATCH 213/319] appsv2: data_dir's owner should have rwx by default --- 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 fafbfb45b..15d139f4b 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -665,7 +665,7 @@ class DatadirAppResource(AppResource): ##### Properties: - `dir`: (default: `/home/yunohost.app/__APP__`) The full path of the data dir - - `owner`: (default: `__APP__:rx`) The owner (and owner permissions) for the data dir + - `owner`: (default: `__APP__:rwx`) The owner (and owner permissions) for the data dir - `group`: (default: `__APP__:rx`) The group (and group permissions) for the data dir ##### Provision/Update: @@ -694,7 +694,7 @@ class DatadirAppResource(AppResource): default_properties: Dict[str, Any] = { "dir": "/home/yunohost.app/__APP__", - "owner": "__APP__:rx", + "owner": "__APP__:rwx", "group": "__APP__:rx", } From 943b9ff89f4314f52a0889890264c428b04cbcd3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Feb 2023 22:25:44 +0100 Subject: [PATCH 214/319] appsv2: fix usage of __DOMAIN__ in permission url --- src/utils/resources.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 15d139f4b..77bd53cb3 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -153,9 +153,6 @@ class AppResource: for key, value in properties.items(): if isinstance(value, str): value = value.replace("__APP__", self.app) - # This one is needed for custom permission urls where the domain might be used - if "__DOMAIN__" in value: - value.replace("__DOMAIN__", self.get_setting("domain")) setattr(self, key, value) def get_setting(self, key): @@ -340,6 +337,11 @@ class PermissionsResource(AppResource): super().__init__({"permissions": properties}, *args, **kwargs) + for perm, infos in self.permissions.items(): + if "__DOMAIN__" in infos.get("url", ""): + infos["url"] = infos["url"].replace("__DOMAIN__", self.get_setting("domain")) + infos["additional_urls"] = [u.replace("__DOMAIN__", self.get_setting("domain")) for u in infos.get("additional_urls")] + def provision_or_update(self, context: Dict = {}): from yunohost.permission import ( permission_create, From ad63e5d38384a80517cb9b9a44654b1fe0e79dca Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Feb 2023 22:27:46 +0100 Subject: [PATCH 215/319] Make the linters god happy... --- src/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings.py b/src/settings.py index dba4703ee..4905049d6 100644 --- a/src/settings.py +++ b/src/settings.py @@ -347,7 +347,7 @@ def reconfigure_dovecot(setting_name, old_value, new_value): environment = os.environ.copy() environment.update({"DEBIAN_FRONTEND": "noninteractive"}) - if new_value == True: + if new_value is True: command = [ "apt-get", "-y", From 41c9d9d8e3e298430e9b9f92d00dea0eb0225df2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Feb 2023 22:32:20 +0100 Subject: [PATCH 216/319] Update changelog for 11.1.11 --- debian/changelog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/debian/changelog b/debian/changelog index 530d3edc0..25d806972 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +yunohost (11.1.11) stable; urgency=low + + - logs: fix decoding errors not handled when trying to read service logs ([#1606](https://github.com/yunohost/yunohost/pull/1606)) + - mail: fix dovecot-pop3d not being installed when enabling pop3 ([#1607](https://github.com/yunohost/yunohost/pull/1607)) + - apps: when creating the app's bash env for script, make sure to use the manifest from the workdir instead of app setting dir, which is important for consistency during edge case when upgrade from v1 to v2 fails (bab27014) + - appsv2: data_dir's owner should have rwx by default (139e54a2) + - appsv2: fix usage of __DOMAIN__ in permission url (943b9ff8) + + Thanks to all contributors <3 ! (Eric Geldmacher, ljf) + + -- Alexandre Aubin Thu, 23 Feb 2023 22:31:02 +0100 + yunohost (11.1.10) stable; urgency=low - apps: add 'YNH_DEBIAN_VERSION' variable in apps contexts (df6a2a2c) From 6210d07c24bf92ed681db76aa7570a952954a7ec Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Thu, 23 Feb 2023 23:17:35 +0000 Subject: [PATCH 217/319] [CI] Format code with Black --- src/service.py | 2 +- src/utils/resources.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/service.py b/src/service.py index c4835263b..47bc1903a 100644 --- a/src/service.py +++ b/src/service.py @@ -775,7 +775,7 @@ def _tail(file, n): f = gzip.open(file) lines = f.read().splitlines() else: - f = open(file, errors='replace') + f = open(file, errors="replace") pos = 1 lines = [] while len(lines) < to_read and pos > 0: diff --git a/src/utils/resources.py b/src/utils/resources.py index 77bd53cb3..1d2b0ed4d 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -339,8 +339,13 @@ class PermissionsResource(AppResource): for perm, infos in self.permissions.items(): if "__DOMAIN__" in infos.get("url", ""): - infos["url"] = infos["url"].replace("__DOMAIN__", self.get_setting("domain")) - infos["additional_urls"] = [u.replace("__DOMAIN__", self.get_setting("domain")) for u in infos.get("additional_urls")] + infos["url"] = infos["url"].replace( + "__DOMAIN__", self.get_setting("domain") + ) + infos["additional_urls"] = [ + u.replace("__DOMAIN__", self.get_setting("domain")) + for u in infos.get("additional_urls") + ] def provision_or_update(self, context: Dict = {}): from yunohost.permission import ( From e05df676dce1c055bec87cb1d6aad5cfc717675d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 24 Feb 2023 01:29:12 +0100 Subject: [PATCH 218/319] appsv2: fix previous commit about __DOMAIN__ because url may be None x_x --- 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 1d2b0ed4d..b5d9f7e1b 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -338,13 +338,13 @@ class PermissionsResource(AppResource): super().__init__({"permissions": properties}, *args, **kwargs) for perm, infos in self.permissions.items(): - if "__DOMAIN__" in infos.get("url", ""): + if infos.get("url") and "__DOMAIN__" in infos.get("url", ""): infos["url"] = infos["url"].replace( "__DOMAIN__", self.get_setting("domain") ) infos["additional_urls"] = [ u.replace("__DOMAIN__", self.get_setting("domain")) - for u in infos.get("additional_urls") + for u in infos.get("additional_urls", []) ] def provision_or_update(self, context: Dict = {}): From 8ce5bb241271b42a1ac6a71d70ea2b0418ef564c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 24 Feb 2023 01:30:42 +0100 Subject: [PATCH 219/319] Update changelog for 11.1.11.1 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 25d806972..df19ca249 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (11.1.11.1) testing; urgency=low + + - appsv2: fix previous commit about __DOMAIN__ because url may be None x_x (e05df676) + + -- Alexandre Aubin Fri, 24 Feb 2023 01:30:14 +0100 + yunohost (11.1.11) stable; urgency=low - logs: fix decoding errors not handled when trying to read service logs ([#1606](https://github.com/yunohost/yunohost/pull/1606)) From 404746c1253c2a6f598da10a43a252076e95450c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 21 Feb 2023 02:52:13 +0100 Subject: [PATCH 220/319] feat: add '--continue-on-failure' to 'yunohost app upgrade --- locales/en.json | 5 ++ share/actionsmap.yml | 4 ++ src/app.py | 57 ++++++++++++---- src/tests/test_apps.py | 150 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 202 insertions(+), 14 deletions(-) diff --git a/locales/en.json b/locales/en.json index 7cc1b96b6..2966beb45 100644 --- a/locales/en.json +++ b/locales/en.json @@ -27,6 +27,7 @@ "app_config_unable_to_apply": "Failed to apply config panel values.", "app_config_unable_to_read": "Failed to read config panel values.", "app_extraction_failed": "Could not extract the installation files", + "app_failed_to_upgrade_but_continue": "App {failed_app} failed to upgrade, continue to next upgrades as requested. Run 'yunohost log show {operation_logger_name}' to see failure log", "app_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.", "app_id_invalid": "Invalid app ID", "app_install_failed": "Unable to install {app}: {error}", @@ -48,6 +49,8 @@ "app_not_installed": "Could not find {app} in the list of installed apps: {all_apps}", "app_not_properly_removed": "{app} has not been properly removed", "app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps' upgrades have been cancelled: {apps}", + "app_not_upgraded_broken_system": "The app '{failed_app}' failed to upgrade and put the system in a broken state, and as a consequence the following apps' upgrades have been cancelled: {apps}", + "app_not_upgraded_broken_system_continue": "The app '{failed_app}' failed to upgrade and put the system in a broken state (so --continue-on-failure is ignored), and as a consequence the following apps' upgrades have been cancelled: {apps}", "app_packaging_format_not_supported": "This app cannot be installed because its packaging format is not supported by your YunoHost version. You should probably consider upgrading your system.", "app_remove_after_failed_install": "Removing the app after installation failure...", "app_removed": "{app} uninstalled", @@ -75,6 +78,8 @@ "apps_catalog_obsolete_cache": "The app catalog cache is empty or obsolete.", "apps_catalog_update_success": "The application catalog has been updated!", "apps_catalog_updating": "Updating application catalog...", + "apps_failed_to_upgrade": "Those applications failed to upgrade:{apps}", + "apps_failed_to_upgrade_line": "\n * {app_id} (to see corresponding log do a 'yunohost log show {operation_logger_name}')", "ask_admin_fullname": "Admin full name", "ask_admin_username": "Admin username", "ask_fullname": "Full name", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 7f0fdabe9..58787790c 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -911,6 +911,10 @@ app: full: --no-safety-backup help: Disable the safety backup during upgrade action: store_true + -c: + full: --continue-on-failure + help: Continue to upgrade apps event if one or more upgrade failed + action: store_true ### app_change_url() change-url: diff --git a/src/app.py b/src/app.py index c2d4d0a89..b1884598f 100644 --- a/src/app.py +++ b/src/app.py @@ -533,7 +533,7 @@ def app_change_url(operation_logger, app, domain, path): hook_callback("post_app_change_url", env=env_dict) -def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False): +def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False, continue_on_failure=False): """ Upgrade app @@ -585,6 +585,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False logger.info(m18n.n("app_upgrade_several_apps", apps=", ".join(apps))) notifications = {} + failed_to_upgrade_apps = [] for number, app_instance_name in enumerate(apps): logger.info(m18n.n("app_upgrade_app_name", app=app_instance_name)) @@ -820,20 +821,43 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False # If upgrade failed or broke the system, # raise an error and interrupt all other pending upgrades if upgrade_failed or broke_the_system: - # display this if there are remaining apps - if apps[number + 1 :]: - not_upgraded_apps = apps[number:] - logger.error( - m18n.n( - "app_not_upgraded", - failed_app=app_instance_name, - apps=", ".join(not_upgraded_apps), - ) + if not continue_on_failure or broke_the_system: + # display this if there are remaining apps + if apps[number + 1 :]: + not_upgraded_apps = apps[number:] + if broke_the_system and not continue_on_failure: + logger.error( + m18n.n( + "app_not_upgraded_broken_system", + failed_app=app_instance_name, + apps=", ".join(not_upgraded_apps), + ) + ) + elif broke_the_system and continue_on_failure: + logger.error( + m18n.n( + "app_not_upgraded_broken_system_continue", + failed_app=app_instance_name, + apps=", ".join(not_upgraded_apps), + ) + ) + else: + logger.error( + m18n.n( + "app_not_upgraded", + failed_app=app_instance_name, + apps=", ".join(not_upgraded_apps), + ) + ) + + raise YunohostError( + failure_message_with_debug_instructions, raw_msg=True ) - raise YunohostError( - failure_message_with_debug_instructions, raw_msg=True - ) + else: + operation_logger.close() + logger.error(m18n.n("app_failed_to_upgrade_but_continue", failed_app=app_instance_name, operation_logger_name=operation_logger.name)) + failed_to_upgrade_apps.append((app_instance_name, operation_logger.name)) # Otherwise we're good and keep going ! now = int(time.time()) @@ -895,6 +919,13 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False logger.success(m18n.n("upgrade_complete")) + if failed_to_upgrade_apps: + apps = "" + for app_id, operation_logger_name in failed_to_upgrade_apps: + apps += m18n.n("apps_failed_to_upgrade_line", app_id=app_id, operation_logger_name=operation_logger_name) + + logger.warning(m18n.n("apps_failed_to_upgrade", apps=apps)) + if Moulinette.interface.type == "api": return {"notifications": {"POST_UPGRADE": notifications}} diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index 965ce5892..830aabf61 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -19,7 +19,7 @@ from yunohost.app import ( app_info, ) from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.tests.test_permission import ( check_LDAP_db_integrity, check_permission_for_apps, @@ -541,3 +541,151 @@ def test_failed_multiple_app_upgrade(mocker, secondary_domain): "legacy": os.path.join(get_test_apps_dir(), "legacy_app_ynh"), }, ) + + +class TestMockedAppUpgrade: + """ + This class is here to test the logical workflow of app_upgrade and thus + mock nearly all side effects + """ + def setup_method(self, method): + self.apps_list = [] + self.upgradable_apps_list = [] + + def _mock_app_upgrade(self, mocker): + # app list + self._installed_apps = mocker.patch("yunohost.app._installed_apps", side_effect=lambda: self.apps_list) + + # just check if an app is really installed + mocker.patch("yunohost.app._is_installed", side_effect=lambda app: app in self.apps_list) + + # app_dict = + mocker.patch("yunohost.app.app_info", side_effect=lambda app, full: { + "upgradable": "yes" if app in self.upgradable_apps_list else "no", + "manifest": {"id": app}, + "version": "?", + }) + + def custom_extract_app(app): + return ({ + "version": "?", + "packaging_format": 1, + "id": app, + "notifications": {"PRE_UPGRADE": None, "POST_UPGRADE": None}, + }, "MOCKED_BY_TEST") + + # return (manifest, extracted_app_folder) + mocker.patch("yunohost.app._extract_app", side_effect=custom_extract_app) + + # for [(name, passed, values, err), ...] in + mocker.patch("yunohost.app._check_manifest_requirements", return_value=[(None, True, None, None)]) + + # raise on failure + mocker.patch("yunohost.app._assert_system_is_sane_for_app", return_value=True) + + from os.path import exists # import the unmocked function + + def custom_os_path_exists(path): + if path.endswith("manifest.toml"): + return True + return exists(path) + + mocker.patch("os.path.exists", side_effect=custom_os_path_exists) + + # manifest = + mocker.patch("yunohost.app.read_toml", return_value={ + "arguments": {"install": []} + }) + + # install_failed, failure_message_with_debug_instructions = + self.hook_exec_with_script_debug_if_failure = mocker.patch("yunohost.hook.hook_exec_with_script_debug_if_failure", return_value=(False, "")) + # settings = + mocker.patch("yunohost.app._get_app_settings", return_value={}) + # return nothing + mocker.patch("yunohost.app._set_app_settings") + + from os import listdir # import the unmocked function + + def custom_os_listdir(path): + if path.endswith("MOCKED_BY_TEST"): + return [] + return listdir(path) + + mocker.patch("os.listdir", side_effect=custom_os_listdir) + mocker.patch("yunohost.app.rm") + mocker.patch("yunohost.app.cp") + mocker.patch("shutil.rmtree") + mocker.patch("yunohost.app.chmod") + mocker.patch("yunohost.app.chown") + mocker.patch("yunohost.app.app_ssowatconf") + + def test_app_upgrade_no_apps(self, mocker): + self._mock_app_upgrade(mocker) + + with pytest.raises(YunohostValidationError): + app_upgrade() + + def test_app_upgrade_app_not_install(self, mocker): + self._mock_app_upgrade(mocker) + + with pytest.raises(YunohostValidationError): + app_upgrade("some_app") + + def test_app_upgrade_one_app(self, mocker): + self._mock_app_upgrade(mocker) + self.apps_list = ["some_app"] + + # yunohost is happy, not apps to upgrade + app_upgrade() + + self.hook_exec_with_script_debug_if_failure.assert_not_called() + + self.upgradable_apps_list.append("some_app") + app_upgrade() + + self.hook_exec_with_script_debug_if_failure.assert_called_once() + assert self.hook_exec_with_script_debug_if_failure.call_args.kwargs["env"]["YNH_APP_ID"] == "some_app" + + def test_app_upgrade_continue_on_failure(self, mocker): + self._mock_app_upgrade(mocker) + self.apps_list = ["a", "b", "c"] + self.upgradable_apps_list = self.apps_list + + def fails_on_b(self, *args, env, **kwargs): + if env["YNH_APP_ID"] == "b": + return True, "failed" + return False, "ok" + + self.hook_exec_with_script_debug_if_failure.side_effect = fails_on_b + + with pytest.raises(YunohostError): + app_upgrade() + + app_upgrade(continue_on_failure=True) + + def test_app_upgrade_continue_on_failure_broken_system(self, mocker): + """--continue-on-failure should stop on a broken system""" + + self._mock_app_upgrade(mocker) + self.apps_list = ["a", "broke_the_system", "c"] + self.upgradable_apps_list = self.apps_list + + def fails_on_b(self, *args, env, **kwargs): + if env["YNH_APP_ID"] == "broke_the_system": + return True, "failed" + return False, "ok" + + self.hook_exec_with_script_debug_if_failure.side_effect = fails_on_b + + def _assert_system_is_sane_for_app(manifest, state): + if state == "post" and manifest["id"] == "broke_the_system": + raise Exception() + return True + + mocker.patch("yunohost.app._assert_system_is_sane_for_app", side_effect=_assert_system_is_sane_for_app) + + with pytest.raises(YunohostError): + app_upgrade() + + with pytest.raises(YunohostError): + app_upgrade(continue_on_failure=True) From 9d214fd3c6cc58e11546078ca5821c2a04633c41 Mon Sep 17 00:00:00 2001 From: John Schmidt Date: Thu, 23 Feb 2023 20:25:24 -0800 Subject: [PATCH 221/319] [Fixes 2158] Create parent dirs when provisioning install_dir Signed-off-by: John Schmidt --- 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 b5d9f7e1b..c1f896841 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -623,7 +623,7 @@ class InstalldirAppResource(AppResource): ) shutil.move(current_install_dir, self.dir) else: - mkdir(self.dir) + mkdir(self.dir, parents=True) owner, owner_perm = self.owner.split(":") group, group_perm = self.group.split(":") From 5d1778211596d11600690d938df36a7a14527f27 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 24 Feb 2023 13:10:37 +0100 Subject: [PATCH 222/319] Update changelog for 11.1.11.2 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index df19ca249..14f2ee73d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (11.1.11.2) stable; urgency=low + + - Rebump version to flag as stable, not testing >_> + + -- Alexandre Aubin Fri, 24 Feb 2023 13:09:48 +0100 + yunohost (11.1.11.1) testing; urgency=low - appsv2: fix previous commit about __DOMAIN__ because url may be None x_x (e05df676) From a3df78fe7e230c536f3f6f4b746cb7a847c338e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= <46165813+ericgaspar@users.noreply.github.com> Date: Fri, 24 Feb 2023 18:46:31 +0100 Subject: [PATCH 223/319] Update resources.py set `w` as default permission on `install_dir` folder --- 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 c1f896841..cff6c6b19 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -557,7 +557,7 @@ class InstalldirAppResource(AppResource): ##### Properties: - `dir`: (default: `/var/www/__APP__`) The full path of the install dir - - `owner`: (default: `__APP__:rx`) The owner (and owner permissions) for the install dir + - `owner`: (default: `__APP__:rwx`) The owner (and owner permissions) for the install dir - `group`: (default: `__APP__:rx`) The group (and group permissions) for the install dir ##### Provision/Update: @@ -586,7 +586,7 @@ class InstalldirAppResource(AppResource): default_properties: Dict[str, Any] = { "dir": "/var/www/__APP__", - "owner": "__APP__:rx", + "owner": "__APP__:rwx", "group": "__APP__:rx", } From 20e8805e3b60178df804b1acc4714f9d4e754572 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 25 Feb 2023 16:01:55 +0100 Subject: [PATCH 224/319] misc: automatic get rid of /etc/profile.d/check_yunohost_is_installed.sh when yunohost is postinstalled --- hooks/conf_regen/01-yunohost | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 51022a4e5..7e03d1978 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -245,6 +245,11 @@ do_post_regen() { rm -f /etc/dpkg/origins/default ln -s /etc/dpkg/origins/yunohost /etc/dpkg/origins/default fi + + if test -e /etc/yunohost/installed && test -e /etc/profile.d/check_yunohost_is_installed.sh + then + rm /etc/profile.d/check_yunohost_is_installed.sh + fi } do_$1_regen ${@:2} From 97c0128c227d538591e89ed4d9d0214ed29626ce Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 12 Feb 2023 17:31:01 +0100 Subject: [PATCH 225/319] regenconf: sometimes ntp doesnt exist --- hooks/conf_regen/01-yunohost | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index 7e03d1978..d0e6fb783 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -125,12 +125,15 @@ EOF fi # Skip ntp if inside a container (inspired from the conf of systemd-timesyncd) - mkdir -p ${pending_dir}/etc/systemd/system/ntp.service.d/ - cat >${pending_dir}/etc/systemd/system/ntp.service.d/ynh-override.conf <${pending_dir}/etc/systemd/system/ntp.service.d/ynh-override.conf < Date: Sun, 26 Feb 2023 15:10:54 +0100 Subject: [PATCH 226/319] nginx/security: fix empty webadmin allowlist breaking nginx conf... --- conf/nginx/yunohost_admin.conf.inc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/conf/nginx/yunohost_admin.conf.inc b/conf/nginx/yunohost_admin.conf.inc index 84c49d30b..0c4a96fdc 100644 --- a/conf/nginx/yunohost_admin.conf.inc +++ b/conf/nginx/yunohost_admin.conf.inc @@ -7,9 +7,11 @@ location /yunohost/admin/ { index index.html; {% if webadmin_allowlist_enabled == "True" %} - {% for ip in webadmin_allowlist.split(',') %} - allow {{ ip }}; - {% endfor %} + {% if webadmin_allowlist.strip() -%} + {% for ip in webadmin_allowlist.strip().split(',') -%} + allow {{ ip.strip() }}; + {% endfor -%} + {% endif -%} deny all; {% endif %} From b40c0de33ca0f4b4e3416a9195799e6c59e6b42e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 26 Feb 2023 17:44:48 +0100 Subject: [PATCH 227/319] Fix pop3_enabled parsing returning 0/1 instead of True/False ... --- hooks/conf_regen/25-dovecot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/conf_regen/25-dovecot b/hooks/conf_regen/25-dovecot index adbb7761e..49ff0c9ba 100755 --- a/hooks/conf_regen/25-dovecot +++ b/hooks/conf_regen/25-dovecot @@ -16,7 +16,7 @@ do_pre_regen() { cp dovecot-ldap.conf "${dovecot_dir}/dovecot-ldap.conf" cp dovecot.sieve "${dovecot_dir}/global_script/dovecot.sieve" - export pop3_enabled="$(yunohost settings get 'email.pop3.pop3_enabled')" + export pop3_enabled="$(yunohost settings get 'email.pop3.pop3_enabled' | int_to_bool)" export main_domain=$(cat /etc/yunohost/current_host) export domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')" From 1a089647b5b090de712bb84c8eaef576799b0701 Mon Sep 17 00:00:00 2001 From: ppr Date: Wed, 22 Feb 2023 18:42:28 +0000 Subject: [PATCH 228/319] Translated using Weblate (French) Currently translated at 99.8% (756 of 757 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index f05699656..a7069f844 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -753,5 +753,8 @@ "global_settings_setting_dns_exposure_help": "NB : Ceci n'affecte que la configuration DNS recommandée et les vérifications de diagnostic. Cela n'affecte pas les configurations du système.", "diagnosis_ip_no_ipv6_tip_important": "IPv6 devrait généralement être configuré automatiquement par le système ou par votre fournisseur d'accès à internet (FAI) s'il est disponible. Sinon, vous devrez peut-être configurer quelques éléments manuellement, comme expliqué dans la documentation ici : https://yunohost.org/#/ipv6.", "domain_config_default_app_help": "Les personnes seront automatiquement redirigées vers cette application lorsqu'elles ouvriront ce domaine. Si aucune application n'est spécifiée, les personnes sont redirigées vers le formulaire de connexion du portail utilisateur.", - "domain_config_xmpp_help": "NB : certaines fonctions XMPP nécessiteront la mise à jour de vos enregistrements DNS et la régénération de votre certificat Lets Encrypt pour être activées" + "domain_config_xmpp_help": "NB : certaines fonctions XMPP nécessiteront la mise à jour de vos enregistrements DNS et la régénération de votre certificat Lets Encrypt pour être activées", + "app_change_url_failed": "Impossible de modifier l'url de {app} : {error}", + "app_change_url_require_full_domain": "{app} ne peut pas être déplacée vers cette nouvelle URL car elle nécessite un domaine complet (c'est-à-dire avec un chemin = /)", + "app_change_url_script_failed": "Une erreur s'est produite dans le script de modification de l'url" } From df7f0ee969a7be8b57fce071092aeca6edd78cdc Mon Sep 17 00:00:00 2001 From: Krakinou Date: Fri, 24 Feb 2023 21:01:03 +0000 Subject: [PATCH 229/319] Translated using Weblate (Italian) Currently translated at 77.8% (589 of 757 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/it.json b/locales/it.json index e94a43a6d..59ba0a6ba 100644 --- a/locales/it.json +++ b/locales/it.json @@ -638,5 +638,6 @@ "global_settings_setting_webadmin_allowlist_enabled_help": "Permetti solo ad alcuni IP di accedere al webadmin.", "global_settings_setting_smtp_allow_ipv6_help": "Permetti l'utilizzo di IPv6 per ricevere e inviare mail", "global_settings_setting_smtp_relay_enabled_help": "Utilizza SMTP relay per inviare mail al posto di questa instanza yunohost. Utile se sei in una di queste situazioni: la tua porta 25 è bloccata dal tuo provider ISP o VPS; hai un IP residenziale listato su DUHL; non sei puoi configurare il DNS inverso; oppure questo server non è direttamente esposto a Internet e vuoi usarne un'altro per spedire email.", - "domain_config_default_app": "Applicazione di default" -} \ No newline at end of file + "domain_config_default_app": "Applicazione di default", + "app_change_url_failed": "Non è possibile cambiare l'URL per {app}:{error}" +} From e926e5ecaa940e60068bd0df13485f3a7bdb8b88 Mon Sep 17 00:00:00 2001 From: Kuba Bazan Date: Sat, 25 Feb 2023 15:51:13 +0000 Subject: [PATCH 230/319] Translated using Weblate (Polish) Currently translated at 20.0% (152 of 757 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 86 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index d66427ac3..e5944d70d 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -1,7 +1,7 @@ { "password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków", "app_already_up_to_date": "{app} jest obecnie aktualna", - "app_already_installed": "{app} jest już zainstalowane", + "app_already_installed": "{app:s} jest już zainstalowana", "already_up_to_date": "Nic do zrobienia. Wszystko jest obecnie aktualne.", "admin_password": "Hasło administratora", "action_invalid": "Nieprawidłowe działanie '{action:s}'", @@ -12,7 +12,7 @@ "app_start_install": "Instalowanie {app}...", "app_unknown": "Nieznana aplikacja", "ask_main_domain": "Domena główna", - "backup_created": "Utworzono kopię zapasową", + "backup_created": "Utworzono kopię zapasową: {name}", "firewall_reloaded": "Przeładowano zaporę sieciową", "user_created": "Utworzono użytkownika", "yunohost_installing": "Instalowanie YunoHost...", @@ -38,7 +38,7 @@ "ask_new_path": "Nowa ścieżka", "downloading": "Pobieranie...", "ask_password": "Hasło", - "backup_deleted": "Usunięto kopię zapasową", + "backup_deleted": "Usunięto kopię zapasową: {name}.", "done": "Gotowe", "diagnosis_description_dnsrecords": "Rekordy DNS", "diagnosis_description_ip": "Połączenie z internetem", @@ -77,5 +77,83 @@ "app_argument_invalid": "Wybierz poprawną wartość dla argumentu '{name}': {błąd}", "all_users": "Wszyscy użytkownicy YunoHost", "app_action_failed": "Nie udało się uruchomić akcji {action} dla aplikacji {app}", - "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_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_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 („{domena}{ścieżka}”), nic nie trzeba robić.", + "app_config_unable_to_read": "Nie udało się odczytać wartości panelu konfiguracji.", + "app_config_unable_to_apply": "Nie udało się zastosować wartości panelu konfiguracji.", + "app_install_failed": "Nie udało się zainstalować {aplikacji}: {błąd}", + "apps_catalog_failed_to_download": "Nie można pobrać katalogu aplikacji app catalog: {error}", + "app_argument_required": "Argument „{nazwa}” jest wymagany", + "app_not_properly_removed": "Aplikacja {app} nie została poprawnie usunięta", + "app_upgrade_failed": "Nie można uaktualnić {app}: {error}", + "backup_abstract_method": "Ta metoda tworzenia kopii zapasowych nie została jeszcze zaimplementowana", + "backup_actually_backuping": "Tworzenie archiwum kopii zapasowej z zebranych plików...", + "backup_applying_method_copy": "Kopiowanie wszystkich plików do kopii zapasowej...", + "backup_applying_method_tar": "Tworzenie kopii zapasowej archiwum TAR..", + "backup_archive_app_not_found": "Nie można znaleźć aplikacji {app} w archiwum kopii zapasowych", + "backup_archive_broken_link": "Nie można uzyskać dostępu do archiwum kopii zapasowych (broken link to {path})", + "backup_csv_addition_failed": "Nie udało się dodać plików do kopii zapasowej do pliku CSV.", + "backup_creation_failed": "Nie udało się utworzyć archiwum kopii zapasowej", + "backup_csv_creation_failed": "Nie udało się utworzyć wymaganego pliku CSV do przywracania.", + "backup_custom_mount_error": "Niestandardowa metoda tworzenia kopii zapasowej nie mogła przejść etapu „mount”", + "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_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.", + "app_manifest_install_ask_domain": "Wybierz domenę, w której ta aplikacja ma zostać zainstalowana", + "app_manifest_install_ask_admin": "Wybierz użytkownika administratora dla tej aplikacji", + "app_manifest_install_ask_password": "Wybierz hasło administratora dla tej aplikacji", + "app_manifest_install_ask_is_public": "Czy ta aplikacja powinna być udostępniana anonimowym użytkownikom?", + "ask_user_domain": "Domena używana dla adresu e-mail użytkownika i konta XMPP", + "app_upgrade_app_name": "Aktualizuję {app}...", + "app_install_script_failed": "Wystąpił błąd w skrypcie instalacyjnym aplikacji", + "apps_catalog_update_success": "Katalog aplikacji został zaktualizowany!", + "apps_catalog_updating": "Aktualizowanie katalogu aplikacji...", + "app_label_deprecated": "To polecenie jest przestarzałe! Użyj nowego polecenia „yunohost user permission update”, aby zarządzać etykietą aplikacji.", + "app_change_url_no_script": "Aplikacja „{app_name}” nie obsługuje jeszcze modyfikacji adresów URL. Możesz spróbować ją zaaktualizować.", + "app_change_url_success": "Adres URL aplikacji {app} to teraz {domain}{path}", + "app_not_upgraded": "Nie udało się zaktualizować aplikacji „{failed_app}”, w związku z czym anulowano aktualizacje następujących aplikacji: {apps}", + "app_upgrade_several_apps": "Następujące aplikacje zostaną uaktualnione: {apps}", + "app_not_correctly_installed": "Wygląda na to, że aplikacja {app} jest nieprawidłowo zainstalowana", + "app_not_installed": "Nie można znaleźć aplikacji {app} na liście zainstalowanych aplikacji: {all_apps}", + "app_requirements_checking": "Sprawdzam wymagania dla aplikacji {app}...", + "app_upgrade_some_app_failed": "Niektórych aplikacji nie udało się zaktualizować", + "backup_app_failed": "Nie udało się utworzyć kopii zapasowej {app}", + "backup_archive_name_exists": "Archiwum kopii zapasowych o tej nazwie już istnieje.", + "backup_archive_open_failed": "Nie można otworzyć archiwum kopii zapasowej", + "backup_archive_writing_error": "Nie udało się dodać plików '{source}' (nazwanych w archiwum '{dest}') do utworzenia kopii zapasowej skompresowanego archiwum '{archive}'", + "backup_ask_for_copying_if_needed": "Czy chcesz wykonać kopię zapasową tymczasowo używając {size} MB? (Ta metoda jest stosowana, ponieważ niektóre pliki nie mogły zostać przygotowane przy użyciu bardziej wydajnej metody.)", + "backup_cant_mount_uncompress_archive": "Nie można zamontować nieskompresowanego archiwum jako chronione przed zapisem", + "backup_copying_to_organize_the_archive": "Kopiowanie {size} MB w celu zorganizowania archiwum", + "backup_couldnt_bind": "Nie udało się powiązać {src} z {dest}.", + "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_restore_failed": "Nie można przywrócić {aplikacji}: {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.", + "app_resource_failed": "Nie udało się zapewnić, anulować obsługi administracyjnej lub zaktualizować zasobów aplikacji {app}: {error}", + "app_manifest_install_ask_path": "Wybierz ścieżkę adresu URL (po domenie), w której ta aplikacja ma zostać zainstalowana", + "app_not_enough_disk": "Ta aplikacja wymaga {required} wolnego miejsca.", + "app_not_enough_ram": "Ta aplikacja wymaga {required} pamięci RAM do zainstalowania/uaktualnienia, ale obecnie dostępna jest tylko {current}.", + "app_start_backup": "Zbieram pliki do utworzenia kopii zapasowej dla {app}...", + "app_yunohost_version_not_supported": "Ta aplikacja wymaga YunoHost >= {required}, ale aktualnie zainstalowana wersja to {current}", + "apps_already_up_to_date": "Wszystkie aplikacje są już aktualne", + "backup_archive_system_part_not_available": "Część systemowa '{part}' jest niedostępna w tej kopii zapasowej", + "backup_custom_backup_error": "Niestandardowa metoda tworzenia kopii zapasowej nie mogła przejść kroku 'backup'.", + "app_argument_password_no_default": "Błąd podczas analizowania argumentu hasła „{name}”: argument hasła nie może mieć wartości domyślnej ze względów bezpieczeństwa", + "app_sources_fetch_failed": "Nie można pobrać plików źródłowych, czy adres URL jest poprawny?", + "app_manifest_install_ask_init_admin_permission": "Kto powinien mieć dostęp do funkcji administracyjnych tej aplikacji? (Można to później zmienić)", + "app_manifest_install_ask_init_main_permission": "Kto powinien mieć dostęp do tej aplikacji? (Można to później zmienić)", + "ask_admin_fullname": "Pełne imię i nazwisko administratora", + "app_change_url_failed": "Nie udało się zmienić adresu URL aplikacji {app}: {error}", + "app_change_url_script_failed": "Wystąpił błąd w skrypcie zmiany adresu URL" } From 53588dcce7a7d8b1d256817df00691a0b5e9ba96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Thu, 23 Feb 2023 06:49:45 +0000 Subject: [PATCH 231/319] Translated using Weblate (Galician) Currently translated at 99.6% (754 of 757 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/gl.json b/locales/gl.json index 9e7c1578b..9550185d1 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -750,5 +750,8 @@ "global_settings_setting_dns_exposure_help": "Nota: Esto só lle afecta á configuración DNS recomendada e diagnóstico do sistema. Non lle afecta aos axustes do sistema.", "diagnosis_ip_no_ipv6_tip_important": "Se está dispoñible, IPv6 debería estar automáticamente configurado polo sistema ou o teu provedor. Se non, pode que teñas que facer algúns axustes manualmente tal como se explica na documentación: https://yunohost.org/#/ipv6.", "domain_config_default_app_help": "As persoas serán automáticamente redirixidas a esta app ao abrir o dominio. Se non se indica ningunha, serán redirixidas ao formulario de acceso no portal de usuarias.", - "domain_config_xmpp_help": "Nota: algunhas características de XMPP para ser utilizadas precisan que teñas ao día os rexistros DNS e rexeneres os certificados Lets Encrypt" + "domain_config_xmpp_help": "Nota: algunhas características de XMPP para ser utilizadas precisan que teñas ao día os rexistros DNS e rexeneres os certificados Lets Encrypt", + "app_change_url_failed": "Non se cambiou o url para {app}: {error}", + "app_change_url_require_full_domain": "{app} non se pode mover a este novo URL porque require un dominio completo propio (ex. con ruta = /)", + "app_change_url_script_failed": "Algo fallou ao executar o script de cambio de url" } From eb6d9df92f7256821bf56a523c81f5e554e65075 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 26 Feb 2023 20:08:59 +0100 Subject: [PATCH 232/319] helpers: add support for a sources.toml to modernize and replace app.src format --- helpers/utils | 166 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 130 insertions(+), 36 deletions(-) diff --git a/helpers/utils b/helpers/utils index f80c22901..d958ae02e 100644 --- a/helpers/utils +++ b/helpers/utils @@ -71,39 +71,78 @@ 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 `app` +# | arg: -s, --source_id= - Name of the source, defaults to `main` (when sources.toml exists) or (legacy) `app` (when no sources.toml exists) # | arg: -k, --keep= - Space-separated list of files/folders that will be backup/restored in $dest_dir, such as a config file you don't want to overwrite. For example 'conf.json secrets.json logs/' # | arg: -r, --full_replace= - Remove previous sources before installing new sources # +# #### New format `.toml` +# +# This helper will read infos from a sources.toml at the root of the app package +# and expect a structure like: +# +# ```toml +# [main] +# url = "https://some.address.to/download/the/app/archive" +# sha256 = "0123456789abcdef" # The sha256 sum of the asset obtained from the URL +# +# +# # Optional flags: +# format = "tar.gz"/xz/bz2 # automatically guessed from the extension of the URL, but can be set explicitly. Will use `tar` to extract +# "zip" # automatically guessed from the extension of the URL, but can be set explicitly. Will use `unzip` to extract +# "docker" # useful to extract files from an already-built docker image (instead of rebuilding them locally). Will use `docker-image-extract` to extract +# "whatever" # an arbitrary value, not really meaningful except to imply that the file won't be extracted +# +# in_subdir = true # default, there's an intermediate subdir in the archive before accessing the actual files +# false # sources are directly in the archive root +# n # (special cases) an integer representing a number of subdirs levels to get rid of +# +# extract = true # default if file is indeed an archive such as .zip, .tar.gz, .tar.bz2, ... +# = false # default if file 'format' is not set and the file is not to be extracted because it is not an archive but a script or binary or whatever asset. +# # in which case the file will only be `mv`ed to the location possibly renamed using the `rename` value +# +# rename = "whatever_your_want" # to be used for convenience when `extract` is false and the default name of the file is not practical +# platform = "linux/amd64" # (defaults to "linux/$YNH_ARCH") to be used in conjonction with `format = "docker"` to specify which architecture to extract for +# ``` +# +# You may also define sublevels for each architectures such as: +# ```toml +# [main] +# autoswitch_per_arch = true +# +# [main.amd64] +# url = "https://some.address.to/download/the/app/archive/when/amd64" +# sha256 = "0123456789abcdef" +# +# [main.armhf] +# url = "https://some.address.to/download/the/app/archive/when/armhf" +# sha256 = "fedcba9876543210" +# ``` +# +# In which case ynh_setup_source --dest_dir="$install_dir" will automatically pick the appropriate source depending on the arch +# +# +# +# #### Legacy format '.src' +# # This helper will read `conf/${source_id}.src`, download and install the sources. # # The src file need to contains: # ``` # SOURCE_URL=Address to download the app archive -# SOURCE_SUM=Control sum -# # (Optional) Program to check the integrity (sha256sum, md5sum...). Default: sha256 -# SOURCE_SUM_PRG=sha256 -# # (Optional) Archive format. Default: tar.gz +# SOURCE_SUM=Sha256 sum # SOURCE_FORMAT=tar.gz -# # (Optional) Put false if sources are directly in the archive root. Default: true -# # Instead of true, SOURCE_IN_SUBDIR could be the number of sub directories to remove. # SOURCE_IN_SUBDIR=false -# # (Optionnal) Name of the local archive (offline setup support). Default: ${src_id}.${src_format} # SOURCE_FILENAME=example.tar.gz -# # (Optional) If it set as false don't extract the source. Default: true -# # (Useful to get a debian package or a python wheel.) # SOURCE_EXTRACT=(true|false) -# # (Optionnal) Name of the plateform. Default: "linux/$YNH_ARCH" # SOURCE_PLATFORM=linux/arm64/v8 # ``` # # The helper will: -# - Check if there is a local source archive in `/opt/yunohost-apps-src/$APP_ID/$SOURCE_FILENAME` -# - Download `$SOURCE_URL` if there is no local archive -# - Check the integrity with `$SOURCE_SUM_PRG -c --status` +# - Download the specific URL if there is no local archive +# - Check the integrity with the specific sha256 sum # - Uncompress the archive to `$dest_dir`. -# - If `$SOURCE_IN_SUBDIR` is true, the first level directory of the archive will be removed. -# - If `$SOURCE_IN_SUBDIR` is a numeric value, the N first level directories will be removed. +# - If `in_subdir` is true, the first level directory of the archive will be removed. +# - If `in_subdir` is a numeric value, the N first level directories will be removed. # - Patches named `sources/patches/${src_id}-*.patch` will be applied to `$dest_dir` # - Extra files in `sources/extra_files/$src_id` will be copied to dest_dir # @@ -118,22 +157,64 @@ ynh_setup_source() { local full_replace # Manage arguments with getopts ynh_handle_getopts_args "$@" - source_id="${source_id:-app}" keep="${keep:-}" full_replace="${full_replace:-0}" - local src_file_path="$YNH_APP_BASEDIR/conf/${source_id}.src" + if test -e $YNH_APP_BASEDIR/sources.toml + then + source_id="${source_id:-main}" + local sources_json=$(cat $YNH_APP_BASEDIR/sources.toml | toml_to_json) + if [[ "$(echo "$sources_json" | jq -r ".$source_id.autoswitch_per_arch")" == "true" ]] + then + source_id=$source_id.$YNH_ARCH + fi - # Load value from configuration file (see above for a small doc about this file - # format) - local src_url=$(grep 'SOURCE_URL=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_sum=$(grep 'SOURCE_SUM=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_format=$(grep 'SOURCE_FORMAT=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_filename=$(grep 'SOURCE_FILENAME=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_extract=$(grep 'SOURCE_EXTRACT=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_plateform=$(grep 'SOURCE_PLATFORM=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_url="$(echo "$sources_json" | jq -r ".$source_id.url" | sed 's/^null$//')" + local src_sum="$(echo "$sources_json" | jq -r ".$source_id.sha256" | sed 's/^null$//')" + local src_sumprg="sha256sum" + local src_format="$(echo "$sources_json" | jq -r ".$source_id.format" | sed 's/^null$//')" + local src_in_subdir="$(echo "$sources_json" | jq -r ".$source_id.in_subdir" | sed 's/^null$//')" + local src_extract="$(echo "$sources_json" | jq -r ".$source_id.extract" | sed 's/^null$//')" + local src_platform="$(echo "$sources_json" | jq -r ".$source_id.platform" | sed 's/^null$//')" + local src_rename="$(echo "$sources_json" | jq -r ".$source_id.rename" | sed 's/^null$//')" + + [[ -n "$src_url" ]] || ynh_die "No URL defined for source $source_id ?" + [[ -n "$src_sum" ]] || ynh_die "No sha256 sum defined for source $source_id ?" + + if [[ -z "$src_format" ]] + then + if [[ "$src_url" =~ ^.*\.zip$ ]] || [[ "$src_url" =~ ^.*/zipball/.*$ ]] + then + src_format="zip" + elif [[ "$src_url" =~ ^.*\.tar\.gz$ ]] || [[ "$src_url" =~ ^.*\.tgz$ ]] || [[ "$src_url" =~ ^.*/tar\.gz/.*$ ]] || [[ "$src_url" =~ ^.*/tarball/.*$ ]] + then + src_format="tar.gz" + elif [[ "$src_url" =~ ^.*\.tar\.xz$ ]] + then + src_format="tar.xz" + elif [[ "$src_url" =~ ^.*\.tar\.bz2$ ]] + then + src_format="tar.bz2" + elif [[ -z "$src_extract" ]] + then + src_extract="false" + fi + fi + else + source_id="${source_id:-app}" + local src_file_path="$YNH_APP_BASEDIR/conf/${source_id}.src" + + # Load value from configuration file (see above for a small doc about this file + # format) + local src_url=$(grep 'SOURCE_URL=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_sum=$(grep 'SOURCE_SUM=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_format=$(grep 'SOURCE_FORMAT=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_rename=$(grep 'SOURCE_FILENAME=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_extract=$(grep 'SOURCE_EXTRACT=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_platform=$(grep 'SOURCE_PLATFORM=' "$src_file_path" | cut --delimiter='=' --fields=2-) + fi # Default value src_sumprg=${src_sumprg:-sha256sum} @@ -141,10 +222,14 @@ ynh_setup_source() { src_format=${src_format:-tar.gz} src_format=$(echo "$src_format" | tr '[:upper:]' '[:lower:]') src_extract=${src_extract:-true} - if [ "$src_filename" = "" ]; then - src_filename="${source_id}.${src_format}" + src_filename="${source_id}.${src_format}" + + if [[ "$src_extract" != "true" ]] && [[ "$src_extract" != "false" ]] + then + ynh_die "For source $source_id, expected either 'true' or 'false' for the extract parameter" fi + # (Unused?) mecanism where one can have the file in a special local cache to not have to download it... local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${src_filename}" @@ -152,7 +237,7 @@ ynh_setup_source() { src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${src_filename}" if [ "$src_format" = "docker" ]; then - src_plateform="${src_plateform:-"linux/$YNH_ARCH"}" + src_platform="${src_platform:-"linux/$YNH_ARCH"}" elif test -e "$local_src"; then cp $local_src $src_filename else @@ -199,11 +284,16 @@ ynh_setup_source() { _ynh_apply_default_permissions $dest_dir fi - if ! "$src_extract"; then - mv $src_filename $dest_dir - elif [ "$src_format" = "docker" ]; then - /usr/share/yunohost/helpers.d/vendor/docker-image-extract/docker-image-extract -p $src_plateform -o $dest_dir $src_url 2>&1 - elif [ "$src_format" = "zip" ]; then + if [[ "$src_extract" == "false" ]]; then + if [[ -z "$src_rename" ]] + then + mv $src_filename $dest_dir + else + mv $src_filename $dest_dir/$src_rename + fi + elif [[ "$src_format" == "docker" ]]; then + /usr/share/yunohost/helpers.d/vendor/docker-image-extract/docker-image-extract -p $src_platform -o $dest_dir $src_url 2>&1 + elif [[ "$src_format" == "zip" ]]; then # Zip format # Using of a temp directory, because unzip doesn't manage --strip-components if $src_in_subdir; then @@ -970,3 +1060,7 @@ _ynh_apply_default_permissions() { int_to_bool() { sed -e 's/^1$/True/g' -e 's/^0$/False/g' } + +toml_to_json() { + python3 -c 'import toml, json, sys; print(json.dumps(toml.load(sys.stdin)))' +} From 433d37b3af98b2a390df972f4b3f71cf3d08433d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 26 Feb 2023 21:26:24 +0100 Subject: [PATCH 233/319] Update locales/pl.json --- locales/pl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/pl.json b/locales/pl.json index e5944d70d..f68036c19 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -1,7 +1,7 @@ { "password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków", "app_already_up_to_date": "{app} jest obecnie aktualna", - "app_already_installed": "{app:s} jest już zainstalowana", + "app_already_installed": "{app} jest już zainstalowana", "already_up_to_date": "Nic do zrobienia. Wszystko jest obecnie aktualne.", "admin_password": "Hasło administratora", "action_invalid": "Nieprawidłowe działanie '{action:s}'", From ca59e0052c1206cb83dacf7188b19eca612b509d Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Sun, 26 Feb 2023 20:54:48 +0000 Subject: [PATCH 234/319] [CI] Reformat / remove stale translated strings --- locales/ar.json | 2 +- locales/ca.json | 1 - locales/de.json | 1 - locales/en.json | 2 +- locales/eo.json | 1 - locales/es.json | 3 +-- locales/eu.json | 3 +-- locales/fa.json | 1 - locales/fr.json | 3 +-- locales/gl.json | 3 +-- locales/it.json | 3 +-- locales/nb_NO.json | 1 - locales/oc.json | 1 - locales/pl.json | 12 ++++++------ locales/tr.json | 2 +- locales/uk.json | 1 - locales/zh_Hans.json | 3 +-- 17 files changed, 15 insertions(+), 28 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 62d392263..e34bb810b 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -246,4 +246,4 @@ "migration_0021_patching_sources_list": "تحديث ملف sources.lists…", "pattern_firstname": "يجب أن يكون اسماً أولياً صالحاً (على الأقل 3 حروف)", "yunohost_configured": "تم إعداد YunoHost الآن" -} +} \ No newline at end of file diff --git a/locales/ca.json b/locales/ca.json index 106d0af89..821e5c3eb 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -165,7 +165,6 @@ "log_available_on_yunopaste": "Aquest registre està disponible via {url}", "log_backup_restore_system": "Restaura el sistema a partir d'una còpia de seguretat", "log_backup_restore_app": "Restaura « {} » a partir d'una còpia de seguretat", - "log_remove_on_failed_restore": "Elimina « {} » després de que la restauració a partir de la còpia de seguretat hagi fallat", "log_remove_on_failed_install": "Elimina « {} » després de que la instal·lació hagi fallat", "log_domain_add": "Afegir el domini « {} » a la configuració del sistema", "log_domain_remove": "Elimina el domini « {} » de la configuració del sistema", diff --git a/locales/de.json b/locales/de.json index c666a7904..8eefa7cd9 100644 --- a/locales/de.json +++ b/locales/de.json @@ -417,7 +417,6 @@ "domain_cannot_remove_main_add_new_one": "Sie können '{domain}' nicht entfernen, da es die Hauptdomäne und Ihre einzige Domäne ist. Sie müssen zuerst eine andere Domäne mit 'yunohost domain add ' hinzufügen, dann als Hauptdomäne mit 'yunohost domain main-domain -n ' festlegen und dann können Sie die Domäne '{domain}' mit 'yunohost domain remove {domain}' entfernen'.'", "diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.", "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen.", - "log_remove_on_failed_restore": "Entfernen von '{}' nach einer fehlgeschlagenen Wiederherstellung aus einem Sicherungsarchiv", "log_backup_restore_app": "Wiederherstellen von '{}' aus einem Sicherungsarchiv", "log_backup_restore_system": "System aus einem Sicherungsarchiv wiederherstellen", "log_available_on_yunopaste": "Das Protokoll ist nun via {url} verfügbar", diff --git a/locales/en.json b/locales/en.json index 7cc1b96b6..6314282f8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -756,4 +756,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/locales/eo.json b/locales/eo.json index 13c96499b..b0bdf280b 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -191,7 +191,6 @@ "unexpected_error": "Io neatendita iris malbone: {error}", "password_listed": "Ĉi tiu pasvorto estas inter la plej uzataj pasvortoj en la mondo. Bonvolu elekti ion pli unikan.", "ssowat_conf_generated": "SSOwat-agordo generita", - "log_remove_on_failed_restore": "Forigu '{}' post malsukcesa restarigo de rezerva ar archiveivo", "dpkg_is_broken": "Vi ne povas fari ĉi tion nun ĉar dpkg/APT (la administrantoj pri pakaĵaj sistemoj) ŝajnas esti rompita stato ... Vi povas provi solvi ĉi tiun problemon per konekto per SSH kaj funkcianta `sudo dpkg --configure -a`.", "certmanager_cert_signing_failed": "Ne povis subskribi la novan atestilon", "log_tools_upgrade": "Ĝisdatigu sistemajn pakaĵojn", diff --git a/locales/es.json b/locales/es.json index d88a730bb..85d7b1f43 100644 --- a/locales/es.json +++ b/locales/es.json @@ -287,7 +287,6 @@ "log_domain_remove": "Eliminar el dominio «{}» de la configuración del sistema", "log_domain_add": "Añadir el dominio «{}» a la configuración del sistema", "log_remove_on_failed_install": "Eliminar «{}» después de una instalación fallida", - "log_remove_on_failed_restore": "Eliminar «{}» después de una restauración fallida desde un archivo de respaldo", "log_backup_restore_app": "Restaurar «{}» desde un archivo de respaldo", "log_backup_restore_system": "Restaurar sistema desde un archivo de respaldo", "log_available_on_yunopaste": "Este registro está ahora disponible vía {url}", @@ -749,4 +748,4 @@ "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 diff --git a/locales/eu.json b/locales/eu.json index 6fb35e5d6..675449fd3 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -398,7 +398,6 @@ "hook_exec_not_terminated": "Aginduak ez du behar bezala amaitu: {path}", "log_corrupted_md_file": "Erregistroei lotutako YAML metadatu fitxategia kaltetuta dago: '{md_file}\nErrorea: {error}'", "log_letsencrypt_cert_renew": "Berriztu '{}' Let's Encrypt ziurtagiria", - "log_remove_on_failed_restore": "Ezabatu '{}' babeskopia baten lehengoratzeak huts egin eta gero", "diagnosis_package_installed_from_sury_details": "Sury izena duen kanpoko biltegi batetik instalatu dira pakete batzuk, nahi gabe. YunoHosten taldeak hobekuntzak egin ditu pakete hauek kudeatzeko, baina litekeena da PHP7.3 aplikazioak Stretch sistema eragilean instalatu zituzten kasu batzuetan arazoak sortzea. Egoera hau konpontzeko, honako komando hau exekutatu beharko zenuke: {cmd_to_fix}", "log_help_to_get_log": "'{desc}' eragiketaren erregistroa ikusteko, exekutatu 'yunohost log show {name}'", "dpkg_is_broken": "Une honetan ezinezkoa da sistemaren dpkg/APT pakateen kudeatzaileek hondatutako itxura dutelako… Arazoa konpontzeko SSH bidez konektatzen saia zaitezke eta ondoren exekutatu 'sudo apt install --fix-broken' edota 'sudo dpkg --configure -a' edota 'sudo dpkg --audit'.", @@ -754,4 +753,4 @@ "global_settings_setting_dns_exposure_help": "Ohart ongi: honek gomendatutako DNS ezarpenei eta diagnostikoari eragiten die soilik. Ez du eraginik sistemaren ezarpenetan.", "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)" -} +} \ No newline at end of file diff --git a/locales/fa.json b/locales/fa.json index 92e05bdad..fe6310c5d 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -414,7 +414,6 @@ "log_domain_remove": "دامنه '{}' را از پیکربندی سیستم حذف کنید", "log_domain_add": "دامنه '{}' را به پیکربندی سیستم اضافه کنید", "log_remove_on_failed_install": "پس از نصب ناموفق '{}' را حذف کنید", - "log_remove_on_failed_restore": "پس از بازیابی ناموفق از بایگانی پشتیبان، '{}' را حذف کنید", "log_backup_restore_app": "بازیابی '{}' از بایگانی پشتیبان", "log_backup_restore_system": "بازیابی سیستم بوسیله آرشیو پشتیبان", "log_backup_create": "بایگانی پشتیبان ایجاد کنید", diff --git a/locales/fr.json b/locales/fr.json index a7069f844..cf50488cc 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -241,7 +241,6 @@ "log_available_on_yunopaste": "Le journal est désormais disponible via {url}", "log_backup_restore_system": "Restaurer le système depuis une archive de sauvegarde", "log_backup_restore_app": "Restaurer '{}' depuis une sauvegarde", - "log_remove_on_failed_restore": "Retirer '{}' après un échec de restauration depuis une archive de sauvegarde", "log_remove_on_failed_install": "Enlever '{}' après une installation échouée", "log_domain_add": "Ajouter le domaine '{}' dans la configuration du système", "log_domain_remove": "Enlever le domaine '{}' de la configuration du système", @@ -757,4 +756,4 @@ "app_change_url_failed": "Impossible de modifier l'url de {app} : {error}", "app_change_url_require_full_domain": "{app} ne peut pas être déplacée vers cette nouvelle URL car elle nécessite un domaine complet (c'est-à-dire avec un chemin = /)", "app_change_url_script_failed": "Une erreur s'est produite dans le script de modification de l'url" -} +} \ No newline at end of file diff --git a/locales/gl.json b/locales/gl.json index 9550185d1..80a94407f 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -347,7 +347,6 @@ "log_domain_remove": "Eliminar o dominio '{}' da configuración do sistema", "log_domain_add": "Engadir dominio '{}' á configuración do sistema", "log_remove_on_failed_install": "Eliminar '{}' tras unha instalación fallida", - "log_remove_on_failed_restore": "Eliminar '{}' tras un intento fallido de restablecemento desde copia", "log_backup_restore_app": "Restablecer '{}' desde unha copia de apoio", "log_backup_restore_system": "Restablecer o sistema desde unha copia de apoio", "log_backup_create": "Crear copia de apoio", @@ -754,4 +753,4 @@ "app_change_url_failed": "Non se cambiou o url para {app}: {error}", "app_change_url_require_full_domain": "{app} non se pode mover a este novo URL porque require un dominio completo propio (ex. con ruta = /)", "app_change_url_script_failed": "Algo fallou ao executar o script de cambio de url" -} +} \ No newline at end of file diff --git a/locales/it.json b/locales/it.json index 59ba0a6ba..21fb52367 100644 --- a/locales/it.json +++ b/locales/it.json @@ -233,7 +233,6 @@ "log_available_on_yunopaste": "Questo registro è ora disponibile via {url}", "log_backup_restore_system": "Ripristina sistema da un archivio di backup", "log_backup_restore_app": "Ripristina '{}' da un archivio di backup", - "log_remove_on_failed_restore": "Rimuovi '{}' dopo un ripristino fallito da un archivio di backup", "log_remove_on_failed_install": "Rimuovi '{}' dopo un'installazione fallita", "log_domain_add": "Aggiungi il dominio '{}' nella configurazione di sistema", "log_domain_remove": "Rimuovi il dominio '{}' dalla configurazione di sistema", @@ -640,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 diff --git a/locales/nb_NO.json b/locales/nb_NO.json index d74f47728..8cacaff6d 100644 --- a/locales/nb_NO.json +++ b/locales/nb_NO.json @@ -29,7 +29,6 @@ "downloading": "Laster ned…", "dyndns_could_not_check_available": "Kunne ikke sjekke om {domain} er tilgjengelig på {provider}.", "mail_domain_unknown": "Ukjent e-postadresse for domenet '{domain}'", - "log_remove_on_failed_restore": "Fjern '{}' etter mislykket gjenoppretting fra sikkerhetskopiarkiv", "log_letsencrypt_cert_install": "Installer et Let's Encrypt-sertifikat på '{}'-domenet", "log_letsencrypt_cert_renew": "Forny '{}'-Let's Encrypt-sertifikat", "log_user_update": "Oppdater brukerinfo for '{}'", diff --git a/locales/oc.json b/locales/oc.json index 6282a6cec..eb142879c 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -238,7 +238,6 @@ "log_available_on_yunopaste": "Lo jornal es ara disponible via {url}", "log_backup_restore_system": "Restaurar lo sistèma a partir d’una salvagarda", "log_backup_restore_app": "Restaurar « {} » a partir d’una salvagarda", - "log_remove_on_failed_restore": "Levar « {} » aprèp un fracàs de restauracion a partir d’una salvagarda", "log_remove_on_failed_install": "Tirar « {} » aprèp una installacion pas reüssida", "log_domain_add": "Ajustar lo domeni « {} » dins la configuracion sistèma", "log_domain_remove": "Tirar lo domeni « {} » d’a la configuracion sistèma", diff --git a/locales/pl.json b/locales/pl.json index f68036c19..2631b42ca 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -74,7 +74,7 @@ "additional_urls_already_removed": "Dodatkowy URL '{url}' już usunięty w dodatkowym URL dla uprawnienia '{permission}'", "additional_urls_already_added": "Dodatkowy URL '{url}' już dodany w dodatkowym URL dla uprawnienia '{permission}'", "app_arch_not_supported": "Ta aplikacja może być zainstalowana tylko na architekturach {required}, a twoja architektura serwera to {current}", - "app_argument_invalid": "Wybierz poprawną wartość dla argumentu '{name}': {błąd}", + "app_argument_invalid": "Wybierz poprawną wartość dla argumentu '{name}': {error}", "all_users": "Wszyscy użytkownicy YunoHost", "app_action_failed": "Nie udało się uruchomić akcji {action} dla aplikacji {app}", "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`", @@ -82,12 +82,12 @@ "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_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 („{domena}{ścieżka}”), nic nie trzeba robić.", + "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.", "app_config_unable_to_apply": "Nie udało się zastosować wartości panelu konfiguracji.", - "app_install_failed": "Nie udało się zainstalować {aplikacji}: {błąd}", + "app_install_failed": "Nie udało się zainstalować {app}: {error}", "apps_catalog_failed_to_download": "Nie można pobrać katalogu aplikacji app catalog: {error}", - "app_argument_required": "Argument „{nazwa}” jest wymagany", + "app_argument_required": "Argument „{name}” jest wymagany", "app_not_properly_removed": "Aplikacja {app} nie została poprawnie usunięta", "app_upgrade_failed": "Nie można uaktualnić {app}: {error}", "backup_abstract_method": "Ta metoda tworzenia kopii zapasowych nie została jeszcze zaimplementowana", @@ -137,7 +137,7 @@ "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_restore_failed": "Nie można przywrócić {aplikacji}: {error}", + "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.", "app_resource_failed": "Nie udało się zapewnić, anulować obsługi administracyjnej lub zaktualizować zasobów aplikacji {app}: {error}", @@ -156,4 +156,4 @@ "ask_admin_fullname": "Pełne imię i nazwisko administratora", "app_change_url_failed": "Nie udało się zmienić adresu URL aplikacji {app}: {error}", "app_change_url_script_failed": "Wystąpił błąd w skrypcie zmiany adresu URL" -} +} \ No newline at end of file diff --git a/locales/tr.json b/locales/tr.json index 43a489d01..1af0ffd54 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -16,4 +16,4 @@ "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 diff --git a/locales/uk.json b/locales/uk.json index 3c960e9fa..0cac77575 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -191,7 +191,6 @@ "log_domain_remove": "Вилучення домену '{}' з конфігурації системи", "log_domain_add": "Додавання домену '{}' в конфігурацію системи", "log_remove_on_failed_install": "Вилучення '{}' після невдалого встановлення", - "log_remove_on_failed_restore": "Вилучення '{}' після невдалого відновлення з резервного архіву", "log_backup_restore_app": "Відновлення '{}' з архіву резервних копій", "log_backup_restore_system": "Відновлення системи з резервного архіву", "log_backup_create": "Створення резервного архіву", diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index f73b16757..18c6430c0 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -481,7 +481,6 @@ "log_domain_remove": "从系统配置中删除 '{}' 域", "log_domain_add": "将 '{}'域添加到系统配置中", "log_remove_on_failed_install": "安装失败后删除 '{}'", - "log_remove_on_failed_restore": "从备份存档还原失败后,删除 '{}'", "log_backup_restore_app": "从备份存档还原 '{}'", "log_backup_restore_system": "从备份档案还原系统", "permission_currently_allowed_for_all_users": "这个权限目前除了授予其他组以外,还授予所有用户。您可能想删除'all_users'权限或删除目前授予它的其他组。", @@ -592,4 +591,4 @@ "ask_admin_fullname": "管理员全名", "ask_admin_username": "管理员用户名", "ask_fullname": "全名" -} +} \ No newline at end of file From 7631d380fb88e9e3c5da39d14522aa00ddb16737 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 27 Feb 2023 17:08:00 +0100 Subject: [PATCH 235/319] helpers: more robust way to grep that the service correctly started ? --- helpers/systemd | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/helpers/systemd b/helpers/systemd index 06551d2b3..761e818ad 100644 --- a/helpers/systemd +++ b/helpers/systemd @@ -61,7 +61,7 @@ ynh_remove_systemd_config() { # | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started. # | arg: -p, --log_path= - Log file - Path to the log file. Default : `/var/log/$app/$app.log` # | arg: -t, --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 300 seconds. -# | arg: -e, --length= - Length of the error log : Default : 20 +# | arg: -e, --length= - Length of the error log displayed for debugging : Default : 20 # # Requires YunoHost version 3.5.0 or higher. ynh_systemd_action() { @@ -110,6 +110,8 @@ ynh_systemd_action() { action="reload-or-restart" fi + local time_start="$(date --utc --rfc-3339=seconds | cut -d+ -f1) UTC" + # If the service fails to perform the action if ! systemctl $action $service_name; then # Show syslog for this service @@ -128,9 +130,17 @@ ynh_systemd_action() { local i=0 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 grep --extended-regexp --quiet "$line_match" "$templog"; then - ynh_print_info --message="The service $service_name has correctly executed the action ${action}." - break + if [ "$log_path" == "systemd" ]; then + # For systemd services, we in fact dont rely on the templog, which for some reason is not reliable, but instead re-read journalctl every iteration, starting at the timestamp where we triggered the action + if journalctl --unit=$service_name --since="$time_start" --quiet --no-pager --no-hostname | grep --extended-regexp --quiet "$line_match"; then + ynh_print_info --message="The service $service_name has correctly executed the action ${action}." + break + fi + else + if grep --extended-regexp --quiet "$line_match" "$templog"; then + ynh_print_info --message="The service $service_name has correctly executed the action ${action}." + break + fi fi if [ $i -eq 30 ]; then echo "(this may take some time)" >&2 From e03f609e9bc2b8ffc09d42fce2bfdf732c62802f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 27 Feb 2023 19:30:18 +0100 Subject: [PATCH 236/319] helpers: tweak behavior of checksum helper in CI context to help debug why file appear as 'manually modified' --- helpers/backup | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/helpers/backup b/helpers/backup index 22737ff86..1aa43240c 100644 --- a/helpers/backup +++ b/helpers/backup @@ -327,6 +327,12 @@ ynh_store_file_checksum() { ynh_app_setting_set --app=$app --key=$checksum_setting_name --value=$(md5sum "$file" | cut --delimiter=' ' --fields=1) + if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then + # Using a base64 is in fact more reversible than "replace / and space by _" ... So we can in fact obtain the original file path in an easy reliable way ... + local file_path_base64=$(echo "$file" | base64) + cat $file > /var/cache/yunohost/appconfbackup/original_${file_path_base64} + fi + # If backup_file_checksum isn't empty, ynh_backup_if_checksum_is_different has made a backup if [ -n "${backup_file_checksum-}" ]; then # Print the diff between the previous file and the new one. @@ -361,11 +367,20 @@ ynh_backup_if_checksum_is_different() { backup_file_checksum="" if [ -n "$checksum_value" ]; then # Proceed only if a value was stored into the app settings if [ -e $file ] && ! echo "$checksum_value $file" | md5sum --check --status; then # If the checksum is now different + backup_file_checksum="/var/cache/yunohost/appconfbackup/$file.backup.$(date '+%Y%m%d.%H%M%S')" mkdir --parents "$(dirname "$backup_file_checksum")" cp --archive "$file" "$backup_file_checksum" # Backup the current file ynh_print_warn "File $file has been manually modified since the installation or last upgrade. So it has been duplicated in $backup_file_checksum" echo "$backup_file_checksum" # Return the name of the backup file + if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then + local file_path_base64=$(echo "$file" | base64) + if test -e /var/cache/yunohost/appconfbackup/original_${file_path_base64} + then + ynh_print_warn "Diff with the original file:" + diff --report-identical-files --unified --color=always /var/cache/yunohost/appconfbackup/original_${file_path_base64} $file >&2 || true + fi + fi fi fi } From 8701d8ec6268eee95016d54a59eb8aa9f172fe1d Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 28 Feb 2023 22:58:17 +0100 Subject: [PATCH 237/319] Handle undefined main permission url --- src/utils/resources.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index cff6c6b19..6e415d2fd 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -320,6 +320,9 @@ class PermissionsResource(AppResource): def __init__(self, properties: Dict[str, Any], *args, **kwargs): # FIXME : if url != None, we should check that there's indeed a domain/path defined ? ie that app is a webapp + if "main" not in properties: + properties["main"] = self.default_perm_properties + for perm, infos in properties.items(): properties[perm] = copy.copy(self.default_perm_properties) properties[perm].update(infos) @@ -327,11 +330,12 @@ class PermissionsResource(AppResource): properties[perm]["show_tile"] = bool(properties[perm]["url"]) if ( - not isinstance(properties["main"].get("url"), str) - or properties["main"]["url"] != "/" + properties["main"]["url"] is not None + and ( not isinstance(properties["main"].get("url"), str) + or properties["main"]["url"] != "/" ) ): raise YunohostError( - "URL for the 'main' permission should be '/' for webapps (or undefined/None for non-webapps). Note that / refers to the install url of the app, i.e $domain.tld/$path/", + "URL for the 'main' permission should be '/' for webapps (or left undefined for non-webapps). Note that / refers to the install url of the app, i.e $domain.tld/$path/", raw_msg=True, ) From 28610669ed246e92a76cedff325ee45fd35424cb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 28 Feb 2023 23:10:06 +0100 Subject: [PATCH 238/319] Update changelog for 11.1.12 --- debian/changelog | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/debian/changelog b/debian/changelog index 14f2ee73d..aa04a1fe3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,21 @@ +yunohost (11.1.12) stable; urgency=low + + - apps: add '--continue-on-failure' to 'yunohost app upgrade ([#1602](https://github.com/yunohost/yunohost/pull/1602)) + - appsv2: Create parent dirs when provisioning install_dir ([#1609](https://github.com/yunohost/yunohost/pull/1609)) + - appsv2: set `w` as default permission on `install_dir` folder ([#1611](https://github.com/yunohost/yunohost/pull/1611)) + - appsv2: Handle undefined main permission url ([#1620](https://github.com/yunohost/yunohost/pull/1620)) + - apps/helpers: tweak behavior of checksum helper in CI context to help debug why file appear as 'manually modified' ([#1618](https://github.com/yunohost/yunohost/pull/1618)) + - apps/helpers: more robust way to grep that the service correctly started ? ([#1617](https://github.com/yunohost/yunohost/pull/1617)) + - regenconf: sometimes ntp doesnt exist (97c0128c) + - nginx/security: fix empty webadmin allowlist breaking nginx conf... (e458d881) + - misc: automatic get rid of /etc/profile.d/check_yunohost_is_installed.sh when yunohost is postinstalled (20e8805e) + - settings: Fix pop3_enabled parsing returning 0/1 instead of True/False ... (b40c0de3) + - [i18n] Translations updated for French, Galician, Italian, Polish + + Thanks to all contributors <3 ! (Éric Gaspar, John Schmidt, José M, Krakinou, Kuba Bazan, Laurent Peuch, ppr, tituspijean) + + -- Alexandre Aubin Tue, 28 Feb 2023 23:08:02 +0100 + yunohost (11.1.11.2) stable; urgency=low - Rebump version to flag as stable, not testing >_> From 76ff5b1844e1781dc6aaa978a63cac3187cb4e83 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Wed, 1 Mar 2023 00:47:18 +0000 Subject: [PATCH 239/319] [CI] Format code with Black --- src/app.py | 27 +++++++++++++++--- src/tests/test_apps.py | 65 +++++++++++++++++++++++++++++------------- src/utils/resources.py | 9 +++--- 3 files changed, 72 insertions(+), 29 deletions(-) diff --git a/src/app.py b/src/app.py index f17c46929..6a7e49e04 100644 --- a/src/app.py +++ b/src/app.py @@ -534,7 +534,14 @@ def app_change_url(operation_logger, app, domain, path): hook_callback("post_app_change_url", env=env_dict) -def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False, continue_on_failure=False): +def app_upgrade( + app=[], + url=None, + file=None, + force=False, + no_safety_backup=False, + continue_on_failure=False, +): """ Upgrade app @@ -857,8 +864,16 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False else: operation_logger.close() - logger.error(m18n.n("app_failed_to_upgrade_but_continue", failed_app=app_instance_name, operation_logger_name=operation_logger.name)) - failed_to_upgrade_apps.append((app_instance_name, operation_logger.name)) + logger.error( + m18n.n( + "app_failed_to_upgrade_but_continue", + failed_app=app_instance_name, + operation_logger_name=operation_logger.name, + ) + ) + failed_to_upgrade_apps.append( + (app_instance_name, operation_logger.name) + ) # Otherwise we're good and keep going ! now = int(time.time()) @@ -923,7 +938,11 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False if failed_to_upgrade_apps: apps = "" for app_id, operation_logger_name in failed_to_upgrade_apps: - apps += m18n.n("apps_failed_to_upgrade_line", app_id=app_id, operation_logger_name=operation_logger_name) + apps += m18n.n( + "apps_failed_to_upgrade_line", + app_id=app_id, + operation_logger_name=operation_logger_name, + ) logger.warning(m18n.n("apps_failed_to_upgrade", apps=apps)) diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index 830aabf61..747eb5dcd 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -548,37 +548,51 @@ class TestMockedAppUpgrade: This class is here to test the logical workflow of app_upgrade and thus mock nearly all side effects """ + def setup_method(self, method): self.apps_list = [] self.upgradable_apps_list = [] def _mock_app_upgrade(self, mocker): # app list - self._installed_apps = mocker.patch("yunohost.app._installed_apps", side_effect=lambda: self.apps_list) + self._installed_apps = mocker.patch( + "yunohost.app._installed_apps", side_effect=lambda: self.apps_list + ) # just check if an app is really installed - mocker.patch("yunohost.app._is_installed", side_effect=lambda app: app in self.apps_list) + mocker.patch( + "yunohost.app._is_installed", side_effect=lambda app: app in self.apps_list + ) # app_dict = - mocker.patch("yunohost.app.app_info", side_effect=lambda app, full: { - "upgradable": "yes" if app in self.upgradable_apps_list else "no", - "manifest": {"id": app}, - "version": "?", - }) + mocker.patch( + "yunohost.app.app_info", + side_effect=lambda app, full: { + "upgradable": "yes" if app in self.upgradable_apps_list else "no", + "manifest": {"id": app}, + "version": "?", + }, + ) def custom_extract_app(app): - return ({ - "version": "?", - "packaging_format": 1, - "id": app, - "notifications": {"PRE_UPGRADE": None, "POST_UPGRADE": None}, - }, "MOCKED_BY_TEST") + return ( + { + "version": "?", + "packaging_format": 1, + "id": app, + "notifications": {"PRE_UPGRADE": None, "POST_UPGRADE": None}, + }, + "MOCKED_BY_TEST", + ) # return (manifest, extracted_app_folder) mocker.patch("yunohost.app._extract_app", side_effect=custom_extract_app) # for [(name, passed, values, err), ...] in - mocker.patch("yunohost.app._check_manifest_requirements", return_value=[(None, True, None, None)]) + mocker.patch( + "yunohost.app._check_manifest_requirements", + return_value=[(None, True, None, None)], + ) # raise on failure mocker.patch("yunohost.app._assert_system_is_sane_for_app", return_value=True) @@ -593,12 +607,15 @@ class TestMockedAppUpgrade: mocker.patch("os.path.exists", side_effect=custom_os_path_exists) # manifest = - mocker.patch("yunohost.app.read_toml", return_value={ - "arguments": {"install": []} - }) + mocker.patch( + "yunohost.app.read_toml", return_value={"arguments": {"install": []}} + ) # install_failed, failure_message_with_debug_instructions = - self.hook_exec_with_script_debug_if_failure = mocker.patch("yunohost.hook.hook_exec_with_script_debug_if_failure", return_value=(False, "")) + self.hook_exec_with_script_debug_if_failure = mocker.patch( + "yunohost.hook.hook_exec_with_script_debug_if_failure", + return_value=(False, ""), + ) # settings = mocker.patch("yunohost.app._get_app_settings", return_value={}) # return nothing @@ -644,7 +661,12 @@ class TestMockedAppUpgrade: app_upgrade() self.hook_exec_with_script_debug_if_failure.assert_called_once() - assert self.hook_exec_with_script_debug_if_failure.call_args.kwargs["env"]["YNH_APP_ID"] == "some_app" + assert ( + self.hook_exec_with_script_debug_if_failure.call_args.kwargs["env"][ + "YNH_APP_ID" + ] + == "some_app" + ) def test_app_upgrade_continue_on_failure(self, mocker): self._mock_app_upgrade(mocker) @@ -682,7 +704,10 @@ class TestMockedAppUpgrade: raise Exception() return True - mocker.patch("yunohost.app._assert_system_is_sane_for_app", side_effect=_assert_system_is_sane_for_app) + mocker.patch( + "yunohost.app._assert_system_is_sane_for_app", + side_effect=_assert_system_is_sane_for_app, + ) with pytest.raises(YunohostError): app_upgrade() diff --git a/src/utils/resources.py b/src/utils/resources.py index 6e415d2fd..35d36da68 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -322,17 +322,16 @@ class PermissionsResource(AppResource): if "main" not in properties: properties["main"] = self.default_perm_properties - + for perm, infos in properties.items(): properties[perm] = copy.copy(self.default_perm_properties) properties[perm].update(infos) if properties[perm]["show_tile"] is None: properties[perm]["show_tile"] = bool(properties[perm]["url"]) - if ( - properties["main"]["url"] is not None - and ( not isinstance(properties["main"].get("url"), str) - or properties["main"]["url"] != "/" ) + if properties["main"]["url"] is not None and ( + not isinstance(properties["main"].get("url"), str) + or properties["main"]["url"] != "/" ): raise YunohostError( "URL for the 'main' permission should be '/' for webapps (or left undefined for non-webapps). Note that / refers to the install url of the app, i.e $domain.tld/$path/", From c24c0a2ae19e643db93d50dac8c4e96f0b3a41e8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 1 Mar 2023 08:06:15 +0100 Subject: [PATCH 240/319] helper: fix previous tweak about debugging diff for manually modified files on the CI @_@ --- helpers/backup | 1 + 1 file changed, 1 insertion(+) diff --git a/helpers/backup b/helpers/backup index 1aa43240c..3dee33de0 100644 --- a/helpers/backup +++ b/helpers/backup @@ -330,6 +330,7 @@ ynh_store_file_checksum() { if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then # Using a base64 is in fact more reversible than "replace / and space by _" ... So we can in fact obtain the original file path in an easy reliable way ... local file_path_base64=$(echo "$file" | base64) + mkdir -p /var/cache/yunohost/appconfbackup/ cat $file > /var/cache/yunohost/appconfbackup/original_${file_path_base64} fi From 59607ab33a5f35cf5c40df624a722540304582a0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 1 Mar 2023 08:09:24 +0100 Subject: [PATCH 241/319] Update changelog for 11.1.12.1 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index aa04a1fe3..365a48de3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (11.1.12.1) stable; urgency=low + + - helper: fix previous tweak about debugging diff for manually modified files on the CI @_@ (fd304008) + + -- Alexandre Aubin Wed, 01 Mar 2023 08:08:55 +0100 + yunohost (11.1.12) stable; urgency=low - apps: add '--continue-on-failure' to 'yunohost app upgrade ([#1602](https://github.com/yunohost/yunohost/pull/1602)) From d04f2085de3345189dfa3ca19e04aa9602a6e149 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 1 Mar 2023 22:12:27 +0100 Subject: [PATCH 242/319] helpers: omg base64 wraps the output by default :| --- helpers/backup | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/backup b/helpers/backup index 3dee33de0..ade3ce5e5 100644 --- a/helpers/backup +++ b/helpers/backup @@ -329,7 +329,7 @@ ynh_store_file_checksum() { if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then # Using a base64 is in fact more reversible than "replace / and space by _" ... So we can in fact obtain the original file path in an easy reliable way ... - local file_path_base64=$(echo "$file" | base64) + local file_path_base64=$(echo "$file" | base64 -w0) mkdir -p /var/cache/yunohost/appconfbackup/ cat $file > /var/cache/yunohost/appconfbackup/original_${file_path_base64} fi @@ -375,7 +375,7 @@ ynh_backup_if_checksum_is_different() { ynh_print_warn "File $file has been manually modified since the installation or last upgrade. So it has been duplicated in $backup_file_checksum" echo "$backup_file_checksum" # Return the name of the backup file if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then - local file_path_base64=$(echo "$file" | base64) + local file_path_base64=$(echo "$file" | base64 -w0) if test -e /var/cache/yunohost/appconfbackup/original_${file_path_base64} then ynh_print_warn "Diff with the original file:" From 74180ded2279293cecb18cdedd6df8a0c0c90508 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 1 Mar 2023 22:13:34 +0100 Subject: [PATCH 243/319] Update changelog for 11.1.12.2 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 365a48de3..fdd2ac8cc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (11.1.12.2) stable; urgency=low + + - helpers: omg base64 wraps the output by default :| (d04f2085) + + -- Alexandre Aubin Wed, 01 Mar 2023 22:12:51 +0100 + yunohost (11.1.12.1) stable; urgency=low - helper: fix previous tweak about debugging diff for manually modified files on the CI @_@ (fd304008) From 030d876329da3974c8e651aad44dfb81533bda15 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 2 Mar 2023 18:40:56 +0100 Subject: [PATCH 244/319] trying to fix _port_is_used --- 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 35d36da68..95118a010 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -916,8 +916,8 @@ class PortsResource(AppResource): % port ) # This second command is mean to cover (most) case where an app is using a port yet ain't currently using it for some reason (typically service ain't up) - cmd2 = f"grep --quiet \"port: '{port}'\" /etc/yunohost/apps/*/settings.yml" - return os.system(cmd1) == 0 and os.system(cmd2) == 0 + cmd2 = f"grep --quiet --extended-regexp \"port: '?{port}'?\" /etc/yunohost/apps/*/settings.yml" + return os.system(cmd1) == 0 or os.system(cmd2) == 0 def provision_or_update(self, context: Dict = {}): from yunohost.firewall import firewall_allow, firewall_disallow From 729868429a500993cdae6f599f03110830ec3195 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Mar 2023 22:54:37 +0100 Subject: [PATCH 245/319] appsv2: when hydrating template, the data may be not-string, eg ports are int --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 6a7e49e04..7fc74a4cb 100644 --- a/src/app.py +++ b/src/app.py @@ -2233,7 +2233,7 @@ def _hydrate_app_template(template, data): varname = stuff.strip("_").lower() if varname in data: - template = template.replace(stuff, data[varname]) + template = template.replace(stuff, str(data[varname])) return template From 3469440ec389241a55b1e8a6c195d9e1fbd4c839 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Thu, 2 Mar 2023 04:31:42 +0000 Subject: [PATCH 246/319] Translated using Weblate (Arabic) Currently translated at 28.3% (216 of 762 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index e34bb810b..2067db43f 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -245,5 +245,6 @@ "migration_0021_main_upgrade": "بداية التحديث الرئيسي…", "migration_0021_patching_sources_list": "تحديث ملف sources.lists…", "pattern_firstname": "يجب أن يكون اسماً أولياً صالحاً (على الأقل 3 حروف)", - "yunohost_configured": "تم إعداد YunoHost الآن" -} \ No newline at end of file + "yunohost_configured": "تم إعداد YunoHost الآن", + "global_settings_setting_backup_compress_tar_archives": "ضغط النُسخ الاحتياطية" +} From 6fe0ed919d18e8c10c3b4cc3d522985a1da3b7a5 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 3 Mar 2023 08:09:09 +0000 Subject: [PATCH 247/319] Translated using Weblate (German) Currently translated at 89.6% (683 of 762 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/locales/de.json b/locales/de.json index 8eefa7cd9..2b7ee0456 100644 --- a/locales/de.json +++ b/locales/de.json @@ -692,5 +692,17 @@ "domain_config_cert_summary_abouttoexpire": "Das aktuelle Zertifikat läuft bald ab. Es sollte bald automatisch erneuert werden.", "domain_config_cert_summary_expired": "ACHTUNG: Das aktuelle Zertifikat ist nicht gültig! HTTPS wird gar nicht funktionieren!", "domain_config_cert_summary_letsencrypt": "Toll! Sie benutzen ein gültiges Let's Encrypt-Zertifikat!", - "domain_config_cert_summary_ok": "Gut, das aktuelle Zertifikat sieht gut aus!" -} \ No newline at end of file + "domain_config_cert_summary_ok": "Gut, das aktuelle Zertifikat sieht gut aus!", + "app_change_url_require_full_domain": "{app} kann nicht auf diese neue URL verschoben werden, weil sie eine vollständige eigene Domäne benötigt (z.B. mit Pfad = /)", + "app_not_upgraded_broken_system_continue": "Die App '{failed_app}' konnte nicht aktualisiert werden und hat Ihr System in einen beschädigten Zustand versetzt (folglich wird --continue-on-failure ignoriert) und als Konsequenz wurde die Aktualisierung der folgenden Apps abgelehnt: {apps}", + "app_yunohost_version_not_supported": "Diese App setzt YunoHost >= {required} voraus aber die gegenwärtig installierte Version ist {current}", + "app_failed_to_upgrade_but_continue": "Die App {failed_app} konnte nicht aktualisiert werden und es wird anforderungsgemäss zur nächsten Aktualisierung fortgefahren. Starten sie 'yunohost log show {operation_logger_name}' um den Fehlerbericht zu sehen", + "app_not_upgraded_broken_system": "Die App '{failed_app}' konnte nicht aktualisiert werden und hat Ihr System in einen beschädigten Zustand versetzt und als Konzequenz wurde die Aktualisierung der folgenden Apps abgelehnt: {apps}", + "apps_failed_to_upgrade": "Diese Apps konnten nicht aktualisiert werden: {apps}", + "app_arch_not_supported": "Diese App kann nur auf bestimmten Architekturen {required} installiert werden, aber Ihre gegenwärtige Serverarchitektur ist {current}", + "app_not_enough_disk": "Diese App benötigt {required} freien Speicherplatz.", + "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}" +} From bb30b43814b49887dfa74884746da1c307b4a422 Mon Sep 17 00:00:00 2001 From: ppr Date: Wed, 1 Mar 2023 19:30:42 +0000 Subject: [PATCH 248/319] Translated using Weblate (French) Currently translated at 99.4% (758 of 762 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index cf50488cc..c5a09d996 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -755,5 +755,10 @@ "domain_config_xmpp_help": "NB : certaines fonctions XMPP nécessiteront la mise à jour de vos enregistrements DNS et la régénération de votre certificat Lets Encrypt pour être activées", "app_change_url_failed": "Impossible de modifier l'url de {app} : {error}", "app_change_url_require_full_domain": "{app} ne peut pas être déplacée vers cette nouvelle URL car elle nécessite un domaine complet (c'est-à-dire avec un chemin = /)", - "app_change_url_script_failed": "Une erreur s'est produite dans le script de modification de l'url" -} \ No newline at end of file + "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é, continuez 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}", + "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}')" +} From 4f11e8fe3469dd572e82783fc1d942a82370c808 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Thu, 2 Mar 2023 04:36:38 +0000 Subject: [PATCH 249/319] Translated using Weblate (Occitan) Currently translated at 40.2% (307 of 762 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/oc/ --- locales/oc.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index eb142879c..bdc9f5360 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -379,7 +379,7 @@ "diagnosis_services_bad_status": "Lo servici {service} es {status} :(", "diagnosis_swap_ok": "Lo sistèma a {total} d’escambi !", "diagnosis_regenconf_allgood": "Totes los fichièrs de configuracion son confòrmes a la configuracion recomandada !", - "diagnosis_regenconf_manually_modified": "Lo fichièr de configuracion {file} foguèt modificat manualament.", + "diagnosis_regenconf_manually_modified": "Lo fichièr de configuracion {file} foguèt modificat manualament.", "diagnosis_regenconf_manually_modified_details": "Es probablament bon tan que sabètz çò que fasètz ;) !", "diagnosis_security_vulnerable_to_meltdown": "Semblatz èsser vulnerable a la vulnerabilitat de seguretat critica de Meltdown", "diagnosis_description_basesystem": "Sistèma de basa", @@ -469,4 +469,4 @@ "global_settings_setting_user_strength": "Fòrça del senhal utilizaire", "global_settings_setting_postfix_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor Postfix. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", "global_settings_setting_ssh_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor SSH. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)" -} \ No newline at end of file +} From d3fb090d4f3e763d99d6f338f693ff411d59073b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Wed, 1 Mar 2023 05:56:08 +0000 Subject: [PATCH 250/319] Translated using Weblate (Galician) Currently translated at 100.0% (762 of 762 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/gl.json b/locales/gl.json index 80a94407f..2b9e89ffb 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -752,5 +752,13 @@ "domain_config_xmpp_help": "Nota: algunhas características de XMPP para ser utilizadas precisan que teñas ao día os rexistros DNS e rexeneres os certificados Lets Encrypt", "app_change_url_failed": "Non se cambiou o url para {app}: {error}", "app_change_url_require_full_domain": "{app} non se pode mover a este novo URL porque require un dominio completo propio (ex. con ruta = /)", - "app_change_url_script_failed": "Algo fallou ao executar o script de cambio de url" -} \ No newline at end of file + "app_change_url_script_failed": "Algo fallou ao executar o script de cambio de url", + "apps_failed_to_upgrade_line": "\n * {app_id} (para ver o rexistro correspondente executa 'yunohost log show {operation_logger_name}')", + "app_failed_to_upgrade_but_continue": "Fallou a actualización de {failed_app}, seguimos coas demáis actualizacións. Executa 'yunohost log show {operation_logger_name}' para ver o rexistro do fallo", + "app_not_upgraded_broken_system": "Fallou a actualización de '{failed_app}' e estragou o sistema, como consecuencia cancelouse a actualización das seguintes apps: {apps}", + "app_not_upgraded_broken_system_continue": "Fallou a actualización de '{failed_app}' e estragou o sistema (polo que ignórase --continue-on-failure), como consecuencia cancelouse a actualización das seguintes apps: {apps}", + "apps_failed_to_upgrade": "Fallou a actualización das seguintes aplicacións:{apps}", + "invalid_shell": "Intérprete de ordes non válido: {shell}", + "log_resource_snippet": "Aprovisionamento/desaprovisionamento/actualización dun recurso", + "app_resource_failed": "Fallou o aprovisionamento, desaprovisionamento ou actualización de recursos para {app}: {error}" +} From 130bd4def209f71b137d24d528fa478fda3cee24 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Mar 2023 22:56:41 +0100 Subject: [PATCH 251/319] 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 c5a09d996..b08b142c5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -756,7 +756,7 @@ "app_change_url_failed": "Impossible de modifier l'url de {app} : {error}", "app_change_url_require_full_domain": "{app} ne peut pas être déplacée vers cette nouvelle URL car elle nécessite un domaine complet (c'est-à-dire avec un chemin = /)", "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é, continuez avec les mises à jour suivantes comme demandé. Lancez 'yunohost log show {operation_logger_name}' pour voir le journal des échecs", + "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}", "apps_failed_to_upgrade": "Ces applications n'ont pas pu être mises à jour : {apps}", From 756b0930c2a179f0c2b8b0582c10e692a17861a2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 3 Mar 2023 22:58:03 +0100 Subject: [PATCH 252/319] Update changelog for 11.1.13 --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index fdd2ac8cc..9f3a685a1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (11.1.13) stable; urgency=low + + - appsv2: fix port already used detection ([#1622](https://github.com/yunohost/yunohost/pull/1622)) + - appsv2: when hydrating template, the data may be not-string, eg ports are int (72986842) + - [i18n] Translations updated for Arabic, French, Galician, German, Occitan + + Thanks to all contributors <3 ! (ButterflyOfFire, Christian Wehrli, José M, Kay0u, ppr) + + -- Alexandre Aubin Fri, 03 Mar 2023 22:57:14 +0100 + yunohost (11.1.12.2) stable; urgency=low - helpers: omg base64 wraps the output by default :| (d04f2085) From 8731f77aa9fb3c26504db0f594f8f1d0364fb852 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 4 Mar 2023 21:35:35 +0100 Subject: [PATCH 253/319] helpers: simplify --time display option for ynh_script_progression .. we don't care about displaying time when below 10 sc --- helpers/logging | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/logging b/helpers/logging index 4601e0b39..ae9c24ea9 100644 --- a/helpers/logging +++ b/helpers/logging @@ -308,8 +308,8 @@ ynh_script_progression() { local progression_bar="${progress_string2:0:$effective_progression}${progress_string1:0:$expected_progression}${progress_string0:0:$left_progression}" local print_exec_time="" - if [ $time -eq 1 ]; then - print_exec_time=" [$(date +%Hh%Mm,%Ss --date="0 + $exec_time sec")]" + if [ $time -eq 1 ] && [ "$exec_time" -gt 10 ]; then + print_exec_time=" [$(bc <<< 'scale=1; 12345 / 60' ) minutes]" fi ynh_print_info "[$progression_bar] > ${message}${print_exec_time}" From 091f7de827e10ff74b3259738161a471424a5c48 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 4 Mar 2023 21:40:59 +0100 Subject: [PATCH 254/319] Typo >_> --- helpers/logging | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/logging b/helpers/logging index ae9c24ea9..82cb2814a 100644 --- a/helpers/logging +++ b/helpers/logging @@ -309,7 +309,7 @@ ynh_script_progression() { local print_exec_time="" if [ $time -eq 1 ] && [ "$exec_time" -gt 10 ]; then - print_exec_time=" [$(bc <<< 'scale=1; 12345 / 60' ) minutes]" + print_exec_time=" [$(bc <<< 'scale=1; $exec_time / 60' ) minutes]" fi ynh_print_info "[$progression_bar] > ${message}${print_exec_time}" From 4102d626e5e381dd57887acca4000d37bb2e1be4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Mar 2023 19:57:12 +0100 Subject: [PATCH 255/319] appsv2/sources: change 'sources.toml' to a new 'sources' app resource instead --- helpers/utils | 55 ++++++++++------ src/app.py | 6 +- src/backup.py | 1 + src/utils/resources.py | 143 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 182 insertions(+), 23 deletions(-) diff --git a/helpers/utils b/helpers/utils index d958ae02e..3ef7c2246 100644 --- a/helpers/utils +++ b/helpers/utils @@ -160,17 +160,19 @@ ynh_setup_source() { keep="${keep:-}" full_replace="${full_replace:-0}" - if test -e $YNH_APP_BASEDIR/sources.toml + if test -e $YNH_APP_BASEDIR/manifest.toml && cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq -e '.resources.sources' >/dev/null then source_id="${source_id:-main}" - local sources_json=$(cat $YNH_APP_BASEDIR/sources.toml | toml_to_json) - if [[ "$(echo "$sources_json" | jq -r ".$source_id.autoswitch_per_arch")" == "true" ]] + local sources_json=$(cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq '.resources.sources') + if ! echo "$sources_json" | jq -re ".$source_id.url" then - source_id=$source_id.$YNH_ARCH + local arch_prefix=".$YNH_ARCH" + else + local arch_prefix="" fi - local src_url="$(echo "$sources_json" | jq -r ".$source_id.url" | sed 's/^null$//')" - local src_sum="$(echo "$sources_json" | jq -r ".$source_id.sha256" | sed 's/^null$//')" + local src_url="$(echo "$sources_json" | jq -r ".$source_id$arch_prefix.url" | sed 's/^null$//')" + local src_sum="$(echo "$sources_json" | jq -r ".$source_id$arch_prefix.sha256" | sed 's/^null$//')" local src_sumprg="sha256sum" local src_format="$(echo "$sources_json" | jq -r ".$source_id.format" | sed 's/^null$//')" local src_in_subdir="$(echo "$sources_json" | jq -r ".$source_id.in_subdir" | sed 's/^null$//')" @@ -178,8 +180,8 @@ ynh_setup_source() { local src_platform="$(echo "$sources_json" | jq -r ".$source_id.platform" | sed 's/^null$//')" local src_rename="$(echo "$sources_json" | jq -r ".$source_id.rename" | sed 's/^null$//')" - [[ -n "$src_url" ]] || ynh_die "No URL defined for source $source_id ?" - [[ -n "$src_sum" ]] || ynh_die "No sha256 sum defined for source $source_id ?" + [[ -n "$src_url" ]] || ynh_die "No URL defined for source $source_id$arch_prefix ?" + [[ -n "$src_sum" ]] || ynh_die "No sha256 sum defined for source $source_id$arch_prefix ?" if [[ -z "$src_format" ]] then @@ -222,7 +224,6 @@ ynh_setup_source() { src_format=${src_format:-tar.gz} src_format=$(echo "$src_format" | tr '[:upper:]' '[:lower:]') src_extract=${src_extract:-true} - src_filename="${source_id}.${src_format}" if [[ "$src_extract" != "true" ]] && [[ "$src_extract" != "false" ]] then @@ -231,10 +232,10 @@ ynh_setup_source() { # (Unused?) mecanism where one can have the file in a special local cache to not have to download it... - local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${src_filename}" + local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${source_id}" mkdir -p /var/cache/yunohost/download/${YNH_APP_ID}/ - src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${src_filename}" + src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${source_id}" if [ "$src_format" = "docker" ]; then src_platform="${src_platform:-"linux/$YNH_ARCH"}" @@ -243,16 +244,30 @@ ynh_setup_source() { else [ -n "$src_url" ] || ynh_die "Couldn't parse SOURCE_URL from $src_file_path ?" - # NB. we have to declare the var as local first, - # otherwise 'local foo=$(false) || echo 'pwet'" does'nt work - # because local always return 0 ... - local out - # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) - out=$(wget --tries 3 --no-dns-cache --timeout 900 --no-verbose --output-document=$src_filename $src_url 2>&1) \ - || ynh_die --message="$out" + # If the file was prefetched but somehow doesn't match the sum, rm and redownload it + if [ -e "$src_filename" ] && ! echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status + then + rm -f "$src_filename" + fi + + # Only redownload the file if it wasnt prefetched + if [ ! -e "$src_filename" ] + then + # NB. we have to declare the var as local first, + # otherwise 'local foo=$(false) || echo 'pwet'" does'nt work + # because local always return 0 ... + local out + # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) + out=$(wget --tries 3 --no-dns-cache --timeout 900 --no-verbose --output-document=$src_filename $src_url 2>&1) \ + || ynh_die --message="$out" + fi + # Check the control sum - echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status \ - || ynh_die --message="Corrupt source for ${src_filename}: Expected ${src_sum} but got $(${src_sumprg} ${src_filename} | cut --delimiter=' ' --fields=1) (size: $(du -hs ${src_filename} | cut --delimiter=' ' --fields=1))." + if ! echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status + then + rm ${src_filename} + ynh_die --message="Corrupt source for ${src_filename}: Expected ${src_sum} but got $(${src_sumprg} ${src_filename} | cut --delimiter=' ' --fields=1) (size: $(du -hs ${src_filename} | cut --delimiter=' ' --fields=1))." + fi fi # Keep files to be backup/restored at the end of the helper diff --git a/src/app.py b/src/app.py index 17ebe96ca..753f17339 100644 --- a/src/app.py +++ b/src/app.py @@ -747,6 +747,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False ).apply( rollback_and_raise_exception_if_failure=True, operation_logger=operation_logger, + action="upgrade", ) # Boring stuff : the resource upgrade may have added/remove/updated setting @@ -1122,6 +1123,7 @@ def app_install( AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( rollback_and_raise_exception_if_failure=True, operation_logger=operation_logger, + action="install", ) except (KeyboardInterrupt, EOFError, Exception) as e: shutil.rmtree(app_setting_path) @@ -1253,7 +1255,7 @@ def app_install( AppResourceManager( app_instance_name, wanted={}, current=manifest - ).apply(rollback_and_raise_exception_if_failure=False) + ).apply(rollback_and_raise_exception_if_failure=False, action="remove") else: # Remove all permission in LDAP for permission_name in user_permission_list()["permissions"].keys(): @@ -1392,7 +1394,7 @@ def app_remove(operation_logger, app, purge=False, force_workdir=None): from yunohost.utils.resources import AppResourceManager AppResourceManager(app, wanted={}, current=manifest).apply( - rollback_and_raise_exception_if_failure=False, purge_data_dir=purge + rollback_and_raise_exception_if_failure=False, purge_data_dir=purge, action="remove" ) else: # Remove all permission in LDAP diff --git a/src/backup.py b/src/backup.py index 0cf73c4ae..ee218607d 100644 --- a/src/backup.py +++ b/src/backup.py @@ -1528,6 +1528,7 @@ class RestoreManager: AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( rollback_and_raise_exception_if_failure=True, operation_logger=operation_logger, + action="restore", ) # Execute the app install script diff --git a/src/utils/resources.py b/src/utils/resources.py index cff6c6b19..6c5e4890d 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -21,6 +21,7 @@ import copy import shutil import random import tempfile +import subprocess from typing import Dict, Any, List from moulinette import m18n @@ -30,7 +31,7 @@ from moulinette.utils.filesystem import mkdir, chown, chmod, write_to_file from moulinette.utils.filesystem import ( rm, ) - +from yunohost.utils.system import system_arch from yunohost.utils.error import YunohostError, YunohostValidationError logger = getActionLogger("yunohost.app_resources") @@ -257,6 +258,146 @@ ynh_abort_if_errors # print(ret) +class SourcesResource(AppResource): + """ + Declare what are the sources / assets used by this app. Typically, this corresponds to some tarball published by the upstream project, that needs to be downloaded and extracted in the install dir using the ynh_setup_source helper. + + This resource is intended both to declare the assets, which will be parsed by ynh_setup_source during the app script runtime, AND to prefetch and validate the sha256sum of those asset before actually running the script, to be able to report an error early when the asset turns out to not be available for some reason. + + Various options are available to accomodate the behavior according to the asset structure + + ##### Example: + + ```toml + [resources.sources] + + [resources.sources.main] + url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz" + sha256 = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + ``` + + Or more complex examples with several element, including one with asset that depends on the arch + + ```toml + [resources.sources] + + [resources.sources.main] + in_subdir = false + amd64.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.amd64.tar.gz" + amd64.sha256 = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + i386.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.amd64.tar.gz" + i386.sha256 = "53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3" + armhf.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.armhf.tar.gz" + armhf.sha256 = "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865" + + [resources.sources.zblerg] + url = "https://zblerg.com/download/zblerg" + sha256sum = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2" + format = "script" + rename = "zblerg.sh" + + ``` + + ##### Properties (for each source): + + - `prefetch` : `true` (default) or `false`, wether or not to pre-fetch this asset during the provisioning phase of the resource. If several arch-dependent url are provided, YunoHost will only prefetch the one for the current system architecture. + - `url` : the asset's URL + - If the asset's URL depend on the architecture, you may instead provide `amd64.url`, `i386.url`, `armhf.url` and `arm64.url` (depending on what architectures are supported), using the same `dpkg --print-architecture` nomenclature as for the supported architecture key in the manifest + - `sha256` : the asset's sha256sum. This is used both as an integrity check, and as a layer of security to protect against malicious actors which could have injected malicious code inside the asset... + - Same as `url` : if the asset's URL depend on the architecture, you may instead provide `amd64.sha256`, `i386.sha256`, ... + - `format` : The "format" of the asset. It is typically automatically guessed from the extension of the URL (or the mention of "tarball", "zipball" in the URL), but can be set explicitly: + - `tar.gz`, `tar.xz`, `tar.bz2` : will use `tar` to extract the archive + - `zip` : will use `unzip` to extract the archive + - `docker` : useful to extract files from an already-built docker image (instead of rebuilding them locally). Will use `docker-image-extract` + - `whatever`: whatever arbitrary value, not really meaningful except to imply that the file won't be extracted (eg because it's a .deb to be manually installed with dpkg/apt, or a script, or ...) + - `in_subdir`: `true` (default) or `false`, depending on if there's an intermediate subdir in the archive before accessing the actual files. Can also be `N` (an integer) to handle special cases where there's `N` level of subdir to get rid of to actually access the files + - `extract` : `true` or `false`. Defaults to `true` for archives such as `zip`, `tar.gz`, `tar.bz2`, ... Or defaults to `false` when `format` is not something that should be extracted. When `extract = false`, the file will only be `mv`ed to the location, possibly renamed using the `rename` value + - `rename`: some string like `whatever_your_want`, to be used for convenience when `extract` is `false` and the default name of the file is not practical + - `platform`: for exampl `linux/amd64` (defaults to `linux/$YNH_ARCH`) to be used in conjonction with `format = "docker"` to specify which architecture to extract for + + + ##### Provision/Update: + - For elements with `prefetch = true`, will download the asset (for the appropriate architecture) and store them in `/var/cache/yunohost/download/$app/$source_id`, to be later picked up by `ynh_setup_source`. (NB: this only happens during install and upgrade, not restore) + + ##### Deprovision: + - Nothing + """ + + type = "sources" + priority = 10 + + default_sources_properties: Dict[str, Any] = { + "prefetch": True, + "url": None, + "sha256": None, + } + + sources: Dict[str, Dict[str, Any]] = {} + + def __init__(self, properties: Dict[str, Any], *args, **kwargs): + + for source_id, infos in properties.items(): + properties[source_id] = copy.copy(self.default_sources_properties) + properties[source_id].update(infos) + + super().__init__({"sources": properties}, *args, **kwargs) + + def deprovision(self, context: Dict = {}): + if os.path.isdir(f"/var/cache/yunohost/download/{self.app}/"): + rm(f"/var/cache/yunohost/download/{self.app}/", recursive=True) + pass + + def provision_or_update(self, context: Dict = {}): + + # Don't prefetch stuff during restore + if context.get("action") == "restore": + return + + import pdb; pdb.set_trace() + + for source_id, infos in self.sources.items(): + + if not infos["prefetch"]: + continue + + if infos["url"] is None: + arch = system_arch() + if arch in infos and isinstance(infos[arch], dict) and isinstance(infos[arch].get("url"), str) and isinstance(infos[arch].get("sha256"), str): + self.prefetch(source_id, infos[arch]["url"], infos[arch]["sha256"]) + else: + raise YunohostError(f"In resources.sources: it looks like you forgot to define url/sha256 or {arch}.url/{arch}.sha256", raw_msg=True) + else: + if infos["sha256"] is None: + raise YunohostError(f"In resources.sources: it looks like the sha256 is missing for {source_id}", raw_msg=True) + self.prefetch(source_id, infos["url"], infos["sha256"]) + + def prefetch(self, source_id, url, expected_sha256): + + logger.debug(f"Prefetching asset {source_id}: {url} ...") + + if not os.path.isdir(f"/var/cache/yunohost/download/{self.app}/"): + mkdir(f"/var/cache/yunohost/download/{self.app}/", parents=True) + filename = f"/var/cache/yunohost/download/{self.app}/{source_id}" + + # NB: we use wget and not requests.get() because we want to output to a file (ie avoid ending up with the full archive in RAM) + # AND the nice --tries, --no-dns-cache, --timeout options ... + p = subprocess.Popen(["/usr/bin/wget", "--tries=3", "--no-dns-cache", "--timeout=900", "--no-verbose", "--output-document=" + filename, url], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out, _ = p.communicate() + returncode = p.returncode + if returncode != 0: + if os.path.exists(filename): + rm(filename) + out = out.decode() + raise YunohostError(f"Failed to download asset {source_id} ({url}) for {self.app}: {out}", raw_msg=True) + + assert os.path.exists(filename), f"For some reason, wget worked but {filename} doesnt exists?" + + computed_sha256 = check_output(f"sha256sum {filename}").split()[0] + if computed_sha256 != expected_sha256: + size = check_output(f"du -hs {filename}").split()[0] + rm(filename) + raise YunohostError(f"Corrupt source for {url} : expected to find {expected_sha256} as sha256sum, but got {computed_sha256} instead ... (file size : {size})", raw_msg=True) + class PermissionsResource(AppResource): """ From 0a937ab0bd9f3dda8345cfeb6434c71a866b55d5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Mar 2023 20:06:11 +0100 Subject: [PATCH 256/319] Unecessary pass statement --- src/utils/resources.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 6c5e4890d..6202f0353 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -320,7 +320,7 @@ class SourcesResource(AppResource): - For elements with `prefetch = true`, will download the asset (for the appropriate architecture) and store them in `/var/cache/yunohost/download/$app/$source_id`, to be later picked up by `ynh_setup_source`. (NB: this only happens during install and upgrade, not restore) ##### Deprovision: - - Nothing + - Nothing (just cleanup the cache) """ type = "sources" @@ -345,7 +345,6 @@ class SourcesResource(AppResource): def deprovision(self, context: Dict = {}): if os.path.isdir(f"/var/cache/yunohost/download/{self.app}/"): rm(f"/var/cache/yunohost/download/{self.app}/", recursive=True) - pass def provision_or_update(self, context: Dict = {}): From acb359bdbfc4cc6e4e8a600011cf258eaee4eaf7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Mar 2023 20:15:20 +0100 Subject: [PATCH 257/319] Forgot to remove pdb D: --- src/utils/resources.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 6202f0353..2b2ba97d8 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -352,8 +352,6 @@ class SourcesResource(AppResource): if context.get("action") == "restore": return - import pdb; pdb.set_trace() - for source_id, infos in self.sources.items(): if not infos["prefetch"]: From ebc9e645fc02eb319c68e222de3ae1a48d732e56 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 8 Mar 2023 16:23:58 +0100 Subject: [PATCH 258/319] Typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Éric Gaspar <46165813+ericgaspar@users.noreply.github.com> --- 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 2b2ba97d8..8427b4811 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -313,7 +313,7 @@ class SourcesResource(AppResource): - `in_subdir`: `true` (default) or `false`, depending on if there's an intermediate subdir in the archive before accessing the actual files. Can also be `N` (an integer) to handle special cases where there's `N` level of subdir to get rid of to actually access the files - `extract` : `true` or `false`. Defaults to `true` for archives such as `zip`, `tar.gz`, `tar.bz2`, ... Or defaults to `false` when `format` is not something that should be extracted. When `extract = false`, the file will only be `mv`ed to the location, possibly renamed using the `rename` value - `rename`: some string like `whatever_your_want`, to be used for convenience when `extract` is `false` and the default name of the file is not practical - - `platform`: for exampl `linux/amd64` (defaults to `linux/$YNH_ARCH`) to be used in conjonction with `format = "docker"` to specify which architecture to extract for + - `platform`: for example `linux/amd64` (defaults to `linux/$YNH_ARCH`) to be used in conjonction with `format = "docker"` to specify which architecture to extract for ##### Provision/Update: From 0d524220e5c679142139ce1e2836ae8664ddf64d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 8 Mar 2023 16:44:52 +0100 Subject: [PATCH 259/319] appsv2/sources: i18n --- locales/en.json | 2 ++ src/utils/resources.py | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index 7cc1b96b6..083ecdc8d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -26,6 +26,8 @@ "app_change_url_success": "{app} URL is now {domain}{path}", "app_config_unable_to_apply": "Failed to apply config panel values.", "app_config_unable_to_read": "Failed to read config panel values.", + "app_failed_to_download_asset": "Failed to download asset '{source_id}' ({url}) for {app}: {out}", + "app_corrupt_source": "YunoHost was able to download the asset '{source_id}' ({url}) for {app}, but the asset doesn't match the expected checksum. This could mean that some temporary network failure happened on your server, OR the asset was somehow changed by the upstream maintainer (or a malicious actor?) and YunoHost packagers need to investigate and update the app manifest to reflect this change.\n Expected sha256 checksum: {expected_sha256}\n Downloaded sha256 checksum: {computed_sha256}\n Downloaded file size: {size}", "app_extraction_failed": "Could not extract the installation files", "app_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.", "app_id_invalid": "Invalid app ID", diff --git a/src/utils/resources.py b/src/utils/resources.py index 8427b4811..f6ff6bb46 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -292,7 +292,7 @@ class SourcesResource(AppResource): [resources.sources.zblerg] url = "https://zblerg.com/download/zblerg" - sha256sum = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2" + sha256 = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2" format = "script" rename = "zblerg.sh" @@ -384,8 +384,7 @@ class SourcesResource(AppResource): if returncode != 0: if os.path.exists(filename): rm(filename) - out = out.decode() - raise YunohostError(f"Failed to download asset {source_id} ({url}) for {self.app}: {out}", raw_msg=True) + raise YunohostError("app_failed_to_download_asset", source_id=source_id, url=url, app=self.app, out=out.decode()) assert os.path.exists(filename), f"For some reason, wget worked but {filename} doesnt exists?" @@ -393,7 +392,7 @@ class SourcesResource(AppResource): if computed_sha256 != expected_sha256: size = check_output(f"du -hs {filename}").split()[0] rm(filename) - raise YunohostError(f"Corrupt source for {url} : expected to find {expected_sha256} as sha256sum, but got {computed_sha256} instead ... (file size : {size})", raw_msg=True) + raise YunohostError("app_corrupt_source", source_id=source_id, url=url, app=self.app, expected_sha256=expected_sha256, computed_sha256=computed_sha256, size=size) class PermissionsResource(AppResource): From cb324232366f8c4c857e85fa728efe442caaacac Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Mar 2023 15:18:29 +0100 Subject: [PATCH 260/319] appsv2/sources: Reflect changes in ynh_setup_source doc --- helpers/utils | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/helpers/utils b/helpers/utils index 3ef7c2246..695b165c0 100644 --- a/helpers/utils +++ b/helpers/utils @@ -71,20 +71,23 @@ 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 sources.toml exists) or (legacy) `app` (when no sources.toml exists) +# | 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: -r, --full_replace= - Remove previous sources before installing new sources # -# #### New format `.toml` +# #### New 'sources' resources # -# This helper will read infos from a sources.toml at the root of the app package +# (See also the resources documentation which may be more complete?) +# +# This helper will read infos from the 'sources' resources in the manifest.toml of the app # and expect a structure like: # # ```toml -# [main] -# url = "https://some.address.to/download/the/app/archive" -# sha256 = "0123456789abcdef" # The sha256 sum of the asset obtained from the URL -# +# [resources.sources] +# [resources.sources.main] +# url = "https://some.address.to/download/the/app/archive" +# sha256 = "0123456789abcdef" # The sha256 sum of the asset obtained from the URL +# ``` # # # Optional flags: # format = "tar.gz"/xz/bz2 # automatically guessed from the extension of the URL, but can be set explicitly. Will use `tar` to extract @@ -102,20 +105,16 @@ fi # # rename = "whatever_your_want" # to be used for convenience when `extract` is false and the default name of the file is not practical # platform = "linux/amd64" # (defaults to "linux/$YNH_ARCH") to be used in conjonction with `format = "docker"` to specify which architecture to extract for -# ``` +# # -# You may also define sublevels for each architectures such as: +# You may also define assets url and checksum per-architectures such as: # ```toml -# [main] -# autoswitch_per_arch = true -# -# [main.amd64] -# url = "https://some.address.to/download/the/app/archive/when/amd64" -# sha256 = "0123456789abcdef" -# -# [main.armhf] -# url = "https://some.address.to/download/the/app/archive/when/armhf" -# sha256 = "fedcba9876543210" +# [resources.sources] +# [resources.sources.main] +# amd64.url = "https://some.address.to/download/the/app/archive/when/amd64" +# amd64.sha256 = "0123456789abcdef" +# armhf.url = "https://some.address.to/download/the/app/archive/when/armhf" +# armhf.sha256 = "fedcba9876543210" # ``` # # In which case ynh_setup_source --dest_dir="$install_dir" will automatically pick the appropriate source depending on the arch From 340fa787515ba585daf7f350431d96117a77869c Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Sat, 4 Mar 2023 12:21:38 +0000 Subject: [PATCH 261/319] Translated using Weblate (Arabic) Currently translated at 28.8% (220 of 762 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index 2067db43f..0ae901004 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -246,5 +246,9 @@ "migration_0021_patching_sources_list": "تحديث ملف sources.lists…", "pattern_firstname": "يجب أن يكون اسماً أولياً صالحاً (على الأقل 3 حروف)", "yunohost_configured": "تم إعداد YunoHost الآن", - "global_settings_setting_backup_compress_tar_archives": "ضغط النُسخ الاحتياطية" + "global_settings_setting_backup_compress_tar_archives": "ضغط النُسخ الاحتياطية", + "diagnosis_description_apps": "التطبيقات", + "danger": "خطر:", + "diagnosis_basesystem_hardware": "بنية الخادم هي {virt} {arch}", + "diagnosis_basesystem_hardware_model": "طراز الخادم {model}" } From ce37d097ad64c341be4b58d11208ca0eab3a891e Mon Sep 17 00:00:00 2001 From: Grzegorz Cichocki Date: Sat, 4 Mar 2023 12:44:41 +0000 Subject: [PATCH 262/319] Translated using Weblate (Polish) Currently translated at 23.0% (176 of 762 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index 2631b42ca..9ce4e0950 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -155,5 +155,29 @@ "app_manifest_install_ask_init_main_permission": "Kto powinien mieć dostęp do tej aplikacji? (Można to później zmienić)", "ask_admin_fullname": "Pełne imię i nazwisko administratora", "app_change_url_failed": "Nie udało się zmienić adresu URL aplikacji {app}: {error}", - "app_change_url_script_failed": "Wystąpił błąd w skrypcie zmiany adresu URL" -} \ No newline at end of file + "app_change_url_script_failed": "Wystąpił błąd w skrypcie zmiany adresu URL", + "app_failed_to_upgrade_but_continue": "Nie udało zaktualizować się aplikacji {failed_app}, przechodzenie do następnych aktualizacji według żądania. Uruchom komendę 'yunohost log show {operation_logger_name}', aby sprawdzić logi dotyczące błędów", + "certmanager_cert_signing_failed": "Nie udało się zarejestrować nowego certyfikatu", + "certmanager_cert_renew_success": "Pomyślne odnowienie certyfikatu Let's Encrypt dla domeny '{domain}'", + "backup_delete_error": "Nie udało się usunąć '{path}'", + "certmanager_attempt_to_renew_nonLE_cert": "Certyfikat dla domeny '{domain}' nie został wystawiony przez Let's Encrypt. Automatyczne odnowienie jest niemożliwe!", + "backup_archive_cant_retrieve_info_json": "Nieudane wczytanie informacji dla archiwum '{archive}'... Plik info.json nie może zostać odzyskany (lub jest niepoprawny).", + "backup_method_custom_finished": "Tworzenie kopii zapasowej według własnej metody '{method}' zakończone", + "backup_nothings_done": "Brak danych do zapisania", + "app_unsupported_remote_type": "Niewspierany typ zdalny użyty w aplikacji", + "backup_archive_name_unknown": "Nieznane, lokalne archiwum kopii zapasowej o nazwie '{name}'", + "backup_output_directory_not_empty": "Należy wybrać pusty katalog dla danych wyjściowych", + "certmanager_attempt_to_renew_valid_cert": "Certyfikat dla domeny '{domain}' nie jest bliski wygaśnięciu! (Możesz użyć komendy z dopiskiem --force jeśli wiesz co robisz)", + "certmanager_cert_install_success": "Pomyślna instalacja certyfikatu Let's Encrypt dla domeny '{domain}'", + "certmanager_attempt_to_replace_valid_cert": "Właśnie zamierzasz nadpisać dobry i poprawny certyfikat dla domeny '{domain}'! (Użyj komendy z dopiskiem --force, aby ominąć)", + "backup_method_copy_finished": "Zakończono tworzenie kopii zapasowej", + "certmanager_certificate_fetching_or_enabling_failed": "Próba użycia nowego certyfikatu dla {domain} zakończyła się niepowodzeniem...", + "backup_method_tar_finished": "Utworzono archiwum kopii zapasowej TAR", + "backup_mount_archive_for_restore": "Przygotowywanie archiwum do przywrócenia...", + "certmanager_cert_install_failed": "Nieudana instalacja certyfikatu Let's Encrypt dla {domains}", + "certmanager_cert_install_failed_selfsigned": "Nieudana instalacja certyfikatu self-signed dla {domains}", + "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" +} From bccfa7f26eb38b13cae8067fc3df6839cdd0e842 Mon Sep 17 00:00:00 2001 From: Tymofii-Lytvynenko Date: Sun, 5 Mar 2023 08:59:45 +0000 Subject: [PATCH 263/319] Translated using Weblate (Ukrainian) Currently translated at 100.0% (762 of 762 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/uk/ --- locales/uk.json | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/locales/uk.json b/locales/uk.json index 0cac77575..f1d689e40 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-ретрансляції", @@ -278,7 +278,7 @@ "domain_cannot_remove_main": "Ви не можете вилучити '{domain}', бо це основний домен, спочатку вам потрібно встановити інший домен в якості основного за допомогою 'yunohost domain main-domain -n '; ось список доменів-кандидатів: {other_domains}", "disk_space_not_sufficient_update": "Недостатньо місця на диску для оновлення цього застосунку", "disk_space_not_sufficient_install": "Недостатньо місця на диску для встановлення цього застосунку", - "diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте команду yunohost settings set security.ssh.ssh port -v YOUR_SSH_PORT, щоб визначити порт SSH, і перевіртеyunohost tools regen-conf ssh --dry-run --with-diff і yunohost tools regen-conf ssh --force, щоб скинути ваш конфіг на рекомендований YunoHost.", + "diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте команду yunohost settings set security.ssh.ssh port -v ВАШ_SSH_ПОРТ, щоб визначити порт SSH, і перевіртеyunohost tools regen-conf ssh --dry-run --with-diff і yunohost tools regen-conf ssh --force, щоб скинути ваш конфіг на рекомендований YunoHost.", "diagnosis_sshd_config_inconsistent": "Схоже, що порт SSH був уручну змінений в /etc/ssh/sshd_config. Починаючи з версії YunoHost 4.2, доступний новий глобальний параметр 'security.ssh.ssh port', що дозволяє уникнути ручного редагування конфігурації.", "diagnosis_sshd_config_insecure": "Схоже, що конфігурація SSH була змінена вручну і є небезпечною, оскільки не містить директив 'AllowGroups' або 'AllowUsers' для обмеження доступу авторизованих користувачів.", "diagnosis_processes_killed_by_oom_reaper": "Деякі процеси було недавно вбито системою через брак пам'яті. Зазвичай це є симптомом нестачі пам'яті в системі або процесу, який з'їв дуже багато пам'яті. Зведення убитих процесів:\n{kills_summary}", @@ -346,7 +346,7 @@ "diagnosis_mail_outgoing_port_25_blocked_details": "Спочатку спробуйте розблокувати вихідний порт 25 в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм заявку в службу підтримки).", "diagnosis_mail_outgoing_port_25_blocked": "Поштовий сервер SMTP не може відправляти електронні листи на інші сервери, оскільки вихідний порт 25 заблоковано в IPv{ipversion}.", "app_manifest_install_ask_path": "Оберіть шлях URL (після домену), за яким має бути встановлено цей застосунок", - "yunohost_postinstall_end_tip": "Післявстановлення завершено! Щоб завершити доналаштування, будь ласка, розгляньте наступні варіанти:\n - додавання першого користувача через розділ 'Користувачі' вебадмініструванні (або 'yunohost user create ' в командному рядку);\n - діагностика можливих проблем через розділ 'Діагностика' вебадмініструванні (або 'yunohost diagnosis run' в командному рядку);\n - прочитання розділів 'Завершення встановлення' і 'Знайомство з YunoHost' у документації адміністратора: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "Післявстановлення завершено! Щоб завершити доналаштування, будь ласка, розгляньте наступні варіанти:\n - діагностика можливих проблем через розділ 'Діагностика' вебадмініструванні (або 'yunohost diagnosis run' в командному рядку);\n - прочитання розділів 'Завершення встановлення' і 'Знайомство з YunoHost' у документації адміністратора: https://yunohost.org/admindoc.", "yunohost_not_installed": "YunoHost установлений неправильно. Будь ласка, запустіть 'yunohost tools postinstall'", "yunohost_installing": "Установлення YunoHost...", "yunohost_configured": "YunoHost вже налаштовано", @@ -488,7 +488,7 @@ "backup_method_custom_finished": "Користувацький спосіб резервного копіювання '{method}' завершено", "backup_method_copy_finished": "Резервне копіювання завершено", "backup_hook_unknown": "Гачок (hook) резервного копіювання '{hook}' невідомий", - "backup_deleted": "Резервна копія видалена", + "backup_deleted": "Резервна копія '{name}' видалена", "backup_delete_error": "Не вдалося видалити '{path}'", "backup_custom_mount_error": "Користувацький спосіб резервного копіювання не зміг пройти етап 'монтування'", "backup_custom_backup_error": "Користувацький спосіб резервного копіювання не зміг пройти етап 'резервне копіювання'", @@ -496,7 +496,7 @@ "backup_csv_addition_failed": "Не вдалося додати файли для резервного копіювання в CSV-файл", "backup_creation_failed": "Не вдалося створити архів резервного копіювання", "backup_create_size_estimation": "Архів буде містити близько {size} даних.", - "backup_created": "Резервна копія створена", + "backup_created": "Резервна копія '{name}' створена", "backup_couldnt_bind": "Не вдалося зв'язати {src} з {dest}.", "backup_copying_to_organize_the_archive": "Копіювання {size} МБ для організації архіву", "backup_cleaning_failed": "Не вдалося очистити тимчасовий каталог резервного копіювання", @@ -654,7 +654,7 @@ "global_settings_setting_admin_strength": "Надійність пароля адміністратора", "global_settings_setting_user_strength": "Надійність пароля користувача", "global_settings_setting_postfix_compatibility_help": "Компроміс між сумісністю і безпекою для сервера Postfix. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", - "global_settings_setting_ssh_compatibility_help": "Компроміс між сумісністю і безпекою для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", + "global_settings_setting_ssh_compatibility_help": "Компроміс між сумісністю і безпекою для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою).", "global_settings_setting_ssh_password_authentication_help": "Дозволити автентифікацію паролем для SSH", "global_settings_setting_ssh_port": "SSH-порт", "global_settings_setting_webadmin_allowlist_help": "IP-адреси, яким дозволений доступ до вебадмініструванні. Через кому.", @@ -735,5 +735,30 @@ "visitors": "Відвідувачі", "password_confirmation_not_the_same": "Пароль і його підтвердження не збігаються", "password_too_long": "Будь ласка, виберіть пароль коротший за 127 символів", - "pattern_fullname": "Має бути дійсне повне ім’я (принаймні 3 символи)" -} \ No newline at end of file + "pattern_fullname": "Має бути дійсне повне ім’я (принаймні 3 символи)", + "app_failed_to_upgrade_but_continue": "Застосунок {failed_app} не вдалося оновити, продовжуйте наступні оновлення відповідно до запиту. Запустіть 'yunohost log show {назва_логгера_операції}', щоб побачити журнал помилок", + "app_not_upgraded_broken_system": "Застосунок '{failed_app}' не зміг оновитися і перевів систему в неробочий стан, і як наслідок, оновлення наступних застосунків було скасовано: {apps}", + "app_not_upgraded_broken_system_continue": "Застосунок '{failed_app}' не зміг оновитися і перевів систему у неробочий стан (тому --continue-on-failure ігнорується), і як наслідок, оновлення наступних застосунків було скасовано: {apps}", + "confirm_app_insufficient_ram": "НЕБЕЗПЕКА! Цей застосунок вимагає {required} оперативної пам'яті для встановлення/оновлення, але зараз доступно лише {current}. Навіть якби цей застосунок можна було б запустити, процес його встановлення/оновлення вимагає великої кількості оперативної пам'яті, тому ваш сервер може зависнути і вийти з ладу. Якщо ви все одно готові піти на цей ризик, введіть '{answers}'", + "invalid_shell": "Недійсна оболонка: {shell}", + "domain_config_default_app_help": "Користувачі будуть автоматично перенаправлятися на цей застосунок при відкритті цього домену. Якщо застосунок не вказано, люди будуть перенаправлені на форму входу на портал користувача.", + "domain_config_xmpp_help": "Примітка: для ввімкнення деяких функцій XMPP потрібно оновити записи DNS та відновити сертифікат Lets Encrypt", + "global_settings_setting_dns_exposure_help": "Примітка: Це стосується лише рекомендованої конфігурації DNS і діагностичних перевірок. Це не впливає на конфігурацію системи.", + "global_settings_setting_passwordless_sudo": "Дозвіл адміністраторам використовувати \"sudo\" без повторного введення пароля", + "app_change_url_failed": "Не вдалося змінити url для {app}: {error}", + "app_change_url_require_full_domain": "{app} не може бути переміщено на цю нову URL-адресу, оскільки для цього потрібен повний домен (тобто зі шляхом = /)", + "app_change_url_script_failed": "Виникла помилка всередині скрипта зміни URL-адреси", + "app_yunohost_version_not_supported": "Для роботи застосунку потрібен YunoHost мінімум версії {required}, але поточна встановлена версія {current}", + "app_arch_not_supported": "Цей застосунок можна встановити лише на архітектурах {required}, але архітектура вашого сервера {current}", + "global_settings_setting_dns_exposure": "Версії IP, які слід враховувати при конфігурації та діагностиці DNS", + "domain_cannot_add_muc_upload": "Ви не можете додавати домени, що починаються на 'muc.'. Такі імена зарезервовані для багатокористувацького чату XMPP, інтегрованого в YunoHost.", + "confirm_notifications_read": "ПОПЕРЕДЖЕННЯ: Перш ніж продовжити, перевірте сповіщення застосунку вище, там можуть бути важливі повідомлення. [{answers}]", + "global_settings_setting_portal_theme": "Тема порталу", + "global_settings_setting_portal_theme_help": "Подробиці щодо створення користувацьких тем порталу на https://yunohost.org/theming", + "diagnosis_ip_no_ipv6_tip_important": "Зазвичай IPv6 має бути автоматично налаштований системою або вашим провайдером, якщо він доступний. В іншому випадку, можливо, вам доведеться налаштувати деякі речі вручну, як описано в документації тут: https://yunohost.org/#/ipv6.", + "app_not_enough_disk": "Цей застосунок вимагає {required} вільного місця.", + "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 {назва_логгера_операції}')" +} From 7c8f5261cbd5c86310f556244355fd3886530738 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Thu, 9 Mar 2023 12:40:44 +0000 Subject: [PATCH 264/319] Translated using Weblate (Arabic) Currently translated at 29.6% (226 of 762 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index 0ae901004..feb375a94 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -250,5 +250,11 @@ "diagnosis_description_apps": "التطبيقات", "danger": "خطر:", "diagnosis_basesystem_hardware": "بنية الخادم هي {virt} {arch}", - "diagnosis_basesystem_hardware_model": "طراز الخادم {model}" + "diagnosis_basesystem_hardware_model": "طراز الخادم {model}", + "diagnosis_mail_queue_ok": "هناك {nb_pending} رسائل بريد إلكتروني معلقة في قوائم انتظار البريد", + "diagnosis_mail_ehlo_ok": "يمكن الوصول إلى خادم بريد SMTP من الخارج وبالتالي فهو قادر على استقبال رسائل البريد الإلكتروني!", + "diagnosis_dns_good_conf": "تم إعداد سجلات نظام أسماء النطاقات DNS بشكل صحيح للنطاق {domain} (category {category})", + "diagnosis_ip_dnsresolution_working": "تحليل اسم النطاق يعمل!", + "diagnosis_mail_blacklist_listed_by": "إن عنوان الـ IP الخص بك أو نطاقك {item} مُدرَج ضمن قائمة سوداء على {blacklist_name}", + "diagnosis_mail_outgoing_port_25_ok": "خادم بريد SMTP قادر على إرسال رسائل البريد الإلكتروني (منفذ البريد الصادر 25 غير محظور)." } From 4971127b9c117047a78513b71c594b70ba7ede6c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Mar 2023 15:35:12 +0100 Subject: [PATCH 265/319] Update changelog for 11.1.14 --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 9f3a685a1..a29ba223c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (11.1.14) stable; urgency=low + + - helpers: simplify --time display option for ynh_script_progression .. we don't care about displaying time when below 10 sc (8731f77a) + - appsv2: add support for a 'sources' app resources to modernize and replace app.src format ([#1615](https://github.com/yunohost/yunohost/pull/1615)) + - i18n: Translations updated for Arabic, Polish, Ukrainian + + Thanks to all contributors <3 ! (ButterflyOfFire, Grzegorz Cichocki, Tymofii-Lytvynenko) + + -- Alexandre Aubin Thu, 09 Mar 2023 15:34:17 +0100 + yunohost (11.1.13) stable; urgency=low - appsv2: fix port already used detection ([#1622](https://github.com/yunohost/yunohost/pull/1622)) From 98c7b60311ee664d06fb451cca016d41cdb761fe Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Thu, 9 Mar 2023 16:19:40 +0000 Subject: [PATCH 266/319] [CI] Format code with Black --- src/app.py | 4 ++- src/utils/resources.py | 58 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/app.py b/src/app.py index 091dd05d9..b37b680ec 100644 --- a/src/app.py +++ b/src/app.py @@ -1444,7 +1444,9 @@ def app_remove(operation_logger, app, purge=False, force_workdir=None): from yunohost.utils.resources import AppResourceManager AppResourceManager(app, wanted={}, current=manifest).apply( - rollback_and_raise_exception_if_failure=False, purge_data_dir=purge, action="remove" + rollback_and_raise_exception_if_failure=False, + purge_data_dir=purge, + action="remove", ) else: # Remove all permission in LDAP diff --git a/src/utils/resources.py b/src/utils/resources.py index 56ffa9156..87446bdd8 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -258,6 +258,7 @@ ynh_abort_if_errors # print(ret) + class SourcesResource(AppResource): """ Declare what are the sources / assets used by this app. Typically, this corresponds to some tarball published by the upstream project, that needs to be downloaded and extracted in the install dir using the ynh_setup_source helper. @@ -335,7 +336,6 @@ class SourcesResource(AppResource): sources: Dict[str, Dict[str, Any]] = {} def __init__(self, properties: Dict[str, Any], *args, **kwargs): - for source_id, infos in properties.items(): properties[source_id] = copy.copy(self.default_sources_properties) properties[source_id].update(infos) @@ -347,29 +347,37 @@ class SourcesResource(AppResource): rm(f"/var/cache/yunohost/download/{self.app}/", recursive=True) def provision_or_update(self, context: Dict = {}): - # Don't prefetch stuff during restore if context.get("action") == "restore": return for source_id, infos in self.sources.items(): - if not infos["prefetch"]: continue if infos["url"] is None: arch = system_arch() - if arch in infos and isinstance(infos[arch], dict) and isinstance(infos[arch].get("url"), str) and isinstance(infos[arch].get("sha256"), str): + if ( + arch in infos + and isinstance(infos[arch], dict) + and isinstance(infos[arch].get("url"), str) + and isinstance(infos[arch].get("sha256"), str) + ): self.prefetch(source_id, infos[arch]["url"], infos[arch]["sha256"]) else: - raise YunohostError(f"In resources.sources: it looks like you forgot to define url/sha256 or {arch}.url/{arch}.sha256", raw_msg=True) + raise YunohostError( + f"In resources.sources: it looks like you forgot to define url/sha256 or {arch}.url/{arch}.sha256", + raw_msg=True, + ) else: if infos["sha256"] is None: - raise YunohostError(f"In resources.sources: it looks like the sha256 is missing for {source_id}", raw_msg=True) + raise YunohostError( + f"In resources.sources: it looks like the sha256 is missing for {source_id}", + raw_msg=True, + ) self.prefetch(source_id, infos["url"], infos["sha256"]) def prefetch(self, source_id, url, expected_sha256): - logger.debug(f"Prefetching asset {source_id}: {url} ...") if not os.path.isdir(f"/var/cache/yunohost/download/{self.app}/"): @@ -378,21 +386,49 @@ class SourcesResource(AppResource): # NB: we use wget and not requests.get() because we want to output to a file (ie avoid ending up with the full archive in RAM) # AND the nice --tries, --no-dns-cache, --timeout options ... - p = subprocess.Popen(["/usr/bin/wget", "--tries=3", "--no-dns-cache", "--timeout=900", "--no-verbose", "--output-document=" + filename, url], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + p = subprocess.Popen( + [ + "/usr/bin/wget", + "--tries=3", + "--no-dns-cache", + "--timeout=900", + "--no-verbose", + "--output-document=" + filename, + url, + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) out, _ = p.communicate() returncode = p.returncode if returncode != 0: if os.path.exists(filename): rm(filename) - raise YunohostError("app_failed_to_download_asset", source_id=source_id, url=url, app=self.app, out=out.decode()) + raise YunohostError( + "app_failed_to_download_asset", + source_id=source_id, + url=url, + app=self.app, + out=out.decode(), + ) - assert os.path.exists(filename), f"For some reason, wget worked but {filename} doesnt exists?" + assert os.path.exists( + filename + ), f"For some reason, wget worked but {filename} doesnt exists?" computed_sha256 = check_output(f"sha256sum {filename}").split()[0] if computed_sha256 != expected_sha256: size = check_output(f"du -hs {filename}").split()[0] rm(filename) - raise YunohostError("app_corrupt_source", source_id=source_id, url=url, app=self.app, expected_sha256=expected_sha256, computed_sha256=computed_sha256, size=size) + raise YunohostError( + "app_corrupt_source", + source_id=source_id, + url=url, + app=self.app, + expected_sha256=expected_sha256, + computed_sha256=computed_sha256, + size=size, + ) class PermissionsResource(AppResource): From 89d139e47ac28d1a87ded2de0f31aa8ecaa39f7f Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Thu, 9 Mar 2023 16:47:09 +0000 Subject: [PATCH 267/319] [CI] Reformat / remove stale translated strings --- locales/ar.json | 2 +- locales/de.json | 2 +- locales/en.json | 2 +- locales/fr.json | 2 +- locales/gl.json | 2 +- locales/oc.json | 2 +- locales/pl.json | 2 +- locales/uk.json | 6 +++--- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index feb375a94..8ff300109 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -257,4 +257,4 @@ "diagnosis_ip_dnsresolution_working": "تحليل اسم النطاق يعمل!", "diagnosis_mail_blacklist_listed_by": "إن عنوان الـ IP الخص بك أو نطاقك {item} مُدرَج ضمن قائمة سوداء على {blacklist_name}", "diagnosis_mail_outgoing_port_25_ok": "خادم بريد SMTP قادر على إرسال رسائل البريد الإلكتروني (منفذ البريد الصادر 25 غير محظور)." -} +} \ No newline at end of file diff --git a/locales/de.json b/locales/de.json index 2b7ee0456..b61d0a431 100644 --- a/locales/de.json +++ b/locales/de.json @@ -705,4 +705,4 @@ "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 diff --git a/locales/en.json b/locales/en.json index ab606c81c..4dcb00ee6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -26,9 +26,9 @@ "app_change_url_success": "{app} URL is now {domain}{path}", "app_config_unable_to_apply": "Failed to apply config panel values.", "app_config_unable_to_read": "Failed to read config panel values.", - "app_failed_to_download_asset": "Failed to download asset '{source_id}' ({url}) for {app}: {out}", "app_corrupt_source": "YunoHost was able to download the asset '{source_id}' ({url}) for {app}, but the asset doesn't match the expected checksum. This could mean that some temporary network failure happened on your server, OR the asset was somehow changed by the upstream maintainer (or a malicious actor?) and YunoHost packagers need to investigate and update the app manifest to reflect this change.\n Expected sha256 checksum: {expected_sha256}\n Downloaded sha256 checksum: {computed_sha256}\n Downloaded file size: {size}", "app_extraction_failed": "Could not extract the installation files", + "app_failed_to_download_asset": "Failed to download asset '{source_id}' ({url}) for {app}: {out}", "app_failed_to_upgrade_but_continue": "App {failed_app} failed to upgrade, continue to next upgrades as requested. Run 'yunohost log show {operation_logger_name}' to see failure log", "app_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.", "app_id_invalid": "Invalid app ID", diff --git a/locales/fr.json b/locales/fr.json index b08b142c5..440fe1144 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -761,4 +761,4 @@ "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}", "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}')" -} +} \ No newline at end of file diff --git a/locales/gl.json b/locales/gl.json index 2b9e89ffb..065e41686 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -761,4 +761,4 @@ "invalid_shell": "Intérprete de ordes non válido: {shell}", "log_resource_snippet": "Aprovisionamento/desaprovisionamento/actualización dun recurso", "app_resource_failed": "Fallou o aprovisionamento, desaprovisionamento ou actualización de recursos para {app}: {error}" -} +} \ No newline at end of file diff --git a/locales/oc.json b/locales/oc.json index bdc9f5360..1c13fc6b5 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -469,4 +469,4 @@ "global_settings_setting_user_strength": "Fòrça del senhal utilizaire", "global_settings_setting_postfix_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor Postfix. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", "global_settings_setting_ssh_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor SSH. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)" -} +} \ No newline at end of file diff --git a/locales/pl.json b/locales/pl.json index 9ce4e0950..c58f7223e 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -180,4 +180,4 @@ "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 diff --git a/locales/uk.json b/locales/uk.json index f1d689e40..fca0ea360 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -736,7 +736,7 @@ "password_confirmation_not_the_same": "Пароль і його підтвердження не збігаються", "password_too_long": "Будь ласка, виберіть пароль коротший за 127 символів", "pattern_fullname": "Має бути дійсне повне ім’я (принаймні 3 символи)", - "app_failed_to_upgrade_but_continue": "Застосунок {failed_app} не вдалося оновити, продовжуйте наступні оновлення відповідно до запиту. Запустіть 'yunohost log show {назва_логгера_операції}', щоб побачити журнал помилок", + "app_failed_to_upgrade_but_continue": "Застосунок {failed_app} не вдалося оновити, продовжуйте наступні оновлення відповідно до запиту. Запустіть 'yunohost log show {operation_logger_name}', щоб побачити журнал помилок", "app_not_upgraded_broken_system": "Застосунок '{failed_app}' не зміг оновитися і перевів систему в неробочий стан, і як наслідок, оновлення наступних застосунків було скасовано: {apps}", "app_not_upgraded_broken_system_continue": "Застосунок '{failed_app}' не зміг оновитися і перевів систему у неробочий стан (тому --continue-on-failure ігнорується), і як наслідок, оновлення наступних застосунків було скасовано: {apps}", "confirm_app_insufficient_ram": "НЕБЕЗПЕКА! Цей застосунок вимагає {required} оперативної пам'яті для встановлення/оновлення, але зараз доступно лише {current}. Навіть якби цей застосунок можна було б запустити, процес його встановлення/оновлення вимагає великої кількості оперативної пам'яті, тому ваш сервер може зависнути і вийти з ладу. Якщо ви все одно готові піти на цей ризик, введіть '{answers}'", @@ -760,5 +760,5 @@ "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 {назва_логгера_операції}')" -} + "apps_failed_to_upgrade_line": "\n * {app_id} (щоб побачити відповідний журнал, виконайте 'yunohost log show {operation_logger_name}')" +} \ No newline at end of file From c2ba4a90e70b7e5edb7dc209091f2ad7ded9e0a9 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Thu, 9 Mar 2023 15:06:58 +0000 Subject: [PATCH 268/319] Translated using Weblate (Arabic) Currently translated at 29.5% (226 of 764 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ar/ --- locales/ar.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index feb375a94..4953b0179 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -255,6 +255,6 @@ "diagnosis_mail_ehlo_ok": "يمكن الوصول إلى خادم بريد SMTP من الخارج وبالتالي فهو قادر على استقبال رسائل البريد الإلكتروني!", "diagnosis_dns_good_conf": "تم إعداد سجلات نظام أسماء النطاقات DNS بشكل صحيح للنطاق {domain} (category {category})", "diagnosis_ip_dnsresolution_working": "تحليل اسم النطاق يعمل!", - "diagnosis_mail_blacklist_listed_by": "إن عنوان الـ IP الخص بك أو نطاقك {item} مُدرَج ضمن قائمة سوداء على {blacklist_name}", + "diagnosis_mail_blacklist_listed_by": "إن عنوان الـ IP الخاص بك أو نطاقك {item} مُدرَج ضمن قائمة سوداء على {blacklist_name}", "diagnosis_mail_outgoing_port_25_ok": "خادم بريد SMTP قادر على إرسال رسائل البريد الإلكتروني (منفذ البريد الصادر 25 غير محظور)." } From ab1149b1e7609db36e957a907c3cb788219cbb9f Mon Sep 17 00:00:00 2001 From: ppr Date: Thu, 9 Mar 2023 15:02:56 +0000 Subject: [PATCH 269/319] Translated using Weblate (French) Currently translated at 99.3% (759 of 764 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index b08b142c5..9411fec96 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -760,5 +760,7 @@ "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}", "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/log 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é : {taille}" } From 69518b541728d9fdf47ce71b3752248267b97759 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Mar 2023 20:41:29 +0100 Subject: [PATCH 270/319] Bash being bash ~_~ --- helpers/logging | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/logging b/helpers/logging index 82cb2814a..ab5d564aa 100644 --- a/helpers/logging +++ b/helpers/logging @@ -309,7 +309,7 @@ ynh_script_progression() { local print_exec_time="" if [ $time -eq 1 ] && [ "$exec_time" -gt 10 ]; then - print_exec_time=" [$(bc <<< 'scale=1; $exec_time / 60' ) minutes]" + print_exec_time=" [$(bc <<< "scale=1; $exec_time / 60" ) minutes]" fi ynh_print_info "[$progression_bar] > ${message}${print_exec_time}" From 7491dd4c50ff9e99f045c5c6ed9ddb6df1764e9b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Mar 2023 20:57:33 +0100 Subject: [PATCH 271/319] helpers: Fix documentation for ynh_setup_source --- helpers/utils | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/helpers/utils b/helpers/utils index 695b165c0..4a964a14e 100644 --- a/helpers/utils +++ b/helpers/utils @@ -89,7 +89,9 @@ fi # sha256 = "0123456789abcdef" # The sha256 sum of the asset obtained from the URL # ``` # -# # Optional flags: +# ##### Optional flags +# +# ```text # format = "tar.gz"/xz/bz2 # automatically guessed from the extension of the URL, but can be set explicitly. Will use `tar` to extract # "zip" # automatically guessed from the extension of the URL, but can be set explicitly. Will use `unzip` to extract # "docker" # useful to extract files from an already-built docker image (instead of rebuilding them locally). Will use `docker-image-extract` to extract @@ -105,7 +107,7 @@ fi # # rename = "whatever_your_want" # to be used for convenience when `extract` is false and the default name of the file is not practical # platform = "linux/amd64" # (defaults to "linux/$YNH_ARCH") to be used in conjonction with `format = "docker"` to specify which architecture to extract for -# +# ``` # # You may also define assets url and checksum per-architectures such as: # ```toml From 5b58e0e60c2ad231952104298479c30521cf6a46 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Mar 2023 21:17:02 +0100 Subject: [PATCH 272/319] doc: Fix version number in autogenerated resource doc --- doc/generate_resource_doc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/generate_resource_doc.py b/doc/generate_resource_doc.py index 272845104..201d25265 100644 --- a/doc/generate_resource_doc.py +++ b/doc/generate_resource_doc.py @@ -2,7 +2,7 @@ import ast import datetime import subprocess -version = (open("../debian/changelog").readlines()[0].split()[1].strip("()"),) +version = open("../debian/changelog").readlines()[0].split()[1].strip("()") today = datetime.datetime.now().strftime("%d/%m/%Y") From 13ac9dade639cf104b038c45b862e0762e9c518f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 10 Mar 2023 16:00:53 +0100 Subject: [PATCH 273/319] helpers/nodejs: simplify 'n' script install and maintenance --- .github/workflows/n_updater.sh | 78 -- .github/workflows/n_updater.yml | 3 +- helpers/nodejs | 34 +- helpers/vendor/n/LICENSE | 21 + helpers/vendor/n/README.md | 1 + helpers/vendor/n/n | 1621 +++++++++++++++++++++++++++++++ 6 files changed, 1649 insertions(+), 109 deletions(-) delete mode 100644 .github/workflows/n_updater.sh create mode 100644 helpers/vendor/n/LICENSE create mode 100644 helpers/vendor/n/README.md create mode 100755 helpers/vendor/n/n diff --git a/.github/workflows/n_updater.sh b/.github/workflows/n_updater.sh deleted file mode 100644 index a8b0b0eec..000000000 --- a/.github/workflows/n_updater.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -#================================================= -# N UPDATING HELPER -#================================================= - -# This script is meant to be run by GitHub Actions. -# It is derived from the Updater script from the YunoHost-Apps organization. -# It aims to automate the update of `n`, the Node version management system. - -#================================================= -# FETCHING LATEST RELEASE AND ITS ASSETS -#================================================= - -# Fetching information -source helpers/nodejs -current_version="$n_version" -repo="tj/n" -# Some jq magic is needed, because the latest upstream release is not always the latest version (e.g. security patches for older versions) -version=$(curl --silent "https://api.github.com/repos/$repo/releases" | jq -r '.[] | select( .prerelease != true ) | .tag_name' | sort -V | tail -1) - -# Later down the script, we assume the version has only digits and dots -# Sometimes the release name starts with a "v", so let's filter it out. -if [[ ${version:0:1} == "v" || ${version:0:1} == "V" ]]; then - version=${version:1} -fi - -# Setting up the environment variables -echo "Current version: $current_version" -echo "Latest release from upstream: $version" -echo "VERSION=$version" >> $GITHUB_ENV -# For the time being, let's assume the script will fail -echo "PROCEED=false" >> $GITHUB_ENV - -# Proceed only if the retrieved version is greater than the current one -if ! dpkg --compare-versions "$current_version" "lt" "$version" ; then - echo "::warning ::No new version available" - exit 0 -# Proceed only if a PR for this new version does not already exist -elif git ls-remote -q --exit-code --heads https://github.com/${GITHUB_REPOSITORY:-YunoHost/yunohost}.git ci-auto-update-n-v$version ; then - echo "::warning ::A branch already exists for this update" - exit 0 -fi - -#================================================= -# UPDATE SOURCE FILES -#================================================= - -asset_url="https://github.com/tj/n/archive/v${version}.tar.gz" - -echo "Handling asset at $asset_url" - -# Create the temporary directory -tempdir="$(mktemp -d)" - -# Download sources and calculate checksum -filename=${asset_url##*/} -curl --silent -4 -L $asset_url -o "$tempdir/$filename" -checksum=$(sha256sum "$tempdir/$filename" | head -c 64) - -# Delete temporary directory -rm -rf $tempdir - -echo "Calculated checksum for n v${version} is $checksum" - -#================================================= -# GENERIC FINALIZATION -#================================================= - -# Replace new version in helper -sed -i -E "s/^n_version=.*$/n_version=$version/" helpers/nodejs - -# Replace checksum in helper -sed -i -E "s/^n_checksum=.*$/n_checksum=$checksum/" helpers/nodejs - -# The Action will proceed only if the PROCEED environment variable is set to true -echo "PROCEED=true" >> $GITHUB_ENV -exit 0 diff --git a/.github/workflows/n_updater.yml b/.github/workflows/n_updater.yml index 4c422c14c..ce3e9c925 100644 --- a/.github/workflows/n_updater.yml +++ b/.github/workflows/n_updater.yml @@ -21,7 +21,8 @@ jobs: git config --global user.name 'yunohost-bot' git config --global user.email 'yunohost-bot@users.noreply.github.com' # Run the updater script - /bin/bash .github/workflows/n_updater.sh + wget https://raw.githubusercontent.com/tj/n/master/bin/n --output-document=helpers/vendor/n/n + [[ -z "$(git diff helpers/vendor/n/n)" ]] || echo "PROCEED=true" >> $GITHUB_ENV - name: Commit changes id: commit if: ${{ env.PROCEED == 'true' }} diff --git a/helpers/nodejs b/helpers/nodejs index b692bfc70..e3ccf82dd 100644 --- a/helpers/nodejs +++ b/helpers/nodejs @@ -1,32 +1,10 @@ #!/bin/bash -n_version=9.0.1 -n_checksum=ad305e8ee9111aa5b08e6dbde23f01109401ad2d25deecacd880b3f9ea45702b n_install_dir="/opt/node_n" node_version_path="$n_install_dir/n/versions/node" # N_PREFIX is the directory of n, it needs to be loaded as a environment variable. export N_PREFIX="$n_install_dir" -# Install Node version management -# -# [internal] -# -# usage: ynh_install_n -# -# Requires YunoHost version 2.7.12 or higher. -ynh_install_n() { - # Build an app.src for n - echo "SOURCE_URL=https://github.com/tj/n/archive/v${n_version}.tar.gz -SOURCE_SUM=${n_checksum}" >"$YNH_APP_BASEDIR/conf/n.src" - # Download and extract n - ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n - # Install n - ( - cd "$n_install_dir/git" - PREFIX=$N_PREFIX make install 2>&1 - ) -} - # Load the version of node for an app, and set variables. # # usage: ynh_use_nodejs @@ -133,14 +111,10 @@ ynh_install_nodejs() { test -x /usr/bin/node && mv /usr/bin/node /usr/bin/node_n test -x /usr/bin/npm && mv /usr/bin/npm /usr/bin/npm_n - # If n is not previously setup, install it - if ! $n_install_dir/bin/n --version >/dev/null 2>&1; then - ynh_install_n - elif dpkg --compare-versions "$($n_install_dir/bin/n --version)" lt $n_version; then - ynh_install_n - fi - - # Modify the default N_PREFIX in n script + # Install (or update if YunoHost vendor/ folder updated since last install) n + mkdir -p $n_install_dir/bin/ + cp /usr/share/yunohost/helpers.d/vendor/n/n $n_install_dir/bin/n + # Tweak for n to understand it's installed in $N_PREFIX ynh_replace_string --match_string="^N_PREFIX=\${N_PREFIX-.*}$" --replace_string="N_PREFIX=\${N_PREFIX-$N_PREFIX}" --target_file="$n_install_dir/bin/n" # Restore /usr/local/bin in PATH diff --git a/helpers/vendor/n/LICENSE b/helpers/vendor/n/LICENSE new file mode 100644 index 000000000..8e04e8467 --- /dev/null +++ b/helpers/vendor/n/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/helpers/vendor/n/README.md b/helpers/vendor/n/README.md new file mode 100644 index 000000000..9a29a3936 --- /dev/null +++ b/helpers/vendor/n/README.md @@ -0,0 +1 @@ +This is taken from https://github.com/tj/n/ diff --git a/helpers/vendor/n/n b/helpers/vendor/n/n new file mode 100755 index 000000000..2739e2d00 --- /dev/null +++ b/helpers/vendor/n/n @@ -0,0 +1,1621 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2155 +# Disabled "Declare and assign separately to avoid masking return values": https://github.com/koalaman/shellcheck/wiki/SC2155 + +# +# log +# + +log() { + printf " ${SGR_CYAN}%10s${SGR_RESET} : ${SGR_FAINT}%s${SGR_RESET}\n" "$1" "$2" +} + +# +# verbose_log +# Can suppress with --quiet. +# Like log but to stderr rather than stdout, so can also be used from "display" routines. +# + +verbose_log() { + if [[ "${SHOW_VERBOSE_LOG}" == "true" ]]; then + >&2 printf " ${SGR_CYAN}%10s${SGR_RESET} : ${SGR_FAINT}%s${SGR_RESET}\n" "$1" "$2" + fi +} + +# +# Exit with the given +# + +abort() { + >&2 printf "\n ${SGR_RED}Error: %s${SGR_RESET}\n\n" "$*" && exit 1 +} + +# +# Synopsis: trace message ... +# Debugging output to stderr, not used in production code. +# + +function trace() { + >&2 printf "trace: %s\n" "$*" +} + +# +# Synopsis: echo_red message ... +# Highlight message in colour (on stdout). +# + +function echo_red() { + printf "${SGR_RED}%s${SGR_RESET}\n" "$*" +} + +# +# Synopsis: n_grep +# grep wrapper to ensure consistent grep options and circumvent aliases. +# + +function n_grep() { + GREP_OPTIONS='' command grep "$@" +} + +# +# Setup and state +# + +VERSION="v9.0.1" + +N_PREFIX="${N_PREFIX-/usr/local}" +N_PREFIX=${N_PREFIX%/} +readonly N_PREFIX + +N_CACHE_PREFIX="${N_CACHE_PREFIX-${N_PREFIX}}" +N_CACHE_PREFIX=${N_CACHE_PREFIX%/} +CACHE_DIR="${N_CACHE_PREFIX}/n/versions" +readonly N_CACHE_PREFIX CACHE_DIR + +N_NODE_MIRROR=${N_NODE_MIRROR:-${NODE_MIRROR:-https://nodejs.org/dist}} +N_NODE_MIRROR=${N_NODE_MIRROR%/} +readonly N_NODE_MIRROR + +N_NODE_DOWNLOAD_MIRROR=${N_NODE_DOWNLOAD_MIRROR:-https://nodejs.org/download} +N_NODE_DOWNLOAD_MIRROR=${N_NODE_DOWNLOAD_MIRROR%/} +readonly N_NODE_DOWNLOAD_MIRROR + +# Using xz instead of gzip is enabled by default, if xz compatibility checks pass. +# User may set N_USE_XZ to 0 to disable, or set to anything else to enable. +# May also be overridden by command line flags. + +# Normalise external values to true/false +if [[ "${N_USE_XZ}" = "0" ]]; then + N_USE_XZ="false" +elif [[ -n "${N_USE_XZ+defined}" ]]; then + N_USE_XZ="true" +fi +# Not setting to readonly. Overriden by CLI flags, and update_xz_settings_for_version. + +N_MAX_REMOTE_MATCHES=${N_MAX_REMOTE_MATCHES:-20} +# modified by update_mirror_settings_for_version +g_mirror_url=${N_NODE_MIRROR} +g_mirror_folder_name="node" + +# Options for curl and wget. +# Defining commands in variables is fraught (https://mywiki.wooledge.org/BashFAQ/050) +# but we can follow the simple case and store arguments in an array. + +GET_SHOWS_PROGRESS="false" +# --location to follow redirects +# --fail to avoid happily downloading error page from web server for 404 et al +# --show-error to show why failed (on stderr) +CURL_OPTIONS=( "--location" "--fail" "--show-error" ) +if [[ -t 1 ]]; then + CURL_OPTIONS+=( "--progress-bar" ) + command -v curl &> /dev/null && GET_SHOWS_PROGRESS="true" +else + CURL_OPTIONS+=( "--silent" ) +fi +WGET_OPTIONS=( "-q" "-O-" ) + +# Legacy support using unprefixed env. No longer documented in README. +if [ -n "$HTTP_USER" ];then + if [ -z "$HTTP_PASSWORD" ]; then + abort "Must specify HTTP_PASSWORD when supplying HTTP_USER" + fi + CURL_OPTIONS+=( "-u $HTTP_USER:$HTTP_PASSWORD" ) + WGET_OPTIONS+=( "--http-password=$HTTP_PASSWORD" + "--http-user=$HTTP_USER" ) +elif [ -n "$HTTP_PASSWORD" ]; then + abort "Must specify HTTP_USER when supplying HTTP_PASSWORD" +fi + +# Set by set_active_node +g_active_node= + +# set by various lookups to allow mixed logging and return value from function, especially for engine and node +g_target_node= + +DOWNLOAD=false # set to opt-out of activate (install), and opt-in to download (run, exec) +ARCH= +SHOW_VERBOSE_LOG="true" + +# ANSI escape codes +# https://en.wikipedia.org/wiki/ANSI_escape_code +# https://no-color.org +# https://bixense.com/clicolors + +USE_COLOR="true" +if [[ -n "${CLICOLOR_FORCE+defined}" && "${CLICOLOR_FORCE}" != "0" ]]; then + USE_COLOR="true" +elif [[ -n "${NO_COLOR+defined}" || "${CLICOLOR}" = "0" || ! -t 1 ]]; then + USE_COLOR="false" +fi +readonly USE_COLOR +# Select Graphic Rendition codes +if [[ "${USE_COLOR}" = "true" ]]; then + # KISS and use codes rather than tput, avoid dealing with missing tput or TERM. + readonly SGR_RESET="\033[0m" + readonly SGR_FAINT="\033[2m" + readonly SGR_RED="\033[31m" + readonly SGR_CYAN="\033[36m" +else + readonly SGR_RESET= + readonly SGR_FAINT= + readonly SGR_RED= + readonly SGR_CYAN= +fi + +# +# set_arch to override $(uname -a) +# + +set_arch() { + if test -n "$1"; then + ARCH="$1" + else + abort "missing -a|--arch value" + fi +} + +# +# Synopsis: set_insecure +# Globals modified: +# - CURL_OPTIONS +# - WGET_OPTIONS +# + +function set_insecure() { + CURL_OPTIONS+=( "--insecure" ) + WGET_OPTIONS+=( "--no-check-certificate" ) +} + +# +# Synposis: display_major_version numeric-version +# +display_major_version() { + local version=$1 + version="${version#v}" + version="${version%%.*}" + echo "${version}" +} + +# +# Synopsis: update_mirror_settings_for_version version +# e.g. means using download mirror and folder is nightly +# Globals modified: +# - g_mirror_url +# - g_mirror_folder_name +# + +function update_mirror_settings_for_version() { + if is_download_folder "$1" ; then + g_mirror_folder_name="$1" + g_mirror_url="${N_NODE_DOWNLOAD_MIRROR}/${g_mirror_folder_name}" + elif is_download_version "$1"; then + [[ "$1" =~ ^([^/]+)/(.*) ]] + local remote_folder="${BASH_REMATCH[1]}" + g_mirror_folder_name="${remote_folder}" + g_mirror_url="${N_NODE_DOWNLOAD_MIRROR}/${g_mirror_folder_name}" + fi +} + +# +# Synopsis: update_xz_settings_for_version numeric-version +# Globals modified: +# - N_USE_XZ +# + +function update_xz_settings_for_version() { + # tarballs in xz format were available in later version of iojs, but KISS and only use xz from v4. + if [[ "${N_USE_XZ}" = "true" ]]; then + local major_version="$(display_major_version "$1")" + if [[ "${major_version}" -lt 4 ]]; then + N_USE_XZ="false" + fi + fi +} + +# +# Synopsis: update_arch_settings_for_version numeric-version +# Globals modified: +# - ARCH +# + +function update_arch_settings_for_version() { + local tarball_platform="$(display_tarball_platform)" + if [[ -z "${ARCH}" && "${tarball_platform}" = "darwin-arm64" ]]; then + # First native builds were for v16, but can use x64 in rosetta for older versions. + local major_version="$(display_major_version "$1")" + if [[ "${major_version}" -lt 16 ]]; then + ARCH=x64 + fi + fi +} + +# +# Synopsis: is_lts_codename version +# + +function is_lts_codename() { + # https://github.com/nodejs/Release/blob/master/CODENAMES.md + # e.g. argon, Boron + [[ "$1" =~ ^([Aa]rgon|[Bb]oron|[Cc]arbon|[Dd]ubnium|[Ee]rbium|[Ff]ermium|[Gg]allium|[Hh]ydrogen|[Ii]ron|[Jj]od)$ ]] +} + +# +# Synopsis: is_download_folder version +# + +function is_download_folder() { + # e.g. nightly + [[ "$1" =~ ^(next-nightly|nightly|rc|release|test|v8-canary)$ ]] +} + +# +# Synopsis: is_download_version version +# + +function is_download_version() { + # e.g. nightly/, nightly/latest, nightly/v11 + if [[ "$1" =~ ^([^/]+)/(.*) ]]; then + local remote_folder="${BASH_REMATCH[1]}" + is_download_folder "${remote_folder}" + return + fi + return 2 +} + +# +# Synopsis: is_numeric_version version +# + +function is_numeric_version() { + # e.g. 6, v7.1, 8.11.3 + [[ "$1" =~ ^[v]{0,1}[0-9]+(\.[0-9]+){0,2}$ ]] +} + +# +# Synopsis: is_exact_numeric_version version +# + +function is_exact_numeric_version() { + # e.g. 6, v7.1, 8.11.3 + [[ "$1" =~ ^[v]{0,1}[0-9]+\.[0-9]+\.[0-9]+$ ]] +} + +# +# Synopsis: is_node_support_version version +# Reference: https://github.com/nodejs/package-maintenance/issues/236#issue-474783582 +# + +function is_node_support_version() { + [[ "$1" =~ ^(active|lts_active|lts_latest|lts|current|supported)$ ]] +} + +# +# Synopsis: display_latest_node_support_alias version +# Map aliases onto existing n aliases, current and lts +# + +function display_latest_node_support_alias() { + case "$1" in + "active") printf "current" ;; + "lts_active") printf "lts" ;; + "lts_latest") printf "lts" ;; + "lts") printf "lts" ;; + "current") printf "current" ;; + "supported") printf "current" ;; + *) printf "unexpected-version" + esac +} + +# +# Functions used when showing versions installed +# + +enter_fullscreen() { + # Set cursor to be invisible + tput civis 2> /dev/null + # Save screen contents + tput smcup 2> /dev/null + stty -echo +} + +leave_fullscreen() { + # Set cursor to normal + tput cnorm 2> /dev/null + # Restore screen contents + tput rmcup 2> /dev/null + stty echo +} + +handle_sigint() { + leave_fullscreen + S="$?" + kill 0 + exit $S +} + +handle_sigtstp() { + leave_fullscreen + kill -s SIGSTOP $$ +} + +# +# Output usage information. +# + +display_help() { + cat <<-EOF + +Usage: n [options] [COMMAND] [args] + +Commands: + + n Display downloaded Node.js versions and install selection + n latest Install the latest Node.js release (downloading if necessary) + n lts Install the latest LTS Node.js release (downloading if necessary) + n Install Node.js (downloading if necessary) + n install Install Node.js (downloading if necessary) + n run [args ...] Execute downloaded Node.js with [args ...] + n which Output path for downloaded node + n exec [args...] Execute command with modified PATH, so downloaded node and npm first + n rm Remove the given downloaded version(s) + n prune Remove all downloaded versions except the installed version + n --latest Output the latest Node.js version available + n --lts Output the latest LTS Node.js version available + n ls Output downloaded versions + n ls-remote [version] Output matching versions available for download + n uninstall Remove the installed Node.js + +Options: + + -V, --version Output version of n + -h, --help Display help information + -p, --preserve Preserve npm and npx during install of Node.js + -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 + --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. + +Aliases: + + install: i + latest: current + ls: list + lsr: ls-remote + lts: stable + rm: - + run: use, as + which: bin + +Versions: + + Numeric version numbers can be complete or incomplete, with an optional leading 'v'. + Versions can also be specified by label, or codename, + and other downloadable releases by / + + 4.9.1, 8, v6.1 Numeric versions + lts Newest Long Term Support official release + latest, current Newest official release + auto Read version from file: .n-node-version, .node-version, .nvmrc, or package.json + engine Read version from package.json + boron, carbon Codenames for release streams + lts_latest Node.js support aliases + + and nightly, rc/10 et al + +EOF +} + +err_no_installed_print_help() { + display_help + abort "no downloaded versions yet, see above help for commands" +} + +# +# Synopsis: next_version_installed selected_version +# Output version after selected (which may be blank under some circumstances). +# + +function next_version_installed() { + display_cache_versions | n_grep "$1" -A 1 | tail -n 1 +} + +# +# Synopsis: prev_version_installed selected_version +# Output version before selected (which may be blank under some circumstances). +# + +function prev_version_installed() { + display_cache_versions | n_grep "$1" -B 1 | head -n 1 +} + +# +# Output n version. +# + +display_n_version() { + echo "$VERSION" && exit 0 +} + +# +# Synopsis: set_active_node +# Checks cached downloads for a binary matching the active node. +# Globals modified: +# - g_active_node +# + +function set_active_node() { + g_active_node= + local node_path="$(command -v node)" + if [[ -x "${node_path}" ]]; then + local installed_version=$(node --version) + installed_version=${installed_version#v} + for dir in "${CACHE_DIR}"/*/ ; do + local folder_name="${dir%/}" + folder_name="${folder_name##*/}" + if diff &> /dev/null \ + "${CACHE_DIR}/${folder_name}/${installed_version}/bin/node" \ + "${node_path}" ; then + g_active_node="${folder_name}/${installed_version}" + break + fi + done + fi +} + +# +# Display sorted versions directories paths. +# + +display_versions_paths() { + find "$CACHE_DIR" -maxdepth 2 -type d \ + | sed 's|'"$CACHE_DIR"'/||g' \ + | n_grep -E "/[0-9]+\.[0-9]+\.[0-9]+" \ + | sed 's|/|.|' \ + | sort -k 1,1 -k 2,2n -k 3,3n -k 4,4n -t . \ + | sed 's|\.|/|' +} + +# +# Display installed versions with +# + +display_versions_with_selected() { + local selected="$1" + echo + for version in $(display_versions_paths); do + if test "$version" = "$selected"; then + printf " ${SGR_CYAN}ο${SGR_RESET} %s\n" "$version" + else + printf " ${SGR_FAINT}%s${SGR_RESET}\n" "$version" + fi + done + echo + printf "Use up/down arrow keys to select a version, return key to install, d to delete, q to quit" +} + +# +# Synopsis: display_cache_versions +# + +function display_cache_versions() { + for folder_and_version in $(display_versions_paths); do + echo "${folder_and_version}" + done +} + +# +# Display current node --version and others installed. +# + +menu_select_cache_versions() { + enter_fullscreen + set_active_node + local selected="${g_active_node}" + + clear + display_versions_with_selected "${selected}" + + trap handle_sigint INT + trap handle_sigtstp SIGTSTP + + ESCAPE_SEQ=$'\033' + UP=$'A' + DOWN=$'B' + CTRL_P=$'\020' + CTRL_N=$'\016' + + while true; do + read -rsn 1 key + case "$key" in + "$ESCAPE_SEQ") + # Handle ESC sequences followed by other characters, i.e. arrow keys + read -rsn 1 -t 1 tmp + # See "[" if terminal in normal mode, and "0" in application mode + if [[ "$tmp" == "[" || "$tmp" == "O" ]]; then + read -rsn 1 -t 1 arrow + case "$arrow" in + "$UP") + clear + selected="$(prev_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + "$DOWN") + clear + selected="$(next_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + esac + fi + ;; + "d") + if [[ -n "${selected}" ]]; then + clear + # Note: prev/next is constrained to min/max + local after_delete_selection="$(next_version_installed "${selected}")" + if [[ "${after_delete_selection}" == "${selected}" ]]; then + after_delete_selection="$(prev_version_installed "${selected}")" + fi + remove_versions "${selected}" + + if [[ "${after_delete_selection}" == "${selected}" ]]; then + clear + leave_fullscreen + echo "All downloaded versions have been deleted from cache." + exit + fi + + selected="${after_delete_selection}" + display_versions_with_selected "${selected}" + fi + ;; + # Vim or Emacs 'up' key + "k"|"$CTRL_P") + clear + selected="$(prev_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + # Vim or Emacs 'down' key + "j"|"$CTRL_N") + clear + selected="$(next_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + "q") + clear + leave_fullscreen + exit + ;; + "") + # enter key returns empty string + leave_fullscreen + [[ -n "${selected}" ]] && activate "${selected}" + exit + ;; + esac + done +} + +# +# Move up a line and erase. +# + +erase_line() { + printf "\033[1A\033[2K" +} + +# +# Disable PaX mprotect for +# + +disable_pax_mprotect() { + test -z "$1" && abort "binary required" + local binary="$1" + + # try to disable mprotect via XATTR_PAX header + local PAXCTL="$(PATH="/sbin:/usr/sbin:$PATH" command -v paxctl-ng 2>&1)" + local PAXCTL_ERROR=1 + if [ -x "$PAXCTL" ]; then + $PAXCTL -l && $PAXCTL -m "$binary" >/dev/null 2>&1 + PAXCTL_ERROR="$?" + fi + + # try to disable mprotect via PT_PAX header + if [ "$PAXCTL_ERROR" != 0 ]; then + PAXCTL="$(PATH="/sbin:/usr/sbin:$PATH" command -v paxctl 2>&1)" + if [ -x "$PAXCTL" ]; then + $PAXCTL -Cm "$binary" >/dev/null 2>&1 + fi + fi +} + +# +# clean_copy_folder +# + +clean_copy_folder() { + local source="$1" + local target="$2" + if [[ -d "${source}" ]]; then + rm -rf "${target}" + cp -fR "${source}" "${target}" + fi +} + +# +# Activate +# + +activate() { + local version="$1" + local dir="$CACHE_DIR/$version" + local original_node="$(command -v node)" + local installed_node="${N_PREFIX}/bin/node" + log "copying" "$version" + + + # Ideally we would just copy from cache to N_PREFIX, but there are some complications + # - various linux versions use symlinks for folders in /usr/local and also error when copy folder onto symlink + # - we have used cp for years, so keep using it for backwards compatibility (instead of say rsync) + # - we allow preserving npm + # - we want to be somewhat robust to changes in tarball contents, so use find instead of hard-code expected subfolders + # + # This code was purist and concise for a long time. + # Now twice as much code, but using same code path for all uses, and supporting more setups. + + # Copy lib before bin so symlink targets exist. + # lib + mkdir -p "$N_PREFIX/lib" + # Copy everything except node_modules. + find "$dir/lib" -mindepth 1 -maxdepth 1 \! -name node_modules -exec cp -fR "{}" "$N_PREFIX/lib" \; + if [[ -z "${N_PRESERVE_NPM}" ]]; then + mkdir -p "$N_PREFIX/lib/node_modules" + # Copy just npm, skipping possible added global modules after download. Clean copy to avoid version change problems. + clean_copy_folder "$dir/lib/node_modules/npm" "$N_PREFIX/lib/node_modules/npm" + fi + # Takes same steps for corepack (experimental in node 16.9.0) as for npm, to avoid version problems. + if [[ -e "$dir/lib/node_modules/corepack" && -z "${N_PRESERVE_COREPACK}" ]]; then + mkdir -p "$N_PREFIX/lib/node_modules" + clean_copy_folder "$dir/lib/node_modules/corepack" "$N_PREFIX/lib/node_modules/corepack" + fi + + # bin + mkdir -p "$N_PREFIX/bin" + # Remove old node to avoid potential problems with firewall getting confused on Darwin by overwrite. + rm -f "$N_PREFIX/bin/node" + # Copy bin items by hand, in case user has installed global npm modules into cache. + cp -f "$dir/bin/node" "$N_PREFIX/bin" + [[ -e "$dir/bin/node-waf" ]] && cp -f "$dir/bin/node-waf" "$N_PREFIX/bin" # v0.8.x + if [[ -z "${N_PRESERVE_COREPACK}" ]]; then + [[ -e "$dir/bin/corepack" ]] && cp -fR "$dir/bin/corepack" "$N_PREFIX/bin" # from 16.9.0 + fi + if [[ -z "${N_PRESERVE_NPM}" ]]; then + [[ -e "$dir/bin/npm" ]] && cp -fR "$dir/bin/npm" "$N_PREFIX/bin" + [[ -e "$dir/bin/npx" ]] && cp -fR "$dir/bin/npx" "$N_PREFIX/bin" + fi + + # include + mkdir -p "$N_PREFIX/include" + find "$dir/include" -mindepth 1 -maxdepth 1 -exec cp -fR "{}" "$N_PREFIX/include" \; + + # share + mkdir -p "$N_PREFIX/share" + # Copy everything except man, at it is a symlink on some Linux (e.g. archlinux). + find "$dir/share" -mindepth 1 -maxdepth 1 \! -name man -exec cp -fR "{}" "$N_PREFIX/share" \; + mkdir -p "$N_PREFIX/share/man" + find "$dir/share/man" -mindepth 1 -maxdepth 1 -exec cp -fR "{}" "$N_PREFIX/share/man" \; + + disable_pax_mprotect "${installed_node}" + + local active_node="$(command -v node)" + if [[ -e "${active_node}" && -e "${installed_node}" && "${active_node}" != "${installed_node}" ]]; then + # Installed and active are different which might be a PATH problem. List both to give user some clues. + log "installed" "$("${installed_node}" --version) to ${installed_node}" + log "active" "$("${active_node}" --version) at ${active_node}" + else + local npm_version_str="" + local installed_npm="${N_PREFIX}/bin/npm" + local active_npm="$(command -v npm)" + if [[ -z "${N_PRESERVE_NPM}" && -e "${active_npm}" && -e "${installed_npm}" && "${active_npm}" = "${installed_npm}" ]]; then + npm_version_str=" (with npm $(npm --version))" + fi + + log "installed" "$("${installed_node}" --version)${npm_version_str}" + + # Extra tips for changed location. + if [[ -e "${active_node}" && -e "${original_node}" && "${active_node}" != "${original_node}" ]]; then + printf '\nNote: the node command changed location and the old location may be remembered in your current shell.\n' + log old "${original_node}" + log new "${active_node}" + printf 'If "node --version" shows the old version then start a new shell, or reset the location hash with:\nhash -r (for bash, zsh, ash, dash, and ksh)\nrehash (for csh and tcsh)\n' + fi + fi +} + +# +# Install +# + +install() { + [[ -z "$1" ]] && abort "version required" + local version + get_latest_resolved_version "$1" || return 2 + version="${g_target_node}" + [[ -n "${version}" ]] || abort "no version found for '$1'" + update_mirror_settings_for_version "$1" + update_xz_settings_for_version "${version}" + update_arch_settings_for_version "${version}" + + local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + + # Note: decompression flags ignored with default Darwin tar which autodetects. + if test "$N_USE_XZ" = "true"; then + local tarflag="-Jx" + else + local tarflag="-zx" + fi + + if test -d "$dir"; then + if [[ ! -e "$dir/n.lock" ]] ; then + if [[ "$DOWNLOAD" == "false" ]] ; then + activate "${g_mirror_folder_name}/${version}" + fi + exit + fi + fi + + log installing "${g_mirror_folder_name}-v$version" + + local url="$(tarball_url "$version")" + is_ok "${url}" || abort "download preflight failed for '$version' (${url})" + + log mkdir "$dir" + mkdir -p "$dir" || abort "sudo required (or change ownership, or define N_PREFIX)" + touch "$dir/n.lock" + + cd "${dir}" || abort "Failed to cd to ${dir}" + + log fetch "$url" + do_get "${url}" | tar "$tarflag" --strip-components=1 --no-same-owner -f - + pipe_results=( "${PIPESTATUS[@]}" ) + if [[ "${pipe_results[0]}" -ne 0 ]]; then + abort "failed to download archive for $version" + fi + if [[ "${pipe_results[1]}" -ne 0 ]]; then + abort "failed to extract archive for $version" + fi + [ "$GET_SHOWS_PROGRESS" = "true" ] && erase_line + rm -f "$dir/n.lock" + + disable_pax_mprotect bin/node + + if [[ "$DOWNLOAD" == "false" ]]; then + activate "${g_mirror_folder_name}/$version" + fi +} + +# +# Be more silent. +# + +set_quiet() { + SHOW_VERBOSE_LOG="false" + command -v curl > /dev/null && CURL_OPTIONS+=( "--silent" ) && GET_SHOWS_PROGRESS="false" +} + +# +# Synopsis: do_get [option...] url +# Call curl or wget with combination of global and passed options. +# + +function do_get() { + if command -v curl &> /dev/null; then + curl "${CURL_OPTIONS[@]}" "$@" + elif command -v wget &> /dev/null; then + wget "${WGET_OPTIONS[@]}" "$@" + else + abort "curl or wget command required" + fi +} + +# +# Synopsis: do_get_index [option...] url +# Call curl or wget with combination of global and passed options, +# with options tweaked to be more suitable for getting index. +# + +function do_get_index() { + if command -v curl &> /dev/null; then + # --silent to suppress progress et al + curl --silent --compressed "${CURL_OPTIONS[@]}" "$@" + elif command -v wget &> /dev/null; then + wget "${WGET_OPTIONS[@]}" "$@" + else + abort "curl or wget command required" + fi +} + +# +# Synopsis: remove_versions version ... +# + +function remove_versions() { + [[ -z "$1" ]] && abort "version(s) required" + while [[ $# -ne 0 ]]; do + local version + get_latest_resolved_version "$1" || break + version="${g_target_node}" + if [[ -n "${version}" ]]; then + update_mirror_settings_for_version "$1" + local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + if [[ -s "${dir}" ]]; then + rm -rf "${dir}" + else + echo "$1 (${version}) not in downloads cache" + fi + else + echo "No version found for '$1'" + fi + shift + done +} + +# +# Synopsis: prune_cache +# + +function prune_cache() { + set_active_node + + for folder_and_version in $(display_versions_paths); do + if [[ "${folder_and_version}" != "${g_active_node}" ]]; then + echo "${folder_and_version}" + rm -rf "${CACHE_DIR:?}/${folder_and_version}" + fi + done +} + +# +# Synopsis: find_cached_version version +# Finds cache directory for resolved version. +# Globals modified: +# - g_cached_version + +function find_cached_version() { + [[ -z "$1" ]] && abort "version required" + local version + get_latest_resolved_version "$1" || exit 1 + version="${g_target_node}" + [[ -n "${version}" ]] || abort "no version found for '$1'" + + update_mirror_settings_for_version "$1" + g_cached_version="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + if [[ ! -d "${g_cached_version}" && "${DOWNLOAD}" == "true" ]]; then + (install "${version}") + fi + [[ -d "${g_cached_version}" ]] || abort "'$1' (${version}) not in downloads cache" +} + + +# +# Synopsis: display_bin_path_for_version version +# + +function display_bin_path_for_version() { + find_cached_version "$1" + echo "${g_cached_version}/bin/node" +} + +# +# Synopsis: run_with_version version [args...] +# Run the given of node with [args ..] +# + +function run_with_version() { + find_cached_version "$1" + shift # remove version from parameters + exec "${g_cached_version}/bin/node" "$@" +} + +# +# Synopsis: exec_with_version command [args...] +# Modify the path to include and execute command. +# + +function exec_with_version() { + find_cached_version "$1" + shift # remove version from parameters + PATH="${g_cached_version}/bin:$PATH" exec "$@" +} + +# +# Synopsis: is_ok url +# Check the HEAD response of . +# + +function is_ok() { + # Note: both curl and wget can follow redirects, as present on some mirrors (e.g. https://npm.taobao.org/mirrors/node). + # The output is complicated with redirects, so keep it simple and use command status rather than parse output. + if command -v curl &> /dev/null; then + do_get --silent --head "$1" > /dev/null || return 1 + else + do_get --spider "$1" > /dev/null || return 1 + fi +} + +# +# Synopsis: can_use_xz +# Test system to see if xz decompression is supported by tar. +# + +function can_use_xz() { + # Be conservative and only enable if xz is likely to work. Unfortunately we can't directly query tar itself. + # For research, see https://github.com/shadowspawn/nvh/issues/8 + local uname_s="$(uname -s)" + if [[ "${uname_s}" = "Linux" ]] && command -v xz &> /dev/null ; then + # tar on linux is likely to support xz if it is available as a command + return 0 + elif [[ "${uname_s}" = "Darwin" ]]; then + local macos_version="$(sw_vers -productVersion)" + local macos_major_version="$(echo "${macos_version}" | cut -d '.' -f 1)" + local macos_minor_version="$(echo "${macos_version}" | cut -d '.' -f 2)" + if [[ "${macos_major_version}" -gt 10 || "${macos_minor_version}" -gt 8 ]]; then + # tar on recent Darwin has xz support built-in + return 0 + fi + fi + return 2 # not supported +} + +# +# Synopsis: display_tarball_platform +# + +function display_tarball_platform() { + # https://en.wikipedia.org/wiki/Uname + + local os="unexpected_os" + local uname_a="$(uname -a)" + case "${uname_a}" in + Linux*) os="linux" ;; + Darwin*) os="darwin" ;; + SunOS*) os="sunos" ;; + AIX*) os="aix" ;; + CYGWIN*) >&2 echo_red "Cygwin is not supported by n" ;; + MINGW*) >&2 echo_red "Git BASH (MSYS) is not supported by n" ;; + esac + + local arch="unexpected_arch" + local uname_m="$(uname -m)" + case "${uname_m}" in + x86_64) arch=x64 ;; + i386 | i686) arch="x86" ;; + aarch64) arch=arm64 ;; + armv8l) arch=arm64 ;; # armv8l probably supports arm64, and there is no specific armv8l build so give it a go + *) + # e.g. armv6l, armv7l, arm64 + arch="${uname_m}" + ;; + esac + # Override from command line, or version specific adjustment. + [ -n "$ARCH" ] && arch="$ARCH" + + echo "${os}-${arch}" +} + +# +# Synopsis: display_compatible_file_field +# display for current platform, as per field in index.tab, which is different than actual download +# + +function display_compatible_file_field { + local compatible_file_field="$(display_tarball_platform)" + if [[ -z "${ARCH}" && "${compatible_file_field}" = "darwin-arm64" ]]; then + # Look for arm64 for native but also x64 for older versions which can run in rosetta. + # (Downside is will get an install error if install version above 16 with x64 and not arm64.) + compatible_file_field="osx-arm64-tar|osx-x64-tar" + elif [[ "${compatible_file_field}" =~ darwin-(.*) ]]; then + compatible_file_field="osx-${BASH_REMATCH[1]}-tar" + fi + echo "${compatible_file_field}" +} + +# +# Synopsis: tarball_url version +# + +function tarball_url() { + local version="$1" + local ext=gz + [ "$N_USE_XZ" = "true" ] && ext="xz" + echo "${g_mirror_url}/v${version}/node-v${version}-$(display_tarball_platform).tar.${ext}" +} + +# +# Synopsis: get_file_node_version filename +# Sets g_target_node +# + +function get_file_node_version() { + g_target_node= + local filepath="$1" + verbose_log "found" "${filepath}" + # read returns a non-zero status but does still work if there is no line ending + local version + <"${filepath}" read -r version + # trim possible trailing \d from a Windows created file + version="${version%%[[:space:]]}" + verbose_log "read" "${version}" + g_target_node="${version}" +} + +# +# Synopsis: get_package_engine_version\ +# Sets g_target_node +# + +function get_package_engine_version() { + g_target_node= + local filepath="$1" + verbose_log "found" "${filepath}" + command -v node &> /dev/null || abort "an active version of node is required to read 'engines' from package.json" + local range + range="$(node -e "package = require('${filepath}'); if (package && package.engines && package.engines.node) console.log(package.engines.node)")" + verbose_log "read" "${range}" + [[ -n "${range}" ]] || return 2 + if [[ "*" == "${range}" ]]; then + verbose_log "target" "current" + g_target_node="current" + return + fi + + local version + if [[ "${range}" =~ ^([>~^=]|\>\=)?v?([0-9]+(\.[0-9]+){0,2})(.[xX*])?$ ]]; then + local operator="${BASH_REMATCH[1]}" + version="${BASH_REMATCH[2]}" + case "${operator}" in + '' | =) ;; + \> | \>=) version="current" ;; + \~) [[ "${version}" =~ ^([0-9]+\.[0-9]+)\.[0-9]+$ ]] && version="${BASH_REMATCH[1]}" ;; + ^) [[ "${version}" =~ ^([0-9]+) ]] && version="${BASH_REMATCH[1]}" ;; + esac + 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" + verbose_log "resolving" "${range}" + local version_per_line="$(n lsr --all)" + local versions_one_line=$(echo "${version_per_line}" | tr '\n' ' ') + # Using semver@7 so works with older versions of node. + # shellcheck disable=SC2086 + version=$(npm_config_yes=true npx --quiet semver@7 -r "${range}" ${versions_one_line} | tail -n 1) + fi + g_target_node="${version}" +} + +# +# Synopsis: get_nvmrc_version +# Sets g_target_node +# + +function get_nvmrc_version() { + g_target_node= + local filepath="$1" + verbose_log "found" "${filepath}" + local version + <"${filepath}" read -r version + verbose_log "read" "${version}" + # Translate from nvm aliases + case "${version}" in + lts/\*) version="lts" ;; + lts/*) version="${version:4}" ;; + node) version="current" ;; + *) ;; + esac + g_target_node="${version}" +} + +# +# Synopsis: get_engine_version [error-message] +# Sets g_target_node +# + +function get_engine_version() { + g_target_node= + local error_message="${1-package.json not found}" + local parent + parent="${PWD}" + while [[ -n "${parent}" ]]; do + if [[ -e "${parent}/package.json" ]]; then + get_package_engine_version "${parent}/package.json" + else + parent=${parent%/*} + continue + fi + break + done + [[ -n "${parent}" ]] || abort "${error_message}" + [[ -n "${g_target_node}" ]] || abort "did not find supported version of node in 'engines' field of package.json" +} + +# +# Synopsis: get_auto_version +# Sets g_target_node +# + +function get_auto_version() { + g_target_node= + # Search for a version control file first + local parent + parent="${PWD}" + while [[ -n "${parent}" ]]; do + if [[ -e "${parent}/.n-node-version" ]]; then + get_file_node_version "${parent}/.n-node-version" + elif [[ -e "${parent}/.node-version" ]]; then + get_file_node_version "${parent}/.node-version" + elif [[ -e "${parent}/.nvmrc" ]]; then + get_nvmrc_version "${parent}/.nvmrc" + else + parent=${parent%/*} + continue + fi + break + done + # Fallback to package.json + [[ -n "${parent}" ]] || get_engine_version "no file found for auto version (.n-node-version, .node-version, .nvmrc, or package.json)" + [[ -n "${g_target_node}" ]] || abort "file found for auto did not contain target version of node" +} + +# +# Synopsis: get_latest_resolved_version version +# Sets g_target_node +# + +function get_latest_resolved_version() { + g_target_node= + local version=${1} + simple_version=${version#node/} # Only place supporting node/ [sic] + if is_exact_numeric_version "${simple_version}"; then + # Just numbers, already resolved, no need to lookup first. + simple_version="${simple_version#v}" + g_target_node="${simple_version}" + else + # Complicated recognising exact version, KISS and lookup. + g_target_node=$(N_MAX_REMOTE_MATCHES=1 display_remote_versions "$version") + fi +} + +# +# Synopsis: display_remote_index +# index.tab reference: https://github.com/nodejs/nodejs-dist-indexer +# Index fields are: version date files npm v8 uv zlib openssl modules lts security +# KISS and just return fields we currently care about: version files lts +# + +display_remote_index() { + local index_url="${g_mirror_url}/index.tab" + # tail to remove header line + do_get_index "${index_url}" | tail -n +2 | cut -f 1,3,10 + if [[ "${PIPESTATUS[0]}" -ne 0 ]]; then + # Reminder: abort will only exit subshell, but consistent error display + abort "failed to download version index (${index_url})" + fi +} + +# +# Synopsis: display_match_limit limit +# + +function display_match_limit(){ + if [[ "$1" -gt 1 && "$1" -lt 32000 ]]; then + echo "Listing remote... Displaying $1 matches (use --all to see all)." + fi +} + +# +# Synopsis: display_remote_versions version +# + +function display_remote_versions() { + local version="$1" + update_mirror_settings_for_version "${version}" + local match='.' + local match_count="${N_MAX_REMOTE_MATCHES}" + + # 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 [[ -z "${version}" ]]; then + match='.' + elif [[ "${version}" = "lts" || "${version}" = "stable" ]]; then + match_count=1 + # Codename is last field, first one with a name is newest lts + match="${TAB_CHAR}[a-zA-Z]+\$" + elif [[ "${version}" = "latest" || "${version}" = "current" ]]; then + match_count=1 + match='.' + elif is_numeric_version "${version}"; then + version="v${version#v}" + # Avoid restriction message if exact version + is_exact_numeric_version "${version}" && match_count=1 + # Quote any dots in version so they are literal for expression + match="${version//\./\.}" + # Avoid 1.2 matching 1.23 + match="^${match}[^0-9]" + elif is_lts_codename "${version}"; then + # Capitalise (could alternatively make grep case insensitive) + codename="$(echo "${version:0:1}" | tr '[:lower:]' '[:upper:]')${version:1}" + # Codename is last field + match="${TAB_CHAR}${codename}\$" + elif is_download_folder "${version}"; then + match='.' + elif is_download_version "${version}"; then + version="${version#"${g_mirror_folder_name}"/}" + if [[ "${version}" = "latest" || "${version}" = "current" ]]; then + match_count=1 + match='.' + else + version="v${version#v}" + match="${version//\./\.}" + match="^${match}" # prefix + if is_numeric_version "${version}"; then + # Exact numeric match + match="${match}[^0-9]" + fi + fi + else + abort "invalid version '$1'" + fi + display_match_limit "${match_count}" + + # Implementation notes: + # - using awk rather than head so do not close pipe early on curl + # - restrict search to compatible files as not always available, or not at same time + # - return status of curl command (i.e. PIPESTATUS[0]) + display_remote_index \ + | n_grep -E "$(display_compatible_file_field)" \ + | n_grep -E "${match}" \ + | awk "NR<=${match_count}" \ + | cut -f 1 \ + | n_grep -E -o '[^v].*' + return "${PIPESTATUS[0]}" +} + +# +# Synopsis: delete_with_echo target +# + +function delete_with_echo() { + if [[ -e "$1" ]]; then + echo "$1" + rm -rf "$1" + fi +} + +# +# Synopsis: uninstall_installed +# Uninstall the installed node and npm (leaving alone the cache), +# so undo install, and may expose possible system installed versions. +# + +uninstall_installed() { + # npm: https://docs.npmjs.com/misc/removing-npm + # rm -rf /usr/local/{lib/node{,/.npm,_modules},bin,share/man}/npm* + # node: https://stackabuse.com/how-to-uninstall-node-js-from-mac-osx/ + # Doing it by hand rather than scanning cache, so still works if cache deleted first. + # This covers tarballs for at least node 4 through 10. + + while true; do + read -r -p "Do you wish to delete node and npm from ${N_PREFIX}? " yn + case $yn in + [Yy]* ) break ;; + [Nn]* ) exit ;; + * ) echo "Please answer yes or no.";; + esac + done + + echo "" + echo "Uninstalling node and npm" + delete_with_echo "${N_PREFIX}/bin/node" + delete_with_echo "${N_PREFIX}/bin/npm" + delete_with_echo "${N_PREFIX}/bin/npx" + delete_with_echo "${N_PREFIX}/bin/corepack" + delete_with_echo "${N_PREFIX}/include/node" + delete_with_echo "${N_PREFIX}/lib/dtrace/node.d" + delete_with_echo "${N_PREFIX}/lib/node_modules/npm" + delete_with_echo "${N_PREFIX}/lib/node_modules/corepack" + delete_with_echo "${N_PREFIX}/share/doc/node" + delete_with_echo "${N_PREFIX}/share/man/man1/node.1" + delete_with_echo "${N_PREFIX}/share/systemtap/tapset/node.stp" +} + +# +# Synopsis: show_permission_suggestions +# + +function show_permission_suggestions() { + echo "Suggestions:" + echo "- run n with sudo, or" + echo "- define N_PREFIX to a writeable location, or" +} + +# +# Synopsis: show_diagnostics +# Show environment and check for common problems. +# + +function show_diagnostics() { + echo "This information is to help you diagnose issues, and useful when reporting an issue." + echo "Note: some output may contain passwords. Redact before sharing." + + printf "\n\nCOMMAND LOCATIONS AND VERSIONS\n" + + printf "\nbash\n" + command -v bash && bash --version + + printf "\nn\n" + command -v n && n --version + + printf "\nnode\n" + if command -v node &> /dev/null; then + command -v node && node --version + node -e 'if (process.versions.v8) console.log("JavaScript engine: v8");' + + printf "\nnpm\n" + command -v npm && npm --version + fi + + printf "\ntar\n" + if command -v tar &> /dev/null; then + command -v tar && tar --version + else + echo_red "tar not found. Needed for extracting downloads." + fi + + printf "\ncurl or wget\n" + if command -v curl &> /dev/null; then + command -v curl && curl --version + elif command -v wget &> /dev/null; then + command -v wget && wget --version + else + echo_red "Neither curl nor wget found. Need one of them for downloads." + fi + + printf "\nuname\n" + uname -a + + printf "\n\nSETTINGS\n" + + printf "\nn\n" + echo "node mirror: ${N_NODE_MIRROR}" + echo "node downloads mirror: ${N_NODE_DOWNLOAD_MIRROR}" + echo "install destination: ${N_PREFIX}" + [[ -n "${N_PREFIX}" ]] && echo "PATH: ${PATH}" + echo "ls-remote max matches: ${N_MAX_REMOTE_MATCHES}" + [[ -n "${N_PRESERVE_NPM}" ]] && echo "installs preserve npm by default" + [[ -n "${N_PRESERVE_COREPACK}" ]] && echo "installs preserve corepack by default" + + printf "\nProxy\n" + # disable "var is referenced but not assigned": https://github.com/koalaman/shellcheck/wiki/SC2154 + # shellcheck disable=SC2154 + [[ -n "${http_proxy}" ]] && echo "http_proxy: ${http_proxy}" + # shellcheck disable=SC2154 + [[ -n "${https_proxy}" ]] && echo "https_proxy: ${https_proxy}" + if command -v curl &> /dev/null; then + # curl supports lower case and upper case! + # shellcheck disable=SC2154 + [[ -n "${all_proxy}" ]] && echo "all_proxy: ${all_proxy}" + [[ -n "${ALL_PROXY}" ]] && echo "ALL_PROXY: ${ALL_PROXY}" + [[ -n "${HTTP_PROXY}" ]] && echo "HTTP_PROXY: ${HTTP_PROXY}" + [[ -n "${HTTPS_PROXY}" ]] && echo "HTTPS_PROXY: ${HTTPS_PROXY}" + if [[ -e "${CURL_HOME}/.curlrc" ]]; then + echo "have \$CURL_HOME/.curlrc" + elif [[ -e "${HOME}/.curlrc" ]]; then + echo "have \$HOME/.curlrc" + fi + elif command -v wget &> /dev/null; then + if [[ -e "${WGETRC}" ]]; then + echo "have \$WGETRC" + elif [[ -e "${HOME}/.wgetrc" ]]; then + echo "have \$HOME/.wgetrc" + fi + fi + + printf "\n\nCHECKS\n" + + printf "\nChecking n install destination is in PATH...\n" + local install_bin="${N_PREFIX}/bin" + local path_wth_guards=":${PATH}:" + if [[ "${path_wth_guards}" =~ :${install_bin}/?: ]]; then + printf "good\n" + else + echo_red "'${install_bin}' is not in PATH" + fi + if command -v node &> /dev/null; then + printf "\nChecking n install destination priority in PATH...\n" + local node_dir="$(dirname "$(command -v node)")" + + local index=0 + local path_entry + local path_entries + local install_bin_index=0 + local node_index=999 + IFS=':' read -ra path_entries <<< "${PATH}" + for path_entry in "${path_entries[@]}"; do + (( index++ )) + [[ "${path_entry}" =~ ^${node_dir}/?$ ]] && node_index="${index}" + [[ "${path_entry}" =~ ^${install_bin}/?$ ]] && install_bin_index="${index}" + done + if [[ "${node_index}" -lt "${install_bin_index}" ]]; then + echo_red "There is a version of node installed which will be found in PATH before the n installed version." + else + printf "good\n" + fi + fi + + printf "\nChecking permissions for cache folder...\n" + # Most likely problem is ownership rather than than permissions as such. + local cache_root="${N_PREFIX}/n" + if [[ -e "${N_PREFIX}" && ! -w "${N_PREFIX}" && ! -e "${cache_root}" ]]; then + echo_red "You do not have write permission to create: ${cache_root}" + show_permission_suggestions + echo "- make a folder you own:" + echo " sudo mkdir -p \"${cache_root}\"" + echo " sudo chown $(whoami) \"${cache_root}\"" + elif [[ -e "${cache_root}" && ! -w "${cache_root}" ]]; then + echo_red "You do not have write permission to: ${cache_root}" + show_permission_suggestions + echo "- change folder ownership to yourself:" + echo " sudo chown -R $(whoami) \"${cache_root}\"" + elif [[ ! -e "${cache_root}" ]]; then + echo "Cache folder does not exist: ${cache_root}" + echo "This is normal if you have not done an install yet, as cache is only created when needed." + elif [[ -e "${CACHE_DIR}" && ! -w "${CACHE_DIR}" ]]; then + echo_red "You do not have write permission to: ${CACHE_DIR}" + show_permission_suggestions + echo "- change folder ownership to yourself:" + echo " sudo chown -R $(whoami) \"${CACHE_DIR}\"" + else + echo "good" + fi + + if [[ -e "${N_PREFIX}" ]]; then + # Most likely problem is ownership rather than than permissions as such. + printf "\nChecking permissions for install folders...\n" + local install_writeable="true" + for subdir in bin lib include share; do + if [[ -e "${N_PREFIX}/${subdir}" && ! -w "${N_PREFIX}/${subdir}" ]]; then + install_writeable="false" + echo_red "You do not have write permission to: ${N_PREFIX}/${subdir}" + break + fi + done + if [[ "${install_writeable}" = "true" ]]; then + echo "good" + else + show_permission_suggestions + echo "- change folder ownerships to yourself:" + echo " (cd \"${N_PREFIX}\" && sudo chown -R $(whoami) bin lib include share)" + fi + fi + + printf "\nChecking mirror is reachable...\n" + if is_ok "${N_NODE_MIRROR}/"; then + printf "good\n" + else + echo_red "mirror not reachable" + printf "Showing failing command and output\n" + if command -v curl &> /dev/null; then + ( set -x; do_get --head "${N_NODE_MIRROR}/" ) + else + ( set -x; do_get --spider "${N_NODE_MIRROR}/" ) + printf "\n" + fi + fi +} + +# +# Handle arguments. +# + +# First pass. Process the options so they can come before or after commands, +# particularly for `n lsr --all` and `n install --arch x686` +# which feel pretty natural. + +unprocessed_args=() +positional_arg="false" + +while [[ $# -ne 0 ]]; do + case "$1" in + --all) N_MAX_REMOTE_MATCHES=32000 ;; + -V|--version) display_n_version ;; + -h|--help|help) display_help; exit ;; + -q|--quiet) set_quiet ;; + -d|--download) DOWNLOAD="true" ;; + --insecure) set_insecure ;; + -p|--preserve) N_PRESERVE_NPM="true" N_PRESERVE_COREPACK="true" ;; + --no-preserve) N_PRESERVE_NPM="" N_PRESERVE_COREPACK="" ;; + --use-xz) N_USE_XZ="true" ;; + --no-use-xz) N_USE_XZ="false" ;; + --latest) display_remote_versions latest; exit ;; + --stable) display_remote_versions lts; exit ;; # [sic] old terminology + --lts) display_remote_versions lts; exit ;; + -a|--arch) shift; set_arch "$1";; # set arch and continue + exec|run|as|use) + unprocessed_args+=( "$1" ) + positional_arg="true" + ;; + *) + if [[ "${positional_arg}" == "true" ]]; then + unprocessed_args+=( "$@" ) + break + fi + unprocessed_args+=( "$1" ) + ;; + esac + shift +done + +if [[ -z "${N_USE_XZ+defined}" ]]; then + N_USE_XZ="true" # Default to using xz + can_use_xz || N_USE_XZ="false" +fi + +set -- "${unprocessed_args[@]}" + +if test $# -eq 0; then + test -z "$(display_versions_paths)" && err_no_installed_print_help + menu_select_cache_versions +else + while test $# -ne 0; do + case "$1" in + bin|which) display_bin_path_for_version "$2"; exit ;; + run|as|use) shift; run_with_version "$@"; exit ;; + exec) shift; exec_with_version "$@"; exit ;; + doctor) show_diagnostics; exit ;; + rm|-) shift; remove_versions "$@"; exit ;; + prune) prune_cache; exit ;; + latest) install latest; exit ;; + stable) install stable; exit ;; + lts) install lts; exit ;; + ls|list) display_versions_paths; exit ;; + lsr|ls-remote|list-remote) shift; display_remote_versions "$1"; exit ;; + uninstall) uninstall_installed; exit ;; + i|install) shift; install "$1"; exit ;; + N_TEST_DISPLAY_LATEST_RESOLVED_VERSION) shift; get_latest_resolved_version "$1" > /dev/null || exit 2; echo "${g_target_node}"; exit ;; + *) install "$1"; exit ;; + esac + shift + done +fi From eaf7a2904c0c609ee37467f45c2a630449461fb1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 11 Mar 2023 14:57:48 +0100 Subject: [PATCH 274/319] helpers: fix ynh_setup_source, 'source_id' may contain slashes x_x --- helpers/utils | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpers/utils b/helpers/utils index 4a964a14e..167b67d37 100644 --- a/helpers/utils +++ b/helpers/utils @@ -235,7 +235,8 @@ ynh_setup_source() { # (Unused?) mecanism where one can have the file in a special local cache to not have to download it... local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${source_id}" - mkdir -p /var/cache/yunohost/download/${YNH_APP_ID}/ + # Gotta use this trick with 'dirname' because source_id may contain slashes x_x + mkdir -p $(dirname /var/cache/yunohost/download/${YNH_APP_ID}/${source_id}) src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${source_id}" if [ "$src_format" = "docker" ]; then From f9a7016931de4293d4a7bcce3ff5357040356349 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 11 Mar 2023 16:51:42 +0100 Subject: [PATCH 275/319] Update changelog for 11.1.15 --- debian/changelog | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debian/changelog b/debian/changelog index a29ba223c..0373a10b8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +yunohost (11.1.15) stable; urgency=low + + - doc: Fix version number in autogenerated resource doc (5b58e0e6) + - helpers: Fix documentation for ynh_setup_source (7491dd4c) + - helpers: fix ynh_setup_source, 'source_id' may contain slashes x_x (eaf7a290) + - helpers/nodejs: simplify 'n' script install and maintenance ([#1627](https://github.com/yunohost/yunohost/pull/1627)) + + -- Alexandre Aubin Sat, 11 Mar 2023 16:50:50 +0100 + yunohost (11.1.14) stable; urgency=low - helpers: simplify --time display option for ynh_script_progression .. we don't care about displaying time when below 10 sc (8731f77a) From a95d10e50c5b60aac7623fa1acc430799686a79d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 13 Mar 2023 18:48:57 +0100 Subject: [PATCH 276/319] backup: fix boring issue where archive is a broken symlink... --- src/backup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backup.py b/src/backup.py index ee218607d..ce1e8ba2c 100644 --- a/src/backup.py +++ b/src/backup.py @@ -2376,6 +2376,7 @@ def backup_list(with_info=False, human_readable=False): # (we do a realpath() to resolve symlinks) archives = glob(f"{ARCHIVES_PATH}/*.tar.gz") + glob(f"{ARCHIVES_PATH}/*.tar") archives = {os.path.realpath(archive) for archive in archives} + archives = {archive for archive in archives if os.path.exists(archive)} archives = sorted(archives, key=lambda x: os.path.getctime(x)) # Extract only filename without the extension From 3656c199186d47d7f07f1bbd8651c77c95cd2fb6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 14 Mar 2023 18:45:04 +0100 Subject: [PATCH 277/319] helpers/appsv2: don't remove yhh-deps virtual package if ... it doesn't exist. Otherwise when apt fails to install dependency, we end up with another error about failing to remove the ynh-deps package --- helpers/apt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helpers/apt b/helpers/apt index c36f4aa27..a2f2d3de8 100644 --- a/helpers/apt +++ b/helpers/apt @@ -370,7 +370,13 @@ ynh_remove_app_dependencies() { apt-mark unhold ${dep_app}-ynh-deps fi - ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. + # Remove the fake package and its dependencies if they not still used. + # (except if dpkg doesn't know anything about the package, + # which should be symptomatic of a failed install, and we don't want bash to report an error) + if dpkg-query --show ${dep_app}-ynh-deps &>/dev/null + then + ynh_package_autopurge ${dep_app}-ynh-deps + fi } # Install packages from an extra repository properly. From b2596f328751a108852d59acb9677292405c0612 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 14 Mar 2023 19:23:24 +0100 Subject: [PATCH 278/319] appsv2: add validation for expected types for permissions stuff --- src/utils/resources.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/utils/resources.py b/src/utils/resources.py index 87446bdd8..b9bb1fee7 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -497,11 +497,21 @@ class PermissionsResource(AppResource): properties["main"] = self.default_perm_properties for perm, infos in properties.items(): + if "auth_header" in infos and not isinstance(infos.get("auth_header"), bool): + raise YunohostError(f"In manifest, for permission '{perm}', 'auth_header' should be a boolean", raw_msg=True) + if "show_tile" in infos and not isinstance(infos.get("show_tile"), bool): + raise YunohostError(f"In manifest, for permission '{perm}', 'show_tile' should be a boolean", raw_msg=True) + if "protected" in infos and not isinstance(infos.get("protected"), bool): + raise YunohostError(f"In manifest, for permission '{perm}', 'protected' should be a boolean", raw_msg=True) + if "additional_urls" in infos and not isinstance(infos.get("additional_urls"), list): + raise YunohostError(f"In manifest, for permission '{perm}', 'additional_urls' should be a list", raw_msg=True) + properties[perm] = copy.copy(self.default_perm_properties) properties[perm].update(infos) if properties[perm]["show_tile"] is None: properties[perm]["show_tile"] = bool(properties[perm]["url"]) + if properties["main"]["url"] is not None and ( not isinstance(properties["main"].get("url"), str) or properties["main"]["url"] != "/" From 1b2fa91ff02d241f2101fdc30d7e22e78ceacc2d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 20 Mar 2023 15:49:23 +0100 Subject: [PATCH 279/319] ynh_setup_source: fix buggy checksum mismatch handling, can't compute the sha256sum after we delete the file @_@ --- helpers/utils | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/helpers/utils b/helpers/utils index 167b67d37..97bd8e6b5 100644 --- a/helpers/utils +++ b/helpers/utils @@ -267,8 +267,10 @@ ynh_setup_source() { # Check the control sum if ! echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status then - rm ${src_filename} - ynh_die --message="Corrupt source for ${src_filename}: Expected ${src_sum} but got $(${src_sumprg} ${src_filename} | cut --delimiter=' ' --fields=1) (size: $(du -hs ${src_filename} | cut --delimiter=' ' --fields=1))." + local actual_sum="$(${src_sumprg} ${src_filename} | cut --delimiter=' ' --fields=1)" + local actual_size="$(du -hs ${src_filename} | cut --delimiter=' ' --fields=1)" + rm -f ${src_filename} + ynh_die --message="Corrupt source for ${src_filename}: Expected ${src_sum} but got ${actual_sum} (size: ${actual_size})." fi fi From c211b75279077754a3a5392b22538e3d2a3c8100 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 21 Mar 2023 18:31:24 +0100 Subject: [PATCH 280/319] options:tests: add base class Test --- src/tests/test_questions.py | 476 +++++++++++++++++++++++++++++++++++- 1 file changed, 475 insertions(+), 1 deletion(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index cf7c3c6e6..e849b6892 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -1,15 +1,22 @@ +import inspect import sys import pytest import os +from contextlib import contextmanager from mock import patch from io import StringIO +from typing import Any, Literal, Sequence, TypedDict, Union + +from _pytest.mark.structures import ParameterSet + from moulinette import Moulinette - from yunohost import domain, user from yunohost.utils.config import ( + ARGUMENTS_TYPE_PARSERS, ask_questions_and_parse_answers, + DisplayTextQuestion, PasswordQuestion, DomainQuestion, PathQuestion, @@ -44,6 +51,473 @@ User answers: """ +# ╭───────────────────────────────────────────────────────╮ +# │ ┌─╮╭─┐╶┬╴╭─╴╷ ╷╶┬╴╭╮╷╭─╮ │ +# │ ├─╯├─┤ │ │ ├─┤ │ ││││╶╮ │ +# │ ╵ ╵ ╵ ╵ ╰─╴╵ ╵╶┴╴╵╰╯╰─╯ │ +# ╰───────────────────────────────────────────────────────╯ + + +@contextmanager +def patch_isatty(isatty): + with patch.object(os, "isatty", return_value=isatty): + yield + + +@contextmanager +def patch_interface(interface: Literal["api", "cli"] = "api"): + with patch.object(Moulinette.interface, "type", interface), patch_isatty( + interface == "cli" + ): + yield + + +@contextmanager +def patch_prompt(return_value): + with patch_interface("cli"), patch.object( + Moulinette, "prompt", return_value=return_value + ) as prompt: + yield prompt + + +@pytest.fixture +def patch_no_tty(): + with patch_isatty(False): + yield + + +@pytest.fixture +def patch_with_tty(): + with patch_isatty(True): + yield + + +# ╭───────────────────────────────────────────────────────╮ +# │ ╭─╴╭─╴┌─╴╭╮╷╭─┐┌─╮╶┬╴╭─╮╭─╴ │ +# │ ╰─╮│ ├─╴│││├─┤├┬╯ │ │ │╰─╮ │ +# │ ╶─╯╰─╴╰─╴╵╰╯╵ ╵╵ ╰╶┴╴╰─╯╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + + +MinScenario = tuple[Any, Union[Literal["FAIL"], Any]] +PartialScenario = tuple[Any, Union[Literal["FAIL"], Any], dict[str, Any]] +FullScenario = tuple[Any, Union[Literal["FAIL"], Any], dict[str, Any], dict[str, Any]] + +Scenario = Union[ + MinScenario, + PartialScenario, + FullScenario, + "InnerScenario", +] + + +class InnerScenario(TypedDict, total=False): + scenarios: Sequence[Scenario] + raw_options: Sequence[dict[str, Any]] + data: Sequence[dict[str, Any]] + + +# ╭───────────────────────────────────────────────────────╮ +# │ Scenario generators/helpers │ +# ╰───────────────────────────────────────────────────────╯ + + +def get_hydrated_scenarios(raw_options, scenarios, data=[{}]): + """ + Normalize and hydrate a mixed list of scenarios to proper tuple/pytest.param flattened list values. + + Example:: + scenarios = [ + { + "raw_options": [{}, {"optional": True}], + "scenarios": [ + ("", "value", {"default": "value"}), + *unchanged("value", "other"), + ] + }, + *all_fails(-1, 0, 1, raw_options={"optional": True}), + *xfail(scenarios=[(True, "True"), (False, "False)], reason="..."), + ] + # Is exactly the same as + scenarios = [ + ("", "value", {"default": "value"}), + ("", "value", {"optional": True, "default": "value"}), + ("value", "value", {}), + ("value", "value", {"optional": True}), + ("other", "other", {}), + ("other", "other", {"optional": True}), + (-1, FAIL, {"optional": True}), + (0, FAIL, {"optional": True}), + (1, FAIL, {"optional": True}), + pytest.param(True, "True", {}, marks=pytest.mark.xfail(reason="...")), + pytest.param(False, "False", {}, marks=pytest.mark.xfail(reason="...")), + ] + """ + hydrated_scenarios = [] + for raw_option in raw_options: + for mocked_data in data: + for scenario in scenarios: + if isinstance(scenario, dict): + merged_raw_options = [ + {**raw_option, **raw_opt} + for raw_opt in scenario.get("raw_options", [{}]) + ] + hydrated_scenarios += get_hydrated_scenarios( + merged_raw_options, + scenario["scenarios"], + scenario.get("data", [mocked_data]), + ) + elif isinstance(scenario, ParameterSet): + intake, output, custom_raw_option = ( + scenario.values + if len(scenario.values) == 3 + else (*scenario.values, {}) + ) + merged_raw_option = {**raw_option, **custom_raw_option} + hydrated_scenarios.append( + pytest.param( + intake, + output, + merged_raw_option, + mocked_data, + marks=scenario.marks, + ) + ) + elif isinstance(scenario, tuple): + intake, output, custom_raw_option = ( + scenario if len(scenario) == 3 else (*scenario, {}) + ) + merged_raw_option = {**raw_option, **custom_raw_option} + hydrated_scenarios.append( + (intake, output, merged_raw_option, mocked_data) + ) + else: + raise Exception( + "Test scenario should be tuple(intake, output, raw_option), pytest.param(intake, output, raw_option) or dict(raw_options, scenarios, data)" + ) + + return hydrated_scenarios + + +def generate_test_name(intake, output, raw_option, data): + values_as_str = [] + for value in (intake, output): + if isinstance(value, str) and value != FAIL: + values_as_str.append(f"'{value}'") + elif inspect.isclass(value) and issubclass(value, Exception): + values_as_str.append(value.__name__) + else: + values_as_str.append(value) + name = f"{values_as_str[0]} -> {values_as_str[1]}" + + keys = [ + "=".join( + [ + key, + str(raw_option[key]) + if not isinstance(raw_option[key], str) + else f"'{raw_option[key]}'", + ] + ) + for key in raw_option.keys() + if key not in ("id", "type") + ] + if keys: + name += " (" + ",".join(keys) + ")" + return name + + +def pytest_generate_tests(metafunc): + """ + Pytest test factory that, for each `BaseTest` subclasses, parametrize its + methods if it requires it by checking the method's parameters. + For those and based on their `cls.scenarios`, a series of `pytest.param` are + automaticly injected as test values. + """ + if metafunc.cls and issubclass(metafunc.cls, BaseTest): + argnames = [] + argvalues = [] + ids = [] + fn_params = inspect.signature(metafunc.function).parameters + + for params in [ + ["intake", "expected_output", "raw_option", "data"], + ["intake", "expected_normalized", "raw_option", "data"], + ["intake", "expected_humanized", "raw_option", "data"], + ]: + if all(param in fn_params for param in params): + argnames += params + if params[1] == "expected_output": + # Hydrate scenarios with generic raw_option data + argvalues += get_hydrated_scenarios( + [metafunc.cls.raw_option], metafunc.cls.scenarios + ) + ids += [ + generate_test_name(*args.values) + if isinstance(args, ParameterSet) + else generate_test_name(*args) + for args in argvalues + ] + elif params[1] == "expected_normalized": + argvalues += metafunc.cls.normalized + ids += [ + f"{metafunc.cls.raw_option['type']}-normalize-{scenario[0]}" + for scenario in metafunc.cls.normalized + ] + elif params[1] == "expected_humanized": + argvalues += metafunc.cls.humanized + ids += [ + f"{metafunc.cls.raw_option['type']}-normalize-{scenario[0]}" + for scenario in metafunc.cls.humanized + ] + + metafunc.parametrize(argnames, argvalues, ids=ids) + + +# ╭───────────────────────────────────────────────────────╮ +# │ Scenario helpers │ +# ╰───────────────────────────────────────────────────────╯ + +FAIL = YunohostValidationError + + +def nones( + *nones, output, raw_option: dict[str, Any] = {}, fail_if_required: bool = True +) -> list[PartialScenario]: + """ + Returns common scenarios for ~None values. + - required and required + as default -> `FAIL` + - optional and optional + as default -> `expected_output=None` + """ + return [ + (none, FAIL if fail_if_required else output, base_raw_option | raw_option) # type: ignore + for none in nones + for base_raw_option in ({}, {"default": none}) + ] + [ + (none, output, base_raw_option | raw_option) + for none in nones + for base_raw_option in ({"optional": True}, {"optional": True, "default": none}) + ] + + +def unchanged(*args, raw_option: dict[str, Any] = {}) -> list[PartialScenario]: + """ + Returns a series of params for which output is expected to be the same as its intake + + Example:: + # expect `"value"` to output as `"value"`, etc. + unchanged("value", "yes", "none") + + """ + return [(arg, arg, raw_option.copy()) for arg in args] + + +def all_as(*args, output, raw_option: dict[str, Any] = {}) -> list[PartialScenario]: + """ + Returns a series of params for which output is expected to be the same single value + + Example:: + # expect all values to output as `True` + all_as("y", "yes", 1, True, output=True) + """ + return [(arg, output, raw_option.copy()) for arg in args] + + +def all_fails( + *args, raw_option: dict[str, Any] = {}, error=FAIL +) -> list[PartialScenario]: + """ + Returns a series of params for which output is expected to be failing with validation error + """ + return [(arg, error, raw_option.copy()) for arg in args] + + +def xpass(*, scenarios: list[Scenario], reason="unknown") -> list[Scenario]: + """ + Return a pytest param for which test should have fail but currently passes. + """ + return [ + pytest.param( + *scenario, + marks=pytest.mark.xfail( + reason=f"Currently valid but probably shouldn't. details: {reason}." + ), + ) + for scenario in scenarios + ] + + +def xfail(*, scenarios: list[Scenario], reason="unknown") -> list[Scenario]: + """ + Return a pytest param for which test should have passed but currently fails. + """ + return [ + pytest.param( + *scenario, + marks=pytest.mark.xfail( + reason=f"Currently invalid but should probably pass. details: {reason}." + ), + ) + for scenario in scenarios + ] + + +# ╭───────────────────────────────────────────────────────╮ +# │ ╶┬╴┌─╴╭─╴╶┬╴╭─╴ │ +# │ │ ├─╴╰─╮ │ ╰─╮ │ +# │ ╵ ╰─╴╶─╯ ╵ ╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + + +def _fill_or_prompt_one_option(raw_option, intake): + raw_option = raw_option.copy() + id_ = raw_option.pop("id") + 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) + + +def _test_value_is_expected_output(value, expected_output): + """ + Properly compares bools and None + """ + if isinstance(expected_output, bool) or expected_output is None: + assert value is expected_output + else: + assert value == expected_output + + +def _test_intake(raw_option, intake, expected_output): + option, value = _fill_or_prompt_one_option(raw_option, intake) + + _test_value_is_expected_output(value, expected_output) + + +def _test_intake_may_fail(raw_option, intake, expected_output): + if inspect.isclass(expected_output) and issubclass(expected_output, Exception): + with pytest.raises(expected_output): + _fill_or_prompt_one_option(raw_option, intake) + else: + _test_intake(raw_option, intake, expected_output) + + +class BaseTest: + raw_option: dict[str, Any] = {} + prefill: dict[Literal["raw_option", "prefill", "intake"], Any] + scenarios: list[Scenario] + + # 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}), + # *nones(None, "", output=""), + # ] + # fmt: on + # TODO + # - pattern (also on Date for example to see if it override the default pattern) + # - example + # - visible + # - redact + # - regex + # - hooks + + @classmethod + def get_raw_option(cls, raw_option={}, **kwargs): + base_raw_option = cls.raw_option.copy() + base_raw_option.update(**raw_option) + base_raw_option.update(**kwargs) + return base_raw_option + + @classmethod + def _test_basic_attrs(self): + raw_option = self.get_raw_option(optional=True) + id_ = raw_option["id"] + option, value = _fill_or_prompt_one_option(raw_option, None) + + is_special_readonly_option = isinstance(option, DisplayTextQuestion) + + assert isinstance(option, ARGUMENTS_TYPE_PARSERS[raw_option["type"]]) + assert option.type == raw_option["type"] + 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.bind is None + + if is_special_readonly_option: + assert value is None + + return (raw_option, option, value) + + @pytest.mark.usefixtures("patch_no_tty") + def test_basic_attrs(self): + """ + Test basic options factories and BaseOption default attributes values. + """ + # Intermediate method since pytest doesn't like tests that returns something. + # This allow a test class to call `_test_basic_attrs` then do additional checks + self._test_basic_attrs() + + def test_options_prompted_with_ask_help(self, prefill_data=None): + """ + Test that assert that moulinette prompt is called with: + - `message` with translated string and possible choices list + - help` with translated string + - `prefill` is the expected string value from a custom default + - `is_password` is true for `password`s only + - `is_multiline` is true for `text`s only + - `autocomplete` is option choices + + Ran only once with `cls.prefill` data + """ + if prefill_data is None: + prefill_data = self.prefill + + base_raw_option = prefill_data["raw_option"] + prefill = prefill_data["prefill"] + + with patch_prompt("") as prompt: + raw_option = self.get_raw_option( + raw_option=base_raw_option, + ask={"en": "Can i haz question?"}, + help={"en": "Here's help!"}, + ) + option, value = _fill_or_prompt_one_option(raw_option, None) + + expected_message = option.ask["en"] + + if option.choices: + choices = ( + option.choices + if isinstance(option.choices, list) + else option.choices.keys() + ) + expected_message += f" [{' | '.join(choices)}]" + if option.type == "boolean": + expected_message += " [yes | no]" + + prompt.assert_called_with( + message=expected_message, + is_password=option.type == "password", + confirm=False, # FIXME no confirm? + prefill=prefill, + is_multiline=option.type == "text", + autocomplete=option.choices or [], + help=option.help["en"], + ) + + def test_scenarios(self, intake, expected_output, raw_option, data): + with patch_interface("api"): + _test_intake_may_fail( + raw_option, + intake, + expected_output, + ) + + def test_question_empty(): ask_questions_and_parse_answers({}, {}) == [] From 26ca9e5c69f7f188e9a9ce2c48572616a1ed64bd Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 21 Mar 2023 18:37:49 +0100 Subject: [PATCH 281/319] options:tests: replace some string tests --- src/tests/test_questions.py | 281 ++++++++++-------------------------- 1 file changed, 78 insertions(+), 203 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index e849b6892..f8f8f9fef 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -518,40 +518,88 @@ class BaseTest: ) +# ╭───────────────────────────────────────────────────────╮ +# │ STRING │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestString(BaseTest): + raw_option = {"type": "string", "id": "string_id"} + prefill = { + "raw_option": {"default": " custom default"}, + "prefill": " custom default", + } + # fmt: off + 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? + *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"), + (" \n value\n", "value"), + (" \\n value\\n", "\\n value\\n"), + (" \tvalue\t", "value"), + (r" ##value \n \tvalue\n ", r"##value \n \tvalue\n"), + *xpass(scenarios=[ + ("value\nvalue", "value\nvalue"), + (" ##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"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ TEXT │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestText(BaseTest): + raw_option = {"type": "text", "id": "text_id"} + prefill = { + "raw_option": {"default": "some value\nanother line "}, + "prefill": "some value\nanother line ", + } + # fmt: off + 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? + *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 + *xpass(scenarios=[ + ("value\n", "value"), + (" \n value\n", "value"), + (" \\n value\\n", "\\n value\\n"), + (" \tvalue\t", "value"), + (" ##value \n \tvalue\n ", "##value \n \tvalue"), + (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"), + ] + # fmt: on + + def test_question_empty(): ask_questions_and_parse_answers({}, {}) == [] -def test_question_string(): - questions = { - "some_string": { - "type": "string", - } - } - answers = {"some_string": "some_value"} - - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "some_value" - - -def test_question_string_from_query_string(): - questions = { - "some_string": { - "type": "string", - } - } - answers = "foo=bar&some_string=some_value&lorem=ipsum" - - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "some_value" - - def test_question_string_default_type(): questions = {"some_string": {}} answers = {"some_string": "some_value"} @@ -563,179 +611,6 @@ def test_question_string_default_type(): assert out.value == "some_value" -def test_question_string_no_input(): - questions = {"some_string": {}} - answers = {} - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_string_input(): - questions = { - "some_string": { - "ask": "some question", - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="some_value"), 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 == "some_value" - - -def test_question_string_input_no_ask(): - questions = {"some_string": {}} - answers = {} - - with patch.object(Moulinette, "prompt", return_value="some_value"), 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 == "some_value" - - -def test_question_string_no_input_optional(): - questions = {"some_string": {"optional": True}} - 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 == "" - - -def test_question_string_optional_with_input(): - questions = { - "some_string": { - "ask": "some question", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="some_value"), 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 == "some_value" - - -def test_question_string_optional_with_empty_input(): - questions = { - "some_string": { - "ask": "some question", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value=""), 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 == "" - - -def test_question_string_optional_with_input_without_ask(): - questions = { - "some_string": { - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="some_value"), 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 == "some_value" - - -def test_question_string_no_input_default(): - questions = { - "some_string": { - "ask": "some question", - "default": "some_value", - } - } - answers = {} - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "some_value" - - -def test_question_string_input_test_ask(): - ask_text = "some question" - questions = { - "some_string": { - "ask": ask_text, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="some_value" - ) as prompt, patch.object(os, "isatty", return_value=True): - ask_questions_and_parse_answers(questions, answers) - prompt.assert_called_with( - message=ask_text, - is_password=False, - confirm=False, - prefill="", - is_multiline=False, - autocomplete=[], - help=None, - ) - - -def test_question_string_input_test_ask_with_default(): - ask_text = "some question" - default_text = "some example" - questions = { - "some_string": { - "ask": ask_text, - "default": default_text, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="some_value" - ) as prompt, patch.object(os, "isatty", return_value=True): - ask_questions_and_parse_answers(questions, answers) - prompt.assert_called_with( - message=ask_text, - is_password=False, - confirm=False, - prefill=default_text, - is_multiline=False, - autocomplete=[], - help=None, - ) - - @pytest.mark.skip # we should do something with this example def test_question_string_input_test_ask_with_example(): ask_text = "some question" From 38381b8149e374cea81063d33c39a5605316a874 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 21 Mar 2023 18:47:04 +0100 Subject: [PATCH 282/319] options:tests: replace some password tests --- src/tests/test_questions.py | 286 ++++-------------------------------- 1 file changed, 32 insertions(+), 254 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index f8f8f9fef..a8e55a93d 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -596,6 +596,38 @@ class TestText(BaseTest): # fmt: on +# ╭───────────────────────────────────────────────────────╮ +# │ PASSWORD │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestPassword(BaseTest): + raw_option = {"type": "password", "id": "password_id"} + prefill = { + "raw_option": {"default": None, "optional": True}, + "prefill": "", + } + # 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([], ["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=""), + *xpass(scenarios=[ + (" value \n moarc0mpl1cat3d\n ", "value \n moarc0mpl1cat3d"), + (" some_ value", "some_ value"), + ], reason="Should output exactly the same"), + ("s3cr3t!!", "s3cr3t!!"), + ("secret", FAIL), + *[("supersecret" + char, FAIL) for char in PasswordQuestion.forbidden_chars], # FIXME maybe add ` \n` to the list? + # readonly + *xpass(scenarios=[ + ("s3cr3t!!", "s3cr3t!!", {"readonly": True}), + ], reason="Should fail since readonly is forbidden"), + ] + # fmt: on + + def test_question_empty(): ask_questions_and_parse_answers({}, {}) == [] @@ -720,210 +752,6 @@ def test_question_string_with_choice_default(): assert out.value == "en" -def test_question_password(): - questions = { - "some_password": { - "type": "password", - } - } - answers = {"some_password": "some_value"} - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_password" - assert out.type == "password" - assert out.value == "some_value" - - -def test_question_password_no_input(): - questions = { - "some_password": { - "type": "password", - } - } - answers = {} - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_password_input(): - questions = { - "some_password": { - "type": "password", - "ask": "some question", - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_password" - assert out.type == "password" - assert out.value == "some_value" - - -def test_question_password_input_no_ask(): - questions = { - "some_password": { - "type": "password", - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_password" - assert out.type == "password" - assert out.value == "some_value" - - -def test_question_password_no_input_optional(): - questions = { - "some_password": { - "type": "password", - "optional": True, - } - } - answers = {} - - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_password" - assert out.type == "password" - assert out.value == "" - - questions = {"some_password": {"type": "password", "optional": True, "default": ""}} - - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_password" - assert out.type == "password" - assert out.value == "" - - -def test_question_password_optional_with_input(): - questions = { - "some_password": { - "ask": "some question", - "type": "password", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_password" - assert out.type == "password" - assert out.value == "some_value" - - -def test_question_password_optional_with_empty_input(): - questions = { - "some_password": { - "ask": "some question", - "type": "password", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value=""), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_password" - assert out.type == "password" - assert out.value == "" - - -def test_question_password_optional_with_input_without_ask(): - questions = { - "some_password": { - "type": "password", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_password" - assert out.type == "password" - assert out.value == "some_value" - - -def test_question_password_no_input_default(): - questions = { - "some_password": { - "type": "password", - "ask": "some question", - "default": "some_value", - } - } - answers = {} - - # no default for password! - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -@pytest.mark.skip # this should raises -def test_question_password_no_input_example(): - questions = { - "some_password": { - "type": "password", - "ask": "some question", - "example": "some_value", - } - } - answers = {"some_password": "some_value"} - - # no example for password! - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_password_input_test_ask(): - ask_text = "some question" - questions = { - "some_password": { - "type": "password", - "ask": ask_text, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="some_value" - ) as prompt, patch.object(os, "isatty", return_value=True): - ask_questions_and_parse_answers(questions, answers) - prompt.assert_called_with( - message=ask_text, - is_password=True, - confirm=False, - prefill="", - is_multiline=False, - autocomplete=[], - help=None, - ) - - @pytest.mark.skip # we should do something with this example def test_question_password_input_test_ask_with_example(): ask_text = "some question" @@ -966,56 +794,6 @@ def test_question_password_input_test_ask_with_help(): assert help_text in prompt.call_args[1]["message"] -def test_question_password_bad_chars(): - questions = { - "some_password": { - "type": "password", - "ask": "some question", - "example": "some_value", - } - } - - for i in PasswordQuestion.forbidden_chars: - with pytest.raises(YunohostError), patch.object( - os, "isatty", return_value=False - ): - ask_questions_and_parse_answers(questions, {"some_password": i * 8}) - - -def test_question_password_strong_enough(): - questions = { - "some_password": { - "type": "password", - "ask": "some question", - "example": "some_value", - } - } - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - # too short - ask_questions_and_parse_answers(questions, {"some_password": "a"}) - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, {"some_password": "password"}) - - -def test_question_password_optional_strong_enough(): - questions = { - "some_password": { - "ask": "some question", - "type": "password", - "optional": True, - } - } - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - # too short - ask_questions_and_parse_answers(questions, {"some_password": "a"}) - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, {"some_password": "password"}) - - def test_question_path(): questions = { "some_path": { From 70149fe41d2e4cf21cff3a9e86ccfd380d3bb3dc Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 21 Mar 2023 18:50:23 +0100 Subject: [PATCH 283/319] options:tests: replace path tests --- src/tests/test_questions.py | 259 ++++++++---------------------------- 1 file changed, 52 insertions(+), 207 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index a8e55a93d..910b8b5a0 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -628,6 +628,58 @@ class TestPassword(BaseTest): # fmt: on +# ╭───────────────────────────────────────────────────────╮ +# │ PATH │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestWebPath(BaseTest): + raw_option = {"type": "path", "id": "path_id"} + prefill = { + "raw_option": {"default": "some_path"}, + "prefill": "some_path", + } + # fmt: off + scenarios = [ + *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + + *nones(None, "", output=""), + # custom valid + ("/", "/"), + ("/one/two", "/one/two"), + *[ + (v, "/" + v) + for v in ("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value") + ], + ("value\n", "/value"), + ("//value", "/value"), + ("///value///", "/value"), + *xpass(scenarios=[ + ("value\nvalue", "/value\nvalue"), + ("value value", "/value value"), + ("value//value", "/value//value"), + ], reason="Should fail"), + *xpass(scenarios=[ + ("./here", "/./here"), + ("../here", "/../here"), + ("/somewhere/../here", "/somewhere/../here"), + ], reason="Should fail or flattened"), + + *xpass(scenarios=[ + ("/one?withquery=ah", "/one?withquery=ah"), + ], reason="Should fail or query string removed"), + *xpass(scenarios=[ + ("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"), + # FIXME should path have forbidden_chars? + ] + # fmt: on + + def test_question_empty(): ask_questions_and_parse_answers({}, {}) == [] @@ -794,213 +846,6 @@ def test_question_password_input_test_ask_with_help(): assert help_text in prompt.call_args[1]["message"] -def test_question_path(): - questions = { - "some_path": { - "type": "path", - } - } - answers = {"some_path": "/some_value"} - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_path" - assert out.type == "path" - assert out.value == "/some_value" - - -def test_question_path_no_input(): - questions = { - "some_path": { - "type": "path", - } - } - answers = {} - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_path_input(): - questions = { - "some_path": { - "type": "path", - "ask": "some question", - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_path" - assert out.type == "path" - assert out.value == "/some_value" - - -def test_question_path_input_no_ask(): - questions = { - "some_path": { - "type": "path", - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_path" - assert out.type == "path" - assert out.value == "/some_value" - - -def test_question_path_no_input_optional(): - questions = { - "some_path": { - "type": "path", - "optional": True, - } - } - answers = {} - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_path" - assert out.type == "path" - assert out.value == "" - - -def test_question_path_optional_with_input(): - questions = { - "some_path": { - "ask": "some question", - "type": "path", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_path" - assert out.type == "path" - assert out.value == "/some_value" - - -def test_question_path_optional_with_empty_input(): - questions = { - "some_path": { - "ask": "some question", - "type": "path", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value=""), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_path" - assert out.type == "path" - assert out.value == "" - - -def test_question_path_optional_with_input_without_ask(): - questions = { - "some_path": { - "type": "path", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_path" - assert out.type == "path" - assert out.value == "/some_value" - - -def test_question_path_no_input_default(): - questions = { - "some_path": { - "ask": "some question", - "type": "path", - "default": "some_value", - } - } - answers = {} - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_path" - assert out.type == "path" - assert out.value == "/some_value" - - -def test_question_path_input_test_ask(): - ask_text = "some question" - questions = { - "some_path": { - "type": "path", - "ask": ask_text, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="some_value" - ) as prompt, patch.object(os, "isatty", return_value=True): - ask_questions_and_parse_answers(questions, answers) - prompt.assert_called_with( - message=ask_text, - is_password=False, - confirm=False, - prefill="", - is_multiline=False, - autocomplete=[], - help=None, - ) - - -def test_question_path_input_test_ask_with_default(): - ask_text = "some question" - default_text = "someexample" - questions = { - "some_path": { - "type": "path", - "ask": ask_text, - "default": default_text, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="some_value" - ) as prompt, patch.object(os, "isatty", return_value=True): - ask_questions_and_parse_answers(questions, answers) - prompt.assert_called_with( - message=ask_text, - is_password=False, - confirm=False, - prefill=default_text, - is_multiline=False, - autocomplete=[], - help=None, - ) - - @pytest.mark.skip # we should do something with this example def test_question_path_input_test_ask_with_example(): ask_text = "some question" From df6bb228202067332a524e88567de9ed89a00835 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 21 Mar 2023 18:53:55 +0100 Subject: [PATCH 284/319] options:tests: replace boolean tests --- src/tests/test_questions.py | 318 ++++++++---------------------------- 1 file changed, 66 insertions(+), 252 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 910b8b5a0..f8cc5ce98 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -628,6 +628,72 @@ class TestPassword(BaseTest): # fmt: on +# ╭───────────────────────────────────────────────────────╮ +# │ BOOLEAN │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestBoolean(BaseTest): + raw_option = {"type": "boolean", "id": "boolean_id"} + prefill = { + "raw_option": {"default": True}, + "prefill": "yes", + } + # fmt: off + truthy_values = (True, 1, "1", "True", "true", "Yes", "yes", "y", "on") + falsy_values = (False, 0, "0", "False", "false", "No", "no", "n", "off") + scenarios = [ + *all_as(None, "", output=0), + *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"}), + # Unhandled types should fail + *all_fails(1337, "1337", "string", [], "[]", ",", "one,two"), + *all_fails(1337, "1337", "string", [], "[]", ",", "one,two", {"optional": True}), + # Required + *all_as(*truthy_values, output=1), + *all_as(*falsy_values, output=0), + # Optional + *all_as(*truthy_values, output=1, raw_option={"optional": True}), + *all_as(*falsy_values, output=0, raw_option={"optional": True}), + # test values as default, as required option without intake + *[(None, 1, {"default": true for true in truthy_values})], + *[(None, 0, {"default": false for false in falsy_values})], + # custom boolean output + ("", "disallow", {"yes": "allow", "no": "disallow"}), # required -> default to False -> `"disallow"` + ("n", "disallow", {"yes": "allow", "no": "disallow"}), + ("y", "allow", {"yes": "allow", "no": "disallow"}), + ("", False, {"yes": True, "no": False}), # required -> default to False -> `False` + ("n", False, {"yes": True, "no": False}), + ("y", True, {"yes": True, "no": False}), + ("", -1, {"yes": 1, "no": -1}), # required -> default to False -> `-1` + ("n", -1, {"yes": 1, "no": -1}), + ("y", 1, {"yes": 1, "no": -1}), + { + "raw_options": [ + {"yes": "no", "no": "yes", "optional": True}, + {"yes": False, "no": True, "optional": True}, + {"yes": "0", "no": "1", "optional": True}, + ], + # "no" for "yes" and "yes" for "no" should fail + "scenarios": all_fails("", "y", "n", error=AssertionError), + }, + # readonly + *xfail(scenarios=[ + (1, 0, {"readonly": True, "default": 0}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + # ╭───────────────────────────────────────────────────────╮ # │ PATH │ # ╰───────────────────────────────────────────────────────╯ @@ -888,258 +954,6 @@ def test_question_path_input_test_ask_with_help(): assert help_text in prompt.call_args[1]["message"] -def test_question_boolean(): - questions = { - "some_boolean": { - "type": "boolean", - } - } - answers = {"some_boolean": "y"} - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_boolean" - assert out.type == "boolean" - assert out.value == 1 - - -def test_question_boolean_all_yes(): - questions = { - "some_boolean": { - "type": "boolean", - } - } - - for value in ["Y", "yes", "Yes", "YES", "1", 1, True, "True", "TRUE", "true"]: - out = ask_questions_and_parse_answers(questions, {"some_boolean": value})[0] - assert out.name == "some_boolean" - assert out.type == "boolean" - assert out.value == 1 - - -def test_question_boolean_all_no(): - questions = { - "some_boolean": { - "type": "boolean", - } - } - - for value in ["n", "N", "no", "No", "No", "0", 0, False, "False", "FALSE", "false"]: - out = ask_questions_and_parse_answers(questions, {"some_boolean": value})[0] - assert out.name == "some_boolean" - assert out.type == "boolean" - assert out.value == 0 - - -# XXX apparently boolean are always False (0) by default, I'm not sure what to think about that -def test_question_boolean_no_input(): - questions = { - "some_boolean": { - "type": "boolean", - } - } - answers = {} - - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.value == 0 - - -def test_question_boolean_bad_input(): - questions = { - "some_boolean": { - "type": "boolean", - } - } - answers = {"some_boolean": "stuff"} - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_boolean_input(): - questions = { - "some_boolean": { - "type": "boolean", - "ask": "some question", - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="y"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - assert out.value == 1 - - with patch.object(Moulinette, "prompt", return_value="n"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - assert out.value == 0 - - -def test_question_boolean_input_no_ask(): - questions = { - "some_boolean": { - "type": "boolean", - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="y"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - assert out.value == 1 - - -def test_question_boolean_no_input_optional(): - questions = { - "some_boolean": { - "type": "boolean", - "optional": True, - } - } - answers = {} - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - assert out.value == 0 - - -def test_question_boolean_optional_with_input(): - questions = { - "some_boolean": { - "ask": "some question", - "type": "boolean", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="y"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - assert out.value == 1 - - -def test_question_boolean_optional_with_empty_input(): - questions = { - "some_boolean": { - "ask": "some question", - "type": "boolean", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value=""), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.value == 0 - - -def test_question_boolean_optional_with_input_without_ask(): - questions = { - "some_boolean": { - "type": "boolean", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="n"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.value == 0 - - -def test_question_boolean_no_input_default(): - questions = { - "some_boolean": { - "ask": "some question", - "type": "boolean", - "default": 0, - } - } - answers = {} - - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.value == 0 - - -def test_question_boolean_bad_default(): - questions = { - "some_boolean": { - "ask": "some question", - "type": "boolean", - "default": "bad default", - } - } - answers = {} - with pytest.raises(YunohostError): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_boolean_input_test_ask(): - ask_text = "some question" - questions = { - "some_boolean": { - "type": "boolean", - "ask": ask_text, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value=0) as prompt, patch.object( - os, "isatty", return_value=True - ): - ask_questions_and_parse_answers(questions, answers) - prompt.assert_called_with( - message=ask_text + " [yes | no]", - is_password=False, - confirm=False, - prefill="no", - is_multiline=False, - autocomplete=[], - help=None, - ) - - -def test_question_boolean_input_test_ask_with_default(): - ask_text = "some question" - default_text = 1 - questions = { - "some_boolean": { - "type": "boolean", - "ask": ask_text, - "default": default_text, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value=1) as prompt, patch.object( - os, "isatty", return_value=True - ): - ask_questions_and_parse_answers(questions, answers) - prompt.assert_called_with( - message=ask_text + " [yes | no]", - is_password=False, - confirm=False, - prefill="yes", - is_multiline=False, - autocomplete=[], - help=None, - ) - - def test_question_domain_empty(): questions = { "some_domain": { From db1710a0a928affec7ff5be5e1b80330d194171a Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 21 Mar 2023 18:56:02 +0100 Subject: [PATCH 285/319] options:tests: replace domain tests --- src/tests/test_questions.py | 243 ++++++++++-------------------------- 1 file changed, 67 insertions(+), 176 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index f8cc5ce98..a42b501f7 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -746,6 +746,73 @@ class TestWebPath(BaseTest): # fmt: on +# ╭───────────────────────────────────────────────────────╮ +# │ DOMAIN │ +# ╰───────────────────────────────────────────────────────╯ + +main_domain = "ynh.local" +domains1 = ["ynh.local"] +domains2 = ["another.org", "ynh.local", "yet.another.org"] + + +@contextmanager +def patch_domains(*, domains, main_domain): + """ + Data mocking for DomainOption: + - yunohost.domain.domain_list + """ + with patch.object( + domain, + "domain_list", + return_value={"domains": domains, "main": main_domain}, + ), patch.object(domain, "_get_maindomain", return_value=main_domain): + yield + + +class TestDomain(BaseTest): + raw_option = {"type": "domain", "id": "domain_id"} + prefill = { + "raw_option": { + "default": None, + }, + "prefill": main_domain, + } + # fmt: off + scenarios = [ + # Probably not needed to test common types since those are not available as choices + # Also no scenarios with no domains since it should not be possible + { + "data": [{"main_domain": domains1[0], "domains": domains1}], + "scenarios": [ + *nones(None, "", output=domains1[0], fail_if_required=False), + (domains1[0], domains1[0], {}), + ("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"), + ] + }, + { + "data": [{"main_domain": domains2[1], "domains": domains2}], + "scenarios": [ + *nones(None, "", output=domains2[1], fail_if_required=False), + (domains2[1], domains2[1], {}), + (domains2[0], domains2[0], {}), + ("doesnt_exist.pouet", FAIL, {}), + ("fake.com", FAIL, {"choices": ["fake.com"]}), + ] + }, + + ] + # fmt: on + + def test_scenarios(self, intake, expected_output, raw_option, data): + with patch_domains(**data): + super().test_scenarios(intake, expected_output, raw_option, data) + + def test_question_empty(): ask_questions_and_parse_answers({}, {}) == [] @@ -954,182 +1021,6 @@ def test_question_path_input_test_ask_with_help(): assert help_text in prompt.call_args[1]["message"] -def test_question_domain_empty(): - questions = { - "some_domain": { - "type": "domain", - } - } - main_domain = "my_main_domain.com" - answers = {} - - with patch.object( - domain, "_get_maindomain", return_value="my_main_domain.com" - ), patch.object( - domain, "domain_list", return_value={"domains": [main_domain]} - ), patch.object( - os, "isatty", return_value=False - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_domain" - assert out.type == "domain" - assert out.value == main_domain - - -def test_question_domain(): - main_domain = "my_main_domain.com" - domains = [main_domain] - questions = { - "some_domain": { - "type": "domain", - } - } - - answers = {"some_domain": main_domain} - - with patch.object( - domain, "_get_maindomain", return_value=main_domain - ), patch.object(domain, "domain_list", return_value={"domains": domains}): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_domain" - assert out.type == "domain" - assert out.value == main_domain - - -def test_question_domain_two_domains(): - main_domain = "my_main_domain.com" - other_domain = "some_other_domain.tld" - domains = [main_domain, other_domain] - - questions = { - "some_domain": { - "type": "domain", - } - } - answers = {"some_domain": other_domain} - - with patch.object( - domain, "_get_maindomain", return_value=main_domain - ), patch.object(domain, "domain_list", return_value={"domains": domains}): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_domain" - assert out.type == "domain" - assert out.value == other_domain - - answers = {"some_domain": main_domain} - - with patch.object( - domain, "_get_maindomain", return_value=main_domain - ), patch.object(domain, "domain_list", return_value={"domains": domains}): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_domain" - assert out.type == "domain" - assert out.value == main_domain - - -def test_question_domain_two_domains_wrong_answer(): - main_domain = "my_main_domain.com" - other_domain = "some_other_domain.tld" - domains = [main_domain, other_domain] - - questions = { - "some_domain": { - "type": "domain", - } - } - answers = {"some_domain": "doesnt_exist.pouet"} - - with patch.object( - domain, "_get_maindomain", return_value=main_domain - ), patch.object(domain, "domain_list", return_value={"domains": domains}): - with pytest.raises(YunohostError), patch.object( - os, "isatty", return_value=False - ): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_domain_two_domains_default_no_ask(): - main_domain = "my_main_domain.com" - other_domain = "some_other_domain.tld" - domains = [main_domain, other_domain] - - questions = { - "some_domain": { - "type": "domain", - } - } - answers = {} - - with patch.object( - domain, "_get_maindomain", return_value=main_domain - ), patch.object( - domain, "domain_list", return_value={"domains": domains} - ), patch.object( - os, "isatty", return_value=False - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_domain" - assert out.type == "domain" - assert out.value == main_domain - - -def test_question_domain_two_domains_default(): - main_domain = "my_main_domain.com" - other_domain = "some_other_domain.tld" - domains = [main_domain, other_domain] - - questions = {"some_domain": {"type": "domain", "ask": "choose a domain"}} - answers = {} - - with patch.object( - domain, "_get_maindomain", return_value=main_domain - ), patch.object( - domain, "domain_list", return_value={"domains": domains} - ), patch.object( - os, "isatty", return_value=False - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_domain" - assert out.type == "domain" - assert out.value == main_domain - - -def test_question_domain_two_domains_default_input(): - main_domain = "my_main_domain.com" - other_domain = "some_other_domain.tld" - domains = [main_domain, other_domain] - - questions = {"some_domain": {"type": "domain", "ask": "choose a domain"}} - answers = {} - - with patch.object( - domain, "_get_maindomain", return_value=main_domain - ), patch.object( - domain, "domain_list", return_value={"domains": domains} - ), patch.object( - os, "isatty", return_value=True - ): - with patch.object(Moulinette, "prompt", return_value=main_domain): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_domain" - assert out.type == "domain" - assert out.value == main_domain - - with patch.object(Moulinette, "prompt", return_value=other_domain): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_domain" - assert out.type == "domain" - assert out.value == other_domain - - def test_question_user_empty(): users = { "some_user": { From af77e0b62fca9df863dcdaf5d6ac4337d8ad9c48 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 21 Mar 2023 18:58:25 +0100 Subject: [PATCH 286/319] options:tests: replace user tests --- src/tests/test_questions.py | 314 ++++++++++++------------------------ 1 file changed, 106 insertions(+), 208 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index a42b501f7..a74dbe2be 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -813,6 +813,112 @@ class TestDomain(BaseTest): super().test_scenarios(intake, expected_output, raw_option, data) +# ╭───────────────────────────────────────────────────────╮ +# │ USER │ +# ╰───────────────────────────────────────────────────────╯ + +admin_username = "admin_user" +admin_user = { + "ssh_allowed": False, + "username": admin_username, + "mailbox-quota": "0", + "mail": "a@ynh.local", + "mail-aliases": [f"root@{main_domain}"], # Faking "admin" + "fullname": "john doe", + "group": [], +} +regular_username = "normal_user" +regular_user = { + "ssh_allowed": False, + "username": regular_username, + "mailbox-quota": "0", + "mail": "z@ynh.local", + "fullname": "john doe", + "group": [], +} + + +@contextmanager +def patch_users( + *, + users, + admin_username, + main_domain, +): + """ + Data mocking for UserOption: + - yunohost.user.user_list + - yunohost.user.user_info + - yunohost.domain._get_maindomain + """ + admin_info = next( + (user for user in users.values() if user["username"] == admin_username), + {"mail-aliases": []}, + ) + with patch.object(user, "user_list", return_value={"users": users}), patch.object( + user, + "user_info", + return_value=admin_info, # Faking admin user + ), patch.object(domain, "_get_maindomain", return_value=main_domain): + yield + + +class TestUser(BaseTest): + raw_option = {"type": "user", "id": "user_id"} + # fmt: off + scenarios = [ + # No tests for empty users since it should not happens + { + "data": [ + {"users": {admin_username: admin_user}, "admin_username": admin_username, "main_domain": main_domain}, + {"users": {admin_username: admin_user, regular_username: regular_user}, "admin_username": admin_username, "main_domain": main_domain}, + ], + "scenarios": [ + # FIXME User option is not really nullable, even if optional + *nones(None, "", output=admin_username, fail_if_required=False), + ("fake_user", FAIL), + ("fake_user", FAIL, {"choices": ["fake_user"]}), + ] + }, + { + "data": [ + {"users": {admin_username: admin_user, regular_username: regular_user}, "admin_username": admin_username, "main_domain": main_domain}, + ], + "scenarios": [ + *xpass(scenarios=[ + ("", 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"), + ] + }, + ] + # fmt: on + + def test_options_prompted_with_ask_help(self, prefill_data=None): + with patch_users( + users={admin_username: admin_user, regular_username: regular_user}, + admin_username=admin_username, + main_domain=main_domain, + ): + super().test_options_prompted_with_ask_help( + prefill_data={"raw_option": {}, "prefill": admin_username} + ) + # FIXME This should fail, not allowed to set a default + super().test_options_prompted_with_ask_help( + prefill_data={ + "raw_option": {"default": regular_username}, + "prefill": regular_username, + } + ) + + def test_scenarios(self, intake, expected_output, raw_option, data): + with patch_users(**data): + super().test_scenarios(intake, expected_output, raw_option, data) + + def test_question_empty(): ask_questions_and_parse_answers({}, {}) == [] @@ -1021,214 +1127,6 @@ def test_question_path_input_test_ask_with_help(): assert help_text in prompt.call_args[1]["message"] -def test_question_user_empty(): - users = { - "some_user": { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - } - } - - questions = { - "some_user": { - "type": "user", - } - } - answers = {} - - with patch.object(user, "user_list", return_value={"users": users}): - with pytest.raises(YunohostError), patch.object( - os, "isatty", return_value=False - ): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_user(): - username = "some_user" - users = { - username: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - } - } - - questions = { - "some_user": { - "type": "user", - } - } - answers = {"some_user": username} - - with patch.object(user, "user_list", return_value={"users": users}), patch.object( - user, "user_info", return_value={} - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_user" - assert out.type == "user" - assert out.value == username - - -def test_question_user_two_users(): - username = "some_user" - other_user = "some_other_user" - users = { - username: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - }, - other_user: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "z@ynh.local", - "fullname": "john doe", - }, - } - - questions = { - "some_user": { - "type": "user", - } - } - answers = {"some_user": other_user} - - with patch.object(user, "user_list", return_value={"users": users}), patch.object( - user, "user_info", return_value={} - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_user" - assert out.type == "user" - assert out.value == other_user - - answers = {"some_user": username} - - with patch.object(user, "user_list", return_value={"users": users}), patch.object( - user, "user_info", return_value={} - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_user" - assert out.type == "user" - assert out.value == username - - -def test_question_user_two_users_wrong_answer(): - username = "my_username.com" - other_user = "some_other_user" - users = { - username: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - }, - other_user: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "z@ynh.local", - "fullname": "john doe", - }, - } - - questions = { - "some_user": { - "type": "user", - } - } - answers = {"some_user": "doesnt_exist.pouet"} - - with patch.object(user, "user_list", return_value={"users": users}): - with pytest.raises(YunohostError), patch.object( - os, "isatty", return_value=False - ): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_user_two_users_no_default(): - username = "my_username.com" - other_user = "some_other_user.tld" - users = { - username: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - }, - other_user: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "z@ynh.local", - "fullname": "john doe", - }, - } - - questions = {"some_user": {"type": "user", "ask": "choose a user"}} - answers = {} - - with patch.object(user, "user_list", return_value={"users": users}): - with pytest.raises(YunohostError), patch.object( - os, "isatty", return_value=False - ): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_user_two_users_default_input(): - username = "my_username.com" - other_user = "some_other_user.tld" - users = { - username: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - }, - other_user: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "z@ynh.local", - "fullname": "john doe", - }, - } - - questions = {"some_user": {"type": "user", "ask": "choose a user"}} - answers = {} - - with patch.object(user, "user_list", return_value={"users": users}), patch.object( - os, "isatty", return_value=True - ): - with patch.object(user, "user_info", return_value={}): - with patch.object(Moulinette, "prompt", return_value=username): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_user" - assert out.type == "user" - assert out.value == username - - with patch.object(Moulinette, "prompt", return_value=other_user): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_user" - assert out.type == "user" - assert out.value == other_user - - def test_question_number(): questions = { "some_number": { From af0cd78fcce86690c3bcf249568265f2ba2fa29f Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 21 Mar 2023 19:01:45 +0100 Subject: [PATCH 287/319] options:tests: replace number tests --- src/tests/test_questions.py | 279 ++++++------------------------------ 1 file changed, 45 insertions(+), 234 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index a74dbe2be..ac782fc9e 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -628,6 +628,51 @@ class TestPassword(BaseTest): # fmt: on +# ╭───────────────────────────────────────────────────────╮ +# │ NUMBER | RANGE │ +# ╰───────────────────────────────────────────────────────╯ +# Testing only number since "range" is only for webadmin (slider instead of classic intake). + + +class TestNumber(BaseTest): + raw_option = {"type": "number", "id": "number_id"} + prefill = { + "raw_option": {"default": 10}, + "prefill": "10", + } + # fmt: off + scenarios = [ + *all_fails([], ["one"], {}), + *all_fails("none", "_none", "False", "True", "[]", ",", "['one']", "one,two", r"{}", "value"), + + *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("1337", 1337, output=1337), + *xfail(scenarios=[ + ("-1", -1) + ], reason="should output as `-1` instead of failing"), + *all_fails(13.37, "13.37"), + + *unchanged(10, 5000, 10000, raw_option={"min": 10, "max": 10000}), + *all_fails(9, 10001, raw_option={"min": 10, "max": 10000}), + + *all_as(None, "", output=0, raw_option={"default": 0}), + *all_as(None, "", output=0, raw_option={"default": 0, "optional": True}), + (-10, -10, {"default": 10}), + (-10, -10, {"default": 10, "optional": True}), + # readonly + *xfail(scenarios=[ + (1337, 10000, {"readonly": True, "default": 10000}), + ], reason="Should not be overwritten"), + ] + # fmt: on + # FIXME should `step` be some kind of "multiple of"? + + # ╭───────────────────────────────────────────────────────╮ # │ BOOLEAN │ # ╰───────────────────────────────────────────────────────╯ @@ -1127,240 +1172,6 @@ def test_question_path_input_test_ask_with_help(): assert help_text in prompt.call_args[1]["message"] -def test_question_number(): - questions = { - "some_number": { - "type": "number", - } - } - answers = {"some_number": 1337} - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_number" - assert out.type == "number" - assert out.value == 1337 - - -def test_question_number_no_input(): - questions = { - "some_number": { - "type": "number", - } - } - answers = {} - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_number_bad_input(): - questions = { - "some_number": { - "type": "number", - } - } - answers = {"some_number": "stuff"} - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - answers = {"some_number": 1.5} - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_number_input(): - questions = { - "some_number": { - "type": "number", - "ask": "some question", - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_number" - assert out.type == "number" - assert out.value == 1337 - - with patch.object(Moulinette, "prompt", return_value=1337), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_number" - assert out.type == "number" - assert out.value == 1337 - - with patch.object(Moulinette, "prompt", return_value="0"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_number" - assert out.type == "number" - assert out.value == 0 - - -def test_question_number_input_no_ask(): - questions = { - "some_number": { - "type": "number", - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_number" - assert out.type == "number" - assert out.value == 1337 - - -def test_question_number_no_input_optional(): - questions = { - "some_number": { - "type": "number", - "optional": True, - } - } - answers = {} - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_number" - assert out.type == "number" - assert out.value is None - - -def test_question_number_optional_with_input(): - questions = { - "some_number": { - "ask": "some question", - "type": "number", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_number" - assert out.type == "number" - assert out.value == 1337 - - -def test_question_number_optional_with_input_without_ask(): - questions = { - "some_number": { - "type": "number", - "optional": True, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="0"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_number" - assert out.type == "number" - assert out.value == 0 - - -def test_question_number_no_input_default(): - questions = { - "some_number": { - "ask": "some question", - "type": "number", - "default": 1337, - } - } - answers = {} - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_number" - assert out.type == "number" - assert out.value == 1337 - - -def test_question_number_bad_default(): - questions = { - "some_number": { - "ask": "some question", - "type": "number", - "default": "bad default", - } - } - answers = {} - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_number_input_test_ask(): - ask_text = "some question" - questions = { - "some_number": { - "type": "number", - "ask": ask_text, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="1111" - ) as prompt, patch.object(os, "isatty", return_value=True): - ask_questions_and_parse_answers(questions, answers) - prompt.assert_called_with( - message=ask_text, - is_password=False, - confirm=False, - prefill="", - is_multiline=False, - autocomplete=[], - help=None, - ) - - -def test_question_number_input_test_ask_with_default(): - ask_text = "some question" - default_value = 1337 - questions = { - "some_number": { - "type": "number", - "ask": ask_text, - "default": default_value, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="1111" - ) as prompt, patch.object(os, "isatty", return_value=True): - ask_questions_and_parse_answers(questions, answers) - prompt.assert_called_with( - message=ask_text, - is_password=False, - confirm=False, - prefill=str(default_value), - is_multiline=False, - autocomplete=[], - help=None, - ) - - @pytest.mark.skip # we should do something with this example def test_question_number_input_test_ask_with_example(): ask_text = "some question" From eacb7016e2fd9d70975dab33a7ee74d5ccd80e8f Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 21 Mar 2023 19:03:09 +0100 Subject: [PATCH 288/319] options:tests: replace display_text tests --- src/tests/test_questions.py | 50 +++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index ac782fc9e..dffa93d14 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -518,6 +518,45 @@ class BaseTest: ) +# ╭───────────────────────────────────────────────────────╮ +# │ DISPLAY_TEXT │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestDisplayText(BaseTest): + raw_option = {"type": "display_text", "id": "display_text_id"} + prefill = { + "raw_option": {}, + "prefill": " custom default", + } + # fmt: off + scenarios = [ + (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"}}), + ] + # fmt: on + + def test_options_prompted_with_ask_help(self, prefill_data=None): + pytest.skip(reason="no prompt for display types") + + def test_scenarios(self, intake, expected_output, raw_option, data): + _id = raw_option.pop("id") + answers = {_id: intake} if intake is not None else {} + options = None + with patch_interface("cli"): + if inspect.isclass(expected_output) and issubclass( + expected_output, Exception + ): + with pytest.raises(expected_output): + 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( + {_id: raw_option}, answers + ) + assert stdout.getvalue() == f"{options[0].ask['en']}\n" + + # ╭───────────────────────────────────────────────────────╮ # │ STRING │ # ╰───────────────────────────────────────────────────────╯ @@ -1214,17 +1253,6 @@ def test_question_number_input_test_ask_with_help(): assert help_value in prompt.call_args[1]["message"] -def test_question_display_text(): - questions = {"some_app": {"type": "display_text", "ask": "foobar"}} - answers = {} - - with patch.object(sys, "stdout", new_callable=StringIO) as stdout, patch.object( - os, "isatty", return_value=True - ): - ask_questions_and_parse_answers(questions, answers) - assert "foobar" in stdout.getvalue() - - def test_question_file_from_cli(): FileQuestion.clean_upload_dirs() From f4b79068111237edd9c3acadb94de1c5c51eb9a4 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 21 Mar 2023 21:15:29 +0100 Subject: [PATCH 289/319] options:tests: replace file tests --- src/tests/test_questions.py | 281 +++++++++++++++++------------------- 1 file changed, 136 insertions(+), 145 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index dffa93d14..cecb59b80 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -2,6 +2,7 @@ import inspect import sys import pytest import os +import tempfile from contextlib import contextmanager from mock import patch @@ -830,6 +831,141 @@ class TestWebPath(BaseTest): # fmt: on +# ╭───────────────────────────────────────────────────────╮ +# │ FILE │ +# ╰───────────────────────────────────────────────────────╯ + + +@pytest.fixture +def file_clean(): + FileQuestion.clean_upload_dirs() + yield + FileQuestion.clean_upload_dirs() + + +@contextmanager +def patch_file_cli(intake): + upload_dir = tempfile.mkdtemp(prefix="ynh_test_option_file") + _, filename = tempfile.mkstemp(dir=upload_dir) + with open(filename, "w") as f: + f.write(intake) + + yield filename + os.system(f"rm -f {filename}") + + +@contextmanager +def patch_file_api(intake): + from base64 import b64encode + + with patch_interface("api"): + yield b64encode(intake.encode()) + + +def _test_file_intake_may_fail(raw_option, intake, expected_output): + if inspect.isclass(expected_output) and issubclass(expected_output, Exception): + with pytest.raises(expected_output): + _fill_or_prompt_one_option(raw_option, intake) + + option, value = _fill_or_prompt_one_option(raw_option, intake) + + # The file is supposed to be copied somewhere else + assert value != intake + assert value.startswith("/tmp/ynh_filequestion_") + assert os.path.exists(value) + with open(value) as f: + assert f.read() == expected_output + + FileQuestion.clean_upload_dirs() + + assert not os.path.exists(value) + + +file_content1 = "helloworld" +file_content2 = """ +{ + "testy": true, + "test": ["one"] +} +""" + + +class TestFile(BaseTest): + raw_option = {"type": "file", "id": "file_id"} + # Prefill data is generated in `cls.test_options_prompted_with_ask_help` + # fmt: off + scenarios = [ + *nones(None, "", output=""), + *unchanged(file_content1, file_content2), + # other type checks are done in `test_wrong_intake` + ] + # fmt: on + # TODO test readonly + # TODO test accept + + @pytest.mark.usefixtures("patch_no_tty") + def test_basic_attrs(self): + raw_option, option, value = self._test_basic_attrs() + + accept = raw_option.get("accept", "") # accept default + assert option.accept == accept + + def test_options_prompted_with_ask_help(self): + with patch_file_cli(file_content1) as default_filename: + super().test_options_prompted_with_ask_help( + prefill_data={ + "raw_option": { + "default": default_filename, + }, + "prefill": default_filename, + } + ) + + @pytest.mark.usefixtures("file_clean") + def test_scenarios(self, intake, expected_output, raw_option, data): + if intake in (None, ""): + with patch_prompt(intake): + _test_intake_may_fail(raw_option, None, expected_output) + with patch_isatty(False): + _test_intake_may_fail(raw_option, intake, expected_output) + else: + with patch_file_cli(intake) as filename: + with patch_prompt(filename): + _test_file_intake_may_fail(raw_option, None, expected_output) + with patch_file_api(intake) as b64content: + with patch_isatty(False): + _test_file_intake_may_fail(raw_option, b64content, expected_output) + + @pytest.mark.parametrize( + "path", + [ + "/tmp/inexistant_file.txt", + "/tmp", + "/tmp/", + ], + ) + def test_wrong_cli_filename(self, path): + with patch_prompt(path): + with pytest.raises(YunohostValidationError): + _fill_or_prompt_one_option(self.raw_option, None) + + @pytest.mark.parametrize( + "intake", + [ + # fmt: off + False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, + "none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n" + # fmt: on + ], + ) + def test_wrong_intake(self, intake): + with pytest.raises(YunohostValidationError): + with patch_prompt(intake): + _fill_or_prompt_one_option(self.raw_option, None) + with patch_isatty(False): + _fill_or_prompt_one_option(self.raw_option, intake) + + # ╭───────────────────────────────────────────────────────╮ # │ DOMAIN │ # ╰───────────────────────────────────────────────────────╯ @@ -1038,26 +1174,6 @@ def test_question_string_input_test_ask_with_example(): assert example_text in prompt.call_args[1]["message"] -@pytest.mark.skip # we should do something with this help -def test_question_string_input_test_ask_with_help(): - ask_text = "some question" - help_text = "some_help" - questions = { - "some_string": { - "ask": ask_text, - "help": help_text, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="some_value" - ) 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"] - assert help_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"} @@ -1148,27 +1264,6 @@ def test_question_password_input_test_ask_with_example(): assert example_text in prompt.call_args[1]["message"] -@pytest.mark.skip # we should do something with this help -def test_question_password_input_test_ask_with_help(): - ask_text = "some question" - help_text = "some_help" - questions = { - "some_password": { - "type": "password", - "ask": ask_text, - "help": help_text, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="some_value" - ) 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"] - assert help_text in prompt.call_args[1]["message"] - - @pytest.mark.skip # we should do something with this example def test_question_path_input_test_ask_with_example(): ask_text = "some question" @@ -1190,27 +1285,6 @@ def test_question_path_input_test_ask_with_example(): assert example_text in prompt.call_args[1]["message"] -@pytest.mark.skip # we should do something with this help -def test_question_path_input_test_ask_with_help(): - ask_text = "some question" - help_text = "some_help" - questions = { - "some_path": { - "type": "path", - "ask": ask_text, - "help": help_text, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="some_value" - ) 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"] - assert help_text in prompt.call_args[1]["message"] - - @pytest.mark.skip # we should do something with this example def test_question_number_input_test_ask_with_example(): ask_text = "some question" @@ -1232,89 +1306,6 @@ def test_question_number_input_test_ask_with_example(): assert example_value in prompt.call_args[1]["message"] -@pytest.mark.skip # we should do something with this help -def test_question_number_input_test_ask_with_help(): - ask_text = "some question" - help_value = 1337 - questions = { - "some_number": { - "type": "number", - "ask": ask_text, - "help": help_value, - } - } - answers = {} - - with patch.object( - Moulinette, "prompt", return_value="1111" - ) 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"] - assert help_value in prompt.call_args[1]["message"] - - -def test_question_file_from_cli(): - FileQuestion.clean_upload_dirs() - - filename = "/tmp/ynh_test_question_file" - os.system(f"rm -f {filename}") - os.system(f"echo helloworld > {filename}") - - questions = { - "some_file": { - "type": "file", - } - } - answers = {"some_file": filename} - - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_file" - assert out.type == "file" - - # The file is supposed to be copied somewhere else - assert out.value != filename - assert out.value.startswith("/tmp/") - assert os.path.exists(out.value) - assert "helloworld" in open(out.value).read().strip() - - FileQuestion.clean_upload_dirs() - - assert not os.path.exists(out.value) - - -def test_question_file_from_api(): - FileQuestion.clean_upload_dirs() - - from base64 import b64encode - - b64content = b64encode(b"helloworld") - questions = { - "some_file": { - "type": "file", - } - } - answers = {"some_file": b64content} - - interface_type_bkp = Moulinette.interface.type - try: - Moulinette.interface.type = "api" - out = ask_questions_and_parse_answers(questions, answers)[0] - finally: - Moulinette.interface.type = interface_type_bkp - - assert out.name == "some_file" - assert out.type == "file" - - assert out.value.startswith("/tmp/") - assert os.path.exists(out.value) - assert "helloworld" in open(out.value).read().strip() - - FileQuestion.clean_upload_dirs() - - assert not os.path.exists(out.value) - - def test_normalize_boolean_nominal(): assert BooleanQuestion.normalize("yes") == 1 assert BooleanQuestion.normalize("Yes") == 1 From 8e6178a863202e137d7dd5376d0dddbd0ce7b361 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 22 Mar 2023 14:11:00 +0100 Subject: [PATCH 290/319] options:tests: add missing types tests --- src/tests/test_questions.py | 833 +++++++++++++++++++++++++++++++++++- 1 file changed, 832 insertions(+), 1 deletion(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index cecb59b80..4e8133960 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -558,6 +558,77 @@ class TestDisplayText(BaseTest): assert stdout.getvalue() == f"{options[0].ask['en']}\n" +# ╭───────────────────────────────────────────────────────╮ +# │ MARKDOWN │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestMarkdown(TestDisplayText): + raw_option = {"type": "markdown", "id": "markdown_id"} + # in cli this option is exactly the same as "display_text", no markdown support for now + + +# ╭───────────────────────────────────────────────────────╮ +# │ ALERT │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestAlert(TestDisplayText): + raw_option = {"type": "alert", "id": "alert_id"} + prefill = { + "raw_option": {"ask": " Custom info message"}, + "prefill": " custom default", + } + # fmt: off + scenarios = [ + (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"), + ] + # fmt: on + + def test_scenarios(self, intake, expected_output, raw_option, data): + style = raw_option.get("style", "info") + colors = {"danger": "31", "warning": "33", "info": "36", "success": "32"} + answers = {"alert_id": intake} if intake is not None else {} + + with patch_interface("cli"): + if inspect.isclass(expected_output) and issubclass( + expected_output, Exception + ): + with pytest.raises(expected_output): + ask_questions_and_parse_answers( + {"display_text_id": raw_option}, answers + ) + else: + with patch.object(sys, "stdout", new_callable=StringIO) as stdout: + options = ask_questions_and_parse_answers( + {"display_text_id": raw_option}, answers + ) + ask = options[0].ask["en"] + if style in colors: + color = colors[style] + title = style.title() + (":" if style != "success" else "!") + assert ( + stdout.getvalue() + == f"\x1b[{color}m\x1b[1m{title}\x1b[m {ask}\n" + ) + else: + # FIXME should fail + stdout.getvalue() == f"{ask}\n" + + +# ╭───────────────────────────────────────────────────────╮ +# │ BUTTON │ +# ╰───────────────────────────────────────────────────────╯ + + +# TODO + + # ╭───────────────────────────────────────────────────────╮ # │ STRING │ # ╰───────────────────────────────────────────────────────╯ @@ -653,6 +724,10 @@ 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 + *xpass(scenarios=[ + ("s3cr3t!!", "s3cr3t!!", {"example": "SUPAs3cr3t!!"}), # example is forbidden + ], reason="Should fail; example is forbidden"), *xpass(scenarios=[ (" value \n moarc0mpl1cat3d\n ", "value \n moarc0mpl1cat3d"), (" some_ value", "some_ value"), @@ -668,6 +743,49 @@ class TestPassword(BaseTest): # fmt: on +# ╭───────────────────────────────────────────────────────╮ +# │ COLOR │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestColor(BaseTest): + raw_option = {"type": "color", "id": "color_id"} + prefill = { + "raw_option": {"default": "#ff0000"}, + "prefill": "#ff0000", + # "intake": "#ff00ff", + } + # 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}), + *nones(None, "", output=""), + # custom valid + ("#000000", "#000000"), + ("#000", "#000"), + ("#fe100", "#fe100"), + (" #fe100 ", "#fe100"), + ("#ABCDEF", "#ABCDEF"), + # 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 + *xfail(scenarios=[ + ("#ffff00", "#fe100", {"readonly": True, "default": "#fe100"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + # ╭───────────────────────────────────────────────────────╮ # │ NUMBER | RANGE │ # ╰───────────────────────────────────────────────────────╯ @@ -776,6 +894,171 @@ class TestBoolean(BaseTest): (1, 0, {"readonly": True, "default": 0}), ], reason="Should not be overwritten"), ] + + +# ╭───────────────────────────────────────────────────────╮ +# │ DATE │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestDate(BaseTest): + raw_option = {"type": "date", "id": "date_id"} + prefill = { + "raw_option": {"default": "2024-12-29"}, + "prefill": "2024-12-29", + } + # 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}), + *nones(None, "", output=""), + # custom valid + ("2070-12-31", "2070-12-31"), + ("2024-02-29", "2024-02-29"), + *xfail(scenarios=[ + ("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"), + # custom invalid + ("29-12-2070", FAIL), + ("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"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ TIME │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestTime(BaseTest): + raw_option = {"type": "time", "id": "time_id"} + prefill = { + "raw_option": {"default": "12:26"}, + "prefill": "12:26", + } + # 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}), + *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"), + # custom invalid + ("24:00", FAIL), + ("23:1", FAIL), + ("23:005", FAIL), + # readonly + *xfail(scenarios=[ + ("00:00", "08:00", {"readonly": True, "default": "08:00"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ EMAIL │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestEmail(BaseTest): + raw_option = {"type": "email", "id": "email_id"} + prefill = { + "raw_option": {"default": "Abc@example.tld"}, + "prefill": "Abc@example.tld", + } + # fmt: off + scenarios = [ + *all_fails(False, True, 0, 1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "0", "1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + + *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"), + + # 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"), + # 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), + ] # fmt: on @@ -831,6 +1114,110 @@ class TestWebPath(BaseTest): # fmt: on +# ╭───────────────────────────────────────────────────────╮ +# │ URL │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestUrl(BaseTest): + raw_option = {"type": "url", "id": "url_id"} + prefill = { + "raw_option": {"default": "https://domain.tld"}, + "prefill": "https://domain.tld", + } + # 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}), + + *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"), + # 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', + 'http://www.example.com/~username/', + 'http://info.example.com?fred', + 'http://info.example.com/?fred', + 'http://xn--mgbh0fb.xn--kgbechtv/', + 'http://example.com/blue/red%3Fand+green', + 'http://www.example.com/?array%5Bkey%5D=value', + 'http://xn--rsum-bpad.example.org/', + 'http://123.45.67.8/', + 'http://123.45.67.8:8329/', + 'http://[2001:db8::ff00:42]:8329', + 'http://[2001::1]:8329', + 'http://[2001:db8::1]/', + 'http://www.example.com:8000/foo', + 'http://www.cwi.nl:80/%7Eguido/Python.html', + 'https://www.python.org/путь', + 'http://андрей@example.com', + 'https://exam_ple.com/', + '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', + ), + # 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 `/`"), + + # invalid + *all_fails( + 'ftp://example.com/', + "$https://example.org", + "../icons/logo.gif", + "abc", + "..", + "/", + "+http://example.com/", + "ht*tp://example.com/", + ), + *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 + + # ╭───────────────────────────────────────────────────────╮ # │ FILE │ # ╰───────────────────────────────────────────────────────╯ @@ -966,6 +1353,135 @@ class TestFile(BaseTest): _fill_or_prompt_one_option(self.raw_option, intake) +# ╭───────────────────────────────────────────────────────╮ +# │ SELECT │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestSelect(BaseTest): + raw_option = {"type": "select", "id": "select_id"} + prefill = { + "raw_option": {"default": "one", "choices": ["one", "two"]}, + "prefill": "one", + } + # fmt: off + scenarios = [ + { + # ["one", "two"] + "raw_options": [ + {"choices": ["one", "two"]}, + {"choices": {"one": "verbose one", "two": "verbose two"}}, + ], + "scenarios": [ + *nones(None, "", output=""), + *unchanged("one", "two"), + ("three", FAIL), + ] + }, + # custom bash style list as choices (only strings for now) + ("one", "one", {"choices": "one,two"}), + { + # [-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=""), + *unchanged(-1, 0, 1, 10), + *xfail(scenarios=[ + ("-1", -1), + ("0", 0), + ("1", 1), + ("10", 10), + ], reason="str -> int not handled"), + *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]}), + { + # mixed types + "raw_options": [{"choices": ["one", 2, True]}], + "scenarios": [ + *xpass(scenarios=[ + ("one", "one"), + (2, 2), + (True, True), + ], reason="mixed choices, should fail"), + *all_fails("2", "True", "y"), + ] + }, + { + "raw_options": [{"choices": ""}, {"choices": []}], + "scenarios": [ + # FIXME those should fail at option level (wrong default, dev error) + *all_fails(None, ""), + *xpass(scenarios=[ + ("", "", {"optional": True}), + (None, "", {"optional": True}), + ], reason="empty choices, should fail at option instantiation"), + ] + }, + # readonly + *xfail(scenarios=[ + ("one", "two", {"readonly": True, "choices": ["one", "two"], "default": "two"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ TAGS │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestTags(BaseTest): + raw_option = {"type": "tags", "id": "tags_id"} + prefill = { + "raw_option": {"default": ["one", "two"]}, + "prefill": "one,two", + } + # 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"), + { + "raw_options": [ + {}, + {"choices": ["one", "two"]} + ], + "scenarios": [ + *unchanged("one", "one,two"), + (["one"], "one"), + (["one", "two"], "one,two"), + ] + }, + ("three", FAIL, {"choices": ["one", "two"]}), + *unchanged("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", "['one']", "one,two", r"{}", "value"), + (" value\n", "value"), + ([False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}], "False,True,-1,0,1,1337,13.37,[],['one'],{}"), + *(([t], str(t)) for t in (False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {})), + # 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"], {}]}), + # readonly + *xfail(scenarios=[ + ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "default": "one,two"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + # ╭───────────────────────────────────────────────────────╮ # │ DOMAIN │ # ╰───────────────────────────────────────────────────────╯ @@ -1033,6 +1549,124 @@ class TestDomain(BaseTest): super().test_scenarios(intake, expected_output, raw_option, data) +# ╭───────────────────────────────────────────────────────╮ +# │ APP │ +# ╰───────────────────────────────────────────────────────╯ + +installed_webapp = { + "is_webapp": True, + "is_default": True, + "label": "My webapp", + "id": "my_webapp", + "domain_path": "/ynh-dev", +} +installed_non_webapp = { + "is_webapp": False, + "is_default": False, + "label": "My non webapp", + "id": "my_non_webapp", +} + + +@contextmanager +def patch_apps(*, apps): + """ + Data mocking for AppOption: + - yunohost.app.app_list + """ + with patch.object(app, "app_list", return_value={"apps": apps}): + yield + + +class TestApp(BaseTest): + raw_option = {"type": "app", "id": "app_id"} + # fmt: off + scenarios = [ + # Probably not needed to test common types since those are not available as choices + { + "data": [ + {"apps": []}, + {"apps": [installed_webapp]}, + {"apps": [installed_webapp, installed_non_webapp]}, + ], + "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? + *xpass(scenarios=[ + ("_none", "_none"), + ("_none", "_none", {"default": "_none"}), + ], reason="should fail; is required"), + *xpass(scenarios=[ + ("_none", "_none", {"optional": True}), + ("_none", "_none", {"optional": True, "default": "_none"}) + ], reason="Should output chosen none value"), + ("fake_app", FAIL), + ("fake_app", FAIL, {"choices": ["fake_app"]}), + ] + }, + { + "data": [ + {"apps": [installed_webapp]}, + {"apps": [installed_webapp, installed_non_webapp]}, + ], + "scenarios": [ + (installed_webapp["id"], installed_webapp["id"]), + (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}), + ] + }, + { + "data": [{"apps": [installed_webapp, installed_non_webapp]}], + "scenarios": [ + (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"), + ] + }, + ] + # fmt: on + + @pytest.mark.usefixtures("patch_no_tty") + def test_basic_attrs(self): + with patch_apps(apps=[]): + raw_option, option, value = self._test_basic_attrs() + + assert option.choices == {"_none": "---"} + assert option.filter is None + + with patch_apps(apps=[installed_webapp, installed_non_webapp]): + raw_option, option, value = self._test_basic_attrs() + + assert option.choices == { + "_none": "---", + "my_webapp": "My webapp (/ynh-dev)", + "my_non_webapp": "My non webapp (my_non_webapp)", + } + assert option.filter is None + + def test_options_prompted_with_ask_help(self, prefill_data=None): + with patch_apps(apps=[installed_webapp, installed_non_webapp]): + super().test_options_prompted_with_ask_help( + prefill_data={ + "raw_option": {"default": installed_webapp["id"]}, + "prefill": installed_webapp["id"], + } + ) + super().test_options_prompted_with_ask_help( + prefill_data={"raw_option": {"optional": True}, "prefill": ""} + ) + + def test_scenarios(self, intake, expected_output, raw_option, data): + with patch_apps(**data): + super().test_scenarios(intake, expected_output, raw_option, data) + + # ╭───────────────────────────────────────────────────────╮ # │ USER │ # ╰───────────────────────────────────────────────────────╯ @@ -1139,10 +1773,207 @@ class TestUser(BaseTest): super().test_scenarios(intake, expected_output, raw_option, data) -def test_question_empty(): +# ╭───────────────────────────────────────────────────────╮ +# │ GROUP │ +# ╰───────────────────────────────────────────────────────╯ + +groups1 = ["all_users", "visitors", "admins"] +groups2 = ["all_users", "visitors", "admins", "custom_group"] + + +@contextmanager +def patch_groups(*, groups): + """ + Data mocking for GroupOption: + - yunohost.user.user_group_list + """ + with patch.object(user, "user_group_list", return_value={"groups": groups}): + yield + + +class TestGroup(BaseTest): + raw_option = {"type": "group", "id": "group_id"} + # fmt: off + scenarios = [ + # No tests for empty groups since it should not happens + { + "data": [ + {"groups": groups1}, + {"groups": groups2}, + ], + "scenarios": [ + # FIXME Group option is not really nullable, even if optional + *nones(None, "", output="all_users", fail_if_required=False), + ("admins", "admins"), + ("fake_group", FAIL), + ("fake_group", FAIL, {"choices": ["fake_group"]}), + ] + }, + { + "data": [ + {"groups": groups2}, + ], + "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')"), + # readonly + *xpass(scenarios=[ + ("admins", "admins", {"readonly": True}), + ], reason="Should fail since readonly is forbidden"), + ] + }, + ] + # fmt: on + + def test_options_prompted_with_ask_help(self, prefill_data=None): + with patch_groups(groups=groups2): + super().test_options_prompted_with_ask_help( + prefill_data={"raw_option": {}, "prefill": "all_users"} + ) + super().test_options_prompted_with_ask_help( + prefill_data={ + "raw_option": {"default": "admins"}, + "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): + super().test_scenarios(intake, expected_output, raw_option, data) + + +# ╭───────────────────────────────────────────────────────╮ +# │ MULTIPLE │ +# ╰───────────────────────────────────────────────────────╯ + + +@pytest.fixture +def patch_entities(): + with patch_domains(domains=domains2, main_domain=main_domain), patch_apps( + apps=[installed_webapp, installed_non_webapp] + ), patch_users( + users={admin_username: admin_user, regular_username: regular_user}, + admin_username=admin_username, + main_domain=main_domain, + ), patch_groups( + groups=groups2 + ): + yield + + +def test_options_empty(): ask_questions_and_parse_answers({}, {}) == [] +@pytest.mark.usefixtures("patch_entities", "file_clean") +def test_options_query_string(): + raw_options = { + "string_id": {"type": "string"}, + "text_id": {"type": "text"}, + "password_id": {"type": "password"}, + "color_id": {"type": "color"}, + "number_id": {"type": "number"}, + "boolean_id": {"type": "boolean"}, + "date_id": {"type": "date"}, + "time_id": {"type": "time"}, + "email_id": {"type": "email"}, + "path_id": {"type": "path"}, + "url_id": {"type": "url"}, + "file_id": {"type": "file"}, + "select_id": {"type": "select", "choices": ["one", "two"]}, + "tags_id": {"type": "tags", "choices": ["one", "two"]}, + "domain_id": {"type": "domain"}, + "app_id": {"type": "app"}, + "user_id": {"type": "user"}, + "group_id": {"type": "group"}, + } + + results = { + "string_id": "string", + "text_id": "text\ntext", + "password_id": "sUpRSCRT", + "color_id": "#ffff00", + "number_id": 10, + "boolean_id": 1, + "date_id": "2030-03-06", + "time_id": "20:55", + "email_id": "coucou@ynh.local", + "path_id": "/ynh-dev", + "url_id": "https://yunohost.org", + "file_id": file_content1, + "select_id": "one", + "tags_id": "one,two", + "domain_id": main_domain, + "app_id": installed_webapp["id"], + "user_id": regular_username, + "group_id": "admins", + } + + @contextmanager + def patch_query_string(file_repr): + yield ( + "string_id= string" + "&text_id=text\ntext" + "&password_id=sUpRSCRT" + "&color_id=#ffff00" + "&number_id=10" + "&boolean_id=y" + "&date_id=2030-03-06" + "&time_id=20:55" + "&email_id=coucou@ynh.local" + "&path_id=ynh-dev/" + "&url_id=https://yunohost.org" + f"&file_id={file_repr}" + "&select_id=one" + "&tags_id=one,two" + # FIXME We can't test with parse.qs for now, next syntax is available only with config panels + # "&tags_id=one" + # "&tags_id=two" + f"&domain_id={main_domain}" + f"&app_id={installed_webapp['id']}" + f"&user_id={regular_username}" + "&group_id=admins" + # not defined extra values are silently ignored + "&fake_id=fake_value" + ) + + def _assert_correct_values(options, raw_options): + form = {option.name: option.value for option in options} + + for k, v in results.items(): + if k == "file_id": + assert os.path.exists(form["file_id"]) and os.path.isfile( + form["file_id"] + ) + with open(form["file_id"], "r") as f: + assert f.read() == file_content1 + else: + assert form[k] == results[k] + + assert len(options) == len(raw_options.keys()) + assert "fake_id" not in form + + 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) + + 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) + + def test_question_string_default_type(): questions = {"some_string": {}} answers = {"some_string": "some_value"} From f8c1e7c168b885ea23d6017447b9795b9eb041fd Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 22 Mar 2023 14:13:54 +0100 Subject: [PATCH 291/319] options: misc option quick fixes --- src/tests/test_questions.py | 2 +- src/utils/config.py | 42 ++++++++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 4e8133960..8ded2e137 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -13,7 +13,7 @@ from _pytest.mark.structures import ParameterSet from moulinette import Moulinette -from yunohost import domain, user +from yunohost import app, domain, user from yunohost.utils.config import ( ARGUMENTS_TYPE_PARSERS, ask_questions_and_parse_answers, diff --git a/src/utils/config.py b/src/utils/config.py index 6f06ed1fb..37f41f8b2 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -856,7 +856,9 @@ class Question: # Don't restrict choices if there's none specified self.choices = question.get("choices", None) self.pattern = question.get("pattern", self.pattern) - self.ask = question.get("ask", {"en": self.name}) + self.ask = question.get("ask", self.name) + if not isinstance(self.ask, dict): + self.ask = {"en": self.ask} self.help = question.get("help") self.redact = question.get("redact", False) self.filter = question.get("filter", None) @@ -962,7 +964,7 @@ class Question: "app_argument_choice_invalid", name=self.name, value=self.value, - choices=", ".join(self.choices), + choices=", ".join(str(choice) for choice in self.choices), ) if self.pattern and not re.match(self.pattern["regexp"], str(self.value)): raise YunohostValidationError( @@ -1085,13 +1087,13 @@ class TagsQuestion(Question): @staticmethod def humanize(value, option={}): if isinstance(value, list): - return ",".join(value) + return ",".join(str(v) for v in value) return value @staticmethod def normalize(value, option={}): if isinstance(value, list): - return ",".join(value) + return ",".join(str(v) for v in value) if isinstance(value, str): value = value.strip() return value @@ -1102,6 +1104,21 @@ class TagsQuestion(Question): values = values.split(",") elif values is None: values = [] + + if not isinstance(values, list): + if self.choices: + raise YunohostValidationError( + "app_argument_choice_invalid", + name=self.name, + value=self.value, + choices=", ".join(str(choice) for choice in self.choices), + ) + raise YunohostValidationError( + "app_argument_invalid", + name=self.name, + error=f"'{str(self.value)}' is not a list", + ) + for value in values: self.value = value super()._prevalidate() @@ -1152,6 +1169,13 @@ class PathQuestion(Question): def normalize(value, option={}): option = option.__dict__ if isinstance(option, Question) else option + if not isinstance(value, str): + raise YunohostValidationError( + "app_argument_invalid", + name=option.get("name"), + error="Argument for path should be a string.", + ) + if not value.strip(): if option.get("optional"): return "" @@ -1399,7 +1423,7 @@ class NumberQuestion(Question): return int(value) if value in [None, ""]: - return value + return None option = option.__dict__ if isinstance(option, Question) else option raise YunohostValidationError( @@ -1481,8 +1505,12 @@ class FileQuestion(Question): super()._prevalidate() + # Validation should have already failed if required + if self.value in (None, ""): + return self.value + if Moulinette.interface.type != "api": - if not self.value or not os.path.exists(str(self.value)): + if not os.path.exists(str(self.value)) or not os.path.isfile(str(self.value)): raise YunohostValidationError( "app_argument_invalid", name=self.name, @@ -1493,7 +1521,7 @@ class FileQuestion(Question): from base64 import b64decode if not self.value: - return self.value + return "" upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_") _, file_path = tempfile.mkstemp(dir=upload_dir) From 2d03176c7fc5ea29863f0bb2fe1b2878839008ea Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 22 Mar 2023 15:37:39 +0100 Subject: [PATCH 292/319] fix i18n panel+section names --- src/utils/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/config.py b/src/utils/config.py index 6f06ed1fb..7b16d6a23 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -581,7 +581,7 @@ class ConfigPanel: 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"] else {"en": value} + value if key not in ["ask", "help", "name"] or isinstance(value, dict) else {"en": value} ) return out From 63981aacf9941ac779f437e57844d0bf8d1a0daf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 27 Mar 2023 20:34:38 +0200 Subject: [PATCH 293/319] appsv2: Add documentation about the new 'autoupdate' mechanism for app sources --- src/utils/resources.py | 98 +++++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index b9bb1fee7..4c7c09fd3 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -267,7 +267,7 @@ class SourcesResource(AppResource): Various options are available to accomodate the behavior according to the asset structure - ##### Example: + ##### Example ```toml [resources.sources] @@ -275,6 +275,8 @@ class SourcesResource(AppResource): [resources.sources.main] url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz" sha256 = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + + autoupdate.strategy = "latest_github_tag" ``` Or more complex examples with several element, including one with asset that depends on the arch @@ -286,11 +288,16 @@ class SourcesResource(AppResource): in_subdir = false amd64.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.amd64.tar.gz" amd64.sha256 = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" - i386.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.amd64.tar.gz" + i386.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.386.tar.gz" i386.sha256 = "53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3" - armhf.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.armhf.tar.gz" + armhf.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.arm.tar.gz" armhf.sha256 = "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865" + autoupdate.strategy = "latest_github_release" + autoupdate.asset.amd64 = ".*\.amd64.tar.gz" + autoupdate.asset.i386 = ".*\.386.tar.gz" + autoupdate.asset.armhf = ".*\.arm.tar.gz" + [resources.sources.zblerg] url = "https://zblerg.com/download/zblerg" sha256 = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2" @@ -299,7 +306,7 @@ class SourcesResource(AppResource): ``` - ##### Properties (for each source): + ##### Properties (for each source) - `prefetch` : `true` (default) or `false`, wether or not to pre-fetch this asset during the provisioning phase of the resource. If several arch-dependent url are provided, YunoHost will only prefetch the one for the current system architecture. - `url` : the asset's URL @@ -316,11 +323,24 @@ class SourcesResource(AppResource): - `rename`: some string like `whatever_your_want`, to be used for convenience when `extract` is `false` and the default name of the file is not practical - `platform`: for example `linux/amd64` (defaults to `linux/$YNH_ARCH`) to be used in conjonction with `format = "docker"` to specify which architecture to extract for + ###### Regarding `autoupdate` - ##### Provision/Update: + Strictly speaking, this has nothing to do with the actual app install. `autoupdate` is expected to contain metadata for automatic maintenance / update of the app sources info in the manifest. It is meant to be a simpler replacement for "autoupdate" Github workflow mechanism. + + The infos are used by this script : https://github.com/YunoHost/apps/blob/master/tools/autoupdate_app_sources/autoupdate_app_sources.py which is ran by the YunoHost infrastructure periodically and will create the corresponding pull request automatically. + + The script will rely on the code repo specified in the upstream section of the manifest. + + `autoupdate.strategy` is expected to be one of : + - `latest_github_tag` : look for the latest tag (by sorting tags and finding the "largest" version). Then using the corresponding tar.gz url. Tags containing `rc`, `beta`, `alpha`, `start` are ignored, and actually any tag which doesn't look like `x.y.z` or `vx.y.z` + - `latest_github_release` : similar to `latest_github_tags`, but starting from the list of releases. Pre- or draft releases are ignored. Releases may have assets attached to them, in which case you can define: + - `autoupdate.asset = "some regex"` (when there's only one asset to use). The regex is used to find the appropriate asset among the list of all assets + - or several `autoupdate.asset.$arch = "some_regex"` (when the asset is arch-specific). The regex is used to find the appropriate asset for the specific arch among the list of assets + + ##### Provision/Update - For elements with `prefetch = true`, will download the asset (for the appropriate architecture) and store them in `/var/cache/yunohost/download/$app/$source_id`, to be later picked up by `ynh_setup_source`. (NB: this only happens during install and upgrade, not restore) - ##### Deprovision: + ##### Deprovision - Nothing (just cleanup the cache) """ @@ -439,7 +459,7 @@ class PermissionsResource(AppResource): The list of allowed user/groups may be initialized using the content of the `init_{perm}_permission` question from the manifest, hence `init_main_permission` replaces the `is_public` question and shall contain a group name (typically, `all_users` or `visitors`). - ##### Example: + ##### Example ```toml [resources.permissions] main.url = "/" @@ -450,7 +470,7 @@ class PermissionsResource(AppResource): admin.allowed = "admins" # Assuming the "admins" group exists (cf future developments ;)) ``` - ##### Properties (for each perm name): + ##### Properties (for each perm name) - `url`: The relative URI corresponding to this permission. Typically `/` or `/something`. This property may be omitted for non-web permissions. - `show_tile`: (default: `true` if `url` is defined) Wether or not a tile should be displayed for that permission in the user portal - `allowed`: (default: nobody) The group initially allowed to access this perm, if `init_{perm}_permission` is not defined in the manifest questions. Note that the admin may tweak who is allowed/unallowed on that permission later on, this is only meant to **initialize** the permission. @@ -458,14 +478,14 @@ class PermissionsResource(AppResource): - `protected`: (default: `false`) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission. Defaults to 'false'. - `additional_urls`: (default: none) List of additional URL for which access will be allowed/forbidden - ##### Provision/Update: + ##### Provision/Update - Delete any permissions that may exist and be related to this app yet is not declared anymore - Loop over the declared permissions and create them if needed or update them with the new values - ##### Deprovision: + ##### Deprovision - Delete all permission related to this app - ##### Legacy management: + ##### Legacy management - Legacy `is_public` setting will be deleted if it exists """ @@ -627,22 +647,22 @@ class SystemuserAppResource(AppResource): """ Provision a system user to be used by the app. The username is exactly equal to the app id - ##### Example: + ##### Example ```toml [resources.system_user] # (empty - defaults are usually okay) ``` - ##### Properties: + ##### 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 - `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: + ##### Provision/Update - will create the system user if it doesn't exists yet - will add/remove the ssh/sftp.app groups - ##### Deprovision: + ##### Deprovision - deletes the user and group """ @@ -735,28 +755,28 @@ class InstalldirAppResource(AppResource): """ Creates a directory to be used by the app as the installation directory, typically where the app sources and assets are located. The corresponding path is stored in the settings as `install_dir` - ##### Example: + ##### Example ```toml [resources.install_dir] # (empty - defaults are usually okay) ``` - ##### Properties: + ##### Properties - `dir`: (default: `/var/www/__APP__`) The full path of the install dir - `owner`: (default: `__APP__:rwx`) The owner (and owner permissions) for the install dir - `group`: (default: `__APP__:rx`) The group (and group permissions) for the install dir - ##### Provision/Update: + ##### Provision/Update - during install, the folder will be deleted if it already exists (FIXME: is this what we want?) - if the dir path changed and a folder exists at the old location, the folder will be `mv`'ed to the new location - otherwise, creates the directory if it doesn't exists yet - (re-)apply permissions (only on the folder itself, not recursively) - save the value of `dir` as `install_dir` in the app's settings, which can be then used by the app scripts (`$install_dir`) and conf templates (`__INSTALL_DIR__`) - ##### Deprovision: + ##### Deprovision - recursively deletes the directory if it exists - ##### Legacy management: + ##### Legacy management - In the past, the setting was called `final_path`. The code will automatically rename it as `install_dir`. - As explained in the 'Provision/Update' section, the folder will also be moved if the location changed @@ -850,28 +870,28 @@ class DatadirAppResource(AppResource): """ Creates a directory to be used by the app as the data store directory, typically where the app multimedia or large assets added by users are located. The corresponding path is stored in the settings as `data_dir`. This resource behaves very similarly to install_dir. - ##### Example: + ##### Example ```toml [resources.data_dir] # (empty - defaults are usually okay) ``` - ##### Properties: + ##### Properties - `dir`: (default: `/home/yunohost.app/__APP__`) The full path of the data dir - `owner`: (default: `__APP__:rwx`) The owner (and owner permissions) for the data dir - `group`: (default: `__APP__:rx`) The group (and group permissions) for the data dir - ##### Provision/Update: + ##### Provision/Update - if the dir path changed and a folder exists at the old location, the folder will be `mv`'ed to the new location - otherwise, creates the directory if it doesn't exists yet - (re-)apply permissions (only on the folder itself, not recursively) - save the value of `dir` as `data_dir` in the app's settings, which can be then used by the app scripts (`$data_dir`) and conf templates (`__DATA_DIR__`) - ##### Deprovision: + ##### Deprovision - (only if the purge option is chosen by the user) recursively deletes the directory if it exists - also delete the corresponding setting - ##### Legacy management: + ##### Legacy management - In the past, the setting may have been called `datadir`. The code will automatically rename it as `data_dir`. - As explained in the 'Provision/Update' section, the folder will also be moved if the location changed @@ -952,7 +972,7 @@ class AptDependenciesAppResource(AppResource): """ Create a virtual package in apt, depending on the list of specified packages that the app needs. The virtual packages is called `$app-ynh-deps` (with `_` being replaced by `-` in the app name, see `ynh_install_app_dependencies`) - ##### Example: + ##### Example ```toml [resources.apt] packages = "nyancat, lolcat, sl" @@ -963,16 +983,16 @@ class AptDependenciesAppResource(AppResource): extras.yarn.packages = "yarn" ``` - ##### Properties: + ##### Properties - `packages`: Comma-separated 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 - ##### Provision/Update: + ##### Provision/Update - The code literally calls the bash helpers `ynh_install_app_dependencies` and `ynh_install_extra_app_dependencies`, similar to what happens in v1. - Note that when `packages` contains some phpX.Y-foobar dependencies, this will automagically define a `phpversion` setting equal to `X.Y` which can therefore be used in app scripts ($phpversion) or templates (`__PHPVERSION__`) - ##### Deprovision: + ##### Deprovision - The code literally calls the bash helper `ynh_remove_app_dependencies` """ @@ -1031,7 +1051,7 @@ class PortsResource(AppResource): Note that because multiple ports can be booked, each properties is prefixed by the name of the port. `main` is a special name and will correspond to the setting `$port`, whereas for example `xmpp_client` will correspond to the setting `$port_xmpp_client`. - ##### Example: + ##### Example ```toml [resources.ports] # (empty should be fine for most apps... though you can customize stuff if absolutely needed) @@ -1043,21 +1063,21 @@ class PortsResource(AppResource): xmpp_client.exposed = "TCP" # here, we're telling that the port needs to be publicly exposed on TCP on the firewall ``` - ##### Properties (for every port name): + ##### Properties (for every port name) - `default`: The prefered value for the port. If this port is already being used by another process right now, or is booked in another app's setting, the code will increment the value until it finds a free port and store that value as the setting. If no value is specified, a random value between 10000 and 60000 is used. - `exposed`: (default: `false`) Wether this port should be opened on the firewall and be publicly reachable. This should be kept to `false` for the majority of apps than only need a port for internal reverse-proxying! Possible values: `false`, `true`(=`Both`), `Both`, `TCP`, `UDP`. This will result in the port being opened on the firewall, and the diagnosis checking that a program answers on that port. - `fixed`: (default: `false`) Tells that the app absolutely needs the specific value provided in `default`, typically because it's needed for a specific protocol - ##### Provision/Update (for every port name): + ##### Provision/Update (for every port name) - If not already booked, look for a free port, starting with the `default` value (or a random value between 10000 and 60000 if no `default` set) - If `exposed` is not `false`, open the port in the firewall accordingly - otherwise make sure it's closed. - The value of the port is stored in the `$port` setting for the `main` port, or `$port_NAME` for other `NAME`s - ##### Deprovision: + ##### Deprovision - Close the ports on the firewall if relevant - Deletes all the port settings - ##### Legacy management: + ##### Legacy management - In the past, some settings may have been named `NAME_port` instead of `port_NAME`, in which case the code will automatically rename the old setting. """ @@ -1160,25 +1180,25 @@ class DatabaseAppResource(AppResource): NB2: no automagic migration will happen in an suddenly change `type` from `mysql` to `postgresql` or viceversa in its life - ##### Example: + ##### Example ```toml [resources.database] type = "mysql" # or : "postgresql". Only these two values are supported ``` - ##### Properties: + ##### Properties - `type`: The database type, either `mysql` or `postgresql` - ##### Provision/Update: + ##### Provision/Update - (Re)set the `$db_name` and `$db_user` settings with the sanitized app name (replacing `-` and `.` with `_`) - If `$db_pwd` doesn't already exists, pick a random database password and store it in that setting - If the database doesn't exists yet, create the SQL user and DB using `ynh_mysql_create_db` or `ynh_psql_create_db`. - ##### Deprovision: + ##### Deprovision - Drop the DB using `ynh_mysql_remove_db` or `ynh_psql_remove_db` - Deletes the `db_name`, `db_user` and `db_pwd` settings - ##### Legacy management: + ##### Legacy management - In the past, the sql passwords may have been named `mysqlpwd` or `psqlpwd`, in which case it will automatically be renamed as `db_pwd` """ From 306c5e0e102b7eed6eab713bb11de71c5c1054f0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 31 Mar 2023 20:11:25 +0200 Subject: [PATCH 294/319] app resources: add documentation about latest_github_commit strategy for source autoupdate + autoupdate.upstream --- src/utils/resources.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/resources.py b/src/utils/resources.py index 4c7c09fd3..c8e11b990 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -336,6 +336,9 @@ class SourcesResource(AppResource): - `latest_github_release` : similar to `latest_github_tags`, but starting from the list of releases. Pre- or draft releases are ignored. Releases may have assets attached to them, in which case you can define: - `autoupdate.asset = "some regex"` (when there's only one asset to use). The regex is used to find the appropriate asset among the list of all assets - or several `autoupdate.asset.$arch = "some_regex"` (when the asset is arch-specific). The regex is used to find the appropriate asset for the specific arch among the list of assets + - `latest_github_commit` : will use the latest commit on github, and the corresponding tarball. If this is used for the 'main' source, it will also assume that the version is YYYY.MM.DD corresponding to the date of the commit. + + It is also possible to define `autoupdate.upstream` to use a different Git(hub) repository instead of the code repository from the upstream section of the manifest. This can be useful when, for example, the app uses other assets such as plugin from a different repository. ##### Provision/Update - For elements with `prefetch = true`, will download the asset (for the appropriate architecture) and store them in `/var/cache/yunohost/download/$app/$source_id`, to be later picked up by `ynh_setup_source`. (NB: this only happens during install and upgrade, not restore) From 4b46f3220168074598c238de8338c9bdc5478dd0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 31 Mar 2023 20:26:08 +0200 Subject: [PATCH 295/319] appv2: add support for subdirs property in data_dir --- src/utils/resources.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index c8e11b990..3ff3f40d1 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -881,13 +881,15 @@ class DatadirAppResource(AppResource): ##### Properties - `dir`: (default: `/home/yunohost.app/__APP__`) The full path of the data dir + - `subdirs`: (default: empty list) A list of subdirs to initialize inside the data dir. For example, `['foo', 'bar']` - `owner`: (default: `__APP__:rwx`) The owner (and owner permissions) for the data dir - `group`: (default: `__APP__:rx`) The group (and group permissions) for the data dir ##### Provision/Update - if the dir path changed and a folder exists at the old location, the folder will be `mv`'ed to the new location - otherwise, creates the directory if it doesn't exists yet - - (re-)apply permissions (only on the folder itself, not recursively) + - create each subdir declared and which do not exist already + - (re-)apply permissions (only on the folder itself and declared subdirs, not recursively) - save the value of `dir` as `data_dir` in the app's settings, which can be then used by the app scripts (`$data_dir`) and conf templates (`__DATA_DIR__`) ##### Deprovision @@ -910,11 +912,13 @@ class DatadirAppResource(AppResource): default_properties: Dict[str, Any] = { "dir": "/home/yunohost.app/__APP__", + "subdirs": [], "owner": "__APP__:rwx", "group": "__APP__:rx", } dir: str = "" + subdirs: list = [] owner: str = "" group: str = "" @@ -938,6 +942,11 @@ class DatadirAppResource(AppResource): else: mkdir(self.dir) + for subdir in self.subdirs: + full_path = os.path.join(self.dir, subdir) + if not os.path.isdir(full_path): + mkdir(full_path) + owner, owner_perm = self.owner.split(":") group, group_perm = self.group.split(":") owner_perm_octal = ( @@ -956,6 +965,10 @@ class DatadirAppResource(AppResource): # in which case we want to apply the perm to the pointed dir, not to the symlink chmod(os.path.realpath(self.dir), perm_octal) chown(os.path.realpath(self.dir), owner, group) + for subdir in self.subdirs: + full_path = os.path.join(self.dir, subdir) + chmod(os.path.realpath(full_path), perm_octal) + chown(os.path.realpath(full_path), owner, group) self.set_setting("data_dir", self.dir) self.delete_setting("datadir") # Legacy From 821aedefa70ecfdc54378bfd4926633e77dc975f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 31 Mar 2023 20:45:14 +0200 Subject: [PATCH 296/319] users: fix quota parsing being wrong by a factor 1000 ... doveadm returns kilos, not bytes --- src/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user.py b/src/user.py index 12f13f75c..f17a60942 100644 --- a/src/user.py +++ b/src/user.py @@ -631,7 +631,7 @@ def user_info(username): has_value = re.search(r"Value=(\d+)", cmd_result) if has_value: - storage_use = int(has_value.group(1)) + storage_use = int(has_value.group(1)) * 1000 storage_use = binary_to_human(storage_use) if is_limited: From 14bf2ee48b113efd66b4c2b91992bd9dd6c978cb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 2 Apr 2023 20:28:29 +0200 Subject: [PATCH 297/319] appsv2: various fixes regarding sources toml parsing/caching --- helpers/utils | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/helpers/utils b/helpers/utils index 97bd8e6b5..d27b5bca2 100644 --- a/helpers/utils +++ b/helpers/utils @@ -22,7 +22,10 @@ YNH_APP_BASEDIR=${YNH_APP_BASEDIR:-$(realpath ..)} ynh_exit_properly() { local exit_code=$? - rm -rf "/var/cache/yunohost/download/" + if [[ "${YNH_APP_ACTION}" =~ ^install$|^upgrade$|^restore$ ]] + then + rm -rf "/var/cache/yunohost/download/" + fi if [ "$exit_code" -eq 0 ]; then exit 0 # Exit without error if the script ended correctly @@ -164,22 +167,22 @@ ynh_setup_source() { if test -e $YNH_APP_BASEDIR/manifest.toml && cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq -e '.resources.sources' >/dev/null then source_id="${source_id:-main}" - local sources_json=$(cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq '.resources.sources') - if ! echo "$sources_json" | jq -re ".$source_id.url" + local sources_json=$(cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq ".resources.sources[\"$source_id\"]") + if jq -re ".url" <<< "$sources_json" then - local arch_prefix=".$YNH_ARCH" - else local arch_prefix="" + else + local arch_prefix=".$YNH_ARCH" fi - local src_url="$(echo "$sources_json" | jq -r ".$source_id$arch_prefix.url" | sed 's/^null$//')" - local src_sum="$(echo "$sources_json" | jq -r ".$source_id$arch_prefix.sha256" | sed 's/^null$//')" + local src_url="$(jq -r "$arch_prefix.url" <<< "$sources_json" | sed 's/^null$//')" + local src_sum="$(jq -r "$arch_prefix.sha256" <<< "$sources_json" | sed 's/^null$//')" local src_sumprg="sha256sum" - local src_format="$(echo "$sources_json" | jq -r ".$source_id.format" | sed 's/^null$//')" - local src_in_subdir="$(echo "$sources_json" | jq -r ".$source_id.in_subdir" | sed 's/^null$//')" - local src_extract="$(echo "$sources_json" | jq -r ".$source_id.extract" | sed 's/^null$//')" - local src_platform="$(echo "$sources_json" | jq -r ".$source_id.platform" | sed 's/^null$//')" - local src_rename="$(echo "$sources_json" | jq -r ".$source_id.rename" | sed 's/^null$//')" + local src_format="$(jq -r ".format" <<< "$sources_json" | sed 's/^null$//')" + local src_in_subdir="$(jq -r ".in_subdir" <<< "$sources_json" | sed 's/^null$//')" + local src_extract="$(jq -r ".extract" <<< "$sources_json" | sed 's/^null$//')" + local src_platform="$(jq -r ".platform" <<< "$sources_json" | sed 's/^null$//')" + local src_rename="$(jq -r ".rename" <<< "$sources_json" | sed 's/^null$//')" [[ -n "$src_url" ]] || ynh_die "No URL defined for source $source_id$arch_prefix ?" [[ -n "$src_sum" ]] || ynh_die "No sha256 sum defined for source $source_id$arch_prefix ?" @@ -236,8 +239,8 @@ ynh_setup_source() { local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${source_id}" # Gotta use this trick with 'dirname' because source_id may contain slashes x_x - mkdir -p $(dirname /var/cache/yunohost/download/${YNH_APP_ID}/${source_id}) - src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${source_id}" + mkdir -p $(dirname /var/cache/yunohost/download/${YNH_APP_INSTANCE_NAME}/${source_id}) + src_filename="/var/cache/yunohost/download/${YNH_APP_INSTANCE_NAME}/${source_id}" if [ "$src_format" = "docker" ]; then src_platform="${src_platform:-"linux/$YNH_ARCH"}" From 85a4b78e492306948a9791a020ca0240001be179 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 2 Apr 2023 20:32:17 +0200 Subject: [PATCH 298/319] Update changelog for 11.1.16 --- debian/changelog | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/debian/changelog b/debian/changelog index 0373a10b8..3c0cccbc2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,19 @@ +yunohost (11.1.16) stable; urgency=low + + - apps: fix i18n panel+section names ([#1630](https://github.com/yunohost/yunohost/pull/1630)) + - appsv2: don't remove yhh-deps virtual package if it doesn't exist. Otherwise when apt fails to install dependency, we end up with another error about failing to remove the ynh-deps package (3656c199) + - appsv2: add validation for expected types for permissions stuff (b2596f32) + - appsv2: add support for subdirs property in data_dir (4b46f322) + - appsv2: various fixes regarding sources toml parsing/caching (14bf2ee4) + - appsv2: add documentation about the new 'autoupdate' mechanism for app sources (63981aac) + - ynh_setup_source: fix buggy checksum mismatch handling, can't compute the sha256sum after we delete the file @_@ (1b2fa91f) + - users: fix quota parsing being wrong by a factor 1000 ... doveadm returns kilos, not bytes (821aedef) + - backup: fix boring issue where archive is a broken symlink... (a95d10e5) + + Thanks to all contributors <3 ! (axolotle) + + -- Alexandre Aubin Sun, 02 Apr 2023 20:29:33 +0200 + yunohost (11.1.15) stable; urgency=low - doc: Fix version number in autogenerated resource doc (5b58e0e6) From 4e799bfbc3d73aff82e3c76354630a3b2b4248e7 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Sun, 2 Apr 2023 18:52:32 +0000 Subject: [PATCH 299/319] [CI] Format code with Black --- src/utils/config.py | 4 +++- src/utils/resources.py | 29 ++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/utils/config.py b/src/utils/config.py index 7b16d6a23..d5bec7731 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -581,7 +581,9 @@ class ConfigPanel: 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} + value + if key not in ["ask", "help", "name"] or isinstance(value, dict) + else {"en": value} ) return out diff --git a/src/utils/resources.py b/src/utils/resources.py index 3ff3f40d1..8f8393e17 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -520,21 +520,36 @@ class PermissionsResource(AppResource): properties["main"] = self.default_perm_properties for perm, infos in properties.items(): - if "auth_header" in infos and not isinstance(infos.get("auth_header"), bool): - raise YunohostError(f"In manifest, for permission '{perm}', 'auth_header' should be a boolean", raw_msg=True) + if "auth_header" in infos and not isinstance( + infos.get("auth_header"), bool + ): + raise YunohostError( + f"In manifest, for permission '{perm}', 'auth_header' should be a boolean", + raw_msg=True, + ) if "show_tile" in infos and not isinstance(infos.get("show_tile"), bool): - raise YunohostError(f"In manifest, for permission '{perm}', 'show_tile' should be a boolean", raw_msg=True) + raise YunohostError( + f"In manifest, for permission '{perm}', 'show_tile' should be a boolean", + raw_msg=True, + ) if "protected" in infos and not isinstance(infos.get("protected"), bool): - raise YunohostError(f"In manifest, for permission '{perm}', 'protected' should be a boolean", raw_msg=True) - if "additional_urls" in infos and not isinstance(infos.get("additional_urls"), list): - raise YunohostError(f"In manifest, for permission '{perm}', 'additional_urls' should be a list", raw_msg=True) + raise YunohostError( + f"In manifest, for permission '{perm}', 'protected' should be a boolean", + raw_msg=True, + ) + if "additional_urls" in infos and not isinstance( + infos.get("additional_urls"), list + ): + raise YunohostError( + f"In manifest, for permission '{perm}', 'additional_urls' should be a list", + raw_msg=True, + ) properties[perm] = copy.copy(self.default_perm_properties) properties[perm].update(infos) if properties[perm]["show_tile"] is None: properties[perm]["show_tile"] = bool(properties[perm]["url"]) - if properties["main"]["url"] is not None and ( not isinstance(properties["main"].get("url"), str) or properties["main"]["url"] != "/" From a16a164e20183584451d35ad6dacdab7b1965c7d Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 4 Apr 2023 11:36:35 +0200 Subject: [PATCH 300/319] Fix autodns for gandi root domain --- src/dns.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dns.py b/src/dns.py index 3a5e654ec..5fa58fb71 100644 --- a/src/dns.py +++ b/src/dns.py @@ -960,6 +960,9 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy." ) continue + else if registrar == "gandi": + if record["name"] == base_dns_zone: + record["name"] = "@." + record["name"] record["action"] = action query = ( From 74213c6ce9a8f7dea09e281ad19eeb06e5df7832 Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 4 Apr 2023 11:40:02 +0200 Subject: [PATCH 301/319] Typo --- src/dns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dns.py b/src/dns.py index 5fa58fb71..e3a26044c 100644 --- a/src/dns.py +++ b/src/dns.py @@ -960,7 +960,7 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy." ) continue - else if registrar == "gandi": + elif registrar == "gandi": if record["name"] == base_dns_zone: record["name"] = "@." + record["name"] From b5f36626277f40295e2a32b2489ba8ca262d31e9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Apr 2023 13:01:25 +0200 Subject: [PATCH 302/319] Misc syntax --- src/utils/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/config.py b/src/utils/config.py index 37f41f8b2..314f72ce7 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1506,7 +1506,7 @@ class FileQuestion(Question): super()._prevalidate() # Validation should have already failed if required - if self.value in (None, ""): + if self.value in [None, ""]: return self.value if Moulinette.interface.type != "api": From 9c6a7fdf040e77b1f358c82050f02de4893977a6 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 4 Apr 2023 15:43:46 +0200 Subject: [PATCH 303/319] mv config.py to form.py --- src/utils/{config.py => form.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/utils/{config.py => form.py} (100%) diff --git a/src/utils/config.py b/src/utils/form.py similarity index 100% rename from src/utils/config.py rename to src/utils/form.py From d8cb2139a9c2bdb9e449d631ac668be4823eda0c Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 4 Apr 2023 15:50:56 +0200 Subject: [PATCH 304/319] remove ConfigPanel code from form.py --- src/utils/form.py | 656 +--------------------------------------------- 1 file changed, 1 insertion(+), 655 deletions(-) diff --git a/src/utils/form.py b/src/utils/form.py index a48883c38..9907dafb1 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -import glob import os import re import urllib.parse @@ -24,7 +23,6 @@ import tempfile import shutil import ast import operator as op -from collections import OrderedDict from typing import Optional, Dict, List, Union, Any, Mapping, Callable from moulinette.interfaces.cli import colorize @@ -33,18 +31,13 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import ( read_file, write_to_file, - read_toml, - read_yaml, - write_to_yaml, - mkdir, ) from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import OperationLogger -logger = getActionLogger("yunohost.config") -CONFIG_PANEL_VERSION_SUPPORTED = 1.0 +logger = getActionLogger("yunohost.form") # Those js-like evaluate functions are used to eval safely visible attributes @@ -190,653 +183,6 @@ def evaluate_simple_js_expression(expr, context={}): return evaluate_simple_ast(node, context) -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" - - @classmethod - def list(cls): - """ - List available config panel - """ - try: - entities = [ - re.match( - "^" + cls.save_path_tpl.format(entity="(?p)") + "$", f - ).group("entity") - for f in glob.glob(cls.save_path_tpl.format(entity="*")) - if os.path.isfile(f) - ] - except FileNotFoundError: - entities = [] - return entities - - def __init__(self, entity, config_path=None, save_path=None, creation=False): - self.entity = entity - self.config_path = config_path - if not config_path: - self.config_path = self.config_path_tpl.format( - entity=entity, entity_type=self.entity_type - ) - 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 - and self.save_mode != "diff" - and not creation - and not os.path.exists(self.save_path) - ): - raise YunohostValidationError( - f"{self.entity_type}_unknown", **{self.entity_type: entity} - ) - if self.save_path and creation and os.path.exists(self.save_path): - raise YunohostValidationError( - f"{self.entity_type}_exists", **{self.entity_type: entity} - ) - - # Search for hooks in the config panel - self.hooks = { - func: getattr(self, func) - for func in dir(self) - if callable(getattr(self, func)) - and re.match("^(validate|post_ask)__", func) - } - - def get(self, key="", mode="classic"): - self.filter_key = key or "" - - # 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._load_current_values() - self._hydrate() - - # 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) - - option_type = None - for _, _, option_ in self._iterate(): - if option_["id"] == option: - option_type = ARGUMENTS_TYPE_PARSERS[option_["type"]] - break - - return option_type.normalize(value) if option_type else value - - # Format result in 'classic' or 'export' mode - logger.debug(f"Formating result in '{mode}' mode") - result = {} - for panel, section, option in self._iterate(): - if section["is_action_section"] and mode != "full": - continue - - key = f"{panel['id']}.{section['id']}.{option['id']}" - if mode == "export": - result[option["id"]] = option.get("current_value") - 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 == "full": - option["ask"] = ask - question_class = ARGUMENTS_TYPE_PARSERS[option.get("type", "string")] - # FIXME : maybe other properties should be taken from the question, not just choices ?. - option["choices"] = question_class(option).choices - option["default"] = question_class(option).default - option["pattern"] = question_class(option).pattern - else: - result[key] = {"ask": ask} - if "current_value" in option: - question_class = ARGUMENTS_TYPE_PARSERS[ - option.get("type", "string") - ] - result[key]["value"] = question_class.humanize( - option["current_value"], option - ) - # FIXME: semantics, technically here this is not about a prompt... - if question_class.hide_user_input_in_prompt: - result[key][ - "value" - ] = "**************" # Prevent displaying password in `config get` - - if mode == "full": - return self.config - else: - return result - - def list_actions(self): - actions = {} - - # FIXME : meh, loading the entire config panel is again going to cause - # stupid issues for domain (e.g loading registrar stuff when willing to just list available actions ...) - self.filter_key = "" - self._get_config_panel() - for panel, section, option in self._iterate(): - if option["type"] == "button": - key = f"{panel['id']}.{section['id']}.{option['id']}" - actions[key] = _value_for_locale(option["ask"]) - - return actions - - def run_action(self, action=None, args=None, args_file=None, operation_logger=None): - # - # FIXME : this stuff looks a lot like set() ... - # - - self.filter_key = ".".join(action.split(".")[:2]) - action_id = action.split(".")[2] - - # Read config panel toml - self._get_config_panel() - - # FIXME: should also check that there's indeed a key called action - if not self.config: - raise YunohostValidationError(f"No action named {action}", raw_msg=True) - - # Import and parse pre-answered options - logger.debug("Import and parse pre-answered options") - self._parse_pre_answered(args, None, args_file) - - # Read or get values and hydrate the config - self._load_current_values() - self._hydrate() - Question.operation_logger = operation_logger - self._ask(action=action_id) - - # FIXME: here, we could want to check constrains on - # the action's visibility / requirements wrt to the answer to questions ... - - if operation_logger: - operation_logger.start() - - try: - self._run_action(action_id) - except YunohostError: - raise - # Script got manually interrupted ... - # N.B. : KeyboardInterrupt does not inherit from Exception - except (KeyboardInterrupt, EOFError): - error = m18n.n("operation_interrupted") - logger.error(m18n.n("config_action_failed", action=action, error=error)) - raise - # Something wrong happened in Yunohost's code (most probably hook_exec) - except Exception: - import traceback - - error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) - logger.error(m18n.n("config_action_failed", action=action, error=error)) - raise - finally: - # Delete files uploaded from API - # FIXME : this is currently done in the context of config panels, - # but could also happen in the context of app install ... (or anywhere else - # where we may parse args etc...) - FileQuestion.clean_upload_dirs() - - # FIXME: i18n - logger.success(f"Action {action_id} successful") - operation_logger.success() - - def set( - self, key=None, value=None, args=None, args_file=None, operation_logger=None - ): - self.filter_key = key or "" - - # Read config panel toml - self._get_config_panel() - - if not self.config: - raise YunohostValidationError("config_no_panel") - - if (args is not None or args_file is not None) and value is not None: - raise YunohostValidationError( - "You should either provide a value, or a serie of args/args_file, but not both at the same time", - raw_msg=True, - ) - - if self.filter_key.count(".") != 2 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") - self._parse_pre_answered(args, value, args_file) - - # Read or get values and hydrate the config - self._load_current_values() - self._hydrate() - Question.operation_logger = operation_logger - self._ask() - - if operation_logger: - operation_logger.start() - - try: - self._apply() - except YunohostError: - raise - # Script got manually interrupted ... - # N.B. : KeyboardInterrupt does not inherit from Exception - except (KeyboardInterrupt, EOFError): - error = m18n.n("operation_interrupted") - logger.error(m18n.n("config_apply_failed", error=error)) - raise - # Something wrong happened in Yunohost's code (most probably hook_exec) - except Exception: - import traceback - - error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) - logger.error(m18n.n("config_apply_failed", error=error)) - raise - finally: - # Delete files uploaded from API - # FIXME : this is currently done in the context of config panels, - # but could also happen in the context of app install ... (or anywhere else - # where we may parse args etc...) - FileQuestion.clean_upload_dirs() - - self._reload_services() - - logger.success("Config updated as expected") - operation_logger.success() - - def _get_toml(self): - 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, - ) - - if not os.path.exists(self.config_path): - logger.debug(f"Config panel {self.config_path} doesn't exists") - return None - - toml_config_panel = self._get_toml() - - # 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 - - # 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", - # "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") == "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} - ) - return out - - self.config = _build_internal_config_panel(toml_config_panel, "root") - - try: - self.config["panels"][0]["sections"][0]["options"][0] - except (KeyError, IndexError): - raise YunohostValidationError( - "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"] - forbidden_readonly_types = ["password", "app", "domain", "user", "file"] - - for _, _, option in self._iterate(): - if option["id"] in forbidden_keywords: - raise YunohostError("config_forbidden_keyword", keyword=option["id"]) - if ( - option.get("readonly", False) - and option.get("type", "string") in forbidden_readonly_types - ): - raise YunohostError( - "config_forbidden_readonly_type", - type=option["type"], - id=option["id"], - ) - - return self.config - - def _hydrate(self): - # Hydrating config panel with current value - for _, section, option in self._iterate(): - if option["id"] not in self.values: - allowed_empty_types = [ - "alert", - "display_text", - "markdown", - "file", - "button", - ] - - if section["is_action_section"] and option.get("default") is not None: - self.values[option["id"]] = option["default"] - elif ( - option["type"] in allowed_empty_types - or option.get("bind") == "null" - ): - 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["name"]] - - # Allow to use value instead of current_value in app config script. - # e.g. apps may write `echo 'value: "foobar"'` in the config file (which is more intuitive that `echo 'current_value: "foobar"'` - # For example hotspot used it... - # See https://github.com/YunoHost/yunohost/pull/1546 - if ( - isinstance(value, dict) - and "value" in value - and "current_value" not in value - ): - value["current_value"] = value["value"] - - # In general, the value is just a simple value. - # Sometimes it could be a dict used to overwrite the option itself - value = value if isinstance(value, dict) else {"current_value": value} - option.update(value) - - self.values[option["id"]] = value.get("current_value") - - return self.values - - def _ask(self, action=None): - logger.debug("Ask unanswered question and prevalidate data") - - if "i18n" in self.config: - for panel, section, option in self._iterate(): - if "ask" not in option: - option["ask"] = m18n.n(self.config["i18n"] + "_" + option["id"]) - # auto add i18n help text if present in locales - if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"): - option["help"] = m18n.n( - self.config["i18n"] + "_" + option["id"] + "_help" - ) - - def display_header(message): - """CLI panel/section header display""" - if Moulinette.interface.type == "cli" and self.filter_key.count(".") < 2: - Moulinette.display(colorize(message, "purple")) - - 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 - ) - ): - continue - - # Ugly hack to skip action section ... except when when explicitly running actions - if not action: - if section and section["is_action_section"]: - continue - - if panel == obj: - name = _value_for_locale(panel["name"]) - display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}") - else: - name = _value_for_locale(section["name"]) - if name: - display_header(f"\n# {name}") - elif section: - # filter action section options in case of multiple buttons - section["options"] = [ - option - for option in section["options"] - if option.get("type", "string") != "button" - or option["id"] == action - ] - - if panel == obj: - continue - - # Check and ask unanswered questions - prefilled_answers = self.args.copy() - prefilled_answers.update(self.new_values) - - questions = ask_questions_and_parse_answers( - {question["name"]: 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 - for question in questions - if question.value is not None - } - ) - - def _get_default_values(self): - return { - option["id"]: option["default"] - for _, _, option in self._iterate() - if "default" in option - } - - @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 _load_current_values(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 _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)) - 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 - } - - # Save the settings to the .yaml file - write_to_yaml(self.save_path, values_to_save) - - 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 = 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: - 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) - - class Question: hide_user_input_in_prompt = False pattern: Optional[Dict] = None From 478291766e637c6f6c2e3ab50d1fcf7013038575 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 4 Apr 2023 15:51:55 +0200 Subject: [PATCH 305/319] mv config.py to configpanel.py --- src/utils/{config.py => configpanel.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/utils/{config.py => configpanel.py} (100%) diff --git a/src/utils/config.py b/src/utils/configpanel.py similarity index 100% rename from src/utils/config.py rename to src/utils/configpanel.py From b688944d117fc33e044dba00ae6524875c1b0a0e Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 4 Apr 2023 15:54:28 +0200 Subject: [PATCH 306/319] remove form related code from configpanel.py --- src/utils/configpanel.py | 976 +-------------------------------------- 1 file changed, 2 insertions(+), 974 deletions(-) diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index a48883c38..1f1351bcb 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -20,19 +20,13 @@ import glob import os import re import urllib.parse -import tempfile -import shutil -import ast -import operator as op from collections import OrderedDict -from typing import Optional, Dict, List, Union, Any, Mapping, Callable +from typing import Union from moulinette.interfaces.cli import colorize from moulinette import Moulinette, m18n from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import ( - read_file, - write_to_file, read_toml, read_yaml, write_to_yaml, @@ -41,155 +35,11 @@ from moulinette.utils.filesystem import ( from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.log import OperationLogger -logger = getActionLogger("yunohost.config") +logger = getActionLogger("yunohost.configpanel") CONFIG_PANEL_VERSION_SUPPORTED = 1.0 -# Those js-like evaluate functions are used to eval safely visible attributes -# The goal is to evaluate in the same way than js simple-evaluate -# https://github.com/shepherdwind/simple-evaluate -def evaluate_simple_ast(node, context=None): - if context is None: - context = {} - - operators = { - ast.Not: op.not_, - ast.Mult: op.mul, - ast.Div: op.truediv, # number - ast.Mod: op.mod, # number - ast.Add: op.add, # str - ast.Sub: op.sub, # number - ast.USub: op.neg, # Negative number - ast.Gt: op.gt, - ast.Lt: op.lt, - ast.GtE: op.ge, - ast.LtE: op.le, - ast.Eq: op.eq, - ast.NotEq: op.ne, - } - context["true"] = True - context["false"] = False - context["null"] = None - - # Variable - if isinstance(node, ast.Name): # Variable - return context[node.id] - - # Python <=3.7 String - elif isinstance(node, ast.Str): - return node.s - - # Python <=3.7 Number - elif isinstance(node, ast.Num): - return node.n - - # Boolean, None and Python 3.8 for Number, Boolean, String and None - elif isinstance(node, (ast.Constant, ast.NameConstant)): - return node.value - - # + - * / % - elif ( - isinstance(node, ast.BinOp) and type(node.op) in operators - ): # - left = evaluate_simple_ast(node.left, context) - right = evaluate_simple_ast(node.right, context) - if type(node.op) == ast.Add: - if isinstance(left, str) or isinstance(right, str): # support 'I am ' + 42 - left = str(left) - right = str(right) - elif type(left) != type(right): # support "111" - "1" -> 110 - left = float(left) - right = float(right) - - return operators[type(node.op)](left, right) - - # Comparison - # JS and Python don't give the same result for multi operators - # like True == 10 > 2. - elif ( - isinstance(node, ast.Compare) and len(node.comparators) == 1 - ): # - left = evaluate_simple_ast(node.left, context) - right = evaluate_simple_ast(node.comparators[0], context) - operator = node.ops[0] - if isinstance(left, (int, float)) or isinstance(right, (int, float)): - try: - left = float(left) - right = float(right) - except ValueError: - return type(operator) == ast.NotEq - try: - return operators[type(operator)](left, right) - except TypeError: # support "e" > 1 -> False like in JS - return False - - # and / or - elif isinstance(node, ast.BoolOp): # - for value in node.values: - value = evaluate_simple_ast(value, context) - if isinstance(node.op, ast.And) and not value: - return False - elif isinstance(node.op, ast.Or) and value: - return True - return isinstance(node.op, ast.And) - - # not / USub (it's negation number -\d) - elif isinstance(node, ast.UnaryOp): # e.g., -1 - return operators[type(node.op)](evaluate_simple_ast(node.operand, context)) - - # match function call - elif isinstance(node, ast.Call) and node.func.__dict__.get("id") == "match": - return re.match( - evaluate_simple_ast(node.args[1], context), context[node.args[0].id] - ) - - # Unauthorized opcode - else: - opcode = str(type(node)) - raise YunohostError( - f"Unauthorize opcode '{opcode}' in visible attribute", raw_msg=True - ) - - -def js_to_python(expr): - in_string = None - py_expr = "" - i = 0 - escaped = False - for char in expr: - if char in r"\"'": - # Start a string - if not in_string: - in_string = char - - # Finish a string - elif in_string == char and not escaped: - in_string = None - - # If we are not in a string, replace operators - elif not in_string: - if char == "!" and expr[i + 1] != "=": - char = "not " - elif char in "|&" and py_expr[-1:] == char: - py_expr = py_expr[:-1] - char = " and " if char == "&" else " or " - - # Determine if next loop will be in escaped mode - escaped = char == "\\" and not escaped - py_expr += char - i += 1 - return py_expr - - -def evaluate_simple_js_expression(expr, context={}): - if not expr.strip(): - return False - node = ast.parse(js_to_python(expr), mode="eval").body - return evaluate_simple_ast(node, context) - - class ConfigPanel: entity_type = "config" save_path_tpl: Union[str, None] = None @@ -835,825 +685,3 @@ class ConfigPanel: if "option" in trigger: for option in section.get("options", []): yield (panel, section, option) - - -class Question: - hide_user_input_in_prompt = False - pattern: Optional[Dict] = None - - 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.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} - 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 - self.value = question.get("value") - # 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) - - @staticmethod - def normalize(value, option={}): - if isinstance(value, str): - value = value.strip() - return value - - 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), - ) - - 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 - - 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._prevalidate() - 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._post_parse_value() - - # 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 _prevalidate(self): - if self.value in [None, ""] and not self.optional: - raise YunohostValidationError("app_argument_required", name=self.name) - - # 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"], - name=self.name, - value=self.value, - ) - - def _format_text_for_user_input_in_cli(self): - text_for_user_input_in_cli = _value_for_locale(self.ask) - - if self.readonly: - text_for_user_input_in_cli = colorize(text_for_user_input_in_cli, "purple") - if self.choices: - return ( - text_for_user_input_in_cli + f" {self.choices[self.current_value]}" - ) - return text_for_user_input_in_cli + f" {self.humanize(self.current_value)}" - elif self.choices: - # Prevent displaying a shitload of choices - # (e.g. 100+ available users when choosing an app admin...) - 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(choices_to_display) - - text_for_user_input_in_cli += f" [{choices_to_display}]" - - return text_for_user_input_in_cli - - def _post_parse_value(self): - if not self.redact: - return self.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) - data_to_redact += [ - urllib.parse.quote(data) - for data in data_to_redact - if urllib.parse.quote(data) != data - ] - - for operation_logger in OperationLogger._instances: - operation_logger.data_to_redact.extend(data_to_redact) - - return self.value - - -class StringQuestion(Question): - argument_type = "string" - default_value = "" - - -class EmailQuestion(StringQuestion): - pattern = { - "regexp": r"^.+@.+", - "error": "config_validate_email", # i18n: config_validate_email - } - - -class URLQuestion(StringQuestion): - pattern = { - "regexp": r"^https?://.*$", - "error": "config_validate_url", # i18n: config_validate_url - } - - -class DateQuestion(StringQuestion): - pattern = { - "regexp": r"^\d{4}-\d\d-\d\d$", - "error": "config_validate_date", # i18n: config_validate_date - } - - def _prevalidate(self): - from datetime import datetime - - super()._prevalidate() - - if self.value not in [None, ""]: - try: - datetime.strptime(self.value, "%Y-%m-%d") - except ValueError: - raise YunohostValidationError("config_validate_date") - - -class TimeQuestion(StringQuestion): - pattern = { - "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", - "error": "config_validate_time", # i18n: config_validate_time - } - - -class ColorQuestion(StringQuestion): - pattern = { - "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", - "error": "config_validate_color", # i18n: config_validate_color - } - - -class TagsQuestion(Question): - argument_type = "tags" - default_value = "" - - @staticmethod - def humanize(value, option={}): - if isinstance(value, list): - return ",".join(str(v) for v in value) - return value - - @staticmethod - def normalize(value, option={}): - if isinstance(value, list): - return ",".join(str(v) for v in value) - if isinstance(value, str): - value = value.strip() - return value - - def _prevalidate(self): - values = self.value - if isinstance(values, str): - values = values.split(",") - elif values is None: - values = [] - - if not isinstance(values, list): - if self.choices: - raise YunohostValidationError( - "app_argument_choice_invalid", - name=self.name, - value=self.value, - choices=", ".join(str(choice) for choice in self.choices), - ) - raise YunohostValidationError( - "app_argument_invalid", - name=self.name, - error=f"'{str(self.value)}' is not a list", - ) - - for value in values: - self.value = value - super()._prevalidate() - self.value = values - - def _post_parse_value(self): - if isinstance(self.value, list): - self.value = ",".join(self.value) - return super()._post_parse_value() - - -class PasswordQuestion(Question): - hide_user_input_in_prompt = True - argument_type = "password" - default_value = "" - forbidden_chars = "{}" - - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) - self.redact = True - if self.default is not None: - raise YunohostValidationError( - "app_argument_password_no_default", name=self.name - ) - - def _prevalidate(self): - super()._prevalidate() - - if self.value not in [None, ""]: - if any(char in self.value for char in self.forbidden_chars): - raise YunohostValidationError( - "pattern_password_app", forbidden_chars=self.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) - - -class PathQuestion(Question): - argument_type = "path" - default_value = "" - - @staticmethod - def normalize(value, option={}): - option = option.__dict__ if isinstance(option, Question) else option - - if not isinstance(value, str): - raise YunohostValidationError( - "app_argument_invalid", - name=option.get("name"), - error="Argument for path should be a string.", - ) - - if not value.strip(): - if option.get("optional"): - return "" - # Hmpf here we could just have a "else" case - # but we also want PathQuestion.normalize("") to return "/" - # (i.e. if no option is provided, hence .get("optional") is None - elif option.get("optional") is False: - raise YunohostValidationError( - "app_argument_invalid", - name=option.get("name"), - error="Question is mandatory", - ) - - return "/" + value.strip().strip(" /") - - -class BooleanQuestion(Question): - argument_type = "boolean" - default_value = 0 - yes_answers = ["1", "yes", "y", "true", "t", "on"] - no_answers = ["0", "no", "n", "false", "f", "off"] - - @staticmethod - def humanize(value, option={}): - option = option.__dict__ if isinstance(option, Question) else option - - yes = option.get("yes", 1) - no = option.get("no", 0) - - value = BooleanQuestion.normalize(value, option) - - if value == yes: - return "yes" - if value == no: - return "no" - if value is None: - return "" - - raise YunohostValidationError( - "app_argument_choice_invalid", - name=option.get("name"), - value=value, - choices="yes/no", - ) - - @staticmethod - def normalize(value, option={}): - option = option.__dict__ if isinstance(option, Question) else option - - if isinstance(value, str): - value = value.strip() - - technical_yes = option.get("yes", 1) - technical_no = option.get("no", 0) - - no_answers = BooleanQuestion.no_answers - yes_answers = BooleanQuestion.yes_answers - - assert ( - str(technical_yes).lower() not in no_answers - ), f"'yes' value can't be in {no_answers}" - assert ( - 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()] - - strvalue = str(value).lower() - - if strvalue in yes_answers: - return technical_yes - if strvalue in no_answers: - return technical_no - - if strvalue in ["none", ""]: - return None - - raise YunohostValidationError( - "app_argument_choice_invalid", - name=option.get("name"), - value=strvalue, - choices="yes/no", - ) - - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) - self.yes = question.get("yes", 1) - self.no = question.get("no", 0) - if self.default is None: - self.default = self.no - - def _format_text_for_user_input_in_cli(self): - text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() - - if not self.readonly: - text_for_user_input_in_cli += " [yes | no]" - - return text_for_user_input_in_cli - - def get(self, key, default=None): - return getattr(self, key, default) - - -class DomainQuestion(Question): - argument_type = "domain" - - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - from yunohost.domain import domain_list, _get_maindomain - - super().__init__(question, context, hooks) - - 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"] - } - - @staticmethod - def normalize(value, option={}): - if value.startswith("https://"): - value = value[len("https://") :] - elif value.startswith("http://"): - value = value[len("http://") :] - - # Remove trailing slashes - value = value.rstrip("/").lower() - - return value - - -class AppQuestion(Question): - argument_type = "app" - - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - from yunohost.app import app_list - - super().__init__(question, context, hooks) - - apps = app_list(full=True)["apps"] - - if self.filter: - apps = [ - app - for app in apps - if evaluate_simple_js_expression(self.filter, context=app) - ] - - def _app_display(app): - domain_path_or_id = f" ({app.get('domain_path', app['id'])})" - return app["label"] + domain_path_or_id - - self.choices = {"_none": "---"} - self.choices.update({app["id"]: _app_display(app) for app in apps}) - - -class UserQuestion(Question): - argument_type = "user" - - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - from yunohost.user import user_list, user_info - from yunohost.domain import _get_maindomain - - super().__init__(question, context, hooks) - - self.choices = { - username: f"{infos['fullname']} ({infos['mail']})" - for username, infos in user_list()["users"].items() - } - - if not self.choices: - raise YunohostValidationError( - "app_argument_invalid", - name=self.name, - error="You should create a YunoHost user first.", - ) - - if self.default is None: - # FIXME: this code is obsolete with the new admins group - # Should be replaced by something like "any first user we find in the admin group" - root_mail = "root@%s" % _get_maindomain() - for user in self.choices.keys(): - if root_mail in user_info(user).get("mail-aliases", []): - self.default = user - break - - -class GroupQuestion(Question): - argument_type = "group" - - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - from yunohost.user import user_group_list - - super().__init__(question, context) - - self.choices = list( - user_group_list(short=True, include_primary_groups=False)["groups"] - ) - - def _human_readable_group(g): - # i18n: visitors - # i18n: all_users - # i18n: admins - return m18n.n(g) if g in ["visitors", "all_users", "admins"] else g - - self.choices = {g: _human_readable_group(g) for g in self.choices} - - if self.default is None: - self.default = "all_users" - - -class NumberQuestion(Question): - argument_type = "number" - default_value = None - - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) - self.min = question.get("min", None) - self.max = question.get("max", None) - self.step = question.get("step", None) - - @staticmethod - def normalize(value, option={}): - if isinstance(value, int): - return value - - if isinstance(value, str): - value = value.strip() - - if isinstance(value, str) and value.isdigit(): - return int(value) - - if value in [None, ""]: - return None - - option = option.__dict__ if isinstance(option, Question) else option - raise YunohostValidationError( - "app_argument_invalid", - name=option.get("name"), - error=m18n.n("invalid_number"), - ) - - def _prevalidate(self): - super()._prevalidate() - if self.value in [None, ""]: - return - - if self.min is not None and int(self.value) < self.min: - raise YunohostValidationError( - "app_argument_invalid", - name=self.name, - 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, - error=m18n.n("invalid_number_max", max=self.max), - ) - - -class DisplayTextQuestion(Question): - argument_type = "display_text" - - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, 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 FileQuestion(Question): - argument_type = "file" - upload_dirs: List[str] = [] - - @classmethod - def clean_upload_dirs(cls): - # Delete files uploaded from API - for upload_dir in cls.upload_dirs: - if os.path.exists(upload_dir): - shutil.rmtree(upload_dir) - - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) - self.accept = question.get("accept", "") - - def _prevalidate(self): - if self.value is None: - self.value = self.current_value - - super()._prevalidate() - - # 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.name, - error=m18n.n("file_does_not_exist", path=str(self.value)), - ) - - def _post_parse_value(self): - from base64 import b64decode - - if not self.value: - return "" - - upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_") - _, file_path = tempfile.mkstemp(dir=upload_dir) - - FileQuestion.upload_dirs += [upload_dir] - - logger.debug(f"Saving file {self.name} 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) - - write_to_file(file_path, content, file_mode="wb") - - self.value = file_path - - return self.value - - -class ButtonQuestion(Question): - argument_type = "button" - enabled = None - - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) - self.enabled = question.get("enabled", None) - - -ARGUMENTS_TYPE_PARSERS = { - "string": StringQuestion, - "text": StringQuestion, - "select": StringQuestion, - "tags": TagsQuestion, - "email": EmailQuestion, - "url": URLQuestion, - "date": DateQuestion, - "time": TimeQuestion, - "color": ColorQuestion, - "password": PasswordQuestion, - "path": PathQuestion, - "boolean": BooleanQuestion, - "domain": DomainQuestion, - "user": UserQuestion, - "group": GroupQuestion, - "number": NumberQuestion, - "range": NumberQuestion, - "display_text": DisplayTextQuestion, - "alert": DisplayTextQuestion, - "markdown": DisplayTextQuestion, - "file": FileQuestion, - "app": AppQuestion, - "button": ButtonQuestion, -} - - -def ask_questions_and_parse_answers( - raw_questions: Dict, - prefilled_answers: Union[str, Mapping[str, Any]] = {}, - current_values: Mapping[str, Any] = {}, - hooks: Dict[str, Callable[[], None]] = {}, -) -> List[Question]: - """Parse arguments store in either manifest.json or actions.json or from a - config panel against the user answers when they are present. - - Keyword arguments: - raw_questions -- the arguments description store in yunohost - format from actions.json/toml, manifest.json/toml - or config_panel.json/toml - prefilled_answers -- a url "query-string" such as "domain=yolo.test&path=/foobar&admin=sam" - or a dict such as {"domain": "yolo.test", "path": "/foobar", "admin": "sam"} - """ - - if isinstance(prefilled_answers, str): - # FIXME FIXME : this is not uniform with config_set() which uses parse.qs (no l) - # parse_qsl parse single values - # 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) - ) - elif isinstance(prefilled_answers, Mapping): - answers = {**prefilled_answers} - else: - answers = {} - - context = {**current_values, **answers} - out = [] - - for name, raw_question in raw_questions.items(): - raw_question["name"] = name - question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")] - raw_question["value"] = answers.get(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 - - -def hydrate_questions_with_choices(raw_questions: List) -> List: - out = [] - - for raw_question in raw_questions: - question = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]( - raw_question - ) - if question.choices: - raw_question["choices"] = question.choices - raw_question["default"] = question.default - out.append(raw_question) - - return out From 8c25aa9b9faaf190792738277f71d74822f11088 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Apr 2023 15:55:55 +0200 Subject: [PATCH 307/319] helpers: fix previous change about using YNH_APP_ACTION ... which is not defined in config panel context --- helpers/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/utils b/helpers/utils index d27b5bca2..a88be38a8 100644 --- a/helpers/utils +++ b/helpers/utils @@ -22,7 +22,7 @@ YNH_APP_BASEDIR=${YNH_APP_BASEDIR:-$(realpath ..)} ynh_exit_properly() { local exit_code=$? - if [[ "${YNH_APP_ACTION}" =~ ^install$|^upgrade$|^restore$ ]] + if [[ "${YNH_APP_ACTION:-}" =~ ^install$|^upgrade$|^restore$ ]] then rm -rf "/var/cache/yunohost/download/" fi From bee218e560374569cd032a4234adcf88b0242f16 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 4 Apr 2023 16:05:36 +0200 Subject: [PATCH 308/319] fix configpanel.py and form.py imports --- src/app.py | 5 ++--- src/domain.py | 3 ++- src/settings.py | 3 ++- src/tests/test_questions.py | 2 +- src/utils/configpanel.py | 7 +++++++ 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/app.py b/src/app.py index b37b680ec..1daa14d98 100644 --- a/src/app.py +++ b/src/app.py @@ -48,9 +48,8 @@ from moulinette.utils.filesystem import ( chmod, ) -from yunohost.utils.config import ( - ConfigPanel, - ask_questions_and_parse_answers, +from yunohost.utils.configpanel import ConfigPanel, ask_questions_and_parse_answers +from yunohost.utils.form import ( DomainQuestion, PathQuestion, hydrate_questions_with_choices, diff --git a/src/domain.py b/src/domain.py index 7839b988d..9f38d6765 100644 --- a/src/domain.py +++ b/src/domain.py @@ -33,7 +33,8 @@ from yunohost.app import ( _get_conflicting_apps, ) from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf -from yunohost.utils.config import ConfigPanel, Question +from yunohost.utils.configpanel import ConfigPanel +from yunohost.utils.form import Question from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation diff --git a/src/settings.py b/src/settings.py index 4905049d6..5d52329b3 100644 --- a/src/settings.py +++ b/src/settings.py @@ -21,7 +21,8 @@ import subprocess from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.utils.config import ConfigPanel, Question +from yunohost.utils.configpanel import ConfigPanel +from yunohost.utils.form import Question from moulinette.utils.log import getActionLogger from yunohost.regenconf import regen_conf from yunohost.firewall import firewall_reload diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 8ded2e137..506fde077 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -14,7 +14,7 @@ from _pytest.mark.structures import ParameterSet from moulinette import Moulinette from yunohost import app, domain, user -from yunohost.utils.config import ( +from yunohost.utils.form import ( ARGUMENTS_TYPE_PARSERS, ask_questions_and_parse_answers, DisplayTextQuestion, diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 1f1351bcb..e50d0a3ec 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -35,6 +35,13 @@ from moulinette.utils.filesystem import ( from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.form import ( + ARGUMENTS_TYPE_PARSERS, + FileQuestion, + Question, + ask_questions_and_parse_answers, + evaluate_simple_js_expression, +) logger = getActionLogger("yunohost.configpanel") CONFIG_PANEL_VERSION_SUPPORTED = 1.0 From 9a4267ffa41d53ebd7e137108b4e4a38e863faa1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 5 Apr 2023 15:58:07 +0200 Subject: [PATCH 309/319] appsv2: for the dir/subdirs of data_dir, create parent folders if they don't exist --- 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 8f8393e17..bd50cca04 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -955,12 +955,12 @@ class DatadirAppResource(AppResource): ) shutil.move(current_data_dir, self.dir) else: - mkdir(self.dir) + mkdir(self.dir, parents=True) for subdir in self.subdirs: full_path = os.path.join(self.dir, subdir) if not os.path.isdir(full_path): - mkdir(full_path) + mkdir(full_path, parents=True) owner, owner_perm = self.owner.split(":") group, group_perm = self.group.split(":") From 021099aa1e62badb5d5c573a8e521f8d24f9f847 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 5 Apr 2023 16:02:02 +0200 Subject: [PATCH 310/319] Update changelog for 11.1.17 --- debian/changelog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/debian/changelog b/debian/changelog index 3c0cccbc2..9b61a7b45 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +yunohost (11.1.17) stable; urgency=low + + - domains: fix autodns for gandi root domain ([#1634](https://github.com/yunohost/yunohost/pull/1634)) + - helpers: fix previous change about using YNH_APP_ACTION ... which is not defined in config panel context (8c25aa9b) + - appsv2: for the dir/subdirs of data_dir, create parent folders if they don't exist (9a4267ff) + - quality: Split utils/config.py ([#1635](https://github.com/yunohost/yunohost/pull/1635)) + - quality: Rework questions/options tests ([#1629](https://github.com/yunohost/yunohost/pull/1629)) + + Thanks to all contributors <3 ! (axolotle, Kayou) + + -- Alexandre Aubin Wed, 05 Apr 2023 16:00:09 +0200 + yunohost (11.1.16) stable; urgency=low - apps: fix i18n panel+section names ([#1630](https://github.com/yunohost/yunohost/pull/1630)) From 58cd08e60d6d9cf702fc3967bd7575579734a8a6 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Wed, 5 Apr 2023 15:32:22 +0000 Subject: [PATCH 311/319] [CI] Format code with Black --- src/utils/form.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/form.py b/src/utils/form.py index 9907dafb1..31b3d5b87 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -858,7 +858,9 @@ class FileQuestion(Question): return self.value if Moulinette.interface.type != "api": - if not os.path.exists(str(self.value)) or not os.path.isfile(str(self.value)): + if not os.path.exists(str(self.value)) or not os.path.isfile( + str(self.value) + ): raise YunohostValidationError( "app_argument_invalid", name=self.name, From 88ea5f0a902ff220d17c523703b044d5b8936db8 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Thu, 6 Apr 2023 20:11:17 +0000 Subject: [PATCH 312/319] Add support for Porkbun through Lexicon --- share/registrar_list.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/share/registrar_list.toml b/share/registrar_list.toml index 01906becd..3f478a03f 100644 --- a/share/registrar_list.toml +++ b/share/registrar_list.toml @@ -501,6 +501,15 @@ [pointhq.auth_token] type = "string" redact = true + +[porkbun] + [porkbun.auth_key] + type = "string" + redact = true + + [porkbun.auth_secret] + type = "string" + redact = true [powerdns] [powerdns.auth_token] From a66fccbd5bcccbd800eb21ac7041647309b172eb Mon Sep 17 00:00:00 2001 From: tituspijean Date: Thu, 6 Apr 2023 23:21:57 +0200 Subject: [PATCH 313/319] Support variables in permissions declaration --- src/utils/resources.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index bd50cca04..c3c4f6555 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -17,6 +17,7 @@ # along with this program. If not, see . # import os +import re import copy import shutil import random @@ -562,14 +563,16 @@ class PermissionsResource(AppResource): super().__init__({"permissions": properties}, *args, **kwargs) for perm, infos in self.permissions.items(): - if infos.get("url") and "__DOMAIN__" in infos.get("url", ""): - infos["url"] = infos["url"].replace( - "__DOMAIN__", self.get_setting("domain") - ) - infos["additional_urls"] = [ - u.replace("__DOMAIN__", self.get_setting("domain")) - for u in infos.get("additional_urls", []) - ] + if infos.get("url"): + for variable in re.findall(r"(__[A-Z0-9_]+__)", infos.get("url", "")): + infos["url"] = infos["url"].replace( + variable, self.get_setting(variable.lower().replace("__","")) + ) + for i in range(0, len(infos.get("additional_urls", []))): + for variable in re.findall(r"(__[A-Z0-9_]+__)", infos.get("additional_urls", [])[i])): + infos["additional_urls"][i] = infos["additional_urls"][i].replace( + variable, self.get_setting(variable.lower().replace("__","")) + ) def provision_or_update(self, context: Dict = {}): from yunohost.permission import ( From fa26574b512ee289befdaac66d66cc1cbd973e22 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Thu, 6 Apr 2023 23:32:46 +0200 Subject: [PATCH 314/319] Ooops --- 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 c3c4f6555..82c61de8a 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -569,7 +569,7 @@ class PermissionsResource(AppResource): variable, self.get_setting(variable.lower().replace("__","")) ) for i in range(0, len(infos.get("additional_urls", []))): - for variable in re.findall(r"(__[A-Z0-9_]+__)", infos.get("additional_urls", [])[i])): + for variable in re.findall(r"(__[A-Z0-9_]+__)", infos.get("additional_urls", [])[i]): infos["additional_urls"][i] = infos["additional_urls"][i].replace( variable, self.get_setting(variable.lower().replace("__","")) ) From 1cc89246696bfdf1ff53d73e64ccb63e39a5644c Mon Sep 17 00:00:00 2001 From: xabirequejo Date: Sat, 8 Apr 2023 18:23:30 +0000 Subject: [PATCH 315/319] Translated using Weblate (Basque) Currently translated at 97.1% (742 of 764 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/eu/ --- locales/eu.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/locales/eu.json b/locales/eu.json index 675449fd3..4d425789e 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -752,5 +752,8 @@ "global_settings_setting_dns_exposure": "DNS ezarpenetan eta diagnostikoan kontuan hartzeko IP bertsioak", "global_settings_setting_dns_exposure_help": "Ohart ongi: honek gomendatutako DNS ezarpenei eta diagnostikoari eragiten die soilik. Ez du eraginik sistemaren ezarpenetan.", "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)" -} \ No newline at end of file + "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_script_failed": "Errorea gertatu da URLa aldatzeko aginduaren barnean" +} From 57be2082381ca190a7fc509298b57438644c0e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Mon, 10 Apr 2023 06:46:35 +0000 Subject: [PATCH 316/319] Translated using Weblate (Galician) Currently translated at 99.8% (763 of 764 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/gl.json b/locales/gl.json index 065e41686..3dc6d26ad 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -760,5 +760,6 @@ "apps_failed_to_upgrade": "Fallou a actualización das seguintes aplicacións:{apps}", "invalid_shell": "Intérprete de ordes non válido: {shell}", "log_resource_snippet": "Aprovisionamento/desaprovisionamento/actualización dun recurso", - "app_resource_failed": "Fallou o aprovisionamento, desaprovisionamento ou actualización de recursos para {app}: {error}" -} \ No newline at end of file + "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}" +} From aa43e6c22b9d3edf396890675b46cf934a591b64 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Apr 2023 14:34:10 +0200 Subject: [PATCH 317/319] appsv2: fix edge-case when validating packager-provided infos for permissions resource --- src/utils/resources.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index bd50cca04..1c6a34e54 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -516,9 +516,7 @@ class PermissionsResource(AppResource): def __init__(self, properties: Dict[str, Any], *args, **kwargs): # FIXME : if url != None, we should check that there's indeed a domain/path defined ? ie that app is a webapp - if "main" not in properties: - properties["main"] = self.default_perm_properties - + # Validate packager-provided infos for perm, infos in properties.items(): if "auth_header" in infos and not isinstance( infos.get("auth_header"), bool @@ -545,6 +543,10 @@ class PermissionsResource(AppResource): raw_msg=True, ) + if "main" not in properties: + properties["main"] = copy.copy(self.default_perm_properties) + + for perm, infos in properties.items(): properties[perm] = copy.copy(self.default_perm_properties) properties[perm].update(infos) if properties[perm]["show_tile"] is None: From 8ca756dbd362e2c36e0d8df4fc5ba694e5ed917b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 13:57:50 +0200 Subject: [PATCH 318/319] appsv2: simplify code to hydrate url/additional_urls with app settings --- src/utils/resources.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/utils/resources.py b/src/utils/resources.py index 82c61de8a..876fe46a4 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -17,7 +17,6 @@ # along with this program. If not, see . # import os -import re import copy import shutil import random @@ -562,17 +561,15 @@ class PermissionsResource(AppResource): super().__init__({"permissions": properties}, *args, **kwargs) + from yunohost.app import _get_app_settings, _hydrate_app_template + + settings = _get_app_settings(self.app) for perm, infos in self.permissions.items(): - if infos.get("url"): - for variable in re.findall(r"(__[A-Z0-9_]+__)", infos.get("url", "")): - infos["url"] = infos["url"].replace( - variable, self.get_setting(variable.lower().replace("__","")) - ) - for i in range(0, len(infos.get("additional_urls", []))): - for variable in re.findall(r"(__[A-Z0-9_]+__)", infos.get("additional_urls", [])[i]): - infos["additional_urls"][i] = infos["additional_urls"][i].replace( - variable, self.get_setting(variable.lower().replace("__","")) - ) + if infos.get("url") and "__" in infos.get("url"): + infos["url"] = _hydrate_app_template(infos["url"], settings) + + if infos.get("additional_urls"): + infos["additional_urls"] = [_hydrate_app_template(url) for url in infos["additional_urls"]] def provision_or_update(self, context: Dict = {}): from yunohost.permission import ( From e2ea7ad7a00d25e4f7a4ec89e18d1b08e72ea8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Mon, 10 Apr 2023 12:29:32 +0000 Subject: [PATCH 319/319] Translated using Weblate (Galician) Currently translated at 100.0% (764 of 764 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/gl.json b/locales/gl.json index 3dc6d26ad..c5e5c68c0 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -761,5 +761,6 @@ "invalid_shell": "Intérprete de ordes non válido: {shell}", "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_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}" }