From 2d158b5a6d20275bee9d76f0a8611e942b235f35 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Sep 2021 20:21:05 +0200 Subject: [PATCH 001/142] Rework prompt() again --- locales/en.json | 2 +- src/yunohost/user.py | 2 +- src/yunohost/utils/config.py | 76 ++++++++++++++++-------------------- 3 files changed, 35 insertions(+), 45 deletions(-) diff --git a/locales/en.json b/locales/en.json index 4601bd92f..447f137a5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -16,7 +16,7 @@ "app_argument_choice_invalid": "Use one of these choices '{choices}' for the argument '{name}' instead of '{value}'", "app_argument_invalid": "Pick a valid value for the argument '{name}': {error}", "app_argument_password_help_keep": "Press Enter to keep the current value", - "app_argument_password_help_optional": "Type one space to empty the password", + "app_argument_password_help_optional": "Enter a single space to empty the password", "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reason", "app_argument_required": "Argument '{name}' is required", "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain}{path}'), nothing to do.", diff --git a/src/yunohost/user.py b/src/yunohost/user.py index b32e03dfa..22168d3e7 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -420,7 +420,7 @@ def user_update( # without a specified value, change_password will be set to the const 0. # In this case we prompt for the new password. if Moulinette.interface.type == "cli" and not change_password: - change_password = Moulinette.prompt(m18n.n("ask_password"), True, True) + change_password = Moulinette.prompt(m18n.n("ask_password"), is_password=True, confirm=True) # Ensure sufficiently complex password assert_password_is_strong_enough("user", change_password) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 99c898d15..7b3d44b57 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -99,6 +99,9 @@ class ConfigPanel: 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 @@ -480,6 +483,8 @@ class Question(object): @staticmethod def normalize(value, option={}): + if isinstance(value, str): + value = value.strip() return value def _prompt(self, text): @@ -491,9 +496,11 @@ class Question(object): self.value = Moulinette.prompt( message=text, is_password=self.hide_user_input_in_prompt, - confirm=False, # We doesn't want to confirm this kind of password like in webadmin + confirm=False, prefill=prefill, is_multiline=(self.type == "text"), + autocomplete=self.choices, + help=_value_for_locale(self.help) ) def ask_if_needed(self): @@ -558,18 +565,8 @@ class Question(object): choices=", ".join(self.choices), ) - def _format_text_for_user_input_in_cli(self, column=False): - text_for_user_input_in_cli = _value_for_locale(self.ask) - - if self.choices: - text_for_user_input_in_cli += " [{0}]".format(" | ".join(self.choices)) - - if self.help or column: - text_for_user_input_in_cli += ":\033[m" - if self.help: - text_for_user_input_in_cli += "\n - " - text_for_user_input_in_cli += _value_for_locale(self.help) - return text_for_user_input_in_cli + def _format_text_for_user_input_in_cli(self): + return _value_for_locale(self.ask) def _post_parse_value(self): if not self.redact: @@ -659,6 +656,8 @@ class TagsQuestion(Question): def normalize(value, option={}): if isinstance(value, list): return ",".join(value) + if isinstance(value, str): + value = value.strip() return value def _prevalidate(self): @@ -692,12 +691,6 @@ class PasswordQuestion(Question): "app_argument_password_no_default", name=self.name ) - @staticmethod - def humanize(value, option={}): - if value: - return "********" # Avoid to display the password on screen - return "" - def _prevalidate(self): super()._prevalidate() @@ -712,29 +705,6 @@ class PasswordQuestion(Question): assert_password_is_strong_enough("user", self.value) - def _format_text_for_user_input_in_cli(self): - need_column = self.current_value or self.optional - text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli( - need_column - ) - if self.current_value: - text_for_user_input_in_cli += "\n - " + m18n.n( - "app_argument_password_help_keep" - ) - if self.optional: - text_for_user_input_in_cli += "\n - " + m18n.n( - "app_argument_password_help_optional" - ) - - return text_for_user_input_in_cli - - def _prompt(self, text): - super()._prompt(text) - if self.current_value and self.value == "": - self.value = self.current_value - elif self.value == " ": - self.value = "" - class PathQuestion(Question): argument_type = "path" @@ -769,14 +739,30 @@ class BooleanQuestion(Question): "app_argument_choice_invalid", name=option.get("name", ""), value=value, + # FIXME : this doesn't match yes_answers / no_answers... choices="yes, no, y, n, 1, 0", ) @staticmethod def normalize(value, option={}): + if isinstance(value, str): + value = value.strip() + yes = option.get("yes", 1) no = option.get("no", 0) + # + # FIXME: Shouldn't we also check that value == yes ? + # Otherwise is yes = "foobar", normalize is not idempotent + # i.e normalize("true") will return "foobar" + # but normalize(normalize("true")) will raise an exception ? + # + # FIXME: it's also a bit confusing to understand if the + # packager-provided 'yes' value is meant for humans (in which case + # shouldn't it be used as a return value for humanize?) + # or as an internal value for better interfacing with scripts/computers + # + # Also shouldnt we be using normalize() in humanize() ? if str(value).lower() in BooleanQuestion.yes_answers: return yes @@ -881,14 +867,18 @@ class NumberQuestion(Question): 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 value + # FIXME: option.name may not exist if option={}... raise YunohostValidationError( - "app_argument_invalid", name=option.name, error=m18n.n("invalid_number") + "app_argument_invalid", name=option.get("name", ""), error=m18n.n("invalid_number") ) def _prevalidate(self): From d1c371ec380f216b34afda2f054a4d70c1d0cb4f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 00:12:24 +0200 Subject: [PATCH 002/142] cli questions: Restore displaying available choices, though limit to the first 20 available options --- locales/en.json | 1 + src/yunohost/utils/config.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 447f137a5..4572e6f86 100644 --- a/locales/en.json +++ b/locales/en.json @@ -541,6 +541,7 @@ "migrations_to_be_ran_manually": "Migration {id} has to be run manually. Please go to Tools → Migrations on the webadmin page, or run `yunohost tools migrations run`.", "not_enough_disk_space": "Not enough free space on '{path}'", "operation_interrupted": "The operation was manually interrupted?", + "other_available_options": "... and {n} other available options not shown", "packages_upgrade_failed": "Could not upgrade all the packages", "password_listed": "This password is among the most used passwords in the world. Please choose something more unique.", "password_too_simple_1": "The password needs to be at least 8 characters long", diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 7b3d44b57..2b83507f0 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -566,7 +566,22 @@ class Question(object): ) def _format_text_for_user_input_in_cli(self): - return _value_for_locale(self.ask) + + text_for_user_input_in_cli = _value_for_locale(self.ask) + + if self.choices: + + # Prevent displaying a shitload of choices + # (e.g. 100+ available users when choosing an app admin...) + choices_to_display = self.choices[:20] + remaining_choices = len(self.choices[20:]) + + if remaining_choices > 0: + choices_to_display += [m18n.n("other_available_options", n=remaining_choices)] + + text_for_user_input_in_cli += " [{0}]".format(" | ".join(choices_to_display)) + + return text_for_user_input_in_cli def _post_parse_value(self): if not self.redact: From 92f9e15e2fd788bf4fcb0409603e834a70d9a14d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 00:13:18 +0200 Subject: [PATCH 003/142] Stale i18n strings --- locales/en.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 4572e6f86..3354dc19c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -15,8 +15,6 @@ "app_already_up_to_date": "{app} is already up-to-date", "app_argument_choice_invalid": "Use one of these choices '{choices}' for the argument '{name}' instead of '{value}'", "app_argument_invalid": "Pick a valid value for the argument '{name}': {error}", - "app_argument_password_help_keep": "Press Enter to keep the current value", - "app_argument_password_help_optional": "Enter a single space to empty the password", "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reason", "app_argument_required": "Argument '{name}' is required", "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain}{path}'), nothing to do.", From 8ea160b9fe0a17df7f8bc69931db9e1690cf71da Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 00:29:22 +0200 Subject: [PATCH 004/142] Fix tests --- src/yunohost/tests/test_questions.py | 18 ++++++++++++++++++ src/yunohost/utils/config.py | 27 ++++++++++++++------------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index 9753b08e4..62bb61cc2 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -207,6 +207,8 @@ def test_question_string_input_test_ask(): confirm=False, prefill="", is_multiline=False, + autocomplete=[], + help=None, ) @@ -232,6 +234,8 @@ def test_question_string_input_test_ask_with_default(): confirm=False, prefill=default_text, is_multiline=False, + autocomplete=[], + help=None, ) @@ -526,6 +530,8 @@ def test_question_password_input_test_ask(): confirm=False, prefill="", is_multiline=False, + autocomplete=[], + help=None, ) @@ -787,6 +793,8 @@ def test_question_path_input_test_ask(): confirm=False, prefill="", is_multiline=False, + autocomplete=[], + help=None, ) @@ -813,6 +821,8 @@ def test_question_path_input_test_ask_with_default(): confirm=False, prefill=default_text, is_multiline=False, + autocomplete=[], + help=None, ) @@ -1162,6 +1172,8 @@ def test_question_boolean_input_test_ask(): confirm=False, prefill="no", is_multiline=False, + autocomplete=[], + help=None, ) @@ -1188,6 +1200,8 @@ def test_question_boolean_input_test_ask_with_default(): confirm=False, prefill="yes", is_multiline=False, + autocomplete=[], + help=None, ) @@ -1735,6 +1749,8 @@ def test_question_number_input_test_ask(): confirm=False, prefill="", is_multiline=False, + autocomplete=[], + help=None, ) @@ -1761,6 +1777,8 @@ def test_question_number_input_test_ask_with_default(): confirm=False, prefill=str(default_value), is_multiline=False, + autocomplete=[], + help=None, ) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 2b83507f0..d6ddb81f9 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -573,13 +573,16 @@ class Question(object): # Prevent displaying a shitload of choices # (e.g. 100+ available users when choosing an app admin...) - choices_to_display = self.choices[:20] - remaining_choices = len(self.choices[20:]) + choices = list(self.choices.values()) 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)] - text_for_user_input_in_cli += " [{0}]".format(" | ".join(choices_to_display)) + choices_to_display = " | ".join(choices_to_display) + + text_for_user_input_in_cli += f" [{choices_to_display}]" return text_for_user_input_in_cli @@ -752,7 +755,7 @@ class BooleanQuestion(Question): raise YunohostValidationError( "app_argument_choice_invalid", - name=option.get("name", ""), + name=getattr(option, "name", None) or option.get("name"), value=value, # FIXME : this doesn't match yes_answers / no_answers... choices="yes, no, y, n, 1, 0", @@ -788,7 +791,7 @@ class BooleanQuestion(Question): return None raise YunohostValidationError( "app_argument_choice_invalid", - name=option.get("name", ""), + name=getattr(option, "name", None) or option.get("name"), value=value, choices="yes, no, y, n, 1, 0", ) @@ -808,10 +811,7 @@ class BooleanQuestion(Question): return text_for_user_input_in_cli def get(self, key, default=None): - try: - return getattr(self, key) - except AttributeError: - return default + return getattr(self, key, default) class DomainQuestion(Question): @@ -843,7 +843,7 @@ class UserQuestion(Question): from yunohost.domain import _get_maindomain super().__init__(question, user_answers) - self.choices = user_list()["users"] + self.choices = list(user_list()["users"].keys()) if not self.choices: raise YunohostValidationError( @@ -854,7 +854,7 @@ class UserQuestion(Question): if self.default is None: root_mail = "root@%s" % _get_maindomain() - for user in self.choices.keys(): + for user in self.choices: if root_mail in user_info(user).get("mail-aliases", []): self.default = user break @@ -891,9 +891,10 @@ class NumberQuestion(Question): if value in [None, ""]: return value - # FIXME: option.name may not exist if option={}... raise YunohostValidationError( - "app_argument_invalid", name=option.get("name", ""), error=m18n.n("invalid_number") + "app_argument_invalid", + name=getattr(option, "name", None) or option.get("name"), + error=m18n.n("invalid_number") ) def _prevalidate(self): From 78ad3b5da32b509cb91df1106c49e2eabb880d96 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 02:39:39 +0200 Subject: [PATCH 005/142] Fix weird definition for boolean's humanize/normalize --- src/yunohost/utils/config.py | 59 +++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index d6ddb81f9..64b472c39 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -740,25 +740,21 @@ class BooleanQuestion(Question): yes = option.get("yes", 1) no = option.get("no", 0) - value = str(value).lower() - if value == str(yes).lower(): - return "yes" - if value == str(no).lower(): - return "no" - if value in BooleanQuestion.yes_answers: - return "yes" - if value in BooleanQuestion.no_answers: - return "no" - if value in ["none", ""]: + 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=getattr(option, "name", None) or option.get("name"), value=value, - # FIXME : this doesn't match yes_answers / no_answers... - choices="yes, no, y, n, 1, 0", + choices="yes/no", ) @staticmethod @@ -769,31 +765,38 @@ class BooleanQuestion(Question): yes = option.get("yes", 1) no = option.get("no", 0) - # - # FIXME: Shouldn't we also check that value == yes ? - # Otherwise is yes = "foobar", normalize is not idempotent - # i.e normalize("true") will return "foobar" - # but normalize(normalize("true")) will raise an exception ? - # - # FIXME: it's also a bit confusing to understand if the - # packager-provided 'yes' value is meant for humans (in which case - # shouldn't it be used as a return value for humanize?) - # or as an internal value for better interfacing with scripts/computers - # - # Also shouldnt we be using normalize() in humanize() ? - if str(value).lower() in BooleanQuestion.yes_answers: - return yes + strvalue = str(value).lower() - if str(value).lower() in BooleanQuestion.no_answers: + # + # N.B.: + # we check first if the value is equal to yes + # then if equal to no + # then if in yes_answers + # then if in no_answers + # + # This is to hopefully cover the weird edgecase + # where the value for yes/no could be reversed + # such as yes=false or yes=0 + # no=true no=1 + # + + if strvalue == str(yes).lower(): + return yes + if strvalue == str(no).lower(): + return no + if strvalue in BooleanQuestion.yes_answers: + return yes + if strvalue in BooleanQuestion.no_answers: return no if value in [None, ""]: return None + raise YunohostValidationError( "app_argument_choice_invalid", name=getattr(option, "name", None) or option.get("name"), value=value, - choices="yes, no, y, n, 1, 0", + choices="yes/no", ) def __init__(self, question, user_answers): From d023333fa42128d70a87eb49b0f34f55aa8d1540 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 02:58:13 +0200 Subject: [PATCH 006/142] questions prompt: include normalize in the try/except to re-ask question if not properly formated --- src/yunohost/utils/config.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 64b472c39..38cc28894 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -520,12 +520,9 @@ class Question(object): ): self.value = class_default if self.default is None else self.default - # Normalization - # This is done to enforce a certain formating like for boolean - self.value = self.normalize(self.value, self) - - # Prevalidation 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 From 430f53aa73fc68c0b286af11a2104bea20daf240 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 03:39:43 +0200 Subject: [PATCH 007/142] Semantic, simplify code... --- src/yunohost/app.py | 83 +++++------------------------------- src/yunohost/utils/config.py | 25 ++++++----- 2 files changed, 26 insertions(+), 82 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 91f7a41ef..b0ae7f0d5 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -32,7 +32,6 @@ import time import re import subprocess import glob -import urllib.parse import tempfile from collections import OrderedDict @@ -55,7 +54,7 @@ from moulinette.utils.filesystem import ( from yunohost.utils import packages from yunohost.utils.config import ( ConfigPanel, - parse_args_in_yunohost_format, + ask_questions_and_parse_answers, ) from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError @@ -453,16 +452,10 @@ def app_change_url(operation_logger, app, domain, path): # Check the url is available _assert_no_conflicting_apps(domain, path, ignore_app=app) - manifest = _get_manifest_of_app(os.path.join(APPS_SETTING_PATH, app)) - - # Retrieve arguments list for change_url script - # TODO: Allow to specify arguments - args_odict = _parse_args_from_manifest(manifest, "change_url") - tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app, args=args_odict) + env_dict = _make_environment_for_app_script(app) env_dict["YNH_APP_OLD_DOMAIN"] = old_domain env_dict["YNH_APP_OLD_PATH"] = old_path env_dict["YNH_APP_NEW_DOMAIN"] = domain @@ -614,12 +607,8 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) - # Retrieve arguments list for upgrade script - # TODO: Allow to specify arguments - args_odict = _parse_args_from_manifest(manifest, "upgrade") - # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app_instance_name, args=args_odict) + env_dict = _make_environment_for_app_script(app_instance_name) 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) @@ -905,13 +894,11 @@ def app_install( app_instance_name = app_id # Retrieve arguments list for install script - args_dict = ( - {} if not args else dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) - ) - args_odict = _parse_args_from_manifest(manifest, "install", args=args_dict) + questions = manifest.get("arguments", {}).get("install", {}) + args = ask_questions_and_parse_answers(questions, prefilled_answers=args) # Validate domain / path availability for webapps - _validate_and_normalize_webpath(args_odict, extracted_app_folder) + _validate_and_normalize_webpath(args, extracted_app_folder) # Attempt to patch legacy helpers ... _patch_legacy_helpers(extracted_app_folder) @@ -976,11 +963,11 @@ def app_install( ) # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app_instance_name, args=args_odict) + env_dict = _make_environment_for_app_script(app_instance_name, args=args) env_dict["YNH_APP_BASEDIR"] = extracted_app_folder env_dict_for_logging = env_dict.copy() - for arg_name, arg_value_and_type in args_odict.items(): + for arg_name, arg_value_and_type in args.items(): if arg_value_and_type[1] == "password": del env_dict_for_logging["YNH_APP_ARG_%s" % arg_name.upper()] @@ -1642,15 +1629,13 @@ def app_action_run(operation_logger, app, action, args=None): action_declaration = actions[action] # Retrieve arguments list for install script - args_dict = ( - dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {} - ) - args_odict = _parse_args_for_action(actions[action], args=args_dict) + questions = actions[action].get("arguments", {}) + args = ask_questions_and_parse_answers(questions, prefilled_answers=args) tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) env_dict = _make_environment_for_app_script( - app, args=args_odict, args_prefix="ACTION_" + app, args=args, args_prefix="ACTION_" ) env_dict["YNH_ACTION"] = action env_dict["YNH_APP_BASEDIR"] = tmp_workdir_for_app @@ -2380,52 +2365,6 @@ def _check_manifest_requirements(manifest, app_instance_name): ) -def _parse_args_from_manifest(manifest, action, args={}): - """Parse arguments needed for an action from the manifest - - Retrieve specified arguments for the action from the manifest, and parse - given args according to that. If some required arguments are not provided, - its values will be asked if interaction is possible. - Parsed arguments will be returned as an OrderedDict - - Keyword arguments: - manifest -- The app manifest to use - action -- The action to retrieve arguments for - args -- A dictionnary of arguments to parse - - """ - if action not in manifest["arguments"]: - logger.debug("no arguments found for '%s' in manifest", action) - return OrderedDict() - - action_args = manifest["arguments"][action] - return parse_args_in_yunohost_format(args, action_args) - - -def _parse_args_for_action(action, args={}): - """Parse arguments needed for an action from the actions list - - Retrieve specified arguments for the action from the manifest, and parse - given args according to that. If some required arguments are not provided, - its values will be asked if interaction is possible. - Parsed arguments will be returned as an OrderedDict - - Keyword arguments: - action -- The action - args -- A dictionnary of arguments to parse - - """ - args_dict = OrderedDict() - - if "arguments" not in action: - logger.debug("no arguments found for '%s' in manifest", action) - return args_dict - - action_args = action["arguments"] - - return parse_args_in_yunohost_format(args, action_args) - - def _validate_and_normalize_webpath(args_dict, app_folder): # If there's only one "domain" and "path", validate that domain/path diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 38cc28894..3431e9bdc 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -464,14 +464,16 @@ class Question(object): self.name = question["name"] self.type = question.get("type", "string") self.default = question.get("default", None) - self.current_value = question.get("current_value") self.optional = question.get("optional", False) self.choices = question.get("choices", []) self.pattern = question.get("pattern", self.pattern) self.ask = question.get("ask", {"en": self.name}) self.help = question.get("help") - self.value = user_answers.get(self.name) self.redact = question.get("redact", False) + # .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 = user_answers.get(self.name) # Empty value is parsed as empty string if self.default == "": @@ -1063,25 +1065,28 @@ ARGUMENTS_TYPE_PARSERS = { } -def parse_args_in_yunohost_format(user_answers, argument_questions): +def ask_questions_and_parse_answers(questions, prefilled_answers=""): + _setuser_answers, argument_questions): """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: - user_answers -- a dictionnary of arguments from the user (generally - empty in CLI, filed from the admin interface) + prefilled_answers -- a dictionnary of arguments from the user (generally + empty in CLI, filed from the admin interface) argument_questions -- the arguments description store in yunohost format from actions.json/toml, manifest.json/toml or config_panel.json/toml """ - parsed_answers_dict = OrderedDict() - for question in argument_questions: + prefilled_answers = dict(urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True)) + out = OrderedDict() + + for question in questions: question_class = ARGUMENTS_TYPE_PARSERS[question.get("type", "string")] - question = question_class(question, user_answers) + question = question_class(question, prefilled_answers) answer = question.ask_if_needed() if answer is not None: - parsed_answers_dict[question.name] = answer + out[question.name] = answer - return parsed_answers_dict + return out From 9c3ff52d98e1e2c717e10ba470c8c99c5eefc53e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 04:35:22 +0200 Subject: [PATCH 008/142] file questions: we don't need to know the filename, we don't need to validate extension server-side --- src/yunohost/utils/config.py | 70 ++++++++---------------------------- 1 file changed, 15 insertions(+), 55 deletions(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 3431e9bdc..4c9457c94 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -961,83 +961,44 @@ class FileQuestion(Question): def __init__(self, question, user_answers): super().__init__(question, user_answers) - if question.get("accept"): - self.accept = question.get("accept") - else: - self.accept = "" - if Moulinette.interface.type == "api": - if user_answers.get(f"{self.name}[name]"): - self.value = { - "content": self.value, - "filename": user_answers.get(f"{self.name}[name]", self.name), - } + self.accept = question.get("accept", "") def _prevalidate(self): if self.value is None: self.value = self.current_value super()._prevalidate() - if ( - isinstance(self.value, str) - and self.value - and not os.path.exists(self.value) - ): - raise YunohostValidationError( - "app_argument_invalid", - name=self.name, - error=m18n.n("file_does_not_exist", path=self.value), - ) - if self.value in [None, ""] or not self.accept: - return - filename = self.value if isinstance(self.value, str) else self.value["filename"] - if "." not in filename or "." + filename.split(".")[ - -1 - ] not in self.accept.replace(" ", "").split(","): + if not self.value or not os.path.exists(str(self.value)): raise YunohostValidationError( "app_argument_invalid", name=self.name, - error=m18n.n( - "file_extension_not_accepted", file=filename, accept=self.accept - ), + error=m18n.n("file_does_not_exist", path=str(self.value)), ) def _post_parse_value(self): from base64 import b64decode - # Upload files from API - # A file arg contains a string with "FILENAME:BASE64_CONTENT" if not self.value: return self.value - if Moulinette.interface.type == "api" and isinstance(self.value, dict): + # FIXME : in the cli case, don't we want to also copy the file + # to a tmp work dir ? + + if Moulinette.interface.type == "api": upload_dir = tempfile.mkdtemp(prefix="tmp_configpanel_") + _, file_path = tempfile.mkstemp(prefix="foobar", dir=upload_dir) + FileQuestion.upload_dirs += [upload_dir] - filename = self.value["filename"] - logger.debug( - f"Save uploaded file {self.value['filename']} from API into {upload_dir}" - ) + logger.debug(f"Save uploaded file from API into {file_path}") - # Filename is given by user of the API. For security reason, we have replaced - # os.path.join to avoid the user to be able to rewrite a file in filesystem - # i.e. os.path.join("/foo", "/etc/passwd") == "/etc/passwd" - file_path = os.path.normpath(upload_dir + "/" + filename) - if not file_path.startswith(upload_dir + "/"): - raise YunohostError( - f"Filename '{filename}' received from the API got a relative parent path, which is forbidden", - raw_msg=True, - ) - i = 2 - while os.path.exists(file_path): - file_path = os.path.normpath(upload_dir + "/" + filename + (".%d" % i)) - i += 1 - - content = self.value["content"] + content = self.value write_to_file(file_path, b64decode(content), file_mode="wb") self.value = file_path + return self.value @@ -1066,19 +1027,18 @@ ARGUMENTS_TYPE_PARSERS = { def ask_questions_and_parse_answers(questions, prefilled_answers=""): - _setuser_answers, argument_questions): """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: - prefilled_answers -- a dictionnary of arguments from the user (generally - empty in CLI, filed from the admin interface) - argument_questions -- the arguments description store in yunohost + questions -- the arguments description store in yunohost format from actions.json/toml, manifest.json/toml or config_panel.json/toml + prefilled_answers -- a url-formated string such as "domain=yolo.test&path=/foobar&admin=sam" """ prefilled_answers = dict(urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True)) + out = OrderedDict() for question in questions: From d5748b519ac8237b2364642ee41411d8dcab5914 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 05:02:27 +0200 Subject: [PATCH 009/142] config panels: attempt to improve the semantic of 'convert()' --- src/yunohost/utils/config.py | 51 ++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 4c9457c94..558498869 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -201,20 +201,20 @@ class ConfigPanel: # Transform toml format into internal format format_description = { - "toml": { + "root": { "properties": ["version", "i18n"], - "default": {"version": 1.0}, + "defaults": {"version": 1.0}, }, "panels": { "properties": ["name", "services", "actions", "help"], - "default": { + "defaults": { "services": [], "actions": {"apply": {"en": "Apply"}}, }, }, "sections": { "properties": ["name", "services", "optional", "help", "visible"], - "default": { + "defaults": { "name": "", "services": [], "optional": True, @@ -244,11 +244,11 @@ class ConfigPanel: "accept", "redact", ], - "default": {}, + "defaults": {}, }, } - def convert(toml_node, node_type): + 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, @@ -256,48 +256,49 @@ class ConfigPanel: - some properties have default values This function detects all children nodes and put them in a list """ - # Prefill the node default keys if needed - default = format_description[node_type]["default"] - node = {key: toml_node.get(key, value) for key, value in default.items()} - properties = format_description[node_type]["properties"] + defaults = format_description[level]["defaults"] + properties = format_description[level]["properties"] - # Define the filter_key part to use and the children type - i = list(format_description).index(node_type) - subnode_type = ( - list(format_description)[i + 1] if node_type != "options" else None + # 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 toml_node.items(): + for key, value in raw_infos.items(): # Key/value are a child node if ( isinstance(value, OrderedDict) and key not in properties - and subnode_type + and sublevel ): # We exclude all nodes not referenced by the filter_key if search_key and key != search_key: continue - subnode = convert(value, subnode_type) + subnode = _build_internal_config_panel(value, sublevel) subnode["id"] = key - if node_type == "toml": + if level == "root": subnode.setdefault("name", {"en": key.capitalize()}) - elif node_type == "sections": + elif level == "sections": subnode["name"] = key # legacy - subnode.setdefault("optional", toml_node.get("optional", True)) - node.setdefault(subnode_type, []).append(subnode) + subnode.setdefault("optional", raw_infos.get("optional", 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 toml") + logger.warning(f"Unknown key '{key}' found in config panel") # Todo search all i18n keys - node[key] = ( + out[key] = ( value if key not in ["ask", "help", "name"] else {"en": value} ) - return node + return out - self.config = convert(toml_config_panel, "toml") + self.config = _build_internal_config_panel(toml_config_panel, "root") try: self.config["panels"][0]["sections"][0]["options"][0] From f14d7805884f6ba27f400b997d0bdf9b6e6a50b9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 05:09:57 +0200 Subject: [PATCH 010/142] Get rid of unecessary 'user_answers' arg in constructor --- src/yunohost/utils/config.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 558498869..81b651be5 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -461,7 +461,7 @@ class Question(object): hide_user_input_in_prompt = False pattern: Optional[Dict] = None - def __init__(self, question, user_answers): + def __init__(self, question): self.name = question["name"] self.type = question.get("type", "string") self.default = question.get("default", None) @@ -474,7 +474,7 @@ class Question(object): # .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 = user_answers.get(self.name) + self.value = question.get("value") # Empty value is parsed as empty string if self.default == "": @@ -701,8 +701,8 @@ class PasswordQuestion(Question): default_value = "" forbidden_chars = "{}" - def __init__(self, question, user_answers): - super().__init__(question, user_answers) + def __init__(self, question): + super().__init__(question) self.redact = True if self.default is not None: raise YunohostValidationError( @@ -799,8 +799,8 @@ class BooleanQuestion(Question): choices="yes/no", ) - def __init__(self, question, user_answers): - super().__init__(question, user_answers) + def __init__(self, question): + super().__init__(question) self.yes = question.get("yes", 1) self.no = question.get("no", 0) if self.default is None: @@ -820,10 +820,10 @@ class BooleanQuestion(Question): class DomainQuestion(Question): argument_type = "domain" - def __init__(self, question, user_answers): + def __init__(self, question): from yunohost.domain import domain_list, _get_maindomain - super().__init__(question, user_answers) + super().__init__(question) if self.default is None: self.default = _get_maindomain() @@ -841,11 +841,11 @@ class DomainQuestion(Question): class UserQuestion(Question): argument_type = "user" - def __init__(self, question, user_answers): + def __init__(self, question): from yunohost.user import user_list, user_info from yunohost.domain import _get_maindomain - super().__init__(question, user_answers) + super().__init__(question) self.choices = list(user_list()["users"].keys()) if not self.choices: @@ -874,8 +874,8 @@ class NumberQuestion(Question): argument_type = "number" default_value = None - def __init__(self, question, user_answers): - super().__init__(question, user_answers) + def __init__(self, question): + super().__init__(question) self.min = question.get("min", None) self.max = question.get("max", None) self.step = question.get("step", None) @@ -924,8 +924,8 @@ class DisplayTextQuestion(Question): argument_type = "display_text" readonly = True - def __init__(self, question, user_answers): - super().__init__(question, user_answers) + def __init__(self, question): + super().__init__(question) self.optional = True self.style = question.get( @@ -960,8 +960,8 @@ class FileQuestion(Question): if os.path.exists(upload_dir): shutil.rmtree(upload_dir) - def __init__(self, question, user_answers): - super().__init__(question, user_answers) + def __init__(self, question): + super().__init__(question) self.accept = question.get("accept", "") def _prevalidate(self): @@ -1044,7 +1044,8 @@ def ask_questions_and_parse_answers(questions, prefilled_answers=""): for question in questions: question_class = ARGUMENTS_TYPE_PARSERS[question.get("type", "string")] - question = question_class(question, prefilled_answers) + question["value"] = prefilled_answers.get(question["name"]) + question = question_class(question) answer = question.ask_if_needed() if answer is not None: From 4cc2c9787db31c72b176b13297804a73c32e5e9e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 05:20:19 +0200 Subject: [PATCH 011/142] Propagate changes on tests --- src/yunohost/tests/test_questions.py | 251 +++++++++++++-------------- src/yunohost/utils/config.py | 2 +- 2 files changed, 126 insertions(+), 127 deletions(-) diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index 62bb61cc2..a01ad71f0 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -2,7 +2,7 @@ import sys import pytest import os -from mock import patch, MagicMock +from mock import patch from io import StringIO from collections import OrderedDict @@ -10,9 +10,8 @@ from moulinette import Moulinette from yunohost import domain, user from yunohost.utils.config import ( - parse_args_in_yunohost_format, + ask_questions_and_parse_answers, PasswordQuestion, - Question, ) from yunohost.utils.error import YunohostError @@ -41,7 +40,7 @@ User answers: def test_question_empty(): - assert parse_args_in_yunohost_format({}, []) == {} + assert ask_questions_and_parse_answers([], {}) == {} def test_question_string(): @@ -53,7 +52,7 @@ def test_question_string(): ] answers = {"some_string": "some_value"} expected_result = OrderedDict({"some_string": ("some_value", "string")}) - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_default_type(): @@ -64,7 +63,7 @@ def test_question_string_default_type(): ] answers = {"some_string": "some_value"} expected_result = OrderedDict({"some_string": ("some_value", "string")}) - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_no_input(): @@ -76,7 +75,7 @@ def test_question_string_no_input(): answers = {} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_string_input(): @@ -92,7 +91,7 @@ def test_question_string_input(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_input_no_ask(): @@ -107,7 +106,7 @@ def test_question_string_input_no_ask(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_no_input_optional(): @@ -120,7 +119,7 @@ def test_question_string_no_input_optional(): answers = {} expected_result = OrderedDict({"some_string": ("", "string")}) with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_optional_with_input(): @@ -137,7 +136,7 @@ def test_question_string_optional_with_input(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_optional_with_empty_input(): @@ -154,7 +153,7 @@ def test_question_string_optional_with_empty_input(): with patch.object(Moulinette, "prompt", return_value=""), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_optional_with_input_without_ask(): @@ -170,7 +169,7 @@ def test_question_string_optional_with_input_without_ask(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_no_input_default(): @@ -184,7 +183,7 @@ def test_question_string_no_input_default(): answers = {} expected_result = OrderedDict({"some_string": ("some_value", "string")}) with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_input_test_ask(): @@ -200,7 +199,7 @@ def test_question_string_input_test_ask(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) prompt.assert_called_with( message=ask_text, is_password=False, @@ -227,7 +226,7 @@ def test_question_string_input_test_ask_with_default(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) prompt.assert_called_with( message=ask_text, is_password=False, @@ -255,7 +254,7 @@ def test_question_string_input_test_ask_with_example(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) assert ask_text in prompt.call_args[1]["message"] assert example_text in prompt.call_args[1]["message"] @@ -276,7 +275,7 @@ def test_question_string_input_test_ask_with_help(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + 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"] @@ -285,7 +284,7 @@ def test_question_string_with_choice(): questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] answers = {"some_string": "fr"} expected_result = OrderedDict({"some_string": ("fr", "string")}) - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_with_choice_prompt(): @@ -295,7 +294,7 @@ def test_question_string_with_choice_prompt(): with patch.object(Moulinette, "prompt", return_value="fr"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_string_with_choice_bad(): @@ -303,7 +302,7 @@ def test_question_string_with_choice_bad(): answers = {"some_string": "bad"} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) + assert ask_questions_and_parse_answers(questions, answers) def test_question_string_with_choice_ask(): @@ -321,7 +320,7 @@ def test_question_string_with_choice_ask(): with patch.object(Moulinette, "prompt", return_value="ru") as prompt, patch.object( os, "isatty", return_value=True ): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) assert ask_text in prompt.call_args[1]["message"] for choice in choices: @@ -340,7 +339,7 @@ def test_question_string_with_choice_default(): answers = {} expected_result = OrderedDict({"some_string": ("en", "string")}) with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_password(): @@ -352,7 +351,7 @@ def test_question_password(): ] answers = {"some_password": "some_value"} expected_result = OrderedDict({"some_password": ("some_value", "password")}) - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_password_no_input(): @@ -365,7 +364,7 @@ def test_question_password_no_input(): answers = {} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_password_input(): @@ -382,7 +381,7 @@ def test_question_password_input(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_password_input_no_ask(): @@ -398,7 +397,7 @@ def test_question_password_input_no_ask(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_password_no_input_optional(): @@ -413,14 +412,14 @@ def test_question_password_no_input_optional(): expected_result = OrderedDict({"some_password": ("", "password")}) with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result questions = [ {"name": "some_password", "type": "password", "optional": True, "default": ""} ] with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_password_optional_with_input(): @@ -438,7 +437,7 @@ def test_question_password_optional_with_input(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_password_optional_with_empty_input(): @@ -456,7 +455,7 @@ def test_question_password_optional_with_empty_input(): with patch.object(Moulinette, "prompt", return_value=""), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_password_optional_with_input_without_ask(): @@ -473,7 +472,7 @@ def test_question_password_optional_with_input_without_ask(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_password_no_input_default(): @@ -489,7 +488,7 @@ def test_question_password_no_input_default(): # no default for password! with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) @pytest.mark.skip # this should raises @@ -506,7 +505,7 @@ def test_question_password_no_input_example(): # no example for password! with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_password_input_test_ask(): @@ -523,7 +522,7 @@ def test_question_password_input_test_ask(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) prompt.assert_called_with( message=ask_text, is_password=True, @@ -552,7 +551,7 @@ def test_question_password_input_test_ask_with_example(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) assert ask_text in prompt.call_args[1]["message"] assert example_text in prompt.call_args[1]["message"] @@ -574,7 +573,7 @@ def test_question_password_input_test_ask_with_help(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + 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"] @@ -593,7 +592,7 @@ def test_question_password_bad_chars(): with pytest.raises(YunohostError), patch.object( os, "isatty", return_value=False ): - parse_args_in_yunohost_format({"some_password": i * 8}, questions) + ask_questions_and_parse_answers(questions, {"some_password": i * 8}) def test_question_password_strong_enough(): @@ -608,10 +607,10 @@ def test_question_password_strong_enough(): with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): # too short - parse_args_in_yunohost_format({"some_password": "a"}, questions) + ask_questions_and_parse_answers(questions, {"some_password": "a"}) with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format({"some_password": "password"}, questions) + ask_questions_and_parse_answers(questions, {"some_password": "password"}) def test_question_password_optional_strong_enough(): @@ -626,10 +625,10 @@ def test_question_password_optional_strong_enough(): with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): # too short - parse_args_in_yunohost_format({"some_password": "a"}, questions) + ask_questions_and_parse_answers(questions, {"some_password": "a"}) with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format({"some_password": "password"}, questions) + ask_questions_and_parse_answers(questions, {"some_password": "password"}) def test_question_path(): @@ -641,7 +640,7 @@ def test_question_path(): ] answers = {"some_path": "some_value"} expected_result = OrderedDict({"some_path": ("some_value", "path")}) - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_path_no_input(): @@ -654,7 +653,7 @@ def test_question_path_no_input(): answers = {} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_path_input(): @@ -671,7 +670,7 @@ def test_question_path_input(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_path_input_no_ask(): @@ -687,7 +686,7 @@ def test_question_path_input_no_ask(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_path_no_input_optional(): @@ -701,7 +700,7 @@ def test_question_path_no_input_optional(): answers = {} expected_result = OrderedDict({"some_path": ("", "path")}) with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_path_optional_with_input(): @@ -719,7 +718,7 @@ def test_question_path_optional_with_input(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_path_optional_with_empty_input(): @@ -737,7 +736,7 @@ def test_question_path_optional_with_empty_input(): with patch.object(Moulinette, "prompt", return_value=""), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_path_optional_with_input_without_ask(): @@ -754,7 +753,7 @@ def test_question_path_optional_with_input_without_ask(): with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_path_no_input_default(): @@ -769,7 +768,7 @@ def test_question_path_no_input_default(): answers = {} expected_result = OrderedDict({"some_path": ("some_value", "path")}) with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_path_input_test_ask(): @@ -786,7 +785,7 @@ def test_question_path_input_test_ask(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) prompt.assert_called_with( message=ask_text, is_password=False, @@ -814,7 +813,7 @@ def test_question_path_input_test_ask_with_default(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) prompt.assert_called_with( message=ask_text, is_password=False, @@ -843,7 +842,7 @@ def test_question_path_input_test_ask_with_example(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) assert ask_text in prompt.call_args[1]["message"] assert example_text in prompt.call_args[1]["message"] @@ -865,7 +864,7 @@ def test_question_path_input_test_ask_with_help(): with patch.object( Moulinette, "prompt", return_value="some_value" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + 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"] @@ -879,7 +878,7 @@ def test_question_boolean(): ] answers = {"some_boolean": "y"} expected_result = OrderedDict({"some_boolean": (1, "boolean")}) - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_boolean_all_yes(): @@ -891,46 +890,46 @@ def test_question_boolean_all_yes(): ] expected_result = OrderedDict({"some_boolean": (1, "boolean")}) assert ( - parse_args_in_yunohost_format({"some_boolean": "y"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "y"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "Y"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "Y"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "yes"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "yes"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "Yes"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "Yes"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "YES"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "YES"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "1"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "1"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": 1}, questions) == expected_result + ask_questions_and_parse_answers(questions, {"some_boolean": 1}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": True}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": True}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "True"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "True"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "TRUE"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "TRUE"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "true"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "true"}) == expected_result ) @@ -944,46 +943,46 @@ def test_question_boolean_all_no(): ] expected_result = OrderedDict({"some_boolean": (0, "boolean")}) assert ( - parse_args_in_yunohost_format({"some_boolean": "n"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "n"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "N"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "N"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "no"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "no"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "No"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "No"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "No"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "No"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "0"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "0"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": 0}, questions) == expected_result + ask_questions_and_parse_answers(questions, {"some_boolean": 0}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": False}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": False}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "False"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "False"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "FALSE"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "FALSE"}) == expected_result ) assert ( - parse_args_in_yunohost_format({"some_boolean": "false"}, questions) + ask_questions_and_parse_answers(questions, {"some_boolean": "false"}) == expected_result ) @@ -1000,7 +999,7 @@ def test_question_boolean_no_input(): expected_result = OrderedDict({"some_boolean": (0, "boolean")}) with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_boolean_bad_input(): @@ -1013,7 +1012,7 @@ def test_question_boolean_bad_input(): answers = {"some_boolean": "stuff"} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_boolean_input(): @@ -1030,13 +1029,13 @@ def test_question_boolean_input(): with patch.object(Moulinette, "prompt", return_value="y"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result expected_result = OrderedDict({"some_boolean": (0, "boolean")}) with patch.object(Moulinette, "prompt", return_value="n"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_boolean_input_no_ask(): @@ -1052,7 +1051,7 @@ def test_question_boolean_input_no_ask(): with patch.object(Moulinette, "prompt", return_value="y"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_boolean_no_input_optional(): @@ -1066,7 +1065,7 @@ def test_question_boolean_no_input_optional(): answers = {} expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_boolean_optional_with_input(): @@ -1084,7 +1083,7 @@ def test_question_boolean_optional_with_input(): with patch.object(Moulinette, "prompt", return_value="y"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_boolean_optional_with_empty_input(): @@ -1102,7 +1101,7 @@ def test_question_boolean_optional_with_empty_input(): with patch.object(Moulinette, "prompt", return_value=""), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_boolean_optional_with_input_without_ask(): @@ -1119,7 +1118,7 @@ def test_question_boolean_optional_with_input_without_ask(): with patch.object(Moulinette, "prompt", return_value="n"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_boolean_no_input_default(): @@ -1134,7 +1133,7 @@ def test_question_boolean_no_input_default(): answers = {} expected_result = OrderedDict({"some_boolean": (0, "boolean")}) with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_boolean_bad_default(): @@ -1148,7 +1147,7 @@ def test_question_boolean_bad_default(): ] answers = {} with pytest.raises(YunohostError): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_boolean_input_test_ask(): @@ -1165,7 +1164,7 @@ def test_question_boolean_input_test_ask(): with patch.object(Moulinette, "prompt", return_value=0) as prompt, patch.object( os, "isatty", return_value=True ): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) prompt.assert_called_with( message=ask_text + " [yes | no]", is_password=False, @@ -1193,7 +1192,7 @@ def test_question_boolean_input_test_ask_with_default(): with patch.object(Moulinette, "prompt", return_value=1) as prompt, patch.object( os, "isatty", return_value=True ): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) prompt.assert_called_with( message=ask_text + " [yes | no]", is_password=False, @@ -1223,7 +1222,7 @@ def test_question_domain_empty(): ), patch.object( os, "isatty", return_value=False ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_domain(): @@ -1242,7 +1241,7 @@ def test_question_domain(): with patch.object( domain, "_get_maindomain", return_value=main_domain ), patch.object(domain, "domain_list", return_value={"domains": domains}): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_domain_two_domains(): @@ -1262,7 +1261,7 @@ def test_question_domain_two_domains(): with patch.object( domain, "_get_maindomain", return_value=main_domain ), patch.object(domain, "domain_list", return_value={"domains": domains}): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result answers = {"some_domain": main_domain} expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) @@ -1270,7 +1269,7 @@ def test_question_domain_two_domains(): with patch.object( domain, "_get_maindomain", return_value=main_domain ), patch.object(domain, "domain_list", return_value={"domains": domains}): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_domain_two_domains_wrong_answer(): @@ -1292,7 +1291,7 @@ def test_question_domain_two_domains_wrong_answer(): with pytest.raises(YunohostError), patch.object( os, "isatty", return_value=False ): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_domain_two_domains_default_no_ask(): @@ -1316,7 +1315,7 @@ def test_question_domain_two_domains_default_no_ask(): ), patch.object( os, "isatty", return_value=False ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_domain_two_domains_default(): @@ -1335,7 +1334,7 @@ def test_question_domain_two_domains_default(): ), patch.object( os, "isatty", return_value=False ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_domain_two_domains_default_input(): @@ -1355,11 +1354,11 @@ def test_question_domain_two_domains_default_input(): ): expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) with patch.object(Moulinette, "prompt", return_value=main_domain): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result expected_result = OrderedDict({"some_domain": (other_domain, "domain")}) with patch.object(Moulinette, "prompt", return_value=other_domain): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_user_empty(): @@ -1385,7 +1384,7 @@ def test_question_user_empty(): with pytest.raises(YunohostError), patch.object( os, "isatty", return_value=False ): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_user(): @@ -1413,7 +1412,7 @@ def test_question_user(): with patch.object(user, "user_list", return_value={"users": users}), patch.object( user, "user_info", return_value={} ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_user_two_users(): @@ -1448,7 +1447,7 @@ def test_question_user_two_users(): with patch.object(user, "user_list", return_value={"users": users}), patch.object( user, "user_info", return_value={} ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result answers = {"some_user": username} expected_result = OrderedDict({"some_user": (username, "user")}) @@ -1456,7 +1455,7 @@ def test_question_user_two_users(): with patch.object(user, "user_list", return_value={"users": users}), patch.object( user, "user_info", return_value={} ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_user_two_users_wrong_answer(): @@ -1491,7 +1490,7 @@ def test_question_user_two_users_wrong_answer(): with pytest.raises(YunohostError), patch.object( os, "isatty", return_value=False ): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_user_two_users_no_default(): @@ -1521,7 +1520,7 @@ def test_question_user_two_users_no_default(): with pytest.raises(YunohostError), patch.object( os, "isatty", return_value=False ): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_user_two_users_default_input(): @@ -1554,13 +1553,13 @@ def test_question_user_two_users_default_input(): expected_result = OrderedDict({"some_user": (username, "user")}) with patch.object(Moulinette, "prompt", return_value=username): assert ( - parse_args_in_yunohost_format(answers, questions) == expected_result + ask_questions_and_parse_answers(questions, answers) == expected_result ) expected_result = OrderedDict({"some_user": (other_user, "user")}) with patch.object(Moulinette, "prompt", return_value=other_user): assert ( - parse_args_in_yunohost_format(answers, questions) == expected_result + ask_questions_and_parse_answers(questions, answers) == expected_result ) @@ -1573,7 +1572,7 @@ def test_question_number(): ] answers = {"some_number": 1337} expected_result = OrderedDict({"some_number": (1337, "number")}) - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_number_no_input(): @@ -1586,7 +1585,7 @@ def test_question_number_no_input(): answers = {} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_number_bad_input(): @@ -1599,11 +1598,11 @@ def test_question_number_bad_input(): answers = {"some_number": "stuff"} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) answers = {"some_number": 1.5} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_number_input(): @@ -1620,18 +1619,18 @@ def test_question_number_input(): with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result with patch.object(Moulinette, "prompt", return_value=1337), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result expected_result = OrderedDict({"some_number": (0, "number")}) with patch.object(Moulinette, "prompt", return_value="0"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_number_input_no_ask(): @@ -1647,7 +1646,7 @@ def test_question_number_input_no_ask(): with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_number_no_input_optional(): @@ -1661,7 +1660,7 @@ def test_question_number_no_input_optional(): answers = {} expected_result = OrderedDict({"some_number": (None, "number")}) # default to 0 with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_number_optional_with_input(): @@ -1679,7 +1678,7 @@ def test_question_number_optional_with_input(): with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_number_optional_with_input_without_ask(): @@ -1696,7 +1695,7 @@ def test_question_number_optional_with_input_without_ask(): with patch.object(Moulinette, "prompt", return_value="0"), patch.object( os, "isatty", return_value=True ): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_number_no_input_default(): @@ -1711,7 +1710,7 @@ def test_question_number_no_input_default(): answers = {} expected_result = OrderedDict({"some_number": (1337, "number")}) with patch.object(os, "isatty", return_value=False): - assert parse_args_in_yunohost_format(answers, questions) == expected_result + assert ask_questions_and_parse_answers(questions, answers) == expected_result def test_question_number_bad_default(): @@ -1725,7 +1724,7 @@ def test_question_number_bad_default(): ] answers = {} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) def test_question_number_input_test_ask(): @@ -1742,7 +1741,7 @@ def test_question_number_input_test_ask(): with patch.object( Moulinette, "prompt", return_value="1111" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) prompt.assert_called_with( message=ask_text, is_password=False, @@ -1770,7 +1769,7 @@ def test_question_number_input_test_ask_with_default(): with patch.object( Moulinette, "prompt", return_value="1111" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) prompt.assert_called_with( message=ask_text, is_password=False, @@ -1799,7 +1798,7 @@ def test_question_number_input_test_ask_with_example(): with patch.object( Moulinette, "prompt", return_value="1111" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) assert ask_text in prompt.call_args[1]["message"] assert example_value in prompt.call_args[1]["message"] @@ -1821,7 +1820,7 @@ def test_question_number_input_test_ask_with_help(): with patch.object( Moulinette, "prompt", return_value="1111" ) as prompt, patch.object(os, "isatty", return_value=True): - parse_args_in_yunohost_format(answers, questions) + 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"] @@ -1833,5 +1832,5 @@ def test_question_display_text(): with patch.object(sys, "stdout", new_callable=StringIO) as stdout, patch.object( os, "isatty", return_value=True ): - parse_args_in_yunohost_format(answers, questions) + ask_questions_and_parse_answers(questions, answers) assert "foobar" in stdout.getvalue() diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 81b651be5..e83d794ff 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -381,7 +381,7 @@ class ConfigPanel: # Check and ask unanswered questions self.new_values.update( - parse_args_in_yunohost_format(self.args, section["options"]) + ask_questions_and_parse_answers(section["options"], self.args) ) self.new_values = { key: value[0] From 26a49929611434361182585eba5a521cff557462 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 05:33:20 +0200 Subject: [PATCH 012/142] Refactor _normalize_domain_path into DomainQuestion + PathQuestion normalizers --- src/yunohost/app.py | 37 ++++++++++++++---------------------- src/yunohost/utils/config.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index b0ae7f0d5..396ce04e7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -55,6 +55,8 @@ from yunohost.utils import packages from yunohost.utils.config import ( ConfigPanel, ask_questions_and_parse_answers, + DomainQuestion, + PathQuestion ) from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError @@ -441,8 +443,11 @@ def app_change_url(operation_logger, app, domain, path): old_path = app_setting(app, "path") # Normalize path and domain format - old_domain, old_path = _normalize_domain_path(old_domain, old_path) - domain, path = _normalize_domain_path(domain, path) + + domain = DomainQuestion.normalize(domain) + old_domain = DomainQuestion.normalize(old_domain) + path = PathQuestion.normalize(path) + old_path = PathQuestion.normalize(old_path) if (domain, path) == (old_domain, old_path): raise YunohostValidationError( @@ -1459,7 +1464,8 @@ def app_register_url(app, domain, path): permission_sync_to_user, ) - domain, path = _normalize_domain_path(domain, path) + domain = DomainQuestion.normalize(domain) + path = PathQuestion.normalize(path) # We cannot change the url of an app already installed simply by changing # the settings... @@ -2381,7 +2387,9 @@ def _validate_and_normalize_webpath(args_dict, app_folder): domain = domain_args[0][1] path = path_args[0][1] - domain, path = _normalize_domain_path(domain, path) + + domain = DomainQuestion.normalize(domain) + path = PathQuestion.normalize(path) # Check the url is available _assert_no_conflicting_apps(domain, path) @@ -2415,24 +2423,6 @@ def _validate_and_normalize_webpath(args_dict, app_folder): _assert_no_conflicting_apps(domain, "/", full_domain=True) -def _normalize_domain_path(domain, path): - - # We want url to be of the format : - # some.domain.tld/foo - - # Remove http/https prefix if it's there - if domain.startswith("https://"): - domain = domain[len("https://") :] - elif domain.startswith("http://"): - domain = domain[len("http://") :] - - # Remove trailing slashes - domain = domain.rstrip("/").lower() - path = "/" + path.strip("/") - - return domain, path - - def _get_conflicting_apps(domain, path, ignore_app=None): """ Return a list of all conflicting apps with a domain/path (it can be empty) @@ -2445,7 +2435,8 @@ def _get_conflicting_apps(domain, path, ignore_app=None): from yunohost.domain import _assert_domain_exists - domain, path = _normalize_domain_path(domain, path) + domain = DomainQuestion.normalize(domain) + path = PathQuestion.normalize(path) # Abort if domain is unknown _assert_domain_exists(domain) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index e83d794ff..1a39c0da4 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -728,6 +728,10 @@ class PathQuestion(Question): argument_type = "path" default_value = "" + @staticmethod + def normalize(value, option={}): + return "/" + value.strip("/") + class BooleanQuestion(Question): argument_type = "boolean" @@ -837,6 +841,18 @@ class DomainQuestion(Question): error=m18n.n("domain_name_unknown", domain=self.value), ) + @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 UserQuestion(Question): argument_type = "user" From d4c3a6975e06d6dfc8fc8d9eb24d503615bacf7c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 10:38:30 +0200 Subject: [PATCH 013/142] ask_questions_and_parse_answers may take directly a dict as input --- src/yunohost/utils/config.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 1a39c0da4..604e20f4f 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -25,7 +25,7 @@ import urllib.parse import tempfile import shutil from collections import OrderedDict -from typing import Optional, Dict, List +from typing import Optional, Dict, List, Union, Any, Mapping from moulinette.interfaces.cli import colorize from moulinette import Moulinette, m18n @@ -461,7 +461,7 @@ class Question(object): hide_user_input_in_prompt = False pattern: Optional[Dict] = None - def __init__(self, question): + def __init__(self, question: Dict[str, Any]): self.name = question["name"] self.type = question.get("type", "string") self.default = question.get("default", None) @@ -1043,7 +1043,7 @@ ARGUMENTS_TYPE_PARSERS = { } -def ask_questions_and_parse_answers(questions, prefilled_answers=""): +def ask_questions_and_parse_answers(questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}) -> Mapping[str, Any]: """Parse arguments store in either manifest.json or actions.json or from a config panel against the user answers when they are present. @@ -1051,10 +1051,12 @@ def ask_questions_and_parse_answers(questions, prefilled_answers=""): questions -- the arguments description store in yunohost format from actions.json/toml, manifest.json/toml or config_panel.json/toml - prefilled_answers -- a url-formated string such as "domain=yolo.test&path=/foobar&admin=sam" + 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"} """ - prefilled_answers = dict(urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True)) + if isinstance(prefilled_answers, str): + prefilled_answers = dict(urllib.parse.parse_qs(prefilled_answers or "", keep_blank_values=True)) out = OrderedDict() From 68d849f7abcceb451f11e7a18c9d749b77105bcc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 10:45:37 +0200 Subject: [PATCH 014/142] Adapt tests for domain/path normalize --- src/yunohost/tests/test_appurl.py | 18 +----------------- src/yunohost/tests/test_questions.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index f15ed391f..5954d239a 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -4,7 +4,7 @@ import os from .conftest import get_test_apps_dir from yunohost.utils.error import YunohostError -from yunohost.app import app_install, app_remove, _normalize_domain_path +from yunohost.app import app_install, app_remove from yunohost.domain import _get_maindomain, domain_url_available from yunohost.permission import _validate_and_sanitize_permission_url @@ -28,22 +28,6 @@ def teardown_function(function): pass -def test_normalize_domain_path(): - - assert _normalize_domain_path("https://yolo.swag/", "macnuggets") == ( - "yolo.swag", - "/macnuggets", - ) - assert _normalize_domain_path("http://yolo.swag", "/macnuggets/") == ( - "yolo.swag", - "/macnuggets", - ) - assert _normalize_domain_path("yolo.swag/", "macnuggets/") == ( - "yolo.swag", - "/macnuggets", - ) - - def test_urlavailable(): # Except the maindomain/macnuggets to be available diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index a01ad71f0..9680ccb46 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -12,6 +12,8 @@ from yunohost import domain, user from yunohost.utils.config import ( ask_questions_and_parse_answers, PasswordQuestion, + DomainQuestion, + PathQuestion ) from yunohost.utils.error import YunohostError @@ -1834,3 +1836,19 @@ def test_question_display_text(): ): ask_questions_and_parse_answers(questions, answers) assert "foobar" in stdout.getvalue() + + +def test_normalize_domain(): + + assert DomainQuestion("https://yolo.swag/") == "yolo.swag" + assert DomainQuestion("http://yolo.swag") == "yolo.swag" + assert DomainQuestion("yolo.swag/") == "yolo.swag" + + +def test_normalize_path(): + + assert PathQuestion("macnuggets") == "/macnuggets" + assert PathQuestion("mac/nuggets") == "/mac/nuggets" + assert PathQuestion("/macnuggets/") == "/macnuggets" + assert PathQuestion("macnuggets/") == "/macnuggets" + assert PathQuestion("////macnuggets///") == "/macnuggets" From 548042d5036d9458c3dd7fa65598a20a505de666 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 13:07:46 +0200 Subject: [PATCH 015/142] Moar fixes .. + add test for boolean normalize/humanize --- src/yunohost/tests/test_questions.py | 120 ++++++++++++++++++++++----- src/yunohost/utils/config.py | 73 +++++++++------- 2 files changed, 142 insertions(+), 51 deletions(-) diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index 9680ccb46..cf4a8832d 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -13,9 +13,10 @@ from yunohost.utils.config import ( ask_questions_and_parse_answers, PasswordQuestion, DomainQuestion, - PathQuestion + PathQuestion, + BooleanQuestion ) -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError """ @@ -640,8 +641,8 @@ def test_question_path(): "type": "path", } ] - answers = {"some_path": "some_value"} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) + answers = {"some_path": "/some_value"} + expected_result = OrderedDict({"some_path": ("/some_value", "path")}) assert ask_questions_and_parse_answers(questions, answers) == expected_result @@ -667,9 +668,9 @@ def test_question_path_input(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) + expected_result = OrderedDict({"some_path": ("/some_value", "path")}) - with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( os, "isatty", return_value=True ): assert ask_questions_and_parse_answers(questions, answers) == expected_result @@ -683,9 +684,9 @@ def test_question_path_input_no_ask(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) + expected_result = OrderedDict({"some_path": ("/some_value", "path")}) - with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( os, "isatty", return_value=True ): assert ask_questions_and_parse_answers(questions, answers) == expected_result @@ -715,9 +716,9 @@ def test_question_path_optional_with_input(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) + expected_result = OrderedDict({"some_path": ("/some_value", "path")}) - with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( os, "isatty", return_value=True ): assert ask_questions_and_parse_answers(questions, answers) == expected_result @@ -750,9 +751,9 @@ def test_question_path_optional_with_input_without_ask(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) + expected_result = OrderedDict({"some_path": ("/some_value", "path")}) - with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( os, "isatty", return_value=True ): assert ask_questions_and_parse_answers(questions, answers) == expected_result @@ -768,7 +769,7 @@ def test_question_path_no_input_default(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) + expected_result = OrderedDict({"some_path": ("/some_value", "path")}) with patch.object(os, "isatty", return_value=False): assert ask_questions_and_parse_answers(questions, answers) == expected_result @@ -801,7 +802,7 @@ def test_question_path_input_test_ask(): def test_question_path_input_test_ask_with_default(): ask_text = "some question" - default_text = "some example" + default_text = "someexample" questions = [ { "name": "some_path", @@ -1838,17 +1839,92 @@ def test_question_display_text(): assert "foobar" in stdout.getvalue() +def test_normalize_boolean_nominal(): + + assert BooleanQuestion.normalize("yes") == 1 + assert BooleanQuestion.normalize("Yes") == 1 + assert BooleanQuestion.normalize(" yes ") == 1 + assert BooleanQuestion.normalize("y") == 1 + assert BooleanQuestion.normalize("true") == 1 + assert BooleanQuestion.normalize("True") == 1 + assert BooleanQuestion.normalize("on") == 1 + assert BooleanQuestion.normalize("1") == 1 + assert BooleanQuestion.normalize(1) == 1 + + assert BooleanQuestion.normalize("no") == 0 + assert BooleanQuestion.normalize("No") == 0 + assert BooleanQuestion.normalize(" no ") == 0 + assert BooleanQuestion.normalize("n") == 0 + assert BooleanQuestion.normalize("false") == 0 + assert BooleanQuestion.normalize("False") == 0 + assert BooleanQuestion.normalize("off") == 0 + assert BooleanQuestion.normalize("0") == 0 + assert BooleanQuestion.normalize(0) == 0 + + assert BooleanQuestion.normalize("") is None + assert BooleanQuestion.normalize(" ") is None + assert BooleanQuestion.normalize(" none ") is None + assert BooleanQuestion.normalize("None") is None + assert BooleanQuestion.normalize("none") is None + assert BooleanQuestion.normalize(None) is None + + +def test_normalize_boolean_humanize(): + + assert BooleanQuestion.humanize("yes") == "yes" + assert BooleanQuestion.humanize("true") == "yes" + assert BooleanQuestion.humanize("on") == "yes" + + assert BooleanQuestion.humanize("no") == "no" + assert BooleanQuestion.humanize("false") == "no" + assert BooleanQuestion.humanize("off") == "no" + + +def test_normalize_boolean_invalid(): + + with pytest.raises(YunohostValidationError): + BooleanQuestion.normalize("yesno") + with pytest.raises(YunohostValidationError): + BooleanQuestion.normalize("foobar") + with pytest.raises(YunohostValidationError): + BooleanQuestion.normalize("enabled") + + +def test_normalize_boolean_special_yesno(): + + customyesno = {"yes": "enabled", "no": "disabled"} + + assert BooleanQuestion.normalize("yes", customyesno) == "enabled" + assert BooleanQuestion.normalize("true", customyesno) == "enabled" + assert BooleanQuestion.normalize("enabled", customyesno) == "enabled" + assert BooleanQuestion.humanize("yes", customyesno) == "yes" + assert BooleanQuestion.humanize("true", customyesno) == "yes" + assert BooleanQuestion.humanize("enabled", customyesno) == "yes" + + assert BooleanQuestion.normalize("no", customyesno) == "disabled" + assert BooleanQuestion.normalize("false", customyesno) == "disabled" + assert BooleanQuestion.normalize("disabled", customyesno) == "disabled" + assert BooleanQuestion.humanize("no", customyesno) == "no" + assert BooleanQuestion.humanize("false", customyesno) == "no" + assert BooleanQuestion.humanize("disabled", customyesno) == "no" + + def test_normalize_domain(): - assert DomainQuestion("https://yolo.swag/") == "yolo.swag" - assert DomainQuestion("http://yolo.swag") == "yolo.swag" - assert DomainQuestion("yolo.swag/") == "yolo.swag" + 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("macnuggets") == "/macnuggets" - assert PathQuestion("mac/nuggets") == "/mac/nuggets" - assert PathQuestion("/macnuggets/") == "/macnuggets" - assert PathQuestion("macnuggets/") == "/macnuggets" - assert PathQuestion("////macnuggets///") == "/macnuggets" + assert PathQuestion.normalize("") == "/" + assert PathQuestion.normalize("") == "/" + assert PathQuestion.normalize("macnuggets") == "/macnuggets" + assert PathQuestion.normalize("/macnuggets") == "/macnuggets" + assert PathQuestion.normalize(" /macnuggets ") == "/macnuggets" + assert PathQuestion.normalize("/macnuggets") == "/macnuggets" + assert PathQuestion.normalize("mac/nuggets") == "/mac/nuggets" + assert PathQuestion.normalize("/macnuggets/") == "/macnuggets" + assert PathQuestion.normalize("macnuggets/") == "/macnuggets" + assert PathQuestion.normalize("////macnuggets///") == "/macnuggets" diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 604e20f4f..179e9640f 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -730,7 +730,23 @@ class PathQuestion(Question): @staticmethod def normalize(value, option={}): - return "/" + value.strip("/") + + option = option.__dict__ if isinstance(option, Question) else option + + 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): @@ -742,6 +758,8 @@ class BooleanQuestion(Question): @staticmethod def humanize(value, option={}): + option = option.__dict__ if isinstance(option, Question) else option + yes = option.get("yes", 1) no = option.get("no", 0) @@ -756,50 +774,45 @@ class BooleanQuestion(Question): raise YunohostValidationError( "app_argument_choice_invalid", - name=getattr(option, "name", None) or option.get("name"), + 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() - yes = option.get("yes", 1) - no = option.get("no", 0) + 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() - # - # N.B.: - # we check first if the value is equal to yes - # then if equal to no - # then if in yes_answers - # then if in no_answers - # - # This is to hopefully cover the weird edgecase - # where the value for yes/no could be reversed - # such as yes=false or yes=0 - # no=true no=1 - # + if strvalue in yes_answers: + return technical_yes + if strvalue in no_answers: + return technical_no - if strvalue == str(yes).lower(): - return yes - if strvalue == str(no).lower(): - return no - if strvalue in BooleanQuestion.yes_answers: - return yes - if strvalue in BooleanQuestion.no_answers: - return no - - if value in [None, ""]: + if strvalue in ["none", ""]: return None raise YunohostValidationError( "app_argument_choice_invalid", - name=getattr(option, "name", None) or option.get("name"), - value=value, + name=option.get("name"), + value=strvalue, choices="yes/no", ) @@ -898,6 +911,7 @@ class NumberQuestion(Question): @staticmethod def normalize(value, option={}): + if isinstance(value, int): return value @@ -910,9 +924,10 @@ class NumberQuestion(Question): if value in [None, ""]: return value + option = option.__dict__ if isinstance(option, Question) else option raise YunohostValidationError( "app_argument_invalid", - name=getattr(option, "name", None) or option.get("name"), + name=option.get("name"), error=m18n.n("invalid_number") ) From 79126809eb06b54d19d2ac093f4a0ecb72682192 Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 23 Sep 2021 18:39:36 +0200 Subject: [PATCH 016/142] [enh] Bind function for hotspot --- data/helpers.d/config | 291 +++++++++++++++++++++++------------------- 1 file changed, 160 insertions(+), 131 deletions(-) diff --git a/data/helpers.d/config b/data/helpers.d/config index 7a2ccde46..d12316996 100644 --- a/data/helpers.d/config +++ b/data/helpers.d/config @@ -1,6 +1,154 @@ #!/bin/bash +_ynh_app_config_get_one() { + local short_setting="$1" + local type="$2" + local bind="$3" + local getter="get__${short_setting}" + # Get value from getter if exists + if type -t $getter 2>/dev/null | grep -q '^function$' 2>/dev/null; + then + old[$short_setting]="$($getter)" + formats[${short_setting}]="yaml" + + elif [[ "$bind" == *"("* ]] && type -t "get__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; + then + old[$short_setting]="$("get__${bind%%(*}" $short_setting $type $bind)" + formats[${short_setting}]="yaml" + + elif [[ "$bind" == "null" ]] + then + old[$short_setting]="YNH_NULL" + + # Get value from app settings or from another file + elif [[ "$type" == "file" ]] + then + if [[ "$bind" == "settings" ]] + then + ynh_die --message="File '${short_setting}' can't be stored in settings" + fi + old[$short_setting]="$(ls "$(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" 2> /dev/null || echo YNH_NULL)" + file_hash[$short_setting]="true" + + # Get multiline text from settings or from a full file + elif [[ "$type" == "text" ]] + then + if [[ "$bind" == "settings" ]] + then + old[$short_setting]="$(ynh_app_setting_get $app $short_setting)" + elif [[ "$bind" == *":"* ]] + then + ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" + else + old[$short_setting]="$(cat $(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2> /dev/null || echo YNH_NULL)" + fi + + # Get value from a kind of key/value file + else + local bind_after="" + if [[ "$bind" == "settings" ]] + then + bind=":/etc/yunohost/apps/$app/settings.yml" + fi + local bind_key="$(echo "$bind" | cut -d: -f1)" + bind_key=${bind_key:-$short_setting} + if [[ "$bind_key" == *">"* ]]; + then + bind_after="$(echo "${bind_key}" | cut -d'>' -f1)" + bind_key="$(echo "${bind_key}" | cut -d'>' -f2)" + fi + local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key}" --after="${bind_after}")" + + fi +} +_ynh_app_config_apply_one() { + local short_setting="$1" + local setter="set__${short_setting}" + local bind="${binds[$short_setting]}" + local type="${types[$short_setting]}" + if [ "${changed[$short_setting]}" == "true" ] + then + # Apply setter if exists + if type -t $setter 2>/dev/null | grep -q '^function$' 2>/dev/null; + then + $setter + + elif [[ "$bind" == *"("* ]] && type -t "set__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; + then + "set__${bind%%(*}" $short_setting $type $bind + + elif [[ "$bind" == "null" ]] + then + continue + + # Save in a file + elif [[ "$type" == "file" ]] + then + if [[ "$bind" == "settings" ]] + then + ynh_die --message="File '${short_setting}' can't be stored in settings" + fi + local bind_file="$(echo "$bind" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + if [[ "${!short_setting}" == "" ]] + then + ynh_backup_if_checksum_is_different --file="$bind_file" + ynh_secure_remove --file="$bind_file" + ynh_delete_file_checksum --file="$bind_file" --update_only + ynh_print_info --message="File '$bind_file' removed" + else + ynh_backup_if_checksum_is_different --file="$bind_file" + if [[ "${!short_setting}" != "$bind_file" ]] + then + cp "${!short_setting}" "$bind_file" + fi + ynh_store_file_checksum --file="$bind_file" --update_only + ynh_print_info --message="File '$bind_file' overwrited with ${!short_setting}" + fi + + # Save value in app settings + elif [[ "$bind" == "settings" ]] + then + ynh_app_setting_set --app=$app --key=$short_setting --value="${!short_setting}" + ynh_print_info --message="Configuration key '$short_setting' edited in app settings" + + # Save multiline text in a file + elif [[ "$type" == "text" ]] + then + if [[ "$bind" == *":"* ]] + then + ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" + fi + local bind_file="$(echo "$bind" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + ynh_backup_if_checksum_is_different --file="$bind_file" + echo "${!short_setting}" > "$bind_file" + ynh_store_file_checksum --file="$bind_file" --update_only + ynh_print_info --message="File '$bind_file' overwrited with the content you provieded in '${short_setting}' question" + + # Set value into a kind of key/value file + else + local bind_after="" + local bind_key="$(echo "$bind" | cut -d: -f1)" + bind_key=${bind_key:-$short_setting} + if [[ "$bind_key" == *">"* ]]; + then + bind_after="$(echo "${bind_key}" | cut -d'>' -f1)" + bind_key="$(echo "${bind_key}" | cut -d'>' -f2)" + fi + local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + + ynh_backup_if_checksum_is_different --file="$bind_file" + ynh_write_var_in_file --file="${bind_file}" --key="${bind_key}" --value="${!short_setting}" --after="${bind_after}" + ynh_store_file_checksum --file="$bind_file" --update_only + + # We stored the info in settings in order to be able to upgrade the app + ynh_app_setting_set --app=$app --key=$short_setting --value="${!short_setting}" + ynh_print_info --message="Configuration key '$bind_key' edited into $bind_file" + + fi + fi +} _ynh_app_config_get() { # From settings local lines @@ -29,62 +177,11 @@ EOL do # Split line into short_setting, type and bind IFS=';' read short_setting type bind <<< "$line" - local getter="get__${short_setting}" binds[${short_setting}]="$bind" types[${short_setting}]="$type" file_hash[${short_setting}]="" formats[${short_setting}]="" - # Get value from getter if exists - if type -t $getter 2>/dev/null | grep -q '^function$' 2>/dev/null; - then - old[$short_setting]="$($getter)" - formats[${short_setting}]="yaml" - - elif [[ "$bind" == "null" ]] - then - old[$short_setting]="YNH_NULL" - - # Get value from app settings or from another file - elif [[ "$type" == "file" ]] - then - if [[ "$bind" == "settings" ]] - then - ynh_die --message="File '${short_setting}' can't be stored in settings" - fi - old[$short_setting]="$(ls "$(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" 2> /dev/null || echo YNH_NULL)" - file_hash[$short_setting]="true" - - # Get multiline text from settings or from a full file - elif [[ "$type" == "text" ]] - then - if [[ "$bind" == "settings" ]] - then - old[$short_setting]="$(ynh_app_setting_get $app $short_setting)" - elif [[ "$bind" == *":"* ]] - then - ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" - else - old[$short_setting]="$(cat $(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2> /dev/null || echo YNH_NULL)" - fi - - # Get value from a kind of key/value file - else - local bind_after="" - if [[ "$bind" == "settings" ]] - then - bind=":/etc/yunohost/apps/$app/settings.yml" - fi - local bind_key="$(echo "$bind" | cut -d: -f1)" - bind_key=${bind_key:-$short_setting} - if [[ "$bind_key" == *">"* ]]; - then - bind_after="$(echo "${bind_key}" | cut -d'>' -f1)" - bind_key="$(echo "${bind_key}" | cut -d'>' -f2)" - fi - local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" - old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key}" --after="${bind_after}")" - - fi + ynh_app_config_get_one $short_setting $type $bind done @@ -93,85 +190,7 @@ EOL _ynh_app_config_apply() { for short_setting in "${!old[@]}" do - local setter="set__${short_setting}" - local bind="${binds[$short_setting]}" - local type="${types[$short_setting]}" - if [ "${changed[$short_setting]}" == "true" ] - then - # Apply setter if exists - if type -t $setter 2>/dev/null | grep -q '^function$' 2>/dev/null; - then - $setter - - elif [[ "$bind" == "null" ]] - then - continue - - # Save in a file - elif [[ "$type" == "file" ]] - then - if [[ "$bind" == "settings" ]] - then - ynh_die --message="File '${short_setting}' can't be stored in settings" - fi - local bind_file="$(echo "$bind" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" - if [[ "${!short_setting}" == "" ]] - then - ynh_backup_if_checksum_is_different --file="$bind_file" - ynh_secure_remove --file="$bind_file" - ynh_delete_file_checksum --file="$bind_file" --update_only - ynh_print_info --message="File '$bind_file' removed" - else - ynh_backup_if_checksum_is_different --file="$bind_file" - if [[ "${!short_setting}" != "$bind_file" ]] - then - cp "${!short_setting}" "$bind_file" - fi - ynh_store_file_checksum --file="$bind_file" --update_only - ynh_print_info --message="File '$bind_file' overwrited with ${!short_setting}" - fi - - # Save value in app settings - elif [[ "$bind" == "settings" ]] - then - ynh_app_setting_set --app=$app --key=$short_setting --value="${!short_setting}" - ynh_print_info --message="Configuration key '$short_setting' edited in app settings" - - # Save multiline text in a file - elif [[ "$type" == "text" ]] - then - if [[ "$bind" == *":"* ]] - then - ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" - fi - local bind_file="$(echo "$bind" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" - ynh_backup_if_checksum_is_different --file="$bind_file" - echo "${!short_setting}" > "$bind_file" - ynh_store_file_checksum --file="$bind_file" --update_only - ynh_print_info --message="File '$bind_file' overwrited with the content you provieded in '${short_setting}' question" - - # Set value into a kind of key/value file - else - local bind_after="" - local bind_key="$(echo "$bind" | cut -d: -f1)" - bind_key=${bind_key:-$short_setting} - if [[ "$bind_key" == *">"* ]]; - then - bind_after="$(echo "${bind_key}" | cut -d'>' -f1)" - bind_key="$(echo "${bind_key}" | cut -d'>' -f2)" - fi - local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" - - ynh_backup_if_checksum_is_different --file="$bind_file" - ynh_write_var_in_file --file="${bind_file}" --key="${bind_key}" --value="${!short_setting}" --after="${bind_after}" - ynh_store_file_checksum --file="$bind_file" --update_only - - # We stored the info in settings in order to be able to upgrade the app - ynh_app_setting_set --app=$app --key=$short_setting --value="${!short_setting}" - ynh_print_info --message="Configuration key '$bind_key' edited into $bind_file" - - fi - fi + ynh_app_config_apply_one $short_setting done } @@ -253,6 +272,9 @@ _ynh_app_config_validate() { if type -t validate__$short_setting | grep -q '^function$' 2>/dev/null; then result="$(validate__$short_setting)" + elif [[ "$bind" == *"("* ]] && type -t "validate__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; + then + "validate__${bind%%(*}" $short_setting fi if [ -n "$result" ] then @@ -283,6 +305,10 @@ _ynh_app_config_validate() { } +ynh_app_config_get_one() { + _ynh_app_config_get_one $1 $2 $3 +} + ynh_app_config_get() { _ynh_app_config_get } @@ -295,6 +321,9 @@ ynh_app_config_validate() { _ynh_app_config_validate } +ynh_app_config_apply_one() { + _ynh_app_config_apply_one $1 +} ynh_app_config_apply() { _ynh_app_config_apply } From 6b8cb0c00508f8a94d2678b02277e08737e3b42b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 18:57:01 +0200 Subject: [PATCH 017/142] Hmpf refactor moar stuff but does this ever ends --- src/yunohost/app.py | 90 ++++++++++++++------------- src/yunohost/tests/test_app_config.py | 6 +- src/yunohost/utils/config.py | 37 ++++++----- 3 files changed, 74 insertions(+), 59 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 396ce04e7..01c0cb0cd 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -34,6 +34,7 @@ import subprocess import glob import tempfile from collections import OrderedDict +from typing import List from moulinette import Moulinette, m18n from moulinette.core import MoulinetteError @@ -55,6 +56,7 @@ from yunohost.utils import packages from yunohost.utils.config import ( ConfigPanel, ask_questions_and_parse_answers, + Question, DomainQuestion, PathQuestion ) @@ -899,11 +901,13 @@ def app_install( app_instance_name = app_id # Retrieve arguments list for install script - questions = manifest.get("arguments", {}).get("install", {}) - args = ask_questions_and_parse_answers(questions, prefilled_answers=args) + raw_questions = manifest.get("arguments", {}).get("install", {}) + questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) + args = {question.name: question.value for question in questions if question.value is not None} # Validate domain / path availability for webapps - _validate_and_normalize_webpath(args, extracted_app_folder) + path_requirement = _guess_webapp_path_requirement(questions, extracted_app_folder) + _validate_webpath_requirement(questions, path_requirement) # Attempt to patch legacy helpers ... _patch_legacy_helpers(extracted_app_folder) @@ -972,9 +976,10 @@ def app_install( env_dict["YNH_APP_BASEDIR"] = extracted_app_folder env_dict_for_logging = env_dict.copy() - for arg_name, arg_value_and_type in args.items(): - if arg_value_and_type[1] == "password": - del env_dict_for_logging["YNH_APP_ARG_%s" % arg_name.upper()] + for question in questions: + # Or should it be more generally question.redact ? + if question.type == "password": + del env_dict_for_logging["YNH_APP_ARG_%s" % question.name.upper()] operation_logger.extra.update({"env": env_dict_for_logging}) @@ -1635,8 +1640,9 @@ def app_action_run(operation_logger, app, action, args=None): action_declaration = actions[action] # Retrieve arguments list for install script - questions = actions[action].get("arguments", {}) - args = ask_questions_and_parse_answers(questions, prefilled_answers=args) + raw_questions = actions[action].get("arguments", {}) + questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) + args = {question.name: question.value for question in questions if question.value is not None} tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) @@ -2371,35 +2377,20 @@ def _check_manifest_requirements(manifest, app_instance_name): ) -def _validate_and_normalize_webpath(args_dict, app_folder): +def _guess_webapp_path_requirement(questions: List[Question], app_folder: str) -> str: # If there's only one "domain" and "path", validate that domain/path # is an available url and normalize the path. - domain_args = [ - (name, value[0]) for name, value in args_dict.items() if value[1] == "domain" - ] - path_args = [ - (name, value[0]) for name, value in args_dict.items() if value[1] == "path" - ] + domain_questions = [question for question in questions if question.type == "domain"] + path_questions = [question for question in questions if question.type == "path"] - if len(domain_args) == 1 and len(path_args) == 1: - - domain = domain_args[0][1] - path = path_args[0][1] - - domain = DomainQuestion.normalize(domain) - path = PathQuestion.normalize(path) - - # Check the url is available - _assert_no_conflicting_apps(domain, path) - - # (We save this normalized path so that the install script have a - # standard path format to deal with no matter what the user inputted) - args_dict[path_args[0][0]] = (path, "path") - - # This is likely to be a full-domain app... - elif len(domain_args) == 1 and len(path_args) == 0: + if len(domain_questions) == 0 and len(path_questions) == 0: + return None + 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 @@ -2409,18 +2400,33 @@ def _validate_and_normalize_webpath(args_dict, app_folder): # 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 = open( - os.path.join(app_folder, "scripts/install") - ).read() + install_script_content = read_file(os.path.join(app_folder, "scripts/install")) if re.search( - r"\npath(_url)?=[\"']?/[\"']?\n", install_script_content + r"\npath(_url)?=[\"']?/[\"']?", install_script_content ) and re.search( - r"(ynh_webpath_register|yunohost app checkurl)", install_script_content + r"ynh_webpath_register", install_script_content ): + return "full_domain" - domain = domain_args[0][1] - _assert_no_conflicting_apps(domain, "/", full_domain=True) + return "?" + + +def _validate_webpath_requirement(questions: List[Question], path_requirement: str) -> None: + + domain_questions = [question for question in questions if question.type == "domain"] + path_questions = [question for question in questions if question.type == "path"] + + if path_requirement == "domain_and_path": + + domain = domain_questions[0].value + path = path_questions[0].value + _assert_no_conflicting_apps(domain, path, full_domain=True) + + elif path_requirement == "full_domain": + + domain = domain_questions[0].value + _assert_no_conflicting_apps(domain, "/", full_domain=True) def _get_conflicting_apps(domain, path, ignore_app=None): @@ -2499,10 +2505,8 @@ def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"): "YNH_APP_MANIFEST_VERSION": manifest.get("version", "?"), } - for arg_name, arg_value_and_type in args.items(): - env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = str( - arg_value_and_type[0] - ) + for arg_name, arg_value in args.items(): + env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = str(arg_value) return env_dict diff --git a/src/yunohost/tests/test_app_config.py b/src/yunohost/tests/test_app_config.py index d705076c4..248f035f5 100644 --- a/src/yunohost/tests/test_app_config.py +++ b/src/yunohost/tests/test_app_config.py @@ -2,9 +2,11 @@ import glob import os import shutil import pytest +from mock import patch from .conftest import get_test_apps_dir +from moulinette import Moulinette from moulinette.utils.filesystem import read_file from yunohost.domain import _get_maindomain @@ -146,7 +148,9 @@ def test_app_config_regular_setting(config_app): assert app_config_get(config_app, "main.components.boolean") == "1" assert app_setting(config_app, "boolean") == "1" - with pytest.raises(YunohostValidationError): + with pytest.raises(YunohostValidationError), \ + patch.object(os, "isatty", return_value=False), \ + patch.object(Moulinette, "prompt", return_value="pwet"): app_config_set(config_app, "main.components.boolean", "pwet") diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 179e9640f..e5f078267 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -380,14 +380,13 @@ class ConfigPanel: display_header(f"\n# {name}") # Check and ask unanswered questions - self.new_values.update( - ask_questions_and_parse_answers(section["options"], self.args) - ) - self.new_values = { - key: value[0] - for key, value in self.new_values.items() - if not value[0] is None - } + questions = ask_questions_and_parse_answers(section["options"], self.args) + self.new_values.update({ + question.name: question.value + for question in questions + if question.value is not None + }) + self.errors = None def _get_default_values(self): @@ -538,9 +537,10 @@ class Question(object): raise break + self.value = self._post_parse_value() - return (self.value, self.argument_type) + return self.value def _prevalidate(self): if self.value in [None, ""] and not self.optional: @@ -1058,7 +1058,7 @@ ARGUMENTS_TYPE_PARSERS = { } -def ask_questions_and_parse_answers(questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}) -> Mapping[str, Any]: +def ask_questions_and_parse_answers(questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}) -> 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. @@ -1071,17 +1071,24 @@ def ask_questions_and_parse_answers(questions: Dict, prefilled_answers: Union[st """ if isinstance(prefilled_answers, str): - prefilled_answers = dict(urllib.parse.parse_qs(prefilled_answers or "", keep_blank_values=True)) + # 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) + prefilled_answers = dict(urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True)) - out = OrderedDict() + if not prefilled_answers: + prefilled_answers = {} + + out = [] for question in questions: question_class = ARGUMENTS_TYPE_PARSERS[question.get("type", "string")] question["value"] = prefilled_answers.get(question["name"]) question = question_class(question) - answer = question.ask_if_needed() - if answer is not None: - out[question.name] = answer + question.ask_if_needed() + out.append(question) return out From 74102b607d54b94aeafdbaf999659ef50cb96eb4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 19:21:48 +0200 Subject: [PATCH 018/142] Simplify error management --- locales/en.json | 2 +- src/yunohost/utils/config.py | 29 ++++++----------------------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/locales/en.json b/locales/en.json index 3354dc19c..99432492c 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_argument_choice_invalid": "Use one of these choices '{choices}' for the argument '{name}' instead of '{value}'", + "app_argument_choice_invalid": "Pick a valid value for argument '{name}': '{value}' is not among the available choices ({choices})", "app_argument_invalid": "Pick a valid value for the argument '{name}': {error}", "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reason", "app_argument_required": "Argument '{name}' is required", diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index e5f078267..012d2ed17 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -549,7 +549,12 @@ class Question(object): # we have an answer, do some post checks if self.value not in [None, ""]: if self.choices and self.value not in self.choices: - self._raise_invalid_answer() + raise YunohostValidationError( + "app_argument_choice_invalid", + name=self.name, + value=self.value, + choices=", ".join(self.choices), + ) if self.pattern and not re.match(self.pattern["regexp"], str(self.value)): raise YunohostValidationError( self.pattern["error"], @@ -557,14 +562,6 @@ class Question(object): value=self.value, ) - def _raise_invalid_answer(self): - raise YunohostValidationError( - "app_argument_choice_invalid", - name=self.name, - value=self.value, - choices=", ".join(self.choices), - ) - def _format_text_for_user_input_in_cli(self): text_for_user_input_in_cli = _value_for_locale(self.ask) @@ -847,13 +844,6 @@ class DomainQuestion(Question): self.choices = domain_list()["domains"] - def _raise_invalid_answer(self): - raise YunohostValidationError( - "app_argument_invalid", - name=self.name, - error=m18n.n("domain_name_unknown", domain=self.value), - ) - @staticmethod def normalize(value, option={}): if value.startswith("https://"): @@ -891,13 +881,6 @@ class UserQuestion(Question): self.default = user break - def _raise_invalid_answer(self): - raise YunohostValidationError( - "app_argument_invalid", - name=self.name, - error=m18n.n("user_unknown", user=self.value), - ) - class NumberQuestion(Question): argument_type = "number" From e001f26f874e112166481bea3eb5cd23b1c8345c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 19:22:54 +0200 Subject: [PATCH 019/142] Stale i18n string --- locales/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 99432492c..1d1bafd58 100644 --- a/locales/en.json +++ b/locales/en.json @@ -364,7 +364,6 @@ "extracting": "Extracting...", "field_invalid": "Invalid field '{}'", "file_does_not_exist": "The file {path} does not exist.", - "file_extension_not_accepted": "Refusing file '{path}' because its extension is not among the accepted extensions: {accept}", "firewall_reload_failed": "Could not reload the firewall", "firewall_reloaded": "Firewall reloaded", "firewall_rules_cmd_failed": "Some firewall rule commands have failed. More info in log.", From 8cc229e19b123dc31afaba4af25b11161b14db93 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 20:04:27 +0200 Subject: [PATCH 020/142] Zblerg propagate changes on tests --- src/yunohost/app.py | 2 +- src/yunohost/tests/test_questions.py | 519 ++++++++++++++++----------- 2 files changed, 304 insertions(+), 217 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 01c0cb0cd..8d24e28c5 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2386,7 +2386,7 @@ def _guess_webapp_path_requirement(questions: List[Question], app_folder: str) - path_questions = [question for question in questions if question.type == "path"] if len(domain_questions) == 0 and len(path_questions) == 0: - return None + return "" if len(domain_questions) == 1 and len(path_questions) == 1: return "domain_and_path" if len(domain_questions) == 1 and len(path_questions) == 0: diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index cf4a8832d..01b46097a 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -43,7 +43,7 @@ User answers: def test_question_empty(): - assert ask_questions_and_parse_answers([], {}) == {} + ask_questions_and_parse_answers([], {}) == [] def test_question_string(): @@ -54,8 +54,12 @@ def test_question_string(): } ] answers = {"some_string": "some_value"} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) - assert ask_questions_and_parse_answers(questions, answers) == expected_result + + 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(): @@ -65,8 +69,13 @@ def test_question_string_default_type(): } ] answers = {"some_string": "some_value"} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) - assert ask_questions_and_parse_answers(questions, answers) == expected_result + + 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(): @@ -89,12 +98,15 @@ def test_question_string_input(): } ] answers = {} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -104,12 +116,15 @@ def test_question_string_input_no_ask(): } ] answers = {} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -120,9 +135,12 @@ def test_question_string_no_input_optional(): } ] answers = {} - expected_result = OrderedDict({"some_string": ("", "string")}) with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -134,12 +152,15 @@ def test_question_string_optional_with_input(): } ] answers = {} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -151,12 +172,15 @@ def test_question_string_optional_with_empty_input(): } ] answers = {} - expected_result = OrderedDict({"some_string": ("", "string")}) with patch.object(Moulinette, "prompt", return_value=""), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -167,12 +191,15 @@ def test_question_string_optional_with_input_without_ask(): } ] answers = {} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -184,9 +211,12 @@ def test_question_string_no_input_default(): } ] answers = {} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -286,18 +316,24 @@ def test_question_string_input_test_ask_with_help(): def test_question_string_with_choice(): questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] answers = {"some_string": "fr"} - expected_result = OrderedDict({"some_string": ("fr", "string")}) - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "fr" def test_question_string_with_choice_prompt(): questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] answers = {"some_string": "fr"} - expected_result = OrderedDict({"some_string": ("fr", "string")}) with patch.object(Moulinette, "prompt", return_value="fr"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "fr" def test_question_string_with_choice_bad(): @@ -305,7 +341,7 @@ def test_question_string_with_choice_bad(): answers = {"some_string": "bad"} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) + ask_questions_and_parse_answers(questions, answers) def test_question_string_with_choice_ask(): @@ -340,9 +376,12 @@ def test_question_string_with_choice_default(): } ] answers = {} - expected_result = OrderedDict({"some_string": ("en", "string")}) with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "en" def test_question_password(): @@ -353,8 +392,11 @@ def test_question_password(): } ] answers = {"some_password": "some_value"} - expected_result = OrderedDict({"some_password": ("some_value", "password")}) - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -379,12 +421,15 @@ def test_question_password_input(): } ] answers = {} - expected_result = OrderedDict({"some_password": ("some_value", "password")}) with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -395,12 +440,15 @@ def test_question_password_input_no_ask(): } ] answers = {} - expected_result = OrderedDict({"some_password": ("some_value", "password")}) with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -412,17 +460,29 @@ def test_question_password_no_input_optional(): } ] answers = {} - expected_result = OrderedDict({"some_password": ("", "password")}) with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_password" + assert out.type == "password" + assert out.value == "" questions = [ - {"name": "some_password", "type": "password", "optional": True, "default": ""} + { + "name": "some_password", + "type": "password", + "optional": True, + "default": "" + } ] with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -435,12 +495,15 @@ def test_question_password_optional_with_input(): } ] answers = {} - expected_result = OrderedDict({"some_password": ("some_value", "password")}) with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -453,12 +516,15 @@ def test_question_password_optional_with_empty_input(): } ] answers = {} - expected_result = OrderedDict({"some_password": ("", "password")}) with patch.object(Moulinette, "prompt", return_value=""), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -470,12 +536,15 @@ def test_question_password_optional_with_input_without_ask(): } ] answers = {} - expected_result = OrderedDict({"some_password": ("some_value", "password")}) with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -642,8 +711,11 @@ def test_question_path(): } ] answers = {"some_path": "/some_value"} - expected_result = OrderedDict({"some_path": ("/some_value", "path")}) - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -668,12 +740,15 @@ def test_question_path_input(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("/some_value", "path")}) with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -684,12 +759,15 @@ def test_question_path_input_no_ask(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("/some_value", "path")}) with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -701,9 +779,12 @@ def test_question_path_no_input_optional(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("", "path")}) with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -716,12 +797,15 @@ def test_question_path_optional_with_input(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("/some_value", "path")}) with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -734,12 +818,15 @@ def test_question_path_optional_with_empty_input(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("", "path")}) with patch.object(Moulinette, "prompt", return_value=""), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -751,12 +838,15 @@ def test_question_path_optional_with_input_without_ask(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("/some_value", "path")}) with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -769,9 +859,12 @@ def test_question_path_no_input_default(): } ] answers = {} - expected_result = OrderedDict({"some_path": ("/some_value", "path")}) with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -880,8 +973,11 @@ def test_question_boolean(): } ] answers = {"some_boolean": "y"} - expected_result = OrderedDict({"some_boolean": (1, "boolean")}) - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -891,50 +987,12 @@ def test_question_boolean_all_yes(): "type": "boolean", } ] - expected_result = OrderedDict({"some_boolean": (1, "boolean")}) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "y"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "Y"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "yes"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "Yes"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "YES"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "1"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": 1}) == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": True}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "True"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "TRUE"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "true"}) - == expected_result - ) + + 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(): @@ -944,50 +1002,12 @@ def test_question_boolean_all_no(): "type": "boolean", } ] - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "n"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "N"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "no"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "No"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "No"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "0"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": 0}) == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": False}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "False"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "FALSE"}) - == expected_result - ) - assert ( - ask_questions_and_parse_answers(questions, {"some_boolean": "false"}) - == expected_result - ) + + 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 @@ -1000,9 +1020,10 @@ def test_question_boolean_no_input(): ] answers = {} - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.value == 0 def test_question_boolean_bad_input(): @@ -1028,17 +1049,17 @@ def test_question_boolean_input(): ] answers = {} - expected_result = OrderedDict({"some_boolean": (1, "boolean")}) with patch.object(Moulinette, "prompt", return_value="y"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + assert out.value == 1 - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) with patch.object(Moulinette, "prompt", return_value="n"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + assert out.value == 0 def test_question_boolean_input_no_ask(): @@ -1049,12 +1070,12 @@ def test_question_boolean_input_no_ask(): } ] answers = {} - expected_result = OrderedDict({"some_boolean": (1, "boolean")}) with patch.object(Moulinette, "prompt", return_value="y"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + assert out.value == 1 def test_question_boolean_no_input_optional(): @@ -1066,9 +1087,9 @@ def test_question_boolean_no_input_optional(): } ] answers = {} - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + assert out.value == 0 def test_question_boolean_optional_with_input(): @@ -1081,12 +1102,12 @@ def test_question_boolean_optional_with_input(): } ] answers = {} - expected_result = OrderedDict({"some_boolean": (1, "boolean")}) with patch.object(Moulinette, "prompt", return_value="y"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + assert out.value == 1 def test_question_boolean_optional_with_empty_input(): @@ -1099,12 +1120,13 @@ def test_question_boolean_optional_with_empty_input(): } ] answers = {} - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false with patch.object(Moulinette, "prompt", return_value=""), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.value == 0 def test_question_boolean_optional_with_input_without_ask(): @@ -1116,12 +1138,13 @@ def test_question_boolean_optional_with_input_without_ask(): } ] answers = {} - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) with patch.object(Moulinette, "prompt", return_value="n"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.value == 0 def test_question_boolean_no_input_default(): @@ -1134,9 +1157,11 @@ def test_question_boolean_no_input_default(): } ] answers = {} - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) + with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.value == 0 def test_question_boolean_bad_default(): @@ -1215,7 +1240,6 @@ def test_question_domain_empty(): } ] main_domain = "my_main_domain.com" - expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) answers = {} with patch.object( @@ -1225,7 +1249,11 @@ def test_question_domain_empty(): ), patch.object( os, "isatty", return_value=False ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1239,12 +1267,15 @@ def test_question_domain(): ] answers = {"some_domain": main_domain} - expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) with patch.object( domain, "_get_maindomain", return_value=main_domain ), patch.object(domain, "domain_list", return_value={"domains": domains}): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1259,20 +1290,26 @@ def test_question_domain_two_domains(): } ] answers = {"some_domain": other_domain} - expected_result = OrderedDict({"some_domain": (other_domain, "domain")}) with patch.object( domain, "_get_maindomain", return_value=main_domain ), patch.object(domain, "domain_list", return_value={"domains": domains}): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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} - expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) with patch.object( domain, "_get_maindomain", return_value=main_domain ), patch.object(domain, "domain_list", return_value={"domains": domains}): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1309,7 +1346,6 @@ def test_question_domain_two_domains_default_no_ask(): } ] answers = {} - expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) with patch.object( domain, "_get_maindomain", return_value=main_domain @@ -1318,7 +1354,11 @@ def test_question_domain_two_domains_default_no_ask(): ), patch.object( os, "isatty", return_value=False ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1328,7 +1368,6 @@ def test_question_domain_two_domains_default(): questions = [{"name": "some_domain", "type": "domain", "ask": "choose a domain"}] answers = {} - expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) with patch.object( domain, "_get_maindomain", return_value=main_domain @@ -1337,7 +1376,11 @@ def test_question_domain_two_domains_default(): ), patch.object( os, "isatty", return_value=False ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1355,13 +1398,19 @@ def test_question_domain_two_domains_default_input(): ), patch.object( os, "isatty", return_value=True ): - expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) with patch.object(Moulinette, "prompt", return_value=main_domain): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_domain" + assert out.type == "domain" + assert out.value == main_domain - expected_result = OrderedDict({"some_domain": (other_domain, "domain")}) with patch.object(Moulinette, "prompt", return_value=other_domain): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1410,12 +1459,14 @@ def test_question_user(): ] answers = {"some_user": username} - expected_result = OrderedDict({"some_user": (username, "user")}) - with patch.object(user, "user_list", return_value={"users": users}), patch.object( user, "user_info", return_value={} ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1445,20 +1496,26 @@ def test_question_user_two_users(): } ] answers = {"some_user": other_user} - expected_result = OrderedDict({"some_user": (other_user, "user")}) with patch.object(user, "user_list", return_value={"users": users}), patch.object( user, "user_info", return_value={} ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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} - expected_result = OrderedDict({"some_user": (username, "user")}) with patch.object(user, "user_list", return_value={"users": users}), patch.object( user, "user_info", return_value={} ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1553,17 +1610,20 @@ def test_question_user_two_users_default_input(): os, "isatty", return_value=True ): with patch.object(user, "user_info", return_value={}): - expected_result = OrderedDict({"some_user": (username, "user")}) - with patch.object(Moulinette, "prompt", return_value=username): - assert ( - ask_questions_and_parse_answers(questions, answers) == expected_result - ) - expected_result = OrderedDict({"some_user": (other_user, "user")}) + 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): - assert ( - ask_questions_and_parse_answers(questions, answers) == expected_result - ) + 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(): @@ -1574,8 +1634,11 @@ def test_question_number(): } ] answers = {"some_number": 1337} - expected_result = OrderedDict({"some_number": (1337, "number")}) - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1618,23 +1681,32 @@ def test_question_number_input(): ] answers = {} - expected_result = OrderedDict({"some_number": (1337, "number")}) with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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 ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value == 1337 - expected_result = OrderedDict({"some_number": (0, "number")}) with patch.object(Moulinette, "prompt", return_value="0"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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 = [ @@ -1644,12 +1716,15 @@ def test_question_number_input_no_ask(): } ] answers = {} - expected_result = OrderedDict({"some_number": (1337, "number")}) with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1661,9 +1736,12 @@ def test_question_number_no_input_optional(): } ] answers = {} - expected_result = OrderedDict({"some_number": (None, "number")}) # default to 0 with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value == None def test_question_number_optional_with_input(): @@ -1676,12 +1754,15 @@ def test_question_number_optional_with_input(): } ] answers = {} - expected_result = OrderedDict({"some_number": (1337, "number")}) with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1693,12 +1774,15 @@ def test_question_number_optional_with_input_without_ask(): } ] answers = {} - expected_result = OrderedDict({"some_number": (0, "number")}) with patch.object(Moulinette, "prompt", return_value="0"), patch.object( os, "isatty", return_value=True ): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1711,9 +1795,12 @@ def test_question_number_no_input_default(): } ] answers = {} - expected_result = OrderedDict({"some_number": (1337, "number")}) with patch.object(os, "isatty", return_value=False): - assert ask_questions_and_parse_answers(questions, answers) == expected_result + 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(): @@ -1865,7 +1952,7 @@ def test_normalize_boolean_nominal(): assert BooleanQuestion.normalize(" ") is None assert BooleanQuestion.normalize(" none ") is None assert BooleanQuestion.normalize("None") is None - assert BooleanQuestion.normalize("none") is None + assert BooleanQuestion.normalize("noNe") is None assert BooleanQuestion.normalize(None) is None From 0693aa45de3aef0bf0ae8352c033d945b205de8f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 20:54:31 +0200 Subject: [PATCH 021/142] Add tests for FileQuestion --- src/yunohost/tests/test_questions.py | 94 +++++++++++++++++++++++++++- src/yunohost/utils/config.py | 47 +++++++------- 2 files changed, 117 insertions(+), 24 deletions(-) diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index 01b46097a..d141f53cc 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -14,7 +14,8 @@ from yunohost.utils.config import ( PasswordQuestion, DomainQuestion, PathQuestion, - BooleanQuestion + BooleanQuestion, + FileQuestion ) from yunohost.utils.error import YunohostError, YunohostValidationError @@ -62,6 +63,23 @@ def test_question_string(): assert out.value == "some_value" +def test_question_string_from_query_string(): + + questions = [ + { + "name": "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 = [ { @@ -1916,7 +1934,13 @@ def test_question_number_input_test_ask_with_help(): def test_question_display_text(): - questions = [{"name": "some_app", "type": "display_text", "ask": "foobar"}] + questions = [ + { + "name": "some_app", + "type": "display_text", + "ask": "foobar" + } + ] answers = {} with patch.object(sys, "stdout", new_callable=StringIO) as stdout, patch.object( @@ -1926,6 +1950,72 @@ def test_question_display_text(): assert "foobar" in stdout.getvalue() +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 = [ + { + "name": "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("helloworld".encode()) + questions = [ + { + "name": "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 diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 012d2ed17..78fb52252 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -31,6 +31,7 @@ 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, @@ -167,6 +168,9 @@ class ConfigPanel: 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() @@ -969,10 +973,9 @@ class FileQuestion(Question): @classmethod def clean_upload_dirs(cls): # Delete files uploaded from API - if Moulinette.interface.type == "api": - for upload_dir in cls.upload_dirs: - if os.path.exists(upload_dir): - shutil.rmtree(upload_dir) + for upload_dir in cls.upload_dirs: + if os.path.exists(upload_dir): + shutil.rmtree(upload_dir) def __init__(self, question): super().__init__(question) @@ -984,12 +987,13 @@ class FileQuestion(Question): super()._prevalidate() - if not self.value or not os.path.exists(str(self.value)): - raise YunohostValidationError( - "app_argument_invalid", - name=self.name, - error=m18n.n("file_does_not_exist", path=str(self.value)), - ) + if Moulinette.interface.type != "api": + if not self.value or not os.path.exists(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 @@ -997,22 +1001,21 @@ class FileQuestion(Question): if not self.value: return self.value - # FIXME : in the cli case, don't we want to also copy the file - # to a tmp work dir ? + 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}") + if Moulinette.interface.type != "api": + content = read_file(str(self.value), file_mode="rb") if Moulinette.interface.type == "api": + content = b64decode(self.value) - upload_dir = tempfile.mkdtemp(prefix="tmp_configpanel_") - _, file_path = tempfile.mkstemp(prefix="foobar", dir=upload_dir) + write_to_file(file_path, content, file_mode="wb") - FileQuestion.upload_dirs += [upload_dir] - logger.debug(f"Save uploaded file from API into {file_path}") - - content = self.value - - write_to_file(file_path, b64decode(content), file_mode="wb") - - self.value = file_path + self.value = file_path return self.value From d64f2cdf1a14ba5abf07bb66b79d7936d5e8a3a7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 21:28:12 +0200 Subject: [PATCH 022/142] regenconf: Missing mkdir for dpkgorigins --- data/hooks/conf_regen/01-yunohost | 1 + 1 file changed, 1 insertion(+) diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index 9085b3dbc..14af66933 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -151,6 +151,7 @@ EOF touch ${pending_dir}/etc/systemd/system/proc-hidepid.service fi + mkdir -p ${pending_dir}/etc/dpkg/origins/ cp dpkg-origins ${pending_dir}/etc/dpkg/origins/yunohost } From 9f0caedb6b3ed43d74bb19d593b1df13ed13c921 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 21:30:37 +0200 Subject: [PATCH 023/142] tests: .coveragerc should be at the root of the repo --- .coveragerc | 2 ++ src/yunohost/.coveragerc | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 .coveragerc delete mode 100644 src/yunohost/.coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..73a4c45b4 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[report] +omit=src/yunohost/tests/*,src/yunohost/vendor/*,/usr/lib/moulinette/yunohost/ diff --git a/src/yunohost/.coveragerc b/src/yunohost/.coveragerc deleted file mode 100644 index 43e152271..000000000 --- a/src/yunohost/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[report] -omit=tests/*,vendor/*,/usr/lib/moulinette/yunohost/ From a5580caf4ec967228c2fcac6ba3fa82acd1a3c17 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 21:46:35 +0200 Subject: [PATCH 024/142] Lint --- src/yunohost/tests/test_questions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index d141f53cc..99b5339ca 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -4,7 +4,6 @@ import os from mock import patch from io import StringIO -from collections import OrderedDict from moulinette import Moulinette @@ -1759,7 +1758,7 @@ def test_question_number_no_input_optional(): assert out.name == "some_number" assert out.type == "number" - assert out.value == None + assert out.value is None def test_question_number_optional_with_input(): From c2fc2c4e51fcd4b6a0299285828f9fb01f280f25 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 21:36:20 +0200 Subject: [PATCH 025/142] Typo --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 1d1bafd58..cf24cfc09 100644 --- a/locales/en.json +++ b/locales/en.json @@ -654,7 +654,7 @@ "service_stop_failed": "Unable to stop the service '{service}'\n\nRecent service logs:{logs}", "service_stopped": "Service '{service}' stopped", "service_unknown": "Unknown service '{service}'", - "show_tile_cant_be_enabled_for_regex": "You cannot enable 'show_tile' right no, because the URL for the permission '{permission}' is a regex", + "show_tile_cant_be_enabled_for_regex": "You cannot enable 'show_tile' right now, because the URL for the permission '{permission}' is a regex", "show_tile_cant_be_enabled_for_url_not_defined": "You cannot enable 'show_tile' right now, because you must first define an URL for the permission '{permission}'", "ssowat_conf_generated": "SSOwat configuration regenerated", "ssowat_conf_updated": "SSOwat configuration updated", From ec5e5d6b730adcfa6db71ba80a93094480c75c07 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Thu, 23 Sep 2021 20:18:27 +0000 Subject: [PATCH 026/142] [CI] Format code --- src/yunohost/app.py | 26 ++++++++----- src/yunohost/tests/test_app_config.py | 6 +-- src/yunohost/tests/test_questions.py | 19 ++------- src/yunohost/user.py | 4 +- src/yunohost/utils/config.py | 56 +++++++++++++++++---------- 5 files changed, 62 insertions(+), 49 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 8d24e28c5..0013fcd82 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -58,7 +58,7 @@ from yunohost.utils.config import ( ask_questions_and_parse_answers, Question, DomainQuestion, - PathQuestion + PathQuestion, ) from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError @@ -903,7 +903,11 @@ def app_install( # Retrieve arguments list for install script raw_questions = manifest.get("arguments", {}).get("install", {}) questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) - args = {question.name: question.value for question in questions if question.value is not None} + args = { + question.name: question.value + for question in questions + if question.value is not None + } # Validate domain / path availability for webapps path_requirement = _guess_webapp_path_requirement(questions, extracted_app_folder) @@ -1642,13 +1646,15 @@ def app_action_run(operation_logger, app, action, args=None): # Retrieve arguments list for install script raw_questions = actions[action].get("arguments", {}) questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) - args = {question.name: question.value for question in questions if question.value is not None} + args = { + question.name: question.value + for question in questions + if question.value is not None + } tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) - env_dict = _make_environment_for_app_script( - app, args=args, args_prefix="ACTION_" - ) + env_dict = _make_environment_for_app_script(app, args=args, args_prefix="ACTION_") env_dict["YNH_ACTION"] = action env_dict["YNH_APP_BASEDIR"] = tmp_workdir_for_app @@ -2404,15 +2410,15 @@ def _guess_webapp_path_requirement(questions: List[Question], app_folder: str) - if re.search( r"\npath(_url)?=[\"']?/[\"']?", install_script_content - ) and re.search( - r"ynh_webpath_register", install_script_content - ): + ) and re.search(r"ynh_webpath_register", install_script_content): return "full_domain" return "?" -def _validate_webpath_requirement(questions: List[Question], path_requirement: str) -> None: +def _validate_webpath_requirement( + questions: List[Question], path_requirement: str +) -> None: domain_questions = [question for question in questions if question.type == "domain"] path_questions = [question for question in questions if question.type == "path"] diff --git a/src/yunohost/tests/test_app_config.py b/src/yunohost/tests/test_app_config.py index 248f035f5..0eb813672 100644 --- a/src/yunohost/tests/test_app_config.py +++ b/src/yunohost/tests/test_app_config.py @@ -148,9 +148,9 @@ def test_app_config_regular_setting(config_app): assert app_config_get(config_app, "main.components.boolean") == "1" assert app_setting(config_app, "boolean") == "1" - with pytest.raises(YunohostValidationError), \ - patch.object(os, "isatty", return_value=False), \ - patch.object(Moulinette, "prompt", return_value="pwet"): + with pytest.raises(YunohostValidationError), patch.object( + os, "isatty", return_value=False + ), patch.object(Moulinette, "prompt", return_value="pwet"): app_config_set(config_app, "main.components.boolean", "pwet") diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index 99b5339ca..cf4e67733 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -14,7 +14,7 @@ from yunohost.utils.config import ( DomainQuestion, PathQuestion, BooleanQuestion, - FileQuestion + FileQuestion, ) from yunohost.utils.error import YunohostError, YunohostValidationError @@ -94,7 +94,6 @@ def test_question_string_default_type(): assert out.value == "some_value" - def test_question_string_no_input(): questions = [ { @@ -486,12 +485,7 @@ def test_question_password_no_input_optional(): assert out.value == "" questions = [ - { - "name": "some_password", - "type": "password", - "optional": True, - "default": "" - } + {"name": "some_password", "type": "password", "optional": True, "default": ""} ] with patch.object(os, "isatty", return_value=False): @@ -1725,6 +1719,7 @@ def test_question_number_input(): assert out.type == "number" assert out.value == 0 + def test_question_number_input_no_ask(): questions = [ { @@ -1933,13 +1928,7 @@ def test_question_number_input_test_ask_with_help(): def test_question_display_text(): - questions = [ - { - "name": "some_app", - "type": "display_text", - "ask": "foobar" - } - ] + questions = [{"name": "some_app", "type": "display_text", "ask": "foobar"}] answers = {} with patch.object(sys, "stdout", new_callable=StringIO) as stdout, patch.object( diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 22168d3e7..7d89af443 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -420,7 +420,9 @@ def user_update( # without a specified value, change_password will be set to the const 0. # In this case we prompt for the new password. if Moulinette.interface.type == "cli" and not change_password: - change_password = Moulinette.prompt(m18n.n("ask_password"), is_password=True, confirm=True) + change_password = Moulinette.prompt( + m18n.n("ask_password"), is_password=True, confirm=True + ) # Ensure sufficiently complex password assert_password_is_strong_enough("user", change_password) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 78fb52252..27a9e1533 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -102,7 +102,9 @@ class ConfigPanel: ) # 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` + result[key][ + "value" + ] = "**************" # Prevent displaying password in `config get` if mode == "full": return self.config @@ -269,9 +271,7 @@ class ConfigPanel: # Now fill the sublevels (+ apply filter_key) i = list(format_description).index(level) - sublevel = ( - list(format_description)[i + 1] if level != "options" else None - ) + 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(): @@ -385,11 +385,13 @@ class ConfigPanel: # Check and ask unanswered questions questions = ask_questions_and_parse_answers(section["options"], self.args) - self.new_values.update({ - question.name: question.value - for question in questions - if question.value is not None - }) + self.new_values.update( + { + question.name: question.value + for question in questions + if question.value is not None + } + ) self.errors = None @@ -506,7 +508,7 @@ class Question(object): prefill=prefill, is_multiline=(self.type == "text"), autocomplete=self.choices, - help=_value_for_locale(self.help) + help=_value_for_locale(self.help), ) def ask_if_needed(self): @@ -574,12 +576,18 @@ class Question(object): # Prevent displaying a shitload of choices # (e.g. 100+ available users when choosing an app admin...) - choices = list(self.choices.values()) if isinstance(self.choices, dict) else self.choices + choices = ( + list(self.choices.values()) + 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 += [ + m18n.n("other_available_options", n=remaining_choices) + ] choices_to_display = " | ".join(choices_to_display) @@ -744,7 +752,7 @@ class PathQuestion(Question): raise YunohostValidationError( "app_argument_invalid", name=option.get("name"), - error="Question is mandatory" + error="Question is mandatory", ) return "/" + value.strip().strip(" /") @@ -794,8 +802,12 @@ class BooleanQuestion(Question): 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}" + 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()] @@ -851,9 +863,9 @@ class DomainQuestion(Question): @staticmethod def normalize(value, option={}): if value.startswith("https://"): - value = value[len("https://"):] + value = value[len("https://") :] elif value.startswith("http://"): - value = value[len("http://"):] + value = value[len("http://") :] # Remove trailing slashes value = value.rstrip("/").lower() @@ -915,7 +927,7 @@ class NumberQuestion(Question): raise YunohostValidationError( "app_argument_invalid", name=option.get("name"), - error=m18n.n("invalid_number") + error=m18n.n("invalid_number"), ) def _prevalidate(self): @@ -1044,7 +1056,9 @@ ARGUMENTS_TYPE_PARSERS = { } -def ask_questions_and_parse_answers(questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}) -> List[Question]: +def ask_questions_and_parse_answers( + questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {} +) -> 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. @@ -1062,7 +1076,9 @@ def ask_questions_and_parse_answers(questions: Dict, prefilled_answers: Union[st # 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) - prefilled_answers = dict(urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True)) + prefilled_answers = dict( + urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True) + ) if not prefilled_answers: prefilled_answers = {} From be41ab92c46840cc3a6cb5298528a64e58f1fdab Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 24 Sep 2021 19:35:51 +0200 Subject: [PATCH 027/142] ci: missing wildcard in coveragerc --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 73a4c45b4..ed13dfa68 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,2 @@ [report] -omit=src/yunohost/tests/*,src/yunohost/vendor/*,/usr/lib/moulinette/yunohost/ +omit=src/yunohost/tests/*,src/yunohost/vendor/*,/usr/lib/moulinette/yunohost/* From 8a82fe03926f188f64c2200a69a27c25650ac385 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 24 Sep 2021 19:43:02 +0200 Subject: [PATCH 028/142] Force yunohost to forget about avahi-daemon files --- src/yunohost/regenconf.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/yunohost/regenconf.py b/src/yunohost/regenconf.py index ef3c29b32..1beef8a44 100644 --- a/src/yunohost/regenconf.py +++ b/src/yunohost/regenconf.py @@ -135,6 +135,9 @@ def regen_conf( if "glances" in names: names.remove("glances") + if "avahi-daemon" in names: + names.remove("avahi-daemon") + # [Optimization] We compute and feed the domain list to the conf regen # hooks to avoid having to call "yunohost domain list" so many times which # ends up in wasted time (about 3~5 seconds per call on a RPi2) @@ -455,6 +458,10 @@ def _save_regenconf_infos(infos): if "glances" in infos: del infos["glances"] + # Ugly hack to get rid of legacy avahi stuff + if "avahi-daemon" in infos: + del infos["avahi-daemon"] + try: with open(REGEN_CONF_FILE, "w") as f: yaml.safe_dump(infos, f, default_flow_style=False) From 919ec7587709269acc5f223daf2e586b544a0c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Sun, 26 Sep 2021 10:37:26 +0200 Subject: [PATCH 029/142] Revamp of yunomdns : * Use ifaddr (also used by zeroconf) to find ip addresses * Use python type hinting * small cleanups --- bin/yunomdns | 166 ++++++++++++++++----------------------------------- 1 file changed, 53 insertions(+), 113 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index 862a1f477..8202b93c4 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -4,104 +4,42 @@ Pythonic declaration of mDNS .local domains for YunoHost """ -import subprocess -import re import sys import yaml - -import socket from time import sleep from typing import List, Dict +import ifaddr from zeroconf import Zeroconf, ServiceInfo -# Helper command taken from Moulinette -def check_output(args, stderr=subprocess.STDOUT, shell=True, **kwargs): - """Run command with arguments and return its output as a byte string - Overwrite some of the arguments to capture standard error in the result - and use shell by default before calling subprocess.check_output. + +def get_network_local_interfaces() -> Dict[str, Dict[str, List[str]]]: """ - return ( - subprocess.check_output(args, stderr=stderr, shell=shell, **kwargs) - .decode("utf-8") - .strip() - ) - -# Helper command taken from Moulinette -def _extract_inet(string, skip_netmask=False, skip_loopback=True): + Returns interfaces with their associated local IPs """ - Extract IP addresses (v4 and/or v6) from a string limited to one - address by protocol - Keyword argument: - string -- String to search in - skip_netmask -- True to skip subnet mask extraction - skip_loopback -- False to include addresses reserved for the - loopback interface + def islocal(ip: str) -> bool: + local_prefixes = ["192.168.", "10.", "172.16.", "fc00:"] + return any(ip.startswith(prefix) for prefix in local_prefixes) - Returns: - A dict of {protocol: address} with protocol one of 'ipv4' or 'ipv6' - - """ - ip4_pattern = ( - r"((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}" - ) - ip6_pattern = r"(((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::?((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)" - ip4_pattern += r"/[0-9]{1,2})" if not skip_netmask else ")" - ip6_pattern += r"/[0-9]{1,3})" if not skip_netmask else ")" - result = {} - - for m in re.finditer(ip4_pattern, string): - addr = m.group(1) - if skip_loopback and addr.startswith("127."): - continue - - # Limit to only one result - result["ipv4"] = addr - break - - for m in re.finditer(ip6_pattern, string): - addr = m.group(1) - if skip_loopback and addr == "::1": - continue - - # Limit to only one result - result["ipv6"] = addr - break - - return result - -# Helper command taken from Moulinette -def get_network_interfaces(): - - # Get network devices and their addresses (raw infos from 'ip addr') - devices_raw = {} - output = check_output("ip --brief a").split("\n") - for line in output: - line = line.split() - iname = line[0] - ips = ' '.join(line[2:]) - - devices_raw[iname] = ips - - # Parse relevant informations for each of them - devices = { - name: _extract_inet(addrs) - for name, addrs in devices_raw.items() - if name != "lo" + interfaces = { + adapter.name: { + "ipv4": [ip.ip for ip in adapter.ips if ip.is_IPv4 and islocal(ip.ip)], + "ipv6": [ip.ip[0] for ip in adapter.ips if ip.is_IPv6 and islocal(ip.ip[0])], + } + for adapter in ifaddr.get_adapters() + if adapter.name != "lo" } + return interfaces - return devices - -if __name__ == '__main__': +def main() -> bool: ### # CONFIG ### with open('/etc/yunohost/mdns.yml', 'r') as f: config = yaml.safe_load(f) or {} - updated = False required_fields = ["interfaces", "domains"] missing_fields = [field for field in required_fields if field not in config] @@ -111,47 +49,44 @@ if __name__ == '__main__': if config['interfaces'] is None: print('No interface listed for broadcast.') - sys.exit(0) + return True if 'yunohost.local' not in config['domains']: config['domains'].append('yunohost.local') - zcs = {} - interfaces = get_network_interfaces() + zcs: Dict[Zeroconf, List[ServiceInfo]] = {} + + interfaces = get_network_local_interfaces() for interface in config['interfaces']: - infos = [] # List of ServiceInfo objects, to feed Zeroconf - ips = [] # Human-readable IPs - b_ips = [] # Binary-convered IPs + if interface not in interfaces: + print(f'Interface {interface} of config file is not present on system.') + continue - ipv4 = interfaces[interface]['ipv4'].split('/')[0] - if ipv4: - ips.append(ipv4) - b_ips.append(socket.inet_pton(socket.AF_INET, ipv4)) - - ipv6 = interfaces[interface]['ipv6'].split('/')[0] - if ipv6: - ips.append(ipv6) - b_ips.append(socket.inet_pton(socket.AF_INET6, ipv6)) + ips: List[str] = interfaces[interface]['ipv4'] + interfaces[interface]['ipv6'] # If at least one IP is listed - if ips: - # Create a Zeroconf object, and store the ServiceInfos - zc = Zeroconf(interfaces=ips) - zcs[zc]=[] - for d in config['domains']: - d_domain=d.replace('.local','') - if '.' in d_domain: - print(d_domain+'.local: subdomains are not supported.') - else: - # Create a ServiceInfo object for each .local domain - zcs[zc].append(ServiceInfo( - type_='_device-info._tcp.local.', - name=interface+': '+d_domain+'._device-info._tcp.local.', - addresses=b_ips, - port=80, - server=d+'.', - )) - print('Adding '+d+' with addresses '+str(ips)+' on interface '+interface) + if not ips: + continue + # Create a Zeroconf object, and store the ServiceInfos + zc = Zeroconf(interfaces=ips) # type: ignore + zcs[zc] = [] + + for d in config['domains']: + d_domain = d.replace('.local', '') + if '.' in d_domain: + print(f'{d_domain}.local: subdomains are not supported.') + continue + # Create a ServiceInfo object for each .local domain + zcs[zc].append( + ServiceInfo( + type_='_device-info._tcp.local.', + name=f'{interface}: {d_domain}._device-info._tcp.local.', + parsed_addresses=ips, + port=80, + server=f'{d}.', + ) + ) + print(f'Adding {d} with addresses {ips} on interface {interface}') # Run registration print("Registering...") @@ -168,6 +103,11 @@ if __name__ == '__main__': finally: print("Unregistering...") for zc, infos in zcs.items(): - for info in infos: - zc.unregister_service(info) + zc.unregister_all_services() zc.close() + + return True + + +if __name__ == "__main__": + sys.exit(0 if main() else 1) From d4395f2b4aa69424245218601993e3c37c141df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Sun, 26 Sep 2021 10:42:57 +0200 Subject: [PATCH 030/142] Use double-quotes --- bin/yunomdns | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index 8202b93c4..f31132514 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -38,31 +38,31 @@ def main() -> bool: # CONFIG ### - with open('/etc/yunohost/mdns.yml', 'r') as f: + with open("/etc/yunohost/mdns.yml", "r") as f: config = yaml.safe_load(f) or {} required_fields = ["interfaces", "domains"] missing_fields = [field for field in required_fields if field not in config] if missing_fields: - print("The fields %s are required" % ', '.join(missing_fields)) + print("The fields %s are required" % ", ".join(missing_fields)) - if config['interfaces'] is None: - print('No interface listed for broadcast.') + if config["interfaces"] is None: + print("No interface listed for broadcast.") return True - if 'yunohost.local' not in config['domains']: - config['domains'].append('yunohost.local') + if "yunohost.local" not in config["domains"]: + config["domains"].append("yunohost.local") zcs: Dict[Zeroconf, List[ServiceInfo]] = {} interfaces = get_network_local_interfaces() - for interface in config['interfaces']: + for interface in config["interfaces"]: if interface not in interfaces: - print(f'Interface {interface} of config file is not present on system.') + print(f"Interface {interface} of config file is not present on system.") continue - ips: List[str] = interfaces[interface]['ipv4'] + interfaces[interface]['ipv6'] + ips: List[str] = interfaces[interface]["ipv4"] + interfaces[interface]["ipv6"] # If at least one IP is listed if not ips: @@ -71,22 +71,22 @@ def main() -> bool: zc = Zeroconf(interfaces=ips) # type: ignore zcs[zc] = [] - for d in config['domains']: - d_domain = d.replace('.local', '') - if '.' in d_domain: - print(f'{d_domain}.local: subdomains are not supported.') + for d in config["domains"]: + d_domain = d.replace(".local", "") + if "." in d_domain: + print(f"{d_domain}.local: subdomains are not supported.") continue # Create a ServiceInfo object for each .local domain zcs[zc].append( ServiceInfo( - type_='_device-info._tcp.local.', - name=f'{interface}: {d_domain}._device-info._tcp.local.', + type_="_device-info._tcp.local.", + name=f"{interface}: {d_domain}._device-info._tcp.local.", parsed_addresses=ips, port=80, - server=f'{d}.', + server=f"{d}.", ) ) - print(f'Adding {d} with addresses {ips} on interface {interface}') + print(f"Adding {d} with addresses {ips} on interface {interface}") # Run registration print("Registering...") From 358885dc622d9e18c2cc283c4a1c52236537b73b Mon Sep 17 00:00:00 2001 From: tituspijean Date: Mon, 20 Sep 2021 22:07:45 +0200 Subject: [PATCH 031/142] [mdns] Avoid miserably failing if another service exists on the network also, service names do not clash anymore accross same device but different interfaces --- bin/yunomdns | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/yunomdns b/bin/yunomdns index f31132514..dfd58deee 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -92,7 +92,7 @@ def main() -> bool: print("Registering...") for zc, infos in zcs.items(): for info in infos: - zc.register_service(info) + zc.register_service(info, allow_name_change=True, cooperating_responders=True) try: print("Registered. Press Ctrl+C or stop service to stop.") From bcb48b4948a900fd153e87e325ba07c8c56f6895 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Mon, 20 Sep 2021 22:14:00 +0200 Subject: [PATCH 032/142] [mdns] Make the service logs appear in systemd journal --- data/templates/mdns/yunomdns.service | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/mdns/yunomdns.service b/data/templates/mdns/yunomdns.service index ce2641b5d..c1f1b7b06 100644 --- a/data/templates/mdns/yunomdns.service +++ b/data/templates/mdns/yunomdns.service @@ -6,6 +6,7 @@ After=network.target User=mdns Group=mdns Type=simple +Environment=PYTHONUNBUFFERED=1 ExecStart=/usr/bin/yunomdns StandardOutput=syslog From a313a86b8b6eb39b8836c47cc8c107058a6da6ed Mon Sep 17 00:00:00 2001 From: tituspijean Date: Tue, 21 Sep 2021 00:09:49 +0200 Subject: [PATCH 033/142] [mdns] Allow for multiple yunohost.local --- bin/yunomdns | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index dfd58deee..aa0697f5f 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -10,7 +10,7 @@ from time import sleep from typing import List, Dict import ifaddr -from zeroconf import Zeroconf, ServiceInfo +from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser def get_network_local_interfaces() -> Dict[str, Dict[str, List[str]]]: @@ -32,6 +32,23 @@ def get_network_local_interfaces() -> Dict[str, Dict[str, List[str]]]: } return interfaces +# Listener class, to detect duplicates on the network +# Stores the list of servers in its list property +class Listener: + + def __init__(self): + self.list = [] + + def remove_service(self, zeroconf, type, name): + info = zeroconf.get_service_info(type, name) + self.list.remove(info.server) + + def update_service(self, zeroconf, type, name): + pass + + def add_service(self, zeroconf, type, name): + info = zeroconf.get_service_info(type, name) + self.list.append(info.server[:-1]) def main() -> bool: ### @@ -51,8 +68,25 @@ def main() -> bool: print("No interface listed for broadcast.") return True - if "yunohost.local" not in config["domains"]: - config["domains"].append("yunohost.local") + # Let's discover currently published .local domains accross the network + zc = Zeroconf() + listener = Listener() + browser = ServiceBrowser(zc, "_device-info._tcp.local.", listener) + sleep(2) + browser.cancel() + zc.close() + # If yunohost.local already exists, try yunohost-2.local, and so on. + def yunohost_local(i): + return "yunohost.local" if i < 2 else "yunohost-"+str(i)+".local" + i=1 + while yunohost_local(i) in listener.list: + print("Uh oh, "+yunohost_local(i)+" already exists on the network...") + if yunohost_local(i) in config['domains']: + config['domains'].remove(yunohost_local(i)) + i += 1 + if yunohost_local(i) not in config['domains']: + print("Adding "+yunohost_local(i)+" to the domains to publish.") + config['domains'].append(yunohost_local(i)) zcs: Dict[Zeroconf, List[ServiceInfo]] = {} From 07c381cec4690df90022e11c0574dfca78906c2f Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 26 Sep 2021 12:05:35 +0200 Subject: [PATCH 034/142] Use ipaddress lib to find private addresses --- bin/yunomdns | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index aa0697f5f..f50afd964 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -10,6 +10,7 @@ from time import sleep from typing import List, Dict import ifaddr +from ipaddress import ip_address from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser @@ -18,14 +19,10 @@ def get_network_local_interfaces() -> Dict[str, Dict[str, List[str]]]: Returns interfaces with their associated local IPs """ - def islocal(ip: str) -> bool: - local_prefixes = ["192.168.", "10.", "172.16.", "fc00:"] - return any(ip.startswith(prefix) for prefix in local_prefixes) - interfaces = { adapter.name: { - "ipv4": [ip.ip for ip in adapter.ips if ip.is_IPv4 and islocal(ip.ip)], - "ipv6": [ip.ip[0] for ip in adapter.ips if ip.is_IPv6 and islocal(ip.ip[0])], + "ipv4": [ip.ip for ip in adapter.ips if ip.is_IPv4 and ip_address(ip.ip).is_private], + "ipv6": [ip.ip[0] for ip in adapter.ips if ip.is_IPv6 and ip_address(ip.ip[0]).is_private], } for adapter in ifaddr.get_adapters() if adapter.name != "lo" From 63504febaaf17a7efbf93f111c409764e84a0b17 Mon Sep 17 00:00:00 2001 From: tituspijean Date: Sun, 26 Sep 2021 19:40:46 +0200 Subject: [PATCH 035/142] [mdns] refine ipv6 selection with ipaddress lib Co-authored-by: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= --- bin/yunomdns | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/yunomdns b/bin/yunomdns index f50afd964..f302f1f8c 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -22,7 +22,7 @@ def get_network_local_interfaces() -> Dict[str, Dict[str, List[str]]]: interfaces = { adapter.name: { "ipv4": [ip.ip for ip in adapter.ips if ip.is_IPv4 and ip_address(ip.ip).is_private], - "ipv6": [ip.ip[0] for ip in adapter.ips if ip.is_IPv6 and ip_address(ip.ip[0]).is_private], + "ipv6": [ip.ip[0] for ip in adapter.ips if ip.is_IPv6 and ip_address(ip.ip[0]).is_private and not ip_address(ip.ip[0]).is_link_local], } for adapter in ifaddr.get_adapters() if adapter.name != "lo" From da1b9089bf6fd6e39d0c905bbe8e6573658315a9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 28 Sep 2021 23:36:24 +0200 Subject: [PATCH 036/142] Fix dns zone for dynette domains (otherwise breaks dyndns update) --- src/yunohost/dns.py | 5 +++-- src/yunohost/tests/test_dns.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/yunohost/dns.py b/src/yunohost/dns.py index 0581fa82c..c003b7a56 100644 --- a/src/yunohost/dns.py +++ b/src/yunohost/dns.py @@ -428,7 +428,8 @@ def _get_dns_zone_for_domain(domain): # ... otherwise we end up constantly doing a bunch of dig requests for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS: if domain.endswith("." + ynh_dyndns_domain): - return ynh_dyndns_domain + # Keep only foo.nohost.me even if we have subsub.sub.foo.nohost.me + return '.'.join(domain.rsplit('.', 3)[-3:]) # Check cache cache_folder = "/var/cache/yunohost/dns_zones" @@ -520,7 +521,7 @@ def _get_registrar_config_section(domain): # TODO big project, integrate yunohost's dynette as a registrar-like provider # TODO big project, integrate other dyndns providers such as netlib.re, or cf the list of dyndns providers supported by cloudron... - if dns_zone in YNH_DYNDNS_DOMAINS: + if any(dns_zone.endswith('.' + ynh_dyndns_domain) for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS): registrar_infos["registrar"] = OrderedDict( { "type": "alert", diff --git a/src/yunohost/tests/test_dns.py b/src/yunohost/tests/test_dns.py index 497cab2fd..cdd37b2ea 100644 --- a/src/yunohost/tests/test_dns.py +++ b/src/yunohost/tests/test_dns.py @@ -34,8 +34,10 @@ def test_get_dns_zone_from_domain_existing(): assert ( _get_dns_zone_for_domain("non-existing-domain.yunohost.org") == "yunohost.org" ) - assert _get_dns_zone_for_domain("yolo.nohost.me") == "nohost.me" - assert _get_dns_zone_for_domain("foo.yolo.nohost.me") == "nohost.me" + assert _get_dns_zone_for_domain("yolo.nohost.me") == "yolo.nohost.me" + assert _get_dns_zone_for_domain("foo.yolo.nohost.me") == "yolo.nohost.me" + assert _get_dns_zone_for_domain("bar.foo.yolo.nohost.me") == "yolo.nohost.me" + assert _get_dns_zone_for_domain("yolo.tld") == "yolo.tld" assert _get_dns_zone_for_domain("foo.yolo.tld") == "yolo.tld" From 7d4ab4a886fd568179d642759947aef5ff26d714 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Tue, 28 Sep 2021 21:52:29 +0000 Subject: [PATCH 037/142] [CI] Format code --- src/yunohost/dns.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/yunohost/dns.py b/src/yunohost/dns.py index c003b7a56..e62469a53 100644 --- a/src/yunohost/dns.py +++ b/src/yunohost/dns.py @@ -429,7 +429,7 @@ def _get_dns_zone_for_domain(domain): for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS: if domain.endswith("." + ynh_dyndns_domain): # Keep only foo.nohost.me even if we have subsub.sub.foo.nohost.me - return '.'.join(domain.rsplit('.', 3)[-3:]) + return ".".join(domain.rsplit(".", 3)[-3:]) # Check cache cache_folder = "/var/cache/yunohost/dns_zones" @@ -521,7 +521,10 @@ def _get_registrar_config_section(domain): # TODO big project, integrate yunohost's dynette as a registrar-like provider # TODO big project, integrate other dyndns providers such as netlib.re, or cf the list of dyndns providers supported by cloudron... - if any(dns_zone.endswith('.' + ynh_dyndns_domain) for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS): + if any( + dns_zone.endswith("." + ynh_dyndns_domain) + for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS + ): registrar_infos["registrar"] = OrderedDict( { "type": "alert", From dc8c16c96da7bc7f5676af31f372ce2887250512 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 18:33:31 +0200 Subject: [PATCH 038/142] Wording --- locales/en.json | 2 +- locales/fr.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index cf24cfc09..1977f8278 100644 --- a/locales/en.json +++ b/locales/en.json @@ -201,7 +201,7 @@ "diagnosis_domain_expiration_warning": "Some domains will expire soon!", "diagnosis_domain_expires_in": "{domain} expires in {days} days.", "diagnosis_domain_not_found_details": "The domain {domain} doesn't exist in WHOIS database or is expired!", - "diagnosis_everything_ok": "Everything looks good for {category}!", + "diagnosis_everything_ok": "Everything looks OK for {category}!", "diagnosis_failed": "Failed to fetch diagnosis result for category '{category}': {error}", "diagnosis_failed_for_category": "Diagnosis failed for category '{category}': {error}", "diagnosis_found_errors": "Found {errors} significant issue(s) related to {category}!", diff --git a/locales/fr.json b/locales/fr.json index 46535719e..bd29ad774 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -431,7 +431,7 @@ "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}.", - "diagnosis_everything_ok": "Tout semble bien pour {category} !", + "diagnosis_everything_ok": "Tout semble OK pour {category} !", "diagnosis_failed": "Échec de la récupération du résultat du diagnostic pour la catégorie '{category}' : {error}", "diagnosis_ip_connected_ipv4": "Le serveur est connecté à Internet en IPv4 !", "diagnosis_ip_no_ipv4": "Le serveur ne dispose pas d'une adresse IPv4.", @@ -676,4 +676,4 @@ "service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}", "app_argument_password_help_keep": "Tapez sur Entrée pour conserver la valeur actuelle", "app_argument_password_help_optional": "Tapez un espace pour vider le mot de passe" -} \ No newline at end of file +} From ad3602a24fffe9fa16c0d767a1d151e0ea966de6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 18:36:24 +0200 Subject: [PATCH 039/142] YNH_APP_PURGE to be 1 or 0 to be consistent with other bash bools --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0013fcd82..56e4c344b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1160,7 +1160,7 @@ def app_remove(operation_logger, app, purge=False): env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") - env_dict["YNH_APP_PURGE"] = str(purge) + env_dict["YNH_APP_PURGE"] = str(1 if purge else 0) env_dict["YNH_APP_BASEDIR"] = tmp_workdir_for_app operation_logger.extra.update({"env": env_dict}) From 14d3265389ad122c31944f54c5b17db65f20fb7b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 18:40:31 +0200 Subject: [PATCH 040/142] Try to include diagnosis hooks in coverage report --- .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 b3aea606f..6a422ea22 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 -p the_password --ignore-dyndns --force-diskspace script: - - python3 -m pytest --cov=yunohost tests/ src/yunohost/tests/ --junitxml=report.xml + - python3 -m pytest --cov=yunohost tests/ src/yunohost/tests/ data/hooks/diagnosis/ --junitxml=report.xml - cd tests - bash test_helpers.sh needs: From ab1100048b91dc1b9404c0996642ccca68b09062 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 19:00:05 +0200 Subject: [PATCH 041/142] yunomdns: Minor cleanups --- bin/yunomdns | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index f302f1f8c..41a9ede3e 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -29,6 +29,7 @@ def get_network_local_interfaces() -> Dict[str, Dict[str, List[str]]]: } return interfaces + # Listener class, to detect duplicates on the network # Stores the list of servers in its list property class Listener: @@ -47,6 +48,7 @@ class Listener: info = zeroconf.get_service_info(type, name) self.list.append(info.server[:-1]) + def main() -> bool: ### # CONFIG @@ -59,7 +61,8 @@ def main() -> bool: missing_fields = [field for field in required_fields if field not in config] if missing_fields: - print("The fields %s are required" % ", ".join(missing_fields)) + print(f"The fields {missing_fields} are required in mdns.yml") + return False if config["interfaces"] is None: print("No interface listed for broadcast.") @@ -72,17 +75,20 @@ def main() -> bool: sleep(2) browser.cancel() zc.close() + # If yunohost.local already exists, try yunohost-2.local, and so on. def yunohost_local(i): - return "yunohost.local" if i < 2 else "yunohost-"+str(i)+".local" - i=1 + return "yunohost.local" if i < 2 else f"yunohost-{i}.local" + + i = 1 while yunohost_local(i) in listener.list: - print("Uh oh, "+yunohost_local(i)+" already exists on the network...") + print(f"Uh oh, {yunohost_local(i)} already exists on the network...") if yunohost_local(i) in config['domains']: config['domains'].remove(yunohost_local(i)) i += 1 + if yunohost_local(i) not in config['domains']: - print("Adding "+yunohost_local(i)+" to the domains to publish.") + print(f"Adding {yunohost_local(i)} to the domains to publish.") config['domains'].append(yunohost_local(i)) zcs: Dict[Zeroconf, List[ServiceInfo]] = {} From f49666d22e7e17cedfab49a98a1789c12f62fc7e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 19:14:21 +0200 Subject: [PATCH 042/142] yunomdns: fallback to domain-i.local if domain.local already published --- bin/yunomdns | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index 41a9ede3e..8a9682ff9 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -76,20 +76,27 @@ def main() -> bool: browser.cancel() zc.close() - # If yunohost.local already exists, try yunohost-2.local, and so on. - def yunohost_local(i): - return "yunohost.local" if i < 2 else f"yunohost-{i}.local" + # Always attempt to publish yunohost.local + if "yunohost.local" not in config["domains"]: + config["domains"].append("yunohost.local") - i = 1 - while yunohost_local(i) in listener.list: - print(f"Uh oh, {yunohost_local(i)} already exists on the network...") - if yunohost_local(i) in config['domains']: - config['domains'].remove(yunohost_local(i)) - i += 1 + def find_domain_not_already_published(domain): - if yunohost_local(i) not in config['domains']: - print(f"Adding {yunohost_local(i)} to the domains to publish.") - config['domains'].append(yunohost_local(i)) + # Try domain.local ... but if it's already published by another entity, + # try domain-2.local, domain-3.local, ... + + i = 1 + domain_i = domain + + while domain_i in listener.list: + print(f"Uh oh, {domain_i} already exists on the network...") + + i += 1 + domain_i = domain.replace(".local", f"-{i}.local") + + return domain_i + + config['domains'] = [find_domain_not_already_published(domain) for domain in config['domains']] zcs: Dict[Zeroconf, List[ServiceInfo]] = {} From a5e1d7e318158da38b1e3dc415a92f0e6250a460 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 20:02:32 +0200 Subject: [PATCH 043/142] yunomdns: broadcast on all interfaces with local IP by default + add a 'ban_interfaces' setting in config --- bin/yunomdns | 23 +++++++++++++++++------ data/hooks/conf_regen/37-mdns | 7 ------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index 8a9682ff9..b9f8cf2a7 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -57,16 +57,23 @@ def main() -> bool: with open("/etc/yunohost/mdns.yml", "r") as f: config = yaml.safe_load(f) or {} - required_fields = ["interfaces", "domains"] + required_fields = ["domains"] missing_fields = [field for field in required_fields if field not in config] + interfaces = get_network_local_interfaces() if missing_fields: print(f"The fields {missing_fields} are required in mdns.yml") return False - if config["interfaces"] is None: - print("No interface listed for broadcast.") - return True + if "interfaces" not in config: + config["interfaces"] = [interface + for interface, local_ips in interfaces.items() + if local_ips["ipv4"]] + + if "ban_interfaces" in config: + config["interfaces"] = [interface + for interface in config["interfaces"] + if interface not in config["ban_interfaces"]] # Let's discover currently published .local domains accross the network zc = Zeroconf() @@ -100,10 +107,10 @@ def main() -> bool: zcs: Dict[Zeroconf, List[ServiceInfo]] = {} - interfaces = get_network_local_interfaces() for interface in config["interfaces"]: + if interface not in interfaces: - print(f"Interface {interface} of config file is not present on system.") + print(f"Interface {interface} listed in config file is not present on system.") continue ips: List[str] = interfaces[interface]["ipv4"] + interfaces[interface]["ipv6"] @@ -111,6 +118,10 @@ def main() -> bool: # If at least one IP is listed if not ips: continue + + print(f"Publishing on {interface} ...") + print(ips) + # Create a Zeroconf object, and store the ServiceInfos zc = Zeroconf(interfaces=ips) # type: ignore zcs[zc] = [] diff --git a/data/hooks/conf_regen/37-mdns b/data/hooks/conf_regen/37-mdns index 1d7381e26..b2a3efe95 100755 --- a/data/hooks/conf_regen/37-mdns +++ b/data/hooks/conf_regen/37-mdns @@ -12,13 +12,6 @@ _generate_config() { [[ "$domain" =~ ^[^.]+\.local$ ]] || continue echo " - $domain" done - - echo "interfaces:" - local_network_interfaces="$(ip --brief a | grep ' 10\.\| 192\.168\.' | awk '{print $1}')" - for interface in $local_network_interfaces - do - echo " - $interface" - done } do_init_regen() { From 391de52ff214a46efe1f990c6bd7164eebc852ad Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 20:10:00 +0200 Subject: [PATCH 044/142] yunomdns: disable ipv6 for now + remove debug stuff --- bin/yunomdns | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bin/yunomdns b/bin/yunomdns index b9f8cf2a7..0aee28195 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -113,15 +113,17 @@ def main() -> bool: print(f"Interface {interface} listed in config file is not present on system.") continue - ips: List[str] = interfaces[interface]["ipv4"] + interfaces[interface]["ipv6"] + # Only broadcast IPv4 because IPv6 is buggy ... because we ain't using python3-ifaddr >= 0.1.7 + # Buster only ships 0.1.6 + # Bullseye ships 0.1.7 + # To be re-enabled once we're on bullseye... + # ips: List[str] = interfaces[interface]["ipv4"] + interfaces[interface]["ipv6"] + ips: List[str] = interfaces[interface]["ipv4"] # If at least one IP is listed if not ips: continue - print(f"Publishing on {interface} ...") - print(ips) - # Create a Zeroconf object, and store the ServiceInfos zc = Zeroconf(interfaces=ips) # type: ignore zcs[zc] = [] From 17aafe6f6ad74e136802221d7073160b9aa2c8f8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 20:33:07 +0200 Subject: [PATCH 045/142] General improvement for special-use TLD / ynh dyndns domains --- data/hooks/diagnosis/12-dnsrecords.py | 18 ++++-------- data/hooks/diagnosis/21-web.py | 5 ++-- locales/en.json | 5 ++-- src/yunohost/dns.py | 40 ++++++++++++++++++--------- src/yunohost/tests/test_dns.py | 3 ++ src/yunohost/utils/dns.py | 12 ++++++++ 6 files changed, 53 insertions(+), 30 deletions(-) diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index 16841721f..368c3d878 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -8,13 +8,11 @@ from publicsuffix import PublicSuffixList from moulinette.utils.process import check_output -from yunohost.utils.dns import dig, YNH_DYNDNS_DOMAINS +from yunohost.utils.dns import dig, YNH_DYNDNS_DOMAINS, is_yunohost_dyndns_domain, is_special_use_tld from yunohost.diagnosis import Diagnoser from yunohost.domain import domain_list, _get_maindomain from yunohost.dns import _build_dns_conf, _get_dns_zone_for_domain -SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"] - class DNSRecordsDiagnoser(Diagnoser): @@ -29,13 +27,10 @@ class DNSRecordsDiagnoser(Diagnoser): all_domains = domain_list(exclude_subdomains=True)["domains"] for domain in all_domains: self.logger_debug("Diagnosing DNS conf for %s" % domain) - is_specialusedomain = any( - domain.endswith("." + tld) for tld in SPECIAL_USE_TLDS - ) + for report in self.check_domain( domain, domain == main_domain, - is_specialusedomain=is_specialusedomain, ): yield report @@ -53,7 +48,7 @@ class DNSRecordsDiagnoser(Diagnoser): for report in self.check_expiration_date(domains_from_registrar): yield report - def check_domain(self, domain, is_main_domain, is_specialusedomain): + def check_domain(self, domain, is_main_domain): base_dns_zone = _get_dns_zone_for_domain(domain) basename = domain.replace(base_dns_zone, "").rstrip(".") or "@" @@ -64,7 +59,7 @@ class DNSRecordsDiagnoser(Diagnoser): categories = ["basic", "mail", "xmpp", "extra"] - if is_specialusedomain: + if is_special_use_tld(domain): categories = [] yield dict( meta={"domain": domain}, @@ -140,10 +135,7 @@ class DNSRecordsDiagnoser(Diagnoser): if discrepancies: # For ynh-managed domains (nohost.me etc...), tell people to try to "yunohost dyndns update --force" - if any( - domain.endswith(ynh_dyndns_domain) - for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS - ): + if is_yunohost_dyndns_domain(domain): output["details"] = ["diagnosis_dns_try_dyndns_update_force"] # Otherwise point to the documentation else: diff --git a/data/hooks/diagnosis/21-web.py b/data/hooks/diagnosis/21-web.py index 2072937e5..450296e7e 100644 --- a/data/hooks/diagnosis/21-web.py +++ b/data/hooks/diagnosis/21-web.py @@ -8,6 +8,7 @@ from moulinette.utils.filesystem import read_file from yunohost.diagnosis import Diagnoser from yunohost.domain import domain_list +from yunohost.utils.dns import is_special_use_tld DIAGNOSIS_SERVER = "diagnosis.yunohost.org" @@ -34,11 +35,11 @@ class WebDiagnoser(Diagnoser): summary="diagnosis_http_nginx_conf_not_up_to_date", details=["diagnosis_http_nginx_conf_not_up_to_date_details"], ) - elif domain.endswith(".local"): + elif is_special_use_tld(domain): yield dict( meta={"domain": domain}, status="INFO", - summary="diagnosis_http_localdomain", + summary="diagnosis_http_special_use_tld", ) else: domains_to_check.append(domain) diff --git a/locales/en.json b/locales/en.json index 1977f8278..5d242b2e8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -192,7 +192,7 @@ "diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})", "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}", "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help about configuring DNS records.", - "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) and is therefore not expected to have actual DNS records.", + "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force.", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", "diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains", @@ -214,7 +214,7 @@ "diagnosis_http_could_not_diagnose_details": "Error: {error}", "diagnosis_http_hairpinning_issue": "Your local network does not seem to have hairpinning enabled.", "diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at https://yunohost.org/dns_local_network", - "diagnosis_http_localdomain": "Domain {domain}, with a .local TLD, is not expected to be exposed outside the local network.", + "diagnosis_http_special_use_tld": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to be exposed outside the local network.", "diagnosis_http_nginx_conf_not_up_to_date": "This domain's nginx configuration appears to have been modified manually, and prevents YunoHost from diagnosing if it's reachable on HTTP.", "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference with the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok, apply the changes with yunohost tools regen-conf nginx --force.", "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.", @@ -308,6 +308,7 @@ "domain_deleted": "Domain deleted", "domain_deletion_failed": "Unable to delete domain {domain}: {error}", "domain_dns_conf_is_just_a_recommendation": "This command shows you the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.", + "domain_dns_conf_special_use_tld": "This domain is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "The domain already exists", diff --git a/src/yunohost/dns.py b/src/yunohost/dns.py index e62469a53..72c9f7c72 100644 --- a/src/yunohost/dns.py +++ b/src/yunohost/dns.py @@ -41,7 +41,7 @@ from yunohost.domain import ( _get_domain_settings, _set_domain_settings, ) -from yunohost.utils.dns import dig, YNH_DYNDNS_DOMAINS +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.log import is_unit_operation @@ -61,6 +61,9 @@ def domain_dns_suggest(domain): """ + if is_special_use_tld(domain): + return m18n.n("domain_dns_conf_special_use_tld") + _assert_domain_exists(domain) dns_conf = _build_dns_conf(domain) @@ -169,10 +172,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): # If this is a ynh_dyndns_domain, we're not gonna include all the subdomains in the conf # Because dynette only accept a specific list of name/type # And the wildcard */A already covers the bulk of use cases - if any( - base_domain.endswith("." + ynh_dyndns_domain) - for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS - ): + if is_yunohost_dyndns_domain(base_domain): subdomains = [] else: subdomains = _list_subdomains_of(base_domain) @@ -426,10 +426,14 @@ def _get_dns_zone_for_domain(domain): # First, check if domain is a nohost.me / noho.st / ynh.fr # This is mainly meant to speed up things for "dyndns update" # ... otherwise we end up constantly doing a bunch of dig requests - for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS: - if domain.endswith("." + ynh_dyndns_domain): - # Keep only foo.nohost.me even if we have subsub.sub.foo.nohost.me - return ".".join(domain.rsplit(".", 3)[-3:]) + if is_yunohost_dyndns_domain(domain): + # Keep only foo.nohost.me even if we have subsub.sub.foo.nohost.me + return ".".join(domain.rsplit(".", 3)[-3:]) + + # Same thing with .local, .test, ... domains + if is_special_use_tld(domain): + # Keep only foo.local even if we have subsub.sub.foo.local + return ".".join(domain.rsplit(".", 2)[-2:]) # Check cache cache_folder = "/var/cache/yunohost/dns_zones" @@ -521,10 +525,7 @@ def _get_registrar_config_section(domain): # TODO big project, integrate yunohost's dynette as a registrar-like provider # TODO big project, integrate other dyndns providers such as netlib.re, or cf the list of dyndns providers supported by cloudron... - if any( - dns_zone.endswith("." + ynh_dyndns_domain) - for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS - ): + if is_yunohost_dyndns_domain(dns_zone): registrar_infos["registrar"] = OrderedDict( { "type": "alert", @@ -534,6 +535,15 @@ def _get_registrar_config_section(domain): } ) return OrderedDict(registrar_infos) + elif is_special_use_tld(dns_zone): + registrar_infos["registrar"] = OrderedDict( + { + "type": "alert", + "style": "info", + "ask": m18n.n("domain_dns_conf_special_use_tld"), + "value": None, + } + ) try: registrar = _relevant_provider_for_domain(dns_zone)[0] @@ -607,6 +617,10 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= _assert_domain_exists(domain) + if is_special_use_tld(domain): + logger.info(m18n.n("domain_dns_conf_special_use_tld")) + return {} + if not registrar or registrar == "None": # yes it's None as a string raise YunohostValidationError("domain_dns_push_not_applicable", domain=domain) diff --git a/src/yunohost/tests/test_dns.py b/src/yunohost/tests/test_dns.py index cdd37b2ea..a23ac7982 100644 --- a/src/yunohost/tests/test_dns.py +++ b/src/yunohost/tests/test_dns.py @@ -38,6 +38,9 @@ def test_get_dns_zone_from_domain_existing(): assert _get_dns_zone_for_domain("foo.yolo.nohost.me") == "yolo.nohost.me" assert _get_dns_zone_for_domain("bar.foo.yolo.nohost.me") == "yolo.nohost.me" + assert _get_dns_zone_for_domain("yolo.test") == "yolo.test" + assert _get_dns_zone_for_domain("foo.yolo.test") == "yolo.test" + assert _get_dns_zone_for_domain("yolo.tld") == "yolo.tld" assert _get_dns_zone_for_domain("foo.yolo.tld") == "yolo.tld" diff --git a/src/yunohost/utils/dns.py b/src/yunohost/utils/dns.py index 3db75f949..4ab17416d 100644 --- a/src/yunohost/utils/dns.py +++ b/src/yunohost/utils/dns.py @@ -23,6 +23,8 @@ from typing import List from moulinette.utils.filesystem import read_file +SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"] + YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] # Lazy dev caching to avoid re-reading the file multiple time when calling @@ -30,6 +32,16 @@ YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] 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_ From 85e6ddbeae1c2c2f43f2d84cda2faa52e8479917 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 20:50:01 +0200 Subject: [PATCH 046/142] firewall.py: fix encoding issue, remove prependline stuff --- src/yunohost/firewall.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 4be6810ec..a1c0b187f 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -31,7 +31,6 @@ from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils import process from moulinette.utils.log import getActionLogger -from moulinette.utils.text import prependlines FIREWALL_FILE = "/etc/yunohost/firewall.yml" UPNP_CRON_JOB = "/etc/cron.d/yunohost-firewall-upnp" @@ -240,7 +239,7 @@ def firewall_reload(skip_upnp=False): except process.CalledProcessError as e: logger.debug( "iptables seems to be not available, it outputs:\n%s", - prependlines(e.output.rstrip(), "> "), + e.output.decode().strip(), ) logger.warning(m18n.n("iptables_unavailable")) else: @@ -273,7 +272,7 @@ def firewall_reload(skip_upnp=False): except process.CalledProcessError as e: logger.debug( "ip6tables seems to be not available, it outputs:\n%s", - prependlines(e.output.rstrip(), "> "), + e.output.decode().strip(), ) logger.warning(m18n.n("ip6tables_unavailable")) else: @@ -526,6 +525,6 @@ def _on_rule_command_error(returncode, cmd, output): '"%s" returned non-zero exit status %d:\n%s', cmd, returncode, - prependlines(output.rstrip(), "> "), + output.decode().strip(), ) return True From c44560b6f7cac9486d96a7f111059bfb2b4a4ec3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 21:38:03 +0200 Subject: [PATCH 047/142] Misc cleanups in dns diagnoser --- data/hooks/diagnosis/12-dnsrecords.py | 33 ++++++++++++++++----------- src/yunohost/dns.py | 6 +++++ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index 368c3d878..85f837af5 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -24,8 +24,8 @@ class DNSRecordsDiagnoser(Diagnoser): main_domain = _get_maindomain() - all_domains = domain_list(exclude_subdomains=True)["domains"] - for domain in all_domains: + major_domains = domain_list(exclude_subdomains=True)["domains"] + for domain in major_domains: self.logger_debug("Diagnosing DNS conf for %s" % domain) for report in self.check_domain( @@ -37,7 +37,7 @@ class DNSRecordsDiagnoser(Diagnoser): # Check if a domain buy by the user will expire soon psl = PublicSuffixList() domains_from_registrar = [ - psl.get_public_suffix(domain) for domain in all_domains + psl.get_public_suffix(domain) for domain in major_domains ] domains_from_registrar = [ domain for domain in domains_from_registrar if "." in domain @@ -50,15 +50,6 @@ class DNSRecordsDiagnoser(Diagnoser): def check_domain(self, domain, is_main_domain): - base_dns_zone = _get_dns_zone_for_domain(domain) - basename = domain.replace(base_dns_zone, "").rstrip(".") or "@" - - expected_configuration = _build_dns_conf( - domain, include_empty_AAAA_if_no_ipv6=True - ) - - categories = ["basic", "mail", "xmpp", "extra"] - if is_special_use_tld(domain): categories = [] yield dict( @@ -68,6 +59,15 @@ class DNSRecordsDiagnoser(Diagnoser): summary="diagnosis_dns_specialusedomain", ) + base_dns_zone = _get_dns_zone_for_domain(domain) + basename = domain.replace(base_dns_zone, "").rstrip(".") or "@" + + expected_configuration = _build_dns_conf( + domain, include_empty_AAAA_if_no_ipv6=True + ) + + categories = ["basic", "mail", "xmpp", "extra"] + for category in categories: records = expected_configuration[category] @@ -79,7 +79,8 @@ class DNSRecordsDiagnoser(Diagnoser): id_ = r["type"] + ":" + r["name"] fqdn = r["name"] + "." + base_dns_zone if r["name"] != "@" else domain - # Ugly hack to not check mail records for subdomains stuff, otherwise will end up in a shitstorm of errors for people with many subdomains... + # Ugly hack to not check mail records for subdomains stuff, + # otherwise will end up in a shitstorm of errors for people with many subdomains... # Should find a cleaner solution in the suggested conf... if r["type"] in ["MX", "TXT"] and fqdn not in [ domain, @@ -126,6 +127,12 @@ class DNSRecordsDiagnoser(Diagnoser): status = "SUCCESS" summary = "diagnosis_dns_good_conf" + # If status is okay and there's actually no expected records + # (e.g. XMPP disabled) + # then let's not yield any diagnosis line + if not records and "status" == "SUCCESS": + continue + output = dict( meta={"domain": domain, "category": category}, data=results, diff --git a/src/yunohost/dns.py b/src/yunohost/dns.py index 72c9f7c72..493d7ad86 100644 --- a/src/yunohost/dns.py +++ b/src/yunohost/dns.py @@ -297,6 +297,12 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): # Defined by custom hooks ships in apps for example ... + # FIXME : this ain't practical for apps that may want to add + # custom dns records for a subdomain ... there's no easy way for + # an app to compare the base domain is the parent of the subdomain ? + # (On the other hand, in sep 2021, it looks like no app is using + # this mechanism...) + hook_results = hook_callback("custom_dns_rules", args=[base_domain]) for hook_name, results in hook_results.items(): # From b8cb374a4925b2afa44d1f24abdb92831a92ee18 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 21:43:52 +0200 Subject: [PATCH 048/142] Add a proper _get_parent_domain --- src/yunohost/dns.py | 14 ++------------ src/yunohost/domain.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/yunohost/dns.py b/src/yunohost/dns.py index 493d7ad86..fdceee26b 100644 --- a/src/yunohost/dns.py +++ b/src/yunohost/dns.py @@ -40,6 +40,8 @@ from yunohost.domain import ( domain_config_get, _get_domain_settings, _set_domain_settings, + _get_parent_domain_of, + _list_subdomains_of, ) from yunohost.utils.dns import dig, is_yunohost_dyndns_domain, is_special_use_tld from yunohost.utils.error import YunohostValidationError, YunohostError @@ -107,18 +109,6 @@ def domain_dns_suggest(domain): return result -def _list_subdomains_of(parent_domain): - - _assert_domain_exists(parent_domain) - - out = [] - for domain in domain_list()["domains"]: - if domain.endswith(f".{parent_domain}"): - out.append(domain) - - return out - - def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): """ Internal function that will returns a data structure containing the needed diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 1f96ced8a..f5b14748d 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -105,6 +105,33 @@ def _assert_domain_exists(domain): raise YunohostValidationError("domain_name_unknown", domain=domain) +def _list_subdomains_of(parent_domain): + + _assert_domain_exists(parent_domain) + + out = [] + for domain in domain_list()["domains"]: + if domain.endswith(f".{parent_domain}"): + out.append(domain) + + return out + + +def _get_parent_domain_of(domain): + + _assert_domain_exists(domain) + + if "." not in domain: + return domain + + parent_domain = domain.split(".", 1)[-1] + if parent_domain not in domain_list()["domains"]: + return domain # Domain is its own parent + + else: + return _get_parent_domain_of(parent_domain) + + @is_unit_operation() def domain_add(operation_logger, domain, dyndns=False): """ From d75c1a61e82738853a22f0aaec28acb2b207d6fb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 22:11:24 +0200 Subject: [PATCH 049/142] Adapt ready_for_ACME check to the new dnsrecord result format... --- src/yunohost/certificate.py | 43 ++++++++++++++++++++++++++----------- src/yunohost/dns.py | 1 - 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 817f9d57a..0df428c77 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -851,14 +851,9 @@ def _backup_current_cert(domain): def _check_domain_is_ready_for_ACME(domain): - dnsrecords = ( - Diagnoser.get_cached_report( - "dnsrecords", - item={"domain": domain, "category": "basic"}, - warn_if_no_cache=False, - ) - or {} - ) + from yunohost.domain import _get_parent_domain_of + from yunohost.dns import _get_dns_zone_for_domain + httpreachable = ( Diagnoser.get_cached_report( "web", item={"domain": domain}, warn_if_no_cache=False @@ -866,16 +861,38 @@ def _check_domain_is_ready_for_ACME(domain): or {} ) - if not dnsrecords or not httpreachable: + parent_domain = _get_parent_domain_of(domain) + + dnsrecords = ( + Diagnoser.get_cached_report( + "dnsrecords", + item={"domain": parent_domain, "category": "basic"}, + warn_if_no_cache=False, + ) + or {} + ) + + base_dns_zone = _get_dns_zone_for_domain(domain) + record_name = domain.replace(f".{base_dns_zone}", "") if domain != base_dns_zone else "@" + A_record_status = dnsrecords.get("data").get(f"A:{record_name}") + AAAA_record_status = dnsrecords.get("data").get(f"AAAA:{record_name}") + + # Fallback to wildcard in case no result yet for the DNS name? + if not A_record_status: + A_record_status = dnsrecords.get("data").get(f"A:*") + if not AAAA_record_status: + AAAA_record_status = dnsrecords.get("data").get(f"AAAA:*") + + if not httpreachable or not dnsrecords.get("data") or (A_record_status, AAAA_record_status) == (None, None): raise YunohostValidationError( "certmanager_domain_not_diagnosed_yet", domain=domain ) # Check if IP from DNS matches public IP - if not dnsrecords.get("status") in [ - "SUCCESS", - "WARNING", - ]: # Warning is for missing IPv6 record which ain't critical for ACME + # - 'MISSING' for IPv6 ain't critical for ACME + # - IPv4 can be None assuming there's at least an IPv6, and viveversa + # - (the case where both are None is checked before) + if not (A_record_status in [None, "OK"] and AAAA_record_status in [None, "OK", "MISSING"]): raise YunohostValidationError( "certmanager_domain_dns_ip_differs_from_public_ip", domain=domain ) diff --git a/src/yunohost/dns.py b/src/yunohost/dns.py index fdceee26b..689950490 100644 --- a/src/yunohost/dns.py +++ b/src/yunohost/dns.py @@ -40,7 +40,6 @@ from yunohost.domain import ( domain_config_get, _get_domain_settings, _set_domain_settings, - _get_parent_domain_of, _list_subdomains_of, ) from yunohost.utils.dns import dig, is_yunohost_dyndns_domain, is_special_use_tld From 2e96cb28bb0bf79520f1e6e9d210f5a6ee1780b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= Date: Sun, 19 Sep 2021 21:12:58 +0000 Subject: [PATCH 050/142] Translated using Weblate (French) Currently translated at 99.8% (707 of 708 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index bd29ad774..29e6da673 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -675,5 +675,35 @@ "log_app_config_set": "Appliquer la configuration à l'application '{}'", "service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}", "app_argument_password_help_keep": "Tapez sur Entrée pour conserver la valeur actuelle", - "app_argument_password_help_optional": "Tapez un espace pour vider le mot de passe" + "app_argument_password_help_optional": "Tapez un espace pour vider le mot de passe", + "domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.", + "domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.", + "domain_dns_registrar_yunohost": "Ce domaine est nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans autre configuration. (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.", + "domain_dns_registrar_experimental": "Jusqu'à présent, l'interface avec l'API de **{registrar}** n'a pas été correctement testée et revue par la communauté YunoHost. L'assistance est **très expérimentale** - soyez prudent !", + "domain_dns_push_failed_to_authenticate": "Échec de l'authentification sur l'API du bureau d'enregistrement pour le domaine « {domain} ». Très probablement les informations d'identification sont incorrectes ? (Error: {error})", + "domain_dns_push_failed_to_list": "Échec de la liste des enregistrements actuels à l'aide de l'API du registraire : {error}", + "domain_dns_push_already_up_to_date": "Dossiers déjà à jour.", + "domain_dns_pushing": "Transmission des enregistrements DNS...", + "domain_dns_push_record_failed": "Échec de l'enregistrement {action} {type}/{name} : {error}", + "domain_dns_push_success": "Enregistrements DNS mis à jour !", + "domain_dns_push_failed": "La mise à jour des enregistrements DNS a échoué.", + "domain_dns_push_partial_failure": "Enregistrements DNS partiellement mis à jour : certains avertissements/erreurs ont été signalés.", + "domain_config_mail_in": "Emails entrants", + "domain_config_mail_out": "Emails sortants", + "domain_config_xmpp": "Messagerie instantanée (XMPP)", + "domain_config_auth_token": "Jeton d'authentification", + "domain_config_auth_key": "Clé d'authentification", + "domain_config_auth_secret": "Secret d'authentification", + "domain_config_api_protocol": "Protocole API", + "domain_config_auth_entrypoint": "Point d'entrée API", + "domain_config_auth_application_key": "Clé d'application", + "domain_config_auth_application_secret": "Clé secrète de l'application", + "ldap_attribute_already_exists": "L'attribut LDAP '{attribute}' existe déjà avec la valeur '{value}'", + "log_domain_config_set": "Mettre à jour la configuration du domaine '{}'", + "log_domain_dns_push": "Pousser les enregistrements DNS pour le domaine '{}'" } From df0cdd483d33e5bab5e6f1c1603f690835a74425 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 22:20:29 +0200 Subject: [PATCH 051/142] Black --- data/hooks/diagnosis/12-dnsrecords.py | 7 ++++++- src/yunohost/certificate.py | 15 ++++++++++++--- src/yunohost/utils/dns.py | 4 +++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index 85f837af5..677a947a7 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -8,7 +8,12 @@ from publicsuffix import PublicSuffixList from moulinette.utils.process import check_output -from yunohost.utils.dns import dig, YNH_DYNDNS_DOMAINS, is_yunohost_dyndns_domain, is_special_use_tld +from yunohost.utils.dns import ( + dig, + YNH_DYNDNS_DOMAINS, + is_yunohost_dyndns_domain, + is_special_use_tld, +) from yunohost.diagnosis import Diagnoser from yunohost.domain import domain_list, _get_maindomain from yunohost.dns import _build_dns_conf, _get_dns_zone_for_domain diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 0df428c77..e960098f4 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -873,7 +873,9 @@ def _check_domain_is_ready_for_ACME(domain): ) base_dns_zone = _get_dns_zone_for_domain(domain) - record_name = domain.replace(f".{base_dns_zone}", "") if domain != base_dns_zone else "@" + record_name = ( + domain.replace(f".{base_dns_zone}", "") if domain != base_dns_zone else "@" + ) A_record_status = dnsrecords.get("data").get(f"A:{record_name}") AAAA_record_status = dnsrecords.get("data").get(f"AAAA:{record_name}") @@ -883,7 +885,11 @@ def _check_domain_is_ready_for_ACME(domain): if not AAAA_record_status: AAAA_record_status = dnsrecords.get("data").get(f"AAAA:*") - if not httpreachable or not dnsrecords.get("data") or (A_record_status, AAAA_record_status) == (None, None): + if ( + not httpreachable + or not dnsrecords.get("data") + or (A_record_status, AAAA_record_status) == (None, None) + ): raise YunohostValidationError( "certmanager_domain_not_diagnosed_yet", domain=domain ) @@ -892,7 +898,10 @@ def _check_domain_is_ready_for_ACME(domain): # - 'MISSING' for IPv6 ain't critical for ACME # - IPv4 can be None assuming there's at least an IPv6, and viveversa # - (the case where both are None is checked before) - if not (A_record_status in [None, "OK"] and AAAA_record_status in [None, "OK", "MISSING"]): + if not ( + A_record_status in [None, "OK"] + and AAAA_record_status in [None, "OK", "MISSING"] + ): raise YunohostValidationError( "certmanager_domain_dns_ip_differs_from_public_ip", domain=domain ) diff --git a/src/yunohost/utils/dns.py b/src/yunohost/utils/dns.py index 4ab17416d..ccb6c5406 100644 --- a/src/yunohost/utils/dns.py +++ b/src/yunohost/utils/dns.py @@ -34,7 +34,9 @@ external_resolvers_: List[str] = [] def is_yunohost_dyndns_domain(domain): - return any(domain.endswith(f".{dyndns_domain}") for dyndns_domain in YNH_DYNDNS_DOMAINS) + return any( + domain.endswith(f".{dyndns_domain}") for dyndns_domain in YNH_DYNDNS_DOMAINS + ) def is_special_use_tld(domain): From 55598151c0de151ccfb662249e5776f1b6c3cecf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 22:29:28 +0200 Subject: [PATCH 052/142] Unhappy linter is unhappy --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index e960098f4..fe350bf95 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -881,9 +881,9 @@ def _check_domain_is_ready_for_ACME(domain): # Fallback to wildcard in case no result yet for the DNS name? if not A_record_status: - A_record_status = dnsrecords.get("data").get(f"A:*") + A_record_status = dnsrecords.get("data").get("A:*") if not AAAA_record_status: - AAAA_record_status = dnsrecords.get("data").get(f"AAAA:*") + AAAA_record_status = dnsrecords.get("data").get("AAAA:*") if ( not httpreachable From 5c309ecc14edce88d3769fedec3bab70c530d619 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 22:30:44 +0200 Subject: [PATCH 053/142] Update changelog for 4.3.1 --- debian/changelog | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/debian/changelog b/debian/changelog index e6bd5180e..84a29ed70 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +yunohost (4.3.1) testing; urgency=low + + - [fix] diagnosis: new app diagnosis grep reporing comments as issues ([#1333](https://github.com/YunoHost/yunohost/pull/1333)) + - [enh] configpanel: Bind function for hotspot (79126809) + - [enh] cli: Rework/improve prompt mecanic ([#1338](https://github.com/YunoHost/yunohost/pull/1338)) + - [fix] dyndns update broke because of buggy dns record names (da1b9089) + - [enh] dns: general improvement for special-use TLD / ynh dyndns domains (17aafe6f) + - [fix] yunomdns: various fixes/improvements ([#1335](https://github.com/YunoHost/yunohost/pull/1335)) + - [fix] certs: Adapt ready_for_ACME check to the new dnsrecord result format... (d75c1a61) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Éric Gaspar, Félix Piédallu, Kayou, ljf, tituspijean) + + -- Alexandre Aubin Wed, 29 Sep 2021 22:22:42 +0200 + yunohost (4.3.0) testing; urgency=low - [users] Import/export users from/to CSV ([#1089](https://github.com/YunoHost/yunohost/pull/1089)) From 80661ddca9bd2b997fb5ad80dc3454c03e9f910f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Sep 2021 22:43:38 +0200 Subject: [PATCH 054/142] debian: Bump moulinette/ssowat version requirements --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 90bac0a0d..fe18b1de8 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Package: yunohost Essential: yes Architecture: all Depends: ${python3:Depends}, ${misc:Depends} - , moulinette (>= 4.2), ssowat (>= 4.0) + , moulinette (>= 4.3), ssowat (>= 4.3) , python3-psutil, python3-requests, python3-dnspython, python3-openssl , python3-miniupnpc, python3-dbus, python3-jinja2 , python3-toml, python3-packaging, python3-publicsuffix, From f78167a99cb721b3dd18224179ffc94e36b5de6b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 30 Sep 2021 01:08:22 +0200 Subject: [PATCH 055/142] swag: Add test coverage badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9fc93740d..b256bb563 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@
[![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) [![GitHub license](https://img.shields.io/github/license/YunoHost/yunohost)](https://github.com/YunoHost/yunohost/blob/dev/LICENSE) [![Mastodon Follow](https://img.shields.io/mastodon/follow/28084)](https://mastodon.social/@yunohost) From 8cdd5e43d743fcba0cb2ec1b53a454787eb5ffb6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 02:18:35 +0200 Subject: [PATCH 056/142] Simplify / fix inconsistencies in app files kept after install/upgrade --- src/yunohost/app.py | 77 ++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 57 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 56e4c344b..c111de405 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -79,6 +79,17 @@ re_app_instance_name = re.compile( r"^(?P[\w-]+?)(__(?P[1-9][0-9]*))?$" ) +APP_FILES_TO_COPY = [ + "manifest.json", + "manifest.toml", + "actions.json", + "actions.toml", + "config_panel.toml", + "scripts", + "conf", + "hooks", + "doc", +] def app_catalog(full=False, with_categories=False): """ @@ -710,38 +721,11 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False hook_add(app_instance_name, extracted_app_folder + "/hooks/" + hook) # Replace scripts and manifest and conf (if exists) - os.system( - 'rm -rf "%s/scripts" "%s/manifest.toml %s/manifest.json %s/conf"' - % ( - app_setting_path, - app_setting_path, - app_setting_path, - app_setting_path, - ) - ) - - if os.path.exists(os.path.join(extracted_app_folder, "manifest.json")): - os.system( - 'mv "%s/manifest.json" "%s/scripts" %s' - % (extracted_app_folder, extracted_app_folder, app_setting_path) - ) - if os.path.exists(os.path.join(extracted_app_folder, "manifest.toml")): - os.system( - 'mv "%s/manifest.toml" "%s/scripts" %s' - % (extracted_app_folder, extracted_app_folder, app_setting_path) - ) - - for file_to_copy in [ - "actions.json", - "actions.toml", - "config_panel.toml", - "conf", - ]: + # Move scripts and manifest to the right place + for file_to_copy in APP_FILES_TO_COPY: + os.system(f"rm -rf '{app_setting_path}/{file_to_copy}'") if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): - os.system( - "cp -R %s/%s %s" - % (extracted_app_folder, file_to_copy, app_setting_path) - ) + os.system(f"cp -R '{extracted_app_folder}/{file_to_copy} {app_setting_path}'") # Clean and set permissions shutil.rmtree(extracted_app_folder) @@ -945,23 +929,9 @@ def app_install( _set_app_settings(app_instance_name, app_settings) # Move scripts and manifest to the right place - if os.path.exists(os.path.join(extracted_app_folder, "manifest.json")): - os.system("cp %s/manifest.json %s" % (extracted_app_folder, app_setting_path)) - if os.path.exists(os.path.join(extracted_app_folder, "manifest.toml")): - os.system("cp %s/manifest.toml %s" % (extracted_app_folder, app_setting_path)) - os.system("cp -R %s/scripts %s" % (extracted_app_folder, app_setting_path)) - - for file_to_copy in [ - "actions.json", - "actions.toml", - "config_panel.toml", - "conf", - ]: + for file_to_copy in APP_FILES_TO_COPY: if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): - os.system( - "cp -R %s/%s %s" - % (extracted_app_folder, file_to_copy, app_setting_path) - ) + os.system(f"cp -R '{extracted_app_folder}/{file_to_copy}' '{app_setting_path}'" # Initialize the main permission for the app # The permission is initialized with no url associated, and with tile disabled @@ -1038,11 +1008,7 @@ def app_install( logger.warning(m18n.n("app_remove_after_failed_install")) # Setup environment for remove script - env_dict_remove = {} - env_dict_remove["YNH_APP_ID"] = app_id - env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name - env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) - env_dict_remove["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") + env_dict_remove = _make_environment_for_app_script(app_instance_name) env_dict_remove["YNH_APP_BASEDIR"] = extracted_app_folder # Execute remove script @@ -1156,12 +1122,9 @@ def app_remove(operation_logger, app, purge=False): env_dict = {} app_id, app_instance_nb = _parse_app_instance_name(app) - env_dict["YNH_APP_ID"] = app_id - env_dict["YNH_APP_INSTANCE_NAME"] = app - env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") - env_dict["YNH_APP_PURGE"] = str(1 if purge else 0) + env_dict = _make_environment_for_app_script(app) env_dict["YNH_APP_BASEDIR"] = tmp_workdir_for_app + env_dict["YNH_APP_PURGE"] = str(1 if purge else 0) operation_logger.extra.update({"env": env_dict}) operation_logger.flush() From a1ff2ced37a3ef01966e359dfeebe62114f28673 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 02:21:31 +0200 Subject: [PATCH 057/142] Clarify code that extract app from folder/gitrepo + misc other small refactors --- src/yunohost/app.py | 364 ++++++++++++++++++++------------------------ 1 file changed, 163 insertions(+), 201 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index c111de405..88deb5b6b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -566,22 +566,22 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False if file and isinstance(file, dict): # We use this dirty hack to test chained upgrades in unit/functional tests - manifest, extracted_app_folder = _extract_app_from_file( - file[app_instance_name] - ) + new_app_src = file[app_instance_name] elif file: - manifest, extracted_app_folder = _extract_app_from_file(file) + new_app_src = file elif url: - manifest, extracted_app_folder = _fetch_app_from_git(url) + new_app_src = url elif app_dict["upgradable"] == "url_required": logger.warning(m18n.n("custom_app_url_required", app=app_instance_name)) continue elif app_dict["upgradable"] == "yes" or force: - manifest, extracted_app_folder = _fetch_app_from_git(app_instance_name) + new_app_src = app_dict["id"] else: logger.success(m18n.n("app_already_up_to_date", app=app_instance_name)) continue + manifest, extracted_app_folder = _extract_app(new_app_src) + # Manage upgrade type and avoid any upgrade if there is nothing to do upgrade_type = "UNKNOWN" # Get current_version and new version @@ -748,12 +748,7 @@ def app_manifest(app): raw_app_list = _load_apps_catalog()["apps"] - if app in raw_app_list or ("@" in app) or ("http://" in app) or ("https://" in app): - manifest, extracted_app_folder = _fetch_app_from_git(app) - elif os.path.exists(app): - manifest, extracted_app_folder = _extract_app_from_file(app) - else: - raise YunohostValidationError("app_unknown") + manifest, extracted_app_folder = _extract_app(app) shutil.rmtree(extracted_app_folder) @@ -796,19 +791,28 @@ def app_install( ) from yunohost.regenconf import manually_modified_files - def confirm_install(confirm): + # Check if disk space available + if free_space_in_directory("/") <= 512 * 1000 * 1000: + raise YunohostValidationError("disk_space_not_sufficient_install") + + def confirm_install(app): + # Ignore if there's nothing for confirm (good quality app), if --force is used # or if request on the API (confirm already implemented on the API side) - if confirm is None or force or Moulinette.interface.type == "api": + if force or Moulinette.interface.type == "api": + return + + quality = app_quality(app) + if quality == "success": return # i18n: confirm_app_install_warning # i18n: confirm_app_install_danger # i18n: confirm_app_install_thirdparty - if confirm in ["danger", "thirdparty"]: + if quality in ["danger", "thirdparty"]: answer = Moulinette.prompt( - m18n.n("confirm_app_install_" + confirm, answers="Yes, I understand"), + m18n.n("confirm_app_install_" + quality, answers="Yes, I understand"), color="red", ) if answer != "Yes, I understand": @@ -816,51 +820,13 @@ def app_install( else: answer = Moulinette.prompt( - m18n.n("confirm_app_install_" + confirm, answers="Y/N"), color="yellow" + m18n.n("confirm_app_install_" + quality, answers="Y/N"), color="yellow" ) if answer.upper() != "Y": raise YunohostError("aborting") - raw_app_list = _load_apps_catalog()["apps"] - - if app in raw_app_list or ("@" in app) or ("http://" in app) or ("https://" in app): - - # If we got an app name directly (e.g. just "wordpress"), we gonna test this name - if app in raw_app_list: - app_name_to_test = app - # If we got an url like "https://github.com/foo/bar_ynh, we want to - # extract "bar" and test if we know this app - elif ("http://" in app) or ("https://" in app): - app_name_to_test = app.strip("/").split("/")[-1].replace("_ynh", "") - else: - # FIXME : watdo if '@' in app ? - app_name_to_test = None - - if app_name_to_test in raw_app_list: - - state = raw_app_list[app_name_to_test].get("state", "notworking") - level = raw_app_list[app_name_to_test].get("level", None) - confirm = "danger" - if state in ["working", "validated"]: - if isinstance(level, int) and level >= 5: - confirm = None - elif isinstance(level, int) and level > 0: - confirm = "warning" - else: - confirm = "thirdparty" - - confirm_install(confirm) - - manifest, extracted_app_folder = _fetch_app_from_git(app) - elif os.path.exists(app): - confirm_install("thirdparty") - manifest, extracted_app_folder = _extract_app_from_file(app) - else: - raise YunohostValidationError("app_unknown") - - # Check if disk space available - if free_space_in_directory("/") <= 512 * 1000 * 1000: - raise YunohostValidationError("disk_space_not_sufficient_install") + confirm_install(app) + manifest, extracted_app_folder = _extract_app(app) # Check ID if "id" not in manifest or "__" in manifest["id"] or "." in manifest["id"]: @@ -874,7 +840,7 @@ def app_install( _assert_system_is_sane_for_app(manifest, "pre") # Check if app can be forked - instance_number = _installed_instance_number(app_id, last=True) + 1 + instance_number = _next_instance_number_for_app(app_id) if instance_number > 1: if "multi_instance" not in manifest or not is_true(manifest["multi_instance"]): raise YunohostValidationError("app_already_installed", app=app_id) @@ -1914,55 +1880,6 @@ def _set_app_settings(app_id, settings): yaml.safe_dump(settings, f, default_flow_style=False) -def _extract_app_from_file(path): - """ - Unzip / untar / copy application tarball or directory to a tmp work directory - - Keyword arguments: - path -- Path of the tarball or directory - """ - logger.debug(m18n.n("extracting")) - - path = os.path.abspath(path) - - extracted_app_folder = _make_tmp_workdir_for_app() - - if ".zip" in path: - extract_result = os.system( - f"unzip '{path}' -d {extracted_app_folder} > /dev/null 2>&1" - ) - elif ".tar" in path: - extract_result = os.system( - f"tar -xf '{path}' -C {extracted_app_folder} > /dev/null 2>&1" - ) - elif os.path.isdir(path): - shutil.rmtree(extracted_app_folder) - if path[-1] != "/": - path = path + "/" - extract_result = os.system(f"cp -a '{path}' {extracted_app_folder}") - else: - extract_result = 1 - - if extract_result != 0: - raise YunohostError("app_extraction_failed") - - try: - if len(os.listdir(extracted_app_folder)) == 1: - for folder in os.listdir(extracted_app_folder): - extracted_app_folder = extracted_app_folder + "/" + folder - manifest = _get_manifest_of_app(extracted_app_folder) - manifest["lastUpdate"] = int(time.time()) - except IOError: - raise YunohostError("app_install_files_invalid") - except ValueError as e: - raise YunohostError("app_manifest_invalid", error=e) - - logger.debug(m18n.n("done")) - - manifest["remote"] = {"type": "file", "path": path} - return manifest, extracted_app_folder - - def _get_manifest_of_app(path): "Get app manifest stored in json or in toml" @@ -2158,143 +2075,169 @@ def _set_default_ask_questions(arguments): return arguments -def _get_git_last_commit_hash(repository, reference="HEAD"): +def _app_quality(app: str) -> str: """ - Attempt to retrieve the last commit hash of a git repository - - Keyword arguments: - repository -- The URL or path of the repository - + app may in fact be an app name, an url, or a path """ - try: - cmd = "git ls-remote --exit-code {0} {1} | awk '{{print $1}}'".format( - repository, reference - ) - commit = check_output(cmd) - except subprocess.CalledProcessError: - logger.error("unable to get last commit from %s", repository) - raise ValueError("Unable to get last commit with git") + + raw_app_catalog = _load_apps_catalog()["apps"] + if app in raw_app_catalog or ("@" in app) or ("http://" in app) or ("https://" in app): + + # If we got an app name directly (e.g. just "wordpress"), we gonna test this name + if app in raw_app_list: + app_name_to_test = app + # If we got an url like "https://github.com/foo/bar_ynh, we want to + # extract "bar" and test if we know this app + elif ("http://" in app) or ("https://" in app): + app_name_to_test = app.strip("/").split("/")[-1].replace("_ynh", "") + else: + # FIXME : watdo if '@' in app ? + app_name_to_test = None + + if app_name_to_test in raw_app_list: + + state = raw_app_list[app_name_to_test].get("state", "notworking") + level = raw_app_list[app_name_to_test].get("level", None) + if state in ["working", "validated"]: + if isinstance(level, int) and level >= 5: + return "success" + elif isinstance(level, int) and level > 0: + return "warning" + return "danger" + else: + return "thirdparty" + + elif os.path.exists(app): + return "thirdparty" else: - return commit.strip() + raise YunohostValidationError("app_unknown") -def _fetch_app_from_git(app): +def _extract_app(src: str) -> Tuple[Dict, str]: """ - Unzip or untar application tarball to a tmp directory - - Keyword arguments: - app -- App_id or git repo URL + src may be an app name, an url, or a path """ - # Extract URL, branch and revision to download - if ("@" in app) or ("http://" in app) or ("https://" in app): - url = app - branch = "master" - if "/tree/" in url: - url, branch = url.split("/tree/", 1) - revision = "HEAD" - else: - app_dict = _load_apps_catalog()["apps"] + raw_app_catalog = _load_apps_catalog()["apps"] - app_id, _ = _parse_app_instance_name(app) - - if app_id not in app_dict: - raise YunohostValidationError("app_unknown") - elif "git" not in app_dict[app_id]: + # App is an appname in the catalog + if src in raw_app_catalog: + if "git" not in raw_app_catalog[src]: raise YunohostValidationError("app_unsupported_remote_type") - app_info = app_dict[app_id] + app_info = raw_app_catalog[src] 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) + # App is a git repo url + elif ("@" in src) or ("http://" in src) or ("https://" in src): + url = src + branch = "master" + revision = "HEAD" + if "/tree/" in url: + url, branch = url.split("/tree/", 1) + return _extract_app_from_gitrepo(url, branch, revision, {}) + # App is a local folder + elif os.path.exists(src): + return _extract_app_from_folder(src) + else: + raise YunohostValidationError("app_unknown") + + +def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: + """ + Unzip / untar / copy application tarball or directory to a tmp work directory + + Keyword arguments: + path -- Path of the tarball or directory + """ + logger.debug(m18n.n("extracting")) + + path = os.path.abspath(path) extracted_app_folder = _make_tmp_workdir_for_app() + if ".zip" in path: + extract_result = os.system( + f"unzip '{path}' -d {extracted_app_folder} > /dev/null 2>&1" + ) + elif ".tar" in path: + extract_result = os.system( + f"tar -xf '{path}' -C {extracted_app_folder} > /dev/null 2>&1" + ) + elif os.path.isdir(path): + shutil.rmtree(extracted_app_folder) + if path[-1] != "/": + path = path + "/" + extract_result = os.system(f"cp -a '{path}' {extracted_app_folder}") + else: + extract_result = 1 + + if extract_result != 0: + raise YunohostError("app_extraction_failed") + + try: + if len(os.listdir(extracted_app_folder)) == 1: + for folder in os.listdir(extracted_app_folder): + extracted_app_folder = extracted_app_folder + "/" + folder + except IOError: + raise YunohostError("app_install_files_invalid") + + manifest = _get_manifest_of_app(extracted_app_folder) + manifest["lastUpdate"] = int(time.time()) + + logger.debug(m18n.n("done")) + + manifest["remote"] = {"type": "file", "path": path} + return manifest, extracted_app_folder + + +def _extract_app_from_gitrepo(url: str, branch: str, revision: str, app_info={}: Dict) -> Tuple[Dict, str]: + logger.debug(m18n.n("downloading")) + extracted_app_folder = _make_tmp_workdir_for_app() + # Download only this commit try: # We don't use git clone because, git clone can't download # a specific revision only + ref = branch if revision == "HEAD" else revision run_commands([["git", "init", extracted_app_folder]], shell=False) run_commands( [ ["git", "remote", "add", "origin", url], - [ - "git", - "fetch", - "--depth=1", - "origin", - branch if revision == "HEAD" else revision, - ], + ["git", "fetch", "--depth=1", "origin", ref], ["git", "reset", "--hard", "FETCH_HEAD"], ], cwd=extracted_app_folder, shell=False, ) - manifest = _get_manifest_of_app(extracted_app_folder) except subprocess.CalledProcessError: raise YunohostError("app_sources_fetch_failed") - except ValueError as e: - raise YunohostError("app_manifest_invalid", error=e) else: logger.debug(m18n.n("done")) + manifest = _get_manifest_of_app(extracted_app_folder) + # Store remote repository info into the returned manifest manifest["remote"] = {"type": "git", "url": url, "branch": branch} if revision == "HEAD": try: - manifest["remote"]["revision"] = _get_git_last_commit_hash(url, branch) + # Get git last commit hash + cmd = f"git ls-remote --exit-code {url} {branch} | awk '{{print $1}}'" + manifest["remote"]["revision"] = check_output(cmd) except Exception as e: - logger.debug("cannot get last commit hash because: %s ", e) + logger.warning("cannot get last commit hash because: %s ", e) else: manifest["remote"]["revision"] = revision - manifest["lastUpdate"] = app_info["lastUpdate"] + manifest["lastUpdate"] = app_info.get("lastUpdate") return manifest, extracted_app_folder -def _installed_instance_number(app, last=False): - """ - Check if application is installed and return instance number - - Keyword arguments: - app -- id of App to check - last -- Return only last instance number - - Returns: - Number of last installed instance | List or instances - - """ - if last: - number = 0 - try: - installed_apps = os.listdir(APPS_SETTING_PATH) - except OSError: - os.makedirs(APPS_SETTING_PATH) - return 0 - - for installed_app in installed_apps: - if number == 0 and app == installed_app: - number = 1 - elif "__" in installed_app: - if app == installed_app[: installed_app.index("__")]: - if int(installed_app[installed_app.index("__") + 2 :]) > number: - number = int(installed_app[installed_app.index("__") + 2 :]) - - return number - - else: - instance_number_list = [] - instances_dict = app_map(app=app, raw=True) - for key, domain in instances_dict.items(): - for key, path in domain.items(): - instance_number_list.append(path["instance"]) - - return sorted(instance_number_list) - - -def _is_installed(app): +def _is_installed(app: str) -> bool: """ Check if application is installed @@ -2308,18 +2251,18 @@ def _is_installed(app): return os.path.isdir(APPS_SETTING_PATH + app) -def _assert_is_installed(app): +def _assert_is_installed(app: str) -> None: if not _is_installed(app): raise YunohostValidationError( "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() ) -def _installed_apps(): +def _installed_apps() -> List[str]: return os.listdir(APPS_SETTING_PATH) -def _check_manifest_requirements(manifest, app_instance_name): +def _check_manifest_requirements(manifest: Dict, app: str): """Check if required packages are met from the manifest""" packaging_format = int(manifest.get("packaging_format", 0)) @@ -2331,7 +2274,7 @@ def _check_manifest_requirements(manifest, app_instance_name): if not requirements: return - logger.debug(m18n.n("app_requirements_checking", app=app_instance_name)) + logger.debug(m18n.n("app_requirements_checking", app=app)) # Iterate over requirements for pkgname, spec in requirements.items(): @@ -2342,7 +2285,7 @@ def _check_manifest_requirements(manifest, app_instance_name): pkgname=pkgname, version=version, spec=spec, - app=app_instance_name, + app=app, ) @@ -2513,6 +2456,25 @@ def _parse_app_instance_name(app_instance_name): return (appid, app_instance_nb) +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}__")] + + # Find the list of ids, such as [1, 2, 4] + sibling_ids = [_parse_app_instance_name(a)[1] for a in sibling_app_ids] + + # Find the first 'i' that's not in the sibling_ids list already + i = 1 + while True: + if i not in sibling_ids: + return i + else: + i += 1 + + + # # ############################### # # Applications list management # From f84d8618bcc01bcd6f569723b7db8bc9e146a16b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 02:22:23 +0200 Subject: [PATCH 058/142] Offload legacy app patching stuff to utils/legacy.py to limit the size of app.py --- src/yunohost/app.py | 212 +---------------- src/yunohost/backup.py | 6 +- .../0016_php70_to_php73_pools.py | 3 +- src/yunohost/utils/legacy.py | 217 +++++++++++++++++- 4 files changed, 222 insertions(+), 216 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 88deb5b6b..011c953e0 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -534,6 +534,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False ) from yunohost.permission import permission_sync_to_user from yunohost.regenconf import manually_modified_files + from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers apps = app # Check if disk space available @@ -790,6 +791,7 @@ def app_install( permission_sync_to_user, ) from yunohost.regenconf import manually_modified_files + from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers # Check if disk space available if free_space_in_directory("/") <= 512 * 1000 * 1000: @@ -2769,213 +2771,3 @@ def _assert_system_is_sane_for_app(manifest, when): raise YunohostValidationError("dpkg_is_broken") elif when == "post": raise YunohostError("this_action_broke_dpkg") - - -LEGACY_PHP_VERSION_REPLACEMENTS = [ - ("/etc/php5", "/etc/php/7.3"), - ("/etc/php/7.0", "/etc/php/7.3"), - ("/var/run/php5-fpm", "/var/run/php/php7.3-fpm"), - ("/var/run/php/php7.0-fpm", "/var/run/php/php7.3-fpm"), - ("php5", "php7.3"), - ("php7.0", "php7.3"), - ( - 'phpversion="${phpversion:-7.0}"', - 'phpversion="${phpversion:-7.3}"', - ), # Many helpers like the composer ones use 7.0 by default ... - ( - '"$phpversion" == "7.0"', - '$(bc <<< "$phpversion >= 7.3") -eq 1', - ), # patch ynh_install_php to refuse installing/removing php <= 7.3 -] - - -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)) - files_to_patch.extend(glob.glob("%s/scripts/*/*" % app_folder)) - files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder)) - files_to_patch.append("%s/manifest.json" % 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 - - c = ( - "sed -i " - + "".join( - "-e 's@{pattern}@{replace}@g' ".format(pattern=p, replace=r) - for p, r in LEGACY_PHP_VERSION_REPLACEMENTS - ) - + "%s" % filename - ) - os.system(c) - - -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") == "/etc/php/7.0/fpm": - settings["fpm_config_dir"] = "/etc/php/7.3/fpm" - if settings.get("fpm_service") == "php7.0-fpm": - settings["fpm_service"] = "php7.3-fpm" - if settings.get("phpversion") == "7.0": - settings["phpversion"] = "7.3" - - # We delete these checksums otherwise the file will appear as manually modified - list_to_remove = ["checksum__etc_php_7.0_fpm_pool", "checksum__etc_nginx_conf.d"] - settings = { - k: v - for k, v in settings.items() - if not any(k.startswith(to_remove) for to_remove in list_to_remove) - } - - write_to_yaml(app_folder + "/settings.yml", settings) - - -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)) - - stuff_to_replace = { - # Replace - # sudo yunohost app initdb $db_user -p $db_pwd - # by - # ynh_mysql_setup_db --db_user=$db_user --db_name=$db_user --db_pwd=$db_pwd - "yunohost app initdb": { - "pattern": r"(sudo )?yunohost app initdb \"?(\$\{?\w+\}?)\"?\s+-p\s\"?(\$\{?\w+\}?)\"?", - "replace": r"ynh_mysql_setup_db --db_user=\2 --db_name=\2 --db_pwd=\3", - "important": True, - }, - # Replace - # sudo yunohost app checkport whaterver - # by - # ynh_port_available whatever - "yunohost app checkport": { - "pattern": r"(sudo )?yunohost app checkport", - "replace": r"ynh_port_available", - "important": True, - }, - # We can't migrate easily port-available - # .. but at the time of writing this code, only two non-working apps are using it. - "yunohost tools port-available": {"important": True}, - # Replace - # yunohost app checkurl "${domain}${path_url}" -a "${app}" - # by - # ynh_webpath_register --app=${app} --domain=${domain} --path_url=${path_url} - "yunohost app checkurl": { - "pattern": r"(sudo )?yunohost app checkurl \"?(\$\{?\w+\}?)\/?(\$\{?\w+\}?)\"?\s+-a\s\"?(\$\{?\w+\}?)\"?", - "replace": r"ynh_webpath_register --app=\4 --domain=\2 --path_url=\3", - "important": True, - }, - # Remove - # Automatic diagnosis data from YunoHost - # __PRE_TAG1__$(yunohost tools diagnosis | ...)__PRE_TAG2__" - # - "yunohost tools diagnosis": { - "pattern": r"(Automatic diagnosis data from YunoHost( *\n)*)? *(__\w+__)? *\$\(yunohost tools diagnosis.*\)(__\w+__)?", - "replace": r"", - "important": False, - }, - # Old $1, $2 in backup/restore scripts... - "app=$2": { - "only_for": ["scripts/backup", "scripts/restore"], - "pattern": r"app=\$2", - "replace": r"app=$YNH_APP_INSTANCE_NAME", - "important": True, - }, - # Old $1, $2 in backup/restore scripts... - "backup_dir=$1": { - "only_for": ["scripts/backup", "scripts/restore"], - "pattern": r"backup_dir=\$1", - "replace": r"backup_dir=.", - "important": True, - }, - # Old $1, $2 in backup/restore scripts... - "restore_dir=$1": { - "only_for": ["scripts/restore"], - "pattern": r"restore_dir=\$1", - "replace": r"restore_dir=.", - "important": True, - }, - # Old $1, $2 in install scripts... - # We ain't patching that shit because it ain't trivial to patch all args... - "domain=$1": {"only_for": ["scripts/install"], "important": True}, - } - - for helper, infos in stuff_to_replace.items(): - infos["pattern"] = ( - re.compile(infos["pattern"]) if infos.get("pattern") else None - ) - infos["replace"] = infos.get("replace") - - for filename in files_to_patch: - - # Ignore non-regular files - if not os.path.isfile(filename): - continue - - try: - content = read_file(filename) - except MoulinetteError: - continue - - replaced_stuff = False - 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"] - ): - continue - - # If helper is used, attempt to patch the file - if helper in content and infos["pattern"]: - content = infos["pattern"].sub(infos["replace"], content) - replaced_stuff = True - if infos["important"]: - show_warning = True - - # If the helper is *still* in the content, it means that we - # couldn't patch the deprecated helper in the previous lines. In - # that case, abort the install or whichever step is performed - if helper in content and infos["important"]: - raise YunohostValidationError( - "This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.", - raw_msg=True, - ) - - 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 [ - "install", - "remove", - "upgrade", - "backup", - "restore", - ]: - source_helpers = "source /usr/share/yunohost/helpers" - if source_helpers not in content: - content.replace("#!/bin/bash", "#!/bin/bash\n" + source_helpers) - if source_helpers not in content: - content = source_helpers + "\n" + content - - # Actually write the new content in the file - write_to_file(filename, content) - - if show_warning: - # And complain about those damn deprecated helpers - logger.error( - r"/!\ Packagers ! This app uses a very old deprecated helpers ... Yunohost automatically patched the helpers to use the new recommended practice, but please do consider fixing the upstream code right now ..." - ) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index a47fba5f7..b650cb3da 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -49,10 +49,6 @@ from yunohost.app import ( app_info, _is_installed, _make_environment_for_app_script, - _patch_legacy_helpers, - _patch_legacy_php_versions, - _patch_legacy_php_versions_in_settings, - LEGACY_PHP_VERSION_REPLACEMENTS, _make_tmp_workdir_for_app, ) from yunohost.hook import ( @@ -1190,6 +1186,7 @@ class RestoreManager: """ Apply dirty patch to redirect php5 and php7.0 files to php7.3 """ + from yunohost.utils.legacy import LEGACY_PHP_VERSION_REPLACEMENTS backup_csv = os.path.join(self.work_dir, "backup.csv") @@ -1351,6 +1348,7 @@ class RestoreManager: app_instance_name -- (string) The app name to restore (no app with this name should be already install) """ + from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_php_versions_in_settings, _patch_legacy_helpers from yunohost.user import user_group_list from yunohost.permission import ( permission_create, diff --git a/src/yunohost/data_migrations/0016_php70_to_php73_pools.py b/src/yunohost/data_migrations/0016_php70_to_php73_pools.py index 6b424f211..fed96c9c8 100644 --- a/src/yunohost/data_migrations/0016_php70_to_php73_pools.py +++ b/src/yunohost/data_migrations/0016_php70_to_php73_pools.py @@ -4,7 +4,8 @@ from shutil import copy2 from moulinette.utils.log import getActionLogger -from yunohost.app import _is_installed, _patch_legacy_php_versions_in_settings +from yunohost.app import _is_installed +from yunohost.utils.legacy import _patch_legacy_php_versions_in_settings from yunohost.tools import Migration from yunohost.service import _run_service_command diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index eb92dd71f..7ae98bed8 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -1,7 +1,10 @@ import os +import re +import glob from moulinette import m18n +from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import write_to_json, read_yaml +from moulinette.utils.filesystem import read_file, write_to_file, write_to_json, write_to_yaml, read_yaml from yunohost.user import user_list from yunohost.app import ( @@ -14,6 +17,8 @@ from yunohost.permission import ( user_permission_update, permission_sync_to_user, ) +from yunohost.utils.error import YunohostValidationError + logger = getActionLogger("yunohost.legacy") @@ -237,3 +242,213 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): logger.warning( "YunoHost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system" ) + + +LEGACY_PHP_VERSION_REPLACEMENTS = [ + ("/etc/php5", "/etc/php/7.3"), + ("/etc/php/7.0", "/etc/php/7.3"), + ("/var/run/php5-fpm", "/var/run/php/php7.3-fpm"), + ("/var/run/php/php7.0-fpm", "/var/run/php/php7.3-fpm"), + ("php5", "php7.3"), + ("php7.0", "php7.3"), + ( + 'phpversion="${phpversion:-7.0}"', + 'phpversion="${phpversion:-7.3}"', + ), # Many helpers like the composer ones use 7.0 by default ... + ( + '"$phpversion" == "7.0"', + '$(bc <<< "$phpversion >= 7.3") -eq 1', + ), # patch ynh_install_php to refuse installing/removing php <= 7.3 +] + + +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)) + files_to_patch.extend(glob.glob("%s/scripts/*/*" % app_folder)) + files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder)) + files_to_patch.append("%s/manifest.json" % 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 + + c = ( + "sed -i " + + "".join( + "-e 's@{pattern}@{replace}@g' ".format(pattern=p, replace=r) + for p, r in LEGACY_PHP_VERSION_REPLACEMENTS + ) + + "%s" % filename + ) + os.system(c) + + +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") == "/etc/php/7.0/fpm": + settings["fpm_config_dir"] = "/etc/php/7.3/fpm" + if settings.get("fpm_service") == "php7.0-fpm": + settings["fpm_service"] = "php7.3-fpm" + if settings.get("phpversion") == "7.0": + settings["phpversion"] = "7.3" + + # We delete these checksums otherwise the file will appear as manually modified + list_to_remove = ["checksum__etc_php_7.0_fpm_pool", "checksum__etc_nginx_conf.d"] + settings = { + k: v + for k, v in settings.items() + if not any(k.startswith(to_remove) for to_remove in list_to_remove) + } + + write_to_yaml(app_folder + "/settings.yml", settings) + + +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)) + + stuff_to_replace = { + # Replace + # sudo yunohost app initdb $db_user -p $db_pwd + # by + # ynh_mysql_setup_db --db_user=$db_user --db_name=$db_user --db_pwd=$db_pwd + "yunohost app initdb": { + "pattern": r"(sudo )?yunohost app initdb \"?(\$\{?\w+\}?)\"?\s+-p\s\"?(\$\{?\w+\}?)\"?", + "replace": r"ynh_mysql_setup_db --db_user=\2 --db_name=\2 --db_pwd=\3", + "important": True, + }, + # Replace + # sudo yunohost app checkport whaterver + # by + # ynh_port_available whatever + "yunohost app checkport": { + "pattern": r"(sudo )?yunohost app checkport", + "replace": r"ynh_port_available", + "important": True, + }, + # We can't migrate easily port-available + # .. but at the time of writing this code, only two non-working apps are using it. + "yunohost tools port-available": {"important": True}, + # Replace + # yunohost app checkurl "${domain}${path_url}" -a "${app}" + # by + # ynh_webpath_register --app=${app} --domain=${domain} --path_url=${path_url} + "yunohost app checkurl": { + "pattern": r"(sudo )?yunohost app checkurl \"?(\$\{?\w+\}?)\/?(\$\{?\w+\}?)\"?\s+-a\s\"?(\$\{?\w+\}?)\"?", + "replace": r"ynh_webpath_register --app=\4 --domain=\2 --path_url=\3", + "important": True, + }, + # Remove + # Automatic diagnosis data from YunoHost + # __PRE_TAG1__$(yunohost tools diagnosis | ...)__PRE_TAG2__" + # + "yunohost tools diagnosis": { + "pattern": r"(Automatic diagnosis data from YunoHost( *\n)*)? *(__\w+__)? *\$\(yunohost tools diagnosis.*\)(__\w+__)?", + "replace": r"", + "important": False, + }, + # Old $1, $2 in backup/restore scripts... + "app=$2": { + "only_for": ["scripts/backup", "scripts/restore"], + "pattern": r"app=\$2", + "replace": r"app=$YNH_APP_INSTANCE_NAME", + "important": True, + }, + # Old $1, $2 in backup/restore scripts... + "backup_dir=$1": { + "only_for": ["scripts/backup", "scripts/restore"], + "pattern": r"backup_dir=\$1", + "replace": r"backup_dir=.", + "important": True, + }, + # Old $1, $2 in backup/restore scripts... + "restore_dir=$1": { + "only_for": ["scripts/restore"], + "pattern": r"restore_dir=\$1", + "replace": r"restore_dir=.", + "important": True, + }, + # Old $1, $2 in install scripts... + # We ain't patching that shit because it ain't trivial to patch all args... + "domain=$1": {"only_for": ["scripts/install"], "important": True}, + } + + for helper, infos in stuff_to_replace.items(): + infos["pattern"] = ( + re.compile(infos["pattern"]) if infos.get("pattern") else None + ) + infos["replace"] = infos.get("replace") + + for filename in files_to_patch: + + # Ignore non-regular files + if not os.path.isfile(filename): + continue + + try: + content = read_file(filename) + except MoulinetteError: + continue + + replaced_stuff = False + 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"] + ): + continue + + # If helper is used, attempt to patch the file + if helper in content and infos["pattern"]: + content = infos["pattern"].sub(infos["replace"], content) + replaced_stuff = True + if infos["important"]: + show_warning = True + + # If the helper is *still* in the content, it means that we + # couldn't patch the deprecated helper in the previous lines. In + # that case, abort the install or whichever step is performed + if helper in content and infos["important"]: + raise YunohostValidationError( + "This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.", + raw_msg=True, + ) + + 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 [ + "install", + "remove", + "upgrade", + "backup", + "restore", + ]: + source_helpers = "source /usr/share/yunohost/helpers" + if source_helpers not in content: + content.replace("#!/bin/bash", "#!/bin/bash\n" + source_helpers) + if source_helpers not in content: + content = source_helpers + "\n" + content + + # Actually write the new content in the file + write_to_file(filename, content) + + if show_warning: + # And complain about those damn deprecated helpers + logger.error( + r"/!\ Packagers ! This app uses a very old deprecated helpers ... Yunohost automatically patched the helpers to use the new recommended practice, but please do consider fixing the upstream code right now ..." + ) From 206b430a9f66c698821f5fc56d1cfaa967e1361c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 02:50:30 +0200 Subject: [PATCH 059/142] Offload appcatalog stuff to a separate file to limit the size of app.py --- .gitlab/ci/test.gitlab-ci.yml | 4 +- src/yunohost/app.py | 296 ++---------------- ...est_appscatalog.py => test_app_catalog.py} | 2 +- src/yunohost/tools.py | 4 +- 4 files changed, 31 insertions(+), 275 deletions(-) rename src/yunohost/tests/{test_appscatalog.py => test_app_catalog.py} (99%) diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index 6a422ea22..1aad46fbe 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -113,10 +113,10 @@ test-apps: test-appscatalog: extends: .test-stage script: - - python3 -m pytest src/yunohost/tests/test_appscatalog.py + - python3 -m pytest src/yunohost/tests/test_app_catalog.py only: changes: - - src/yunohost/app.py + - src/yunohost/app_calalog.py test-appurl: extends: .test-stage diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 011c953e0..7cddca3e9 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -31,15 +31,12 @@ import yaml import time import re import subprocess -import glob import tempfile from collections import OrderedDict -from typing import List +from typing import List, Tuple, Dict from moulinette import Moulinette, m18n -from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from moulinette.utils.network import download_json from moulinette.utils.process import run_commands, check_output from moulinette.utils.filesystem import ( read_file, @@ -48,8 +45,6 @@ from moulinette.utils.filesystem import ( read_yaml, write_to_file, write_to_json, - write_to_yaml, - mkdir, ) from yunohost.utils import packages @@ -64,17 +59,13 @@ from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.filesystem import free_space_in_directory from yunohost.log import is_unit_operation, OperationLogger +from yunohost.app_catalog import app_catalog, app_search, _load_apps_catalog, app_fetchlist # noqa logger = getActionLogger("yunohost.app") APPS_SETTING_PATH = "/etc/yunohost/apps/" APP_TMP_WORKDIRS = "/var/cache/yunohost/app_tmp_work_dirs" -APPS_CATALOG_CACHE = "/var/cache/yunohost/repo" -APPS_CATALOG_CONF = "/etc/yunohost/apps_catalog.yml" -APPS_CATALOG_API_VERSION = 2 -APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default" - re_app_instance_name = re.compile( r"^(?P[\w-]+?)(__(?P[1-9][0-9]*))?$" ) @@ -91,80 +82,6 @@ APP_FILES_TO_COPY = [ "doc", ] -def app_catalog(full=False, with_categories=False): - """ - Return a dict of apps available to installation from Yunohost's app catalog - """ - - # Get app list from catalog cache - catalog = _load_apps_catalog() - installed_apps = set(_installed_apps()) - - # Trim info for apps if not using --full - for app, infos in catalog["apps"].items(): - infos["installed"] = app in installed_apps - - infos["manifest"]["description"] = _value_for_locale( - infos["manifest"]["description"] - ) - - if not full: - catalog["apps"][app] = { - "description": infos["manifest"]["description"], - "level": infos["level"], - } - else: - infos["manifest"]["arguments"] = _set_default_ask_questions( - infos["manifest"].get("arguments", {}) - ) - - # Trim info for categories if not using --full - for category in catalog["categories"]: - category["title"] = _value_for_locale(category["title"]) - category["description"] = _value_for_locale(category["description"]) - for subtags in category.get("subtags", []): - subtags["title"] = _value_for_locale(subtags["title"]) - - if not full: - catalog["categories"] = [ - {"id": c["id"], "description": c["description"]} - for c in catalog["categories"] - ] - - if not with_categories: - return {"apps": catalog["apps"]} - else: - return {"apps": catalog["apps"], "categories": catalog["categories"]} - - -def app_search(string): - """ - Return a dict of apps whose description or name match the search string - """ - - # Retrieve a simple dict listing all apps - catalog_of_apps = app_catalog() - - # Selecting apps according to a match in app name or description - matching_apps = {"apps": {}} - for app in catalog_of_apps["apps"].items(): - if re.search(string, app[0], flags=re.IGNORECASE) or re.search( - string, app[1]["description"], flags=re.IGNORECASE - ): - matching_apps["apps"][app[0]] = app[1] - - return matching_apps - - -# Old legacy function... -def app_fetchlist(): - logger.warning( - "'yunohost app fetchlist' is deprecated. Please use 'yunohost tools update --apps' instead" - ) - from yunohost.tools import tools_update - - tools_update(target="apps") - def app_list(full=False, installed=False, filter=None): """ @@ -1728,22 +1645,6 @@ ynh_app_config_run $1 return values -def _get_all_installed_apps_id(): - """ - Return something like: - ' * app1 - * app2 - * ...' - """ - - all_apps_ids = sorted(_installed_apps()) - - all_apps_ids_formatted = "\n * ".join(all_apps_ids) - all_apps_ids_formatted = "\n * " + all_apps_ids_formatted - - return all_apps_ids_formatted - - def _get_app_actions(app_id): "Get app config panel stored in json or in toml" actions_toml_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.toml") @@ -2239,6 +2140,13 @@ def _extract_app_from_gitrepo(url: str, branch: str, revision: str, app_info={}: return manifest, extracted_app_folder +# +# ############################### # +# Small utilities # +# ############################### # +# + + def _is_installed(app: str) -> bool: """ Check if application is installed @@ -2264,6 +2172,22 @@ def _installed_apps() -> List[str]: return os.listdir(APPS_SETTING_PATH) +def _get_all_installed_apps_id(): + """ + Return something like: + ' * app1 + * app2 + * ...' + """ + + all_apps_ids = sorted(_installed_apps()) + + all_apps_ids_formatted = "\n * ".join(all_apps_ids) + all_apps_ids_formatted = "\n * " + all_apps_ids_formatted + + return all_apps_ids_formatted + + def _check_manifest_requirements(manifest: Dict, app: str): """Check if required packages are met from the manifest""" @@ -2476,176 +2400,6 @@ def _next_instance_number_for_app(app): i += 1 - -# -# ############################### # -# Applications list management # -# ############################### # -# - - -def _initialize_apps_catalog_system(): - """ - This function is meant to intialize the apps_catalog system with YunoHost's default app catalog. - """ - - default_apps_catalog_list = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}] - - try: - logger.debug( - "Initializing apps catalog system with YunoHost's default app list" - ) - write_to_yaml(APPS_CATALOG_CONF, default_apps_catalog_list) - except Exception as e: - raise YunohostError( - "Could not initialize the apps catalog system... : %s" % str(e) - ) - - logger.success(m18n.n("apps_catalog_init_success")) - - -def _read_apps_catalog_list(): - """ - Read the json corresponding to the list of apps catalogs - """ - - try: - list_ = read_yaml(APPS_CATALOG_CONF) - # Support the case where file exists but is empty - # by returning [] if list_ is None - return list_ if list_ else [] - except Exception as e: - raise YunohostError("Could not read the apps_catalog list ... : %s" % str(e)) - - -def _actual_apps_catalog_api_url(base_url): - - return "{base_url}/v{version}/apps.json".format( - base_url=base_url, version=APPS_CATALOG_API_VERSION - ) - - -def _update_apps_catalog(): - """ - Fetches the json for each apps_catalog and update the cache - - apps_catalog_list is for example : - [ {"id": "default", "url": "https://app.yunohost.org/default/"} ] - - Then for each apps_catalog, the actual json URL to be fetched is like : - https://app.yunohost.org/default/vX/apps.json - - And store it in : - /var/cache/yunohost/repo/default.json - """ - - apps_catalog_list = _read_apps_catalog_list() - - logger.info(m18n.n("apps_catalog_updating")) - - # Create cache folder if needed - if not os.path.exists(APPS_CATALOG_CACHE): - logger.debug("Initialize folder for apps catalog cache") - mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid="root") - - for apps_catalog in apps_catalog_list: - apps_catalog_id = apps_catalog["id"] - actual_api_url = _actual_apps_catalog_api_url(apps_catalog["url"]) - - # Fetch the json - try: - apps_catalog_content = download_json(actual_api_url) - except Exception as e: - raise YunohostError( - "apps_catalog_failed_to_download", - apps_catalog=apps_catalog_id, - error=str(e), - ) - - # Remember the apps_catalog api version for later - apps_catalog_content["from_api_version"] = APPS_CATALOG_API_VERSION - - # Save the apps_catalog data in the cache - cache_file = "{cache_folder}/{list}.json".format( - cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id - ) - try: - write_to_json(cache_file, apps_catalog_content) - except Exception as e: - raise YunohostError( - "Unable to write cache data for %s apps_catalog : %s" - % (apps_catalog_id, str(e)) - ) - - logger.success(m18n.n("apps_catalog_update_success")) - - -def _load_apps_catalog(): - """ - Read all the apps catalog cache files and build a single dict (merged_catalog) - corresponding to all known apps and categories - """ - - merged_catalog = {"apps": {}, "categories": []} - - 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 = "{cache_folder}/{list}.json".format( - cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id - ) - - try: - apps_catalog_content = ( - read_json(cache_file) if os.path.exists(cache_file) else None - ) - except Exception as e: - raise YunohostError( - "Unable to read cache for apps_catalog %s : %s" % (cache_file, e), - raw_msg=True, - ) - - # Check that the version of the data matches version .... - # ... otherwise it means we updated yunohost in the meantime - # and need to update the cache for everything to be consistent - if ( - not apps_catalog_content - or apps_catalog_content.get("from_api_version") != APPS_CATALOG_API_VERSION - ): - logger.info(m18n.n("apps_catalog_obsolete_cache")) - _update_apps_catalog() - apps_catalog_content = read_json(cache_file) - - del apps_catalog_content["from_api_version"] - - # 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"]: - logger.warning( - "Duplicate app %s found between apps catalog %s and %s" - % (app, apps_catalog_id, merged_catalog["apps"][app]["repository"]) - ) - continue - - info["repository"] = apps_catalog_id - merged_catalog["apps"][app] = info - - # Annnnd categories - merged_catalog["categories"] += apps_catalog_content["categories"] - - return merged_catalog - - -# -# ############################### # -# Small utilities # -# ############################### # -# - - def _make_tmp_workdir_for_app(app=None): # Create parent dir if it doesn't exists yet diff --git a/src/yunohost/tests/test_appscatalog.py b/src/yunohost/tests/test_app_catalog.py similarity index 99% rename from src/yunohost/tests/test_appscatalog.py rename to src/yunohost/tests/test_app_catalog.py index a2619a660..8423b868e 100644 --- a/src/yunohost/tests/test_appscatalog.py +++ b/src/yunohost/tests/test_app_catalog.py @@ -9,7 +9,7 @@ from moulinette import m18n from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml from yunohost.utils.error import YunohostError -from yunohost.app import ( +from yunohost.app_catalog import ( _initialize_apps_catalog_system, _read_apps_catalog_list, _update_apps_catalog, diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index ed8c04153..4cdd62d8f 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -37,10 +37,12 @@ from moulinette.utils.process import check_output, call_async_output from moulinette.utils.filesystem import read_yaml, write_to_yaml from yunohost.app import ( - _update_apps_catalog, app_info, app_upgrade, +) +from yunohost.app_catalog import ( _initialize_apps_catalog_system, + _update_apps_catalog, ) from yunohost.domain import domain_add from yunohost.dyndns import _dyndns_available, _dyndns_provides From 1e046dcd37a2f6d0365be4003e86622e58dd285d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 02:50:38 +0200 Subject: [PATCH 060/142] Misc typoz --- src/yunohost/app.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 7cddca3e9..8dcb5aef3 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -609,7 +609,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False if upgrade_failed or broke_the_system: # display this if there are remaining apps - if apps[number + 1 :]: + if apps[number + 1:]: not_upgraded_apps = apps[number:] logger.error( m18n.n( @@ -664,8 +664,6 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False def app_manifest(app): - raw_app_list = _load_apps_catalog()["apps"] - manifest, extracted_app_folder = _extract_app(app) shutil.rmtree(extracted_app_folder) @@ -721,7 +719,7 @@ def app_install( if force or Moulinette.interface.type == "api": return - quality = app_quality(app) + quality = _app_quality(app) if quality == "success": return @@ -816,7 +814,7 @@ def app_install( # Move scripts and manifest to the right place for file_to_copy in APP_FILES_TO_COPY: if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): - os.system(f"cp -R '{extracted_app_folder}/{file_to_copy}' '{app_setting_path}'" + os.system(f"cp -R '{extracted_app_folder}/{file_to_copy}' '{app_setting_path}'") # Initialize the main permission for the app # The permission is initialized with no url associated, and with tile disabled @@ -976,6 +974,7 @@ def app_remove(operation_logger, app, purge=False): purge -- Remove with all app data """ + from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers from yunohost.hook import hook_exec, hook_remove, hook_callback from yunohost.permission import ( user_permission_list, @@ -1987,7 +1986,7 @@ def _app_quality(app: str) -> str: if app in raw_app_catalog or ("@" in app) or ("http://" in app) or ("https://" in app): # If we got an app name directly (e.g. just "wordpress"), we gonna test this name - if app in raw_app_list: + if app in raw_app_catalog: app_name_to_test = app # If we got an url like "https://github.com/foo/bar_ynh, we want to # extract "bar" and test if we know this app @@ -1997,10 +1996,10 @@ def _app_quality(app: str) -> str: # FIXME : watdo if '@' in app ? app_name_to_test = None - if app_name_to_test in raw_app_list: + if app_name_to_test in raw_app_catalog: - state = raw_app_list[app_name_to_test].get("state", "notworking") - level = raw_app_list[app_name_to_test].get("level", None) + 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"]: if isinstance(level, int) and level >= 5: return "success" @@ -2096,7 +2095,7 @@ def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: return manifest, extracted_app_folder -def _extract_app_from_gitrepo(url: str, branch: str, revision: str, app_info={}: Dict) -> Tuple[Dict, str]: +def _extract_app_from_gitrepo(url: str, branch: str, revision: str, app_info: Dict = {}) -> Tuple[Dict, str]: logger.debug(m18n.n("downloading")) From 80f4b892e65b6838146bc520da8feb7991327a32 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 03:05:14 +0200 Subject: [PATCH 061/142] Prevent full-domain app to be moved to a subpath with change-url ? --- src/yunohost/app.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 8dcb5aef3..f221e2406 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -33,7 +33,7 @@ import re import subprocess import tempfile from collections import OrderedDict -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Any from moulinette import Moulinette, m18n from moulinette.utils.log import getActionLogger @@ -51,7 +51,6 @@ from yunohost.utils import packages from yunohost.utils.config import ( ConfigPanel, ask_questions_and_parse_answers, - Question, DomainQuestion, PathQuestion, ) @@ -384,8 +383,9 @@ def app_change_url(operation_logger, app, domain, path): "app_change_url_identical_domains", domain=domain, path=path ) - # Check the url is available - _assert_no_conflicting_apps(domain, path, ignore_app=app) + app_setting_path = os.path.join(APPS_SETTING_PATH, app) + path_requirement = _guess_webapp_path_requirement(app_setting_path) + _validate_webpath_requirement({"domain": domain, "path": path}, path_requirement, ignore_app=app) tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) @@ -777,8 +777,8 @@ def app_install( } # Validate domain / path availability for webapps - path_requirement = _guess_webapp_path_requirement(questions, extracted_app_folder) - _validate_webpath_requirement(questions, path_requirement) + path_requirement = _guess_webapp_path_requirement(extracted_app_folder) + _validate_webpath_requirement(args, path_requirement) # Attempt to patch legacy helpers ... _patch_legacy_helpers(extracted_app_folder) @@ -2214,13 +2214,16 @@ def _check_manifest_requirements(manifest: Dict, app: str): ) -def _guess_webapp_path_requirement(questions: List[Question], app_folder: str) -> str: +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. - domain_questions = [question for question in questions if question.type == "domain"] - path_questions = [question for question in questions if question.type == "path"] + manifest = _get_manifest_of_app(app_folder) + raw_questions = manifest.get("arguments", {}).get("install", {}) + + domain_questions = [question for question in raw_questions if question.get("type") == "domain"] + path_questions = [question for question in raw_questions if question.get("type") == "path"] if len(domain_questions) == 0 and len(path_questions) == 0: return "" @@ -2248,22 +2251,17 @@ def _guess_webapp_path_requirement(questions: List[Question], app_folder: str) - def _validate_webpath_requirement( - questions: List[Question], path_requirement: str + args: Dict[str, Any], path_requirement: str, ignore_app=None ) -> None: - domain_questions = [question for question in questions if question.type == "domain"] - path_questions = [question for question in questions if question.type == "path"] + domain = args.get("domain") + path = args.get("path") if path_requirement == "domain_and_path": - - domain = domain_questions[0].value - path = path_questions[0].value - _assert_no_conflicting_apps(domain, path, full_domain=True) + _assert_no_conflicting_apps(domain, path, ignore_app=ignore_app) elif path_requirement == "full_domain": - - domain = domain_questions[0].value - _assert_no_conflicting_apps(domain, "/", full_domain=True) + _assert_no_conflicting_apps(domain, "/", full_domain=True, ignore_app=ignore_app) def _get_conflicting_apps(domain, path, ignore_app=None): From 5fc493058df2766097494b710f5f628fafaf81a2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 03:10:26 +0200 Subject: [PATCH 062/142] Aaaaaand I forgot to commit app_catalog.py ... --- src/yunohost/app_catalog.py | 255 ++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 src/yunohost/app_catalog.py diff --git a/src/yunohost/app_catalog.py b/src/yunohost/app_catalog.py new file mode 100644 index 000000000..e4ffa1db6 --- /dev/null +++ b/src/yunohost/app_catalog.py @@ -0,0 +1,255 @@ +import os +import re + +from moulinette import m18n +from moulinette.utils.log import getActionLogger +from moulinette.utils.network import download_json +from moulinette.utils.filesystem import ( + read_json, + read_yaml, + write_to_json, + write_to_yaml, + mkdir, +) + +from yunohost.utils.i18n import _value_for_locale +from yunohost.utils.error import YunohostError + +logger = getActionLogger("yunohost.app_catalog") + +APPS_CATALOG_CACHE = "/var/cache/yunohost/repo" +APPS_CATALOG_CONF = "/etc/yunohost/apps_catalog.yml" +APPS_CATALOG_API_VERSION = 2 +APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default" + + +# Old legacy function... +def app_fetchlist(): + logger.warning( + "'yunohost app fetchlist' is deprecated. Please use 'yunohost tools update --apps' instead" + ) + from yunohost.tools import tools_update + + tools_update(target="apps") + + +def app_catalog(full=False, with_categories=False): + """ + Return a dict of apps available to installation from Yunohost's app catalog + """ + + from yunohost.app import _installed_apps, _set_default_ask_questions + + # Get app list from catalog cache + catalog = _load_apps_catalog() + installed_apps = set(_installed_apps()) + + # Trim info for apps if not using --full + for app, infos in catalog["apps"].items(): + infos["installed"] = app in installed_apps + + infos["manifest"]["description"] = _value_for_locale( + infos["manifest"]["description"] + ) + + if not full: + catalog["apps"][app] = { + "description": infos["manifest"]["description"], + "level": infos["level"], + } + else: + infos["manifest"]["arguments"] = _set_default_ask_questions( + infos["manifest"].get("arguments", {}) + ) + + # Trim info for categories if not using --full + for category in catalog["categories"]: + category["title"] = _value_for_locale(category["title"]) + category["description"] = _value_for_locale(category["description"]) + for subtags in category.get("subtags", []): + subtags["title"] = _value_for_locale(subtags["title"]) + + if not full: + catalog["categories"] = [ + {"id": c["id"], "description": c["description"]} + for c in catalog["categories"] + ] + + if not with_categories: + return {"apps": catalog["apps"]} + else: + return {"apps": catalog["apps"], "categories": catalog["categories"]} + + +def app_search(string): + """ + Return a dict of apps whose description or name match the search string + """ + + # Retrieve a simple dict listing all apps + catalog_of_apps = app_catalog() + + # Selecting apps according to a match in app name or description + matching_apps = {"apps": {}} + for app in catalog_of_apps["apps"].items(): + if re.search(string, app[0], flags=re.IGNORECASE) or re.search( + string, app[1]["description"], flags=re.IGNORECASE + ): + matching_apps["apps"][app[0]] = app[1] + + return matching_apps + + +def _initialize_apps_catalog_system(): + """ + This function is meant to intialize the apps_catalog system with YunoHost's default app catalog. + """ + + default_apps_catalog_list = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}] + + try: + logger.debug( + "Initializing apps catalog system with YunoHost's default app list" + ) + write_to_yaml(APPS_CATALOG_CONF, default_apps_catalog_list) + except Exception as e: + raise YunohostError( + "Could not initialize the apps catalog system... : %s" % str(e) + ) + + logger.success(m18n.n("apps_catalog_init_success")) + + +def _read_apps_catalog_list(): + """ + Read the json corresponding to the list of apps catalogs + """ + + try: + list_ = read_yaml(APPS_CATALOG_CONF) + # Support the case where file exists but is empty + # by returning [] if list_ is None + return list_ if list_ else [] + except Exception as e: + raise YunohostError("Could not read the apps_catalog list ... : %s" % str(e)) + + +def _actual_apps_catalog_api_url(base_url): + + return "{base_url}/v{version}/apps.json".format( + base_url=base_url, version=APPS_CATALOG_API_VERSION + ) + + +def _update_apps_catalog(): + """ + Fetches the json for each apps_catalog and update the cache + + apps_catalog_list is for example : + [ {"id": "default", "url": "https://app.yunohost.org/default/"} ] + + Then for each apps_catalog, the actual json URL to be fetched is like : + https://app.yunohost.org/default/vX/apps.json + + And store it in : + /var/cache/yunohost/repo/default.json + """ + + apps_catalog_list = _read_apps_catalog_list() + + logger.info(m18n.n("apps_catalog_updating")) + + # Create cache folder if needed + if not os.path.exists(APPS_CATALOG_CACHE): + logger.debug("Initialize folder for apps catalog cache") + mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid="root") + + for apps_catalog in apps_catalog_list: + apps_catalog_id = apps_catalog["id"] + actual_api_url = _actual_apps_catalog_api_url(apps_catalog["url"]) + + # Fetch the json + try: + apps_catalog_content = download_json(actual_api_url) + except Exception as e: + raise YunohostError( + "apps_catalog_failed_to_download", + apps_catalog=apps_catalog_id, + error=str(e), + ) + + # Remember the apps_catalog api version for later + apps_catalog_content["from_api_version"] = APPS_CATALOG_API_VERSION + + # Save the apps_catalog data in the cache + cache_file = "{cache_folder}/{list}.json".format( + cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id + ) + try: + write_to_json(cache_file, apps_catalog_content) + except Exception as e: + raise YunohostError( + "Unable to write cache data for %s apps_catalog : %s" + % (apps_catalog_id, str(e)) + ) + + logger.success(m18n.n("apps_catalog_update_success")) + + +def _load_apps_catalog(): + """ + Read all the apps catalog cache files and build a single dict (merged_catalog) + corresponding to all known apps and categories + """ + + merged_catalog = {"apps": {}, "categories": []} + + 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 = "{cache_folder}/{list}.json".format( + cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id + ) + + try: + apps_catalog_content = ( + read_json(cache_file) if os.path.exists(cache_file) else None + ) + except Exception as e: + raise YunohostError( + "Unable to read cache for apps_catalog %s : %s" % (cache_file, e), + raw_msg=True, + ) + + # Check that the version of the data matches version .... + # ... otherwise it means we updated yunohost in the meantime + # and need to update the cache for everything to be consistent + if ( + not apps_catalog_content + or apps_catalog_content.get("from_api_version") != APPS_CATALOG_API_VERSION + ): + logger.info(m18n.n("apps_catalog_obsolete_cache")) + _update_apps_catalog() + apps_catalog_content = read_json(cache_file) + + del apps_catalog_content["from_api_version"] + + # 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"]: + logger.warning( + "Duplicate app %s found between apps catalog %s and %s" + % (app, apps_catalog_id, merged_catalog["apps"][app]["repository"]) + ) + continue + + info["repository"] = apps_catalog_id + merged_catalog["apps"][app] = info + + # Annnnd categories + merged_catalog["categories"] += apps_catalog_content["categories"] + + return merged_catalog From 313897d18421e150a119e3de0d66d3fb1f50e340 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 03:33:12 +0200 Subject: [PATCH 063/142] Simplify _check_manifest_requirements --- src/yunohost/app.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f221e2406..87b02cd19 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -538,7 +538,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False upgrade_type = "UPGRADE_FULL" # Check requirements - _check_manifest_requirements(manifest, app_instance_name=app_instance_name) + _check_manifest_requirements(manifest) _assert_system_is_sane_for_app(manifest, "pre") app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) @@ -753,7 +753,7 @@ def app_install( label = label if label else manifest["name"] # Check requirements - _check_manifest_requirements(manifest, app_id) + _check_manifest_requirements(manifest) _assert_system_is_sane_for_app(manifest, "pre") # Check if app can be forked @@ -2187,7 +2187,7 @@ def _get_all_installed_apps_id(): return all_apps_ids_formatted -def _check_manifest_requirements(manifest: Dict, app: str): +def _check_manifest_requirements(manifest: Dict): """Check if required packages are met from the manifest""" packaging_format = int(manifest.get("packaging_format", 0)) @@ -2199,6 +2199,8 @@ def _check_manifest_requirements(manifest: Dict, app: str): if not requirements: return + app = manifest.get("id", "?") + logger.debug(m18n.n("app_requirements_checking", app=app)) # Iterate over requirements From a26145ece0c41c5b84e359e1b157cc42cb587a85 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 03:42:59 +0200 Subject: [PATCH 064/142] Simplify(?) YNH_APP_BASEDIR creation, to happen in _make_env_for_app_script --- src/yunohost/app.py | 23 ++++++++++------------- src/yunohost/backup.py | 13 ++++--------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 87b02cd19..df4dd089c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -390,12 +390,11 @@ def app_change_url(operation_logger, app, domain, path): tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app) + env_dict = _make_environment_for_app_script(app, workdir=tmp_workdir_for_app) 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["YNH_APP_BASEDIR"] = tmp_workdir_for_app if domain != old_domain: operation_logger.related_to.append(("domain", old_domain)) @@ -544,12 +543,11 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app_instance_name) + env_dict = _make_environment_for_app_script(app_instance_name, workdir=extracted_app_folder) env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version) env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version) env_dict["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0" - env_dict["YNH_APP_BASEDIR"] = extracted_app_folder # We'll check that the app didn't brutally edit some system configuration manually_modified_files_before_install = manually_modified_files() @@ -829,8 +827,7 @@ def app_install( ) # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app_instance_name, args=args) - env_dict["YNH_APP_BASEDIR"] = extracted_app_folder + env_dict = _make_environment_for_app_script(app_instance_name, args=args, workdir=extracted_app_folder) env_dict_for_logging = env_dict.copy() for question in questions: @@ -891,8 +888,7 @@ def app_install( logger.warning(m18n.n("app_remove_after_failed_install")) # Setup environment for remove script - env_dict_remove = _make_environment_for_app_script(app_instance_name) - env_dict_remove["YNH_APP_BASEDIR"] = extracted_app_folder + env_dict_remove = _make_environment_for_app_script(app_instance_name, workdir=extracted_app_folder) # Execute remove script operation_logger_remove = OperationLogger( @@ -1006,8 +1002,7 @@ def app_remove(operation_logger, app, purge=False): env_dict = {} app_id, app_instance_nb = _parse_app_instance_name(app) - env_dict = _make_environment_for_app_script(app) - env_dict["YNH_APP_BASEDIR"] = tmp_workdir_for_app + env_dict = _make_environment_for_app_script(app, workdir=tmp_workdir_for_app) env_dict["YNH_APP_PURGE"] = str(1 if purge else 0) operation_logger.extra.update({"env": env_dict}) @@ -1501,9 +1496,8 @@ def app_action_run(operation_logger, app, action, args=None): tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) - env_dict = _make_environment_for_app_script(app, args=args, args_prefix="ACTION_") + env_dict = _make_environment_for_app_script(app, args=args, args_prefix="ACTION_", workdir=tmp_workdir_for_app) env_dict["YNH_ACTION"] = action - env_dict["YNH_APP_BASEDIR"] = tmp_workdir_for_app _, action_script = tempfile.mkstemp(dir=tmp_workdir_for_app) @@ -2328,7 +2322,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_"): +def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_", workdir=None): app_setting_path = os.path.join(APPS_SETTING_PATH, app) @@ -2342,6 +2336,9 @@ def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"): "YNH_APP_MANIFEST_VERSION": manifest.get("version", "?"), } + if workdir: + env_dict["YNH_APP_BASEDIR"] = workdir + for arg_name, arg_value in args.items(): env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = str(arg_value) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index b650cb3da..5664af3a0 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1483,7 +1483,9 @@ class RestoreManager: logger.debug(m18n.n("restore_running_app_script", app=app_instance_name)) # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app_instance_name) + # FIXME : workdir should be a tmp workdir + app_workdir = os.path.join(self.work_dir, "apps", app_instance_name, "settings") + env_dict = _make_environment_for_app_script(app_instance_name, workdir=app_workdir) env_dict.update( { "YNH_BACKUP_DIR": self.work_dir, @@ -1491,9 +1493,6 @@ class RestoreManager: "YNH_APP_BACKUP_DIR": os.path.join( self.work_dir, "apps", app_instance_name, "backup" ), - "YNH_APP_BASEDIR": os.path.join( - self.work_dir, "apps", app_instance_name, "settings" - ), } ) @@ -1530,11 +1529,7 @@ class RestoreManager: 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) - env_dict_remove["YNH_APP_BASEDIR"] = os.path.join( - self.work_dir, "apps", app_instance_name, "settings" - ) - + 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)], From 680e13c0442a9adc590e2f6ae78d245b69da3e4d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 03:45:40 +0200 Subject: [PATCH 065/142] Make mypy happier --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index df4dd089c..0f4c14b7a 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1988,7 +1988,7 @@ def _app_quality(app: str) -> str: app_name_to_test = app.strip("/").split("/")[-1].replace("_ynh", "") else: # FIXME : watdo if '@' in app ? - app_name_to_test = None + return "thirdparty" if app_name_to_test in raw_app_catalog: From 2bda85c80042f0687415c9ed436db53695d04545 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 03:46:13 +0200 Subject: [PATCH 066/142] Stale i18n string --- locales/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 5d242b2e8..53b0a7e83 100644 --- a/locales/en.json +++ b/locales/en.json @@ -36,7 +36,6 @@ "app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?", "app_manifest_install_ask_password": "Choose an administration password for this app", "app_manifest_install_ask_path": "Choose the URL path (after the domain) where this app should be installed", - "app_manifest_invalid": "Something is wrong with the app manifest: {error}", "app_not_correctly_installed": "{app} seems to be incorrectly installed", "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", From 11be8cc4c02f0b60f00f2b2e1aca3f8c83075dcf Mon Sep 17 00:00:00 2001 From: ericgaspar Date: Fri, 1 Oct 2021 09:43:39 +0200 Subject: [PATCH 067/142] Update nodejs --- data/helpers.d/nodejs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/nodejs b/data/helpers.d/nodejs index a796b68fd..06b948d00 100644 --- a/data/helpers.d/nodejs +++ b/data/helpers.d/nodejs @@ -1,6 +1,6 @@ #!/bin/bash -n_version=7.3.0 +n_version=7.5.0 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. @@ -17,7 +17,7 @@ ynh_install_n () { ynh_print_info --message="Installation of N - Node.js version management" # Build an app.src for n echo "SOURCE_URL=https://github.com/tj/n/archive/v${n_version}.tar.gz -SOURCE_SUM=b908b0fc86922ede37e89d1030191285209d7d521507bf136e62895e5797847f" > "$YNH_APP_BASEDIR/conf/n.src" +SOURCE_SUM=d4da7ea91f680de0c9b5876e097e2a793e8234fcd0f7ca87a0599b925be087a3" > "$YNH_APP_BASEDIR/conf/n.src" # Download and extract n ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n # Install n From 171b3b25d9d4cb7aad8b2cbbc5aeb7eee6243a1f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 16:13:57 +0200 Subject: [PATCH 068/142] Improve check that arg is a yunohost repo url --- src/yunohost/app.py | 25 +++++++++++++++++++++++-- src/yunohost/tests/test_appurl.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0f4c14b7a..5b6d1a7f8 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -69,6 +69,10 @@ re_app_instance_name = re.compile( r"^(?P[\w-]+?)(__(?P[1-9][0-9]*))?$" ) +APP_REPO_URL = re.compile( + r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?tree/[a-zA-Z0-9-_]+)?(\.git)?/?$" +) + APP_FILES_TO_COPY = [ "manifest.json", "manifest.toml", @@ -1971,13 +1975,24 @@ def _set_default_ask_questions(arguments): return arguments +def _is_app_repo_url(string: str) -> bool: + + string = string.strip() + + # Dummy test for ssh-based stuff ... should probably be improved somehow + if '@' in string: + return True + + return APP_REPO_URL.match(string) + + def _app_quality(app: str) -> str: """ app may in fact be an app name, an url, or a path """ raw_app_catalog = _load_apps_catalog()["apps"] - if app in raw_app_catalog or ("@" in app) or ("http://" in app) or ("https://" in app): + if app in raw_app_catalog or _is_app_repo_url(app): # If we got an app name directly (e.g. just "wordpress"), we gonna test this name if app in raw_app_catalog: @@ -2027,10 +2042,14 @@ def _extract_app(src: str) -> Tuple[Dict, str]: revision = str(app_info["git"]["revision"]) return _extract_app_from_gitrepo(url, branch, revision, app_info) # App is a git repo url - elif ("@" in src) or ("http://" in src) or ("https://" in src): + elif _is_app_repo_url(src): url = src 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, {}) @@ -2038,6 +2057,8 @@ def _extract_app(src: str) -> Tuple[Dict, str]: elif os.path.exists(src): return _extract_app_from_folder(src) else: + if "http://" in src or "https://" in src: + logger.error(f"{src} is not a valid app url: app url are expected to look like https://domain.tld/path/to/repo_ynh") raise YunohostValidationError("app_unknown") diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index 5954d239a..d50877445 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -4,7 +4,7 @@ import os from .conftest import get_test_apps_dir from yunohost.utils.error import YunohostError -from yunohost.app import app_install, app_remove +from yunohost.app import app_install, app_remove, _is_app_repo_url from yunohost.domain import _get_maindomain, domain_url_available from yunohost.permission import _validate_and_sanitize_permission_url @@ -28,6 +28,34 @@ def teardown_function(function): pass +def test_repo_url_definition(): + assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh") + assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh/") + assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh.git") + assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing") + assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing/") + assert _is_app_repo_url("https://github.com/YunoHost-Apps/foo-bar-123_ynh") + assert _is_app_repo_url("https://github.com/YunoHost-Apps/foo_bar_123_ynh") + assert _is_app_repo_url("https://github.com/YunoHost-Apps/FooBar123_ynh") + assert _is_app_repo_url("https://github.com/labriqueinternet/vpnclient_ynh") + assert _is_app_repo_url("https://framagit.org/YunoHost/apps/nodebb_ynh") + assert _is_app_repo_url("https://framagit.org/YunoHost/apps/nodebb_ynh/-/tree/testing") + assert _is_app_repo_url("https://gitlab.com/yunohost-apps/foobar_ynh") + assert _is_app_repo_url("https://code.antopie.org/miraty/qr_ynh") + assert _is_app_repo_url("https://gitlab.domainepublic.net/Neutrinet/neutrinet_ynh/-/tree/unstable") + assert _is_app_repo_url("git@github.com:YunoHost-Apps/foobar_ynh.git") + + assert not _is_app_repo_url("github.com/YunoHost-Apps/foobar_ynh") + assert not _is_app_repo_url("http://github.com/YunoHost-Apps/foobar_ynh") + assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_wat") + assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh_wat") + assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar/tree/testing") + assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh_wat/tree/testing") + assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/") + assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/pwet") + assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/pwet_foo") + + def test_urlavailable(): # Except the maindomain/macnuggets to be available From 34d9573121a4bd5caea2974f27c20be3cf20726b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 16:46:05 +0200 Subject: [PATCH 069/142] Misc fixes for app repo url check / parsing --- src/yunohost/app.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 5b6d1a7f8..3fede8dcd 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1986,21 +1986,21 @@ def _is_app_repo_url(string: str) -> bool: return APP_REPO_URL.match(string) -def _app_quality(app: str) -> str: +def _app_quality(src: str) -> str: """ app may in fact be an app name, an url, or a path """ raw_app_catalog = _load_apps_catalog()["apps"] - if app in raw_app_catalog or _is_app_repo_url(app): + 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 app in raw_app_catalog: - app_name_to_test = app + if src in raw_app_catalog: + app_name_to_test = src # If we got an url like "https://github.com/foo/bar_ynh, we want to # extract "bar" and test if we know this app - elif ("http://" in app) or ("https://" in app): - app_name_to_test = app.strip("/").split("/")[-1].replace("_ynh", "") + elif ("http://" in src) or ("https://" in src): + app_name_to_test = src.strip("/").split("/")[-1].replace("_ynh", "") else: # FIXME : watdo if '@' in app ? return "thirdparty" @@ -2018,9 +2018,11 @@ def _app_quality(app: str) -> str: else: return "thirdparty" - elif os.path.exists(app): + elif os.path.exists(src): return "thirdparty" else: + if "http://" in src or "https://" in src: + logger.error(f"{src} is not a valid app url: app url are expected to look like https://domain.tld/path/to/repo_ynh") raise YunohostValidationError("app_unknown") @@ -2043,7 +2045,7 @@ def _extract_app(src: str) -> Tuple[Dict, str]: return _extract_app_from_gitrepo(url, branch, revision, app_info) # App is a git repo url elif _is_app_repo_url(src): - url = src + url = src.strip().strip("/") branch = "master" revision = "HEAD" # gitlab urls may look like 'https://domain/org/group/repo/-/tree/testing' From 3883343e1a414fe32a1e29adf2087e4f5051fbfe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 17:51:05 +0200 Subject: [PATCH 070/142] swag: Add version badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b256bb563..df3a4bb9f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@
+![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) [![GitHub license](https://img.shields.io/github/license/YunoHost/yunohost)](https://github.com/YunoHost/yunohost/blob/dev/LICENSE) From 310b664fab6c9ebef3b94575956f3654c366211f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 19:20:21 +0200 Subject: [PATCH 071/142] Clarify return type for mypy --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 3fede8dcd..36b65f873 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1983,7 +1983,7 @@ def _is_app_repo_url(string: str) -> bool: if '@' in string: return True - return APP_REPO_URL.match(string) + return bool(APP_REPO_URL.match(string)) def _app_quality(src: str) -> str: From 0f0f26f5c48af161da16eaf4775e95e6bcab25fc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 19:25:32 +0200 Subject: [PATCH 072/142] For some reason these tests now wanted to interactively ask for is_public arg ... don't know why it was working before ... --- src/yunohost/tests/test_permission.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index b33c2f213..00799d0fd 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -1049,7 +1049,7 @@ def test_permission_app_remove(): def test_permission_app_change_url(): app_install( os.path.join(get_test_apps_dir(), "permissions_app_ynh"), - args="domain=%s&domain_2=%s&path=%s&admin=%s" + args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s" % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), force=True, ) @@ -1072,7 +1072,7 @@ def test_permission_app_change_url(): def test_permission_protection_management_by_helper(): app_install( os.path.join(get_test_apps_dir(), "permissions_app_ynh"), - args="domain=%s&domain_2=%s&path=%s&admin=%s" + args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s" % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), force=True, ) @@ -1135,7 +1135,7 @@ 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" + args="domain=%s&domain_2=%s&path=%s&is_public=1" % (maindomain, other_domains[0], "/legacy"), force=True, ) From 3daac24443eb02f449d5e1c6ce6b2b91d38c5e7b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 19:25:36 +0200 Subject: [PATCH 073/142] Typos --- src/yunohost/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 36b65f873..b6150d152 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -645,7 +645,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False for file_to_copy in APP_FILES_TO_COPY: os.system(f"rm -rf '{app_setting_path}/{file_to_copy}'") if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): - os.system(f"cp -R '{extracted_app_folder}/{file_to_copy} {app_setting_path}'") + os.system(f"cp -R '{extracted_app_folder}/{file_to_copy}' '{app_setting_path}'") # Clean and set permissions shutil.rmtree(extracted_app_folder) @@ -2089,7 +2089,7 @@ def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: shutil.rmtree(extracted_app_folder) if path[-1] != "/": path = path + "/" - extract_result = os.system(f"cp -a '{path}' {extracted_app_folder}") + extract_result = os.system(f"cp -a '{path}' '{extracted_app_folder}'") else: extract_result = 1 From 6c03cf2b118b1c0178aad905819e6fe623ff5ec2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Oct 2021 20:22:59 +0200 Subject: [PATCH 074/142] Yoloattempt to try to cleanup some ugly os.system --- src/yunohost/app.py | 44 +++++++++++++++++++----------------------- src/yunohost/dns.py | 4 ++-- src/yunohost/domain.py | 4 ++-- src/yunohost/dyndns.py | 19 +++++++++--------- src/yunohost/hook.py | 5 ++--- src/yunohost/tools.py | 26 +++++++++++-------------- 6 files changed, 46 insertions(+), 56 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index b6150d152..cf4b47681 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -45,6 +45,10 @@ from moulinette.utils.filesystem import ( read_yaml, write_to_file, write_to_json, + cp, + rm, + chown, + chmod, ) from yunohost.utils import packages @@ -643,15 +647,15 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False # Replace scripts and manifest and conf (if exists) # Move scripts and manifest to the right place for file_to_copy in APP_FILES_TO_COPY: - os.system(f"rm -rf '{app_setting_path}/{file_to_copy}'") + rm(f'{app_setting_path}/{file_to_copy}', recursive=True, force=True) if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): - os.system(f"cp -R '{extracted_app_folder}/{file_to_copy}' '{app_setting_path}'") + cp(f'{extracted_app_folder}/{file_to_copy}', f'{app_setting_path}/{file_to_copy}', recursive=True) # Clean and set permissions shutil.rmtree(extracted_app_folder) - os.system("chmod 600 %s" % app_setting_path) - os.system("chmod 400 %s/settings.yml" % app_setting_path) - os.system("chown -R root: %s" % app_setting_path) + chmod(app_setting_path, 0o600) + chmod(f"{app_setting_path}/settings.yml", 0o400) + chown(app_setting_path, "root", recursive=True) # So much win logger.success(m18n.n("app_upgraded", app=app_instance_name)) @@ -816,7 +820,7 @@ def app_install( # Move scripts and manifest to the right place for file_to_copy in APP_FILES_TO_COPY: if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): - os.system(f"cp -R '{extracted_app_folder}/{file_to_copy}' '{app_setting_path}'") + cp(f'{extracted_app_folder}/{file_to_copy}', f'{app_setting_path}/{file_to_copy}', recursive=True) # Initialize the main permission for the app # The permission is initialized with no url associated, and with tile disabled @@ -955,9 +959,9 @@ def app_install( # Clean and set permissions shutil.rmtree(extracted_app_folder) - os.system("chmod 600 %s" % app_setting_path) - os.system("chmod 400 %s/settings.yml" % app_setting_path) - os.system("chown -R root: %s" % app_setting_path) + chmod(app_setting_path, 0o600) + chmod(f"{app_setting_path}/settings.yml", 0o400) + chown(app_setting_path, "root", recursive=True) logger.success(m18n.n("installation_complete")) @@ -1153,7 +1157,7 @@ def app_makedefault(operation_logger, app, domain=None): write_to_json( "/etc/ssowat/conf.json.persistent", ssowat_conf, sort_keys=True, indent=4 ) - os.system("chmod 644 /etc/ssowat/conf.json.persistent") + chmod("/etc/ssowat/conf.json.persistent", 0o644) logger.success(m18n.n("ssowat_conf_updated")) @@ -2077,24 +2081,16 @@ def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: extracted_app_folder = _make_tmp_workdir_for_app() - if ".zip" in path: - extract_result = os.system( - f"unzip '{path}' -d {extracted_app_folder} > /dev/null 2>&1" - ) - elif ".tar" in path: - extract_result = os.system( - f"tar -xf '{path}' -C {extracted_app_folder} > /dev/null 2>&1" - ) - elif os.path.isdir(path): + if os.path.isdir(path): shutil.rmtree(extracted_app_folder) if path[-1] != "/": path = path + "/" - extract_result = os.system(f"cp -a '{path}' '{extracted_app_folder}'") + cp(path, extracted_app_folder, recursive=True) else: - extract_result = 1 - - if extract_result != 0: - raise YunohostError("app_extraction_failed") + try: + shutil.unpack_archive(path, extracted_app_folder) + except Exception: + raise YunohostError("app_extraction_failed") try: if len(os.listdir(extracted_app_folder)) == 1: diff --git a/src/yunohost/dns.py b/src/yunohost/dns.py index 689950490..b0d40db9e 100644 --- a/src/yunohost/dns.py +++ b/src/yunohost/dns.py @@ -32,7 +32,7 @@ from collections import OrderedDict from moulinette import m18n, Moulinette from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_file, write_to_file, read_toml +from moulinette.utils.filesystem import read_file, write_to_file, read_toml, mkdir from yunohost.domain import ( domain_list, @@ -471,7 +471,7 @@ def _get_dns_zone_for_domain(domain): # Check if there's a NS record for that domain answer = dig(parent, rdtype="NS", full_answers=True, resolvers="force_external") if answer[0] == "ok": - os.system(f"mkdir -p {cache_folder}") + mkdir(cache_folder, parents=True) write_to_file(cache_file, parent) return parent diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index f5b14748d..b40831d25 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -29,7 +29,7 @@ from typing import Dict, Any from moulinette import m18n, Moulinette from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import write_to_file, read_yaml, write_to_yaml +from moulinette.utils.filesystem import write_to_file, read_yaml, write_to_yaml, rm from yunohost.app import ( app_ssowatconf, @@ -328,7 +328,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): ] for stuff in stuff_to_delete: - os.system("rm -rf {stuff}") + rm(stuff, force=True, recursive=True) # Sometime we have weird issues with the regenconf where some files # appears as manually modified even though they weren't touched ... diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 519fbc8f0..e33cf4f22 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -33,7 +33,7 @@ import subprocess from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import write_to_file, read_file +from moulinette.utils.filesystem import write_to_file, read_file, rm, chown, chmod from moulinette.utils.network import download_json from yunohost.utils.error import YunohostError, YunohostValidationError @@ -152,13 +152,12 @@ def dyndns_subscribe( os.system( "cd /etc/yunohost/dyndns && " - "dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s" - % domain - ) - os.system( - "chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private" + f"dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER {domain}" ) + chmod("/etc/yunohost/dyndns", 0o600, recursive=True) + chown("/etc/yunohost/dyndns", "root", recursive=True) + private_file = glob.glob("/etc/yunohost/dyndns/*%s*.private" % domain)[0] key_file = glob.glob("/etc/yunohost/dyndns/*%s*.key" % domain)[0] with open(key_file) as f: @@ -175,12 +174,12 @@ def dyndns_subscribe( timeout=30, ) except Exception as e: - os.system("rm -f %s" % private_file) - os.system("rm -f %s" % key_file) + rm(private_file, force=True) + rm(key_file, force=True) raise YunohostError("dyndns_registration_failed", error=str(e)) if r.status_code != 201: - os.system("rm -f %s" % private_file) - os.system("rm -f %s" % key_file) + rm(private_file, force=True) + rm(key_file, force=True) try: error = json.loads(r.text)["error"] except Exception: diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index c55809fce..20757bf3c 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -34,7 +34,7 @@ from importlib import import_module from moulinette import m18n, Moulinette from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils import log -from moulinette.utils.filesystem import read_yaml +from moulinette.utils.filesystem import read_yaml, cp HOOK_FOLDER = "/usr/share/yunohost/hooks/" CUSTOM_HOOK_FOLDER = "/etc/yunohost/hooks.d/" @@ -60,8 +60,7 @@ def hook_add(app, file): os.makedirs(CUSTOM_HOOK_FOLDER + action) finalpath = CUSTOM_HOOK_FOLDER + action + "/" + priority + "-" + app - os.system("cp %s %s" % (file, finalpath)) - os.system("chown -hR admin: %s" % HOOK_FOLDER) + cp(file, finalpath) return {"hook": finalpath} diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 4cdd62d8f..671f8768c 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -34,7 +34,7 @@ from typing import List from moulinette import Moulinette, m18n from moulinette.utils.log import getActionLogger from moulinette.utils.process import check_output, call_async_output -from moulinette.utils.filesystem import read_yaml, write_to_yaml +from moulinette.utils.filesystem import read_yaml, write_to_yaml, cp, mkdir, rm from yunohost.app import ( app_info, @@ -1147,13 +1147,11 @@ class Migration(object): backup_folder = "/home/yunohost.backup/premigration/" + time.strftime( "%Y%m%d-%H%M%S", time.gmtime() ) - os.makedirs(backup_folder, 0o750) + mkdir(backup_folder, 0o750, parents=True) os.system("systemctl stop slapd") - os.system(f"cp -r --preserve /etc/ldap {backup_folder}/ldap_config") - os.system(f"cp -r --preserve /var/lib/ldap {backup_folder}/ldap_db") - os.system( - f"cp -r --preserve /etc/yunohost/apps {backup_folder}/apps_settings" - ) + cp("/etc/ldap", f"{backup_folder}/ldap_config", recursive=True) + cp("/var/lib/ldap", f"{backup_folder}/ldap_db", recursive=True) + cp("/etc/yunohost/apps", f"{backup_folder}/apps_settings", recursive=True) except Exception as e: raise YunohostError( "migration_ldap_can_not_backup_before_migration", error=str(e) @@ -1169,17 +1167,15 @@ class Migration(object): ) os.system("systemctl stop slapd") # To be sure that we don't keep some part of the old config - os.system("rm -r /etc/ldap/slapd.d") - os.system(f"cp -r --preserve {backup_folder}/ldap_config/. /etc/ldap/") - os.system(f"cp -r --preserve {backup_folder}/ldap_db/. /var/lib/ldap/") - os.system( - f"cp -r --preserve {backup_folder}/apps_settings/. /etc/yunohost/apps/" - ) + rm("/etc/ldap/slapd.d", force=True, recursive=True) + cp(f"{backup_folder}/ldap_config", "/etc/ldap", recursive=True) + cp(f"{backup_folder}/ldap_db", "/var/lib/ldap", recursive=True) + cp(f"{backup_folder}/apps_settings", "/etc/yunohost/apps", recursive=True) os.system("systemctl start slapd") - os.system(f"rm -r {backup_folder}") + rm(backup_folder, force=True, recursive=True) logger.info(m18n.n("migration_ldap_rollback_success")) raise else: - os.system(f"rm -r {backup_folder}") + rm(backup_folder, force=True, recursive=True) return func From 4fda262bc0c69b68d2288d9773215b581bab945c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Oct 2021 01:59:16 +0200 Subject: [PATCH 075/142] fix tests: Missing mkdir on force --- src/yunohost/dns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/dns.py b/src/yunohost/dns.py index b0d40db9e..534ade918 100644 --- a/src/yunohost/dns.py +++ b/src/yunohost/dns.py @@ -471,7 +471,7 @@ def _get_dns_zone_for_domain(domain): # Check if there's a NS record for that domain answer = dig(parent, rdtype="NS", full_answers=True, resolvers="force_external") if answer[0] == "ok": - mkdir(cache_folder, parents=True) + mkdir(cache_folder, parents=True, force=True) write_to_file(cache_file, parent) return parent From a40ecdc98610c67dee5cfd75f83f596e8ba4d634 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Sat, 2 Oct 2021 02:29:22 +0000 Subject: [PATCH 076/142] [CI] Format code --- src/yunohost/app.py | 81 ++++++++++++++++++++++--------- src/yunohost/backup.py | 14 ++++-- src/yunohost/tests/test_appurl.py | 20 ++++++-- src/yunohost/tools.py | 12 ++++- src/yunohost/utils/legacy.py | 8 ++- 5 files changed, 102 insertions(+), 33 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index cf4b47681..fd088bce9 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -62,7 +62,12 @@ from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.filesystem import free_space_in_directory from yunohost.log import is_unit_operation, OperationLogger -from yunohost.app_catalog import app_catalog, app_search, _load_apps_catalog, app_fetchlist # noqa +from yunohost.app_catalog import ( + app_catalog, + app_search, + _load_apps_catalog, + app_fetchlist, +) # noqa logger = getActionLogger("yunohost.app") @@ -393,7 +398,9 @@ def app_change_url(operation_logger, app, domain, path): app_setting_path = os.path.join(APPS_SETTING_PATH, app) path_requirement = _guess_webapp_path_requirement(app_setting_path) - _validate_webpath_requirement({"domain": domain, "path": path}, path_requirement, ignore_app=app) + _validate_webpath_requirement( + {"domain": domain, "path": path}, path_requirement, ignore_app=app + ) tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) @@ -551,7 +558,9 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app_instance_name, workdir=extracted_app_folder) + env_dict = _make_environment_for_app_script( + app_instance_name, workdir=extracted_app_folder + ) env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version) env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version) @@ -615,7 +624,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False if upgrade_failed or broke_the_system: # display this if there are remaining apps - if apps[number + 1:]: + if apps[number + 1 :]: not_upgraded_apps = apps[number:] logger.error( m18n.n( @@ -647,9 +656,13 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False # Replace scripts and manifest and conf (if exists) # Move scripts and manifest to the right place for file_to_copy in APP_FILES_TO_COPY: - rm(f'{app_setting_path}/{file_to_copy}', recursive=True, force=True) + rm(f"{app_setting_path}/{file_to_copy}", recursive=True, force=True) if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): - cp(f'{extracted_app_folder}/{file_to_copy}', f'{app_setting_path}/{file_to_copy}', recursive=True) + cp( + f"{extracted_app_folder}/{file_to_copy}", + f"{app_setting_path}/{file_to_copy}", + recursive=True, + ) # Clean and set permissions shutil.rmtree(extracted_app_folder) @@ -820,7 +833,11 @@ def app_install( # Move scripts and manifest to the right place for file_to_copy in APP_FILES_TO_COPY: if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): - cp(f'{extracted_app_folder}/{file_to_copy}', f'{app_setting_path}/{file_to_copy}', recursive=True) + cp( + f"{extracted_app_folder}/{file_to_copy}", + f"{app_setting_path}/{file_to_copy}", + recursive=True, + ) # Initialize the main permission for the app # The permission is initialized with no url associated, and with tile disabled @@ -835,7 +852,9 @@ def app_install( ) # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app_instance_name, args=args, workdir=extracted_app_folder) + env_dict = _make_environment_for_app_script( + app_instance_name, args=args, workdir=extracted_app_folder + ) env_dict_for_logging = env_dict.copy() for question in questions: @@ -896,7 +915,9 @@ def app_install( logger.warning(m18n.n("app_remove_after_failed_install")) # Setup environment for remove script - env_dict_remove = _make_environment_for_app_script(app_instance_name, workdir=extracted_app_folder) + env_dict_remove = _make_environment_for_app_script( + app_instance_name, workdir=extracted_app_folder + ) # Execute remove script operation_logger_remove = OperationLogger( @@ -1504,7 +1525,9 @@ def app_action_run(operation_logger, app, action, args=None): tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) - env_dict = _make_environment_for_app_script(app, args=args, args_prefix="ACTION_", workdir=tmp_workdir_for_app) + env_dict = _make_environment_for_app_script( + app, args=args, args_prefix="ACTION_", workdir=tmp_workdir_for_app + ) env_dict["YNH_ACTION"] = action _, action_script = tempfile.mkstemp(dir=tmp_workdir_for_app) @@ -1984,7 +2007,7 @@ def _is_app_repo_url(string: str) -> bool: string = string.strip() # Dummy test for ssh-based stuff ... should probably be improved somehow - if '@' in string: + if "@" in string: return True return bool(APP_REPO_URL.match(string)) @@ -1992,7 +2015,7 @@ def _is_app_repo_url(string: str) -> bool: def _app_quality(src: str) -> str: """ - app may in fact be an app name, an url, or a path + app may in fact be an app name, an url, or a path """ raw_app_catalog = _load_apps_catalog()["apps"] @@ -2026,13 +2049,15 @@ def _app_quality(src: str) -> str: return "thirdparty" else: if "http://" in src or "https://" in src: - logger.error(f"{src} is not a valid app url: app url are expected to look like https://domain.tld/path/to/repo_ynh") + logger.error( + f"{src} is not a valid app url: app url are expected to look like https://domain.tld/path/to/repo_ynh" + ) raise YunohostValidationError("app_unknown") def _extract_app(src: str) -> Tuple[Dict, str]: """ - src may be an app name, an url, or a path + src may be an app name, an url, or a path """ raw_app_catalog = _load_apps_catalog()["apps"] @@ -2054,8 +2079,8 @@ def _extract_app(src: str) -> Tuple[Dict, str]: 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 "/-/" in url: + url = url.replace("/-/", "/") if "/tree/" in url: url, branch = url.split("/tree/", 1) return _extract_app_from_gitrepo(url, branch, revision, {}) @@ -2064,7 +2089,9 @@ def _extract_app(src: str) -> Tuple[Dict, str]: return _extract_app_from_folder(src) else: if "http://" in src or "https://" in src: - logger.error(f"{src} is not a valid app url: app url are expected to look like https://domain.tld/path/to/repo_ynh") + logger.error( + f"{src} is not a valid app url: app url are expected to look like https://domain.tld/path/to/repo_ynh" + ) raise YunohostValidationError("app_unknown") @@ -2108,7 +2135,9 @@ def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: return manifest, extracted_app_folder -def _extract_app_from_gitrepo(url: str, branch: str, revision: str, app_info: Dict = {}) -> Tuple[Dict, str]: +def _extract_app_from_gitrepo( + url: str, branch: str, revision: str, app_info: Dict = {} +) -> Tuple[Dict, str]: logger.debug(m18n.n("downloading")) @@ -2237,8 +2266,12 @@ def _guess_webapp_path_requirement(app_folder: str) -> str: manifest = _get_manifest_of_app(app_folder) raw_questions = manifest.get("arguments", {}).get("install", {}) - domain_questions = [question for question in raw_questions if question.get("type") == "domain"] - path_questions = [question for question in raw_questions if question.get("type") == "path"] + domain_questions = [ + question for question in raw_questions if question.get("type") == "domain" + ] + path_questions = [ + question for question in raw_questions if question.get("type") == "path" + ] if len(domain_questions) == 0 and len(path_questions) == 0: return "" @@ -2276,7 +2309,9 @@ def _validate_webpath_requirement( _assert_no_conflicting_apps(domain, path, ignore_app=ignore_app) elif path_requirement == "full_domain": - _assert_no_conflicting_apps(domain, "/", full_domain=True, ignore_app=ignore_app) + _assert_no_conflicting_apps( + domain, "/", full_domain=True, ignore_app=ignore_app + ) def _get_conflicting_apps(domain, path, ignore_app=None): @@ -2341,7 +2376,9 @@ 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): +def _make_environment_for_app_script( + app, args={}, args_prefix="APP_ARG_", workdir=None +): app_setting_path = os.path.join(APPS_SETTING_PATH, app) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 5664af3a0..cce66597a 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1348,7 +1348,11 @@ class RestoreManager: app_instance_name -- (string) The app name to restore (no app with this name should be already install) """ - from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_php_versions_in_settings, _patch_legacy_helpers + from yunohost.utils.legacy import ( + _patch_legacy_php_versions, + _patch_legacy_php_versions_in_settings, + _patch_legacy_helpers, + ) from yunohost.user import user_group_list from yunohost.permission import ( permission_create, @@ -1485,7 +1489,9 @@ class RestoreManager: # Prepare env. var. to pass to script # FIXME : workdir should be a tmp workdir app_workdir = os.path.join(self.work_dir, "apps", app_instance_name, "settings") - env_dict = _make_environment_for_app_script(app_instance_name, workdir=app_workdir) + env_dict = _make_environment_for_app_script( + app_instance_name, workdir=app_workdir + ) env_dict.update( { "YNH_BACKUP_DIR": self.work_dir, @@ -1529,7 +1535,9 @@ class RestoreManager: 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) + 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)], diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index d50877445..186b76cdf 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -32,17 +32,25 @@ def test_repo_url_definition(): assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh") assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh/") assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh.git") - assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing") - assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing/") + assert _is_app_repo_url( + "https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing" + ) + assert _is_app_repo_url( + "https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing/" + ) assert _is_app_repo_url("https://github.com/YunoHost-Apps/foo-bar-123_ynh") assert _is_app_repo_url("https://github.com/YunoHost-Apps/foo_bar_123_ynh") assert _is_app_repo_url("https://github.com/YunoHost-Apps/FooBar123_ynh") assert _is_app_repo_url("https://github.com/labriqueinternet/vpnclient_ynh") assert _is_app_repo_url("https://framagit.org/YunoHost/apps/nodebb_ynh") - assert _is_app_repo_url("https://framagit.org/YunoHost/apps/nodebb_ynh/-/tree/testing") + assert _is_app_repo_url( + "https://framagit.org/YunoHost/apps/nodebb_ynh/-/tree/testing" + ) assert _is_app_repo_url("https://gitlab.com/yunohost-apps/foobar_ynh") assert _is_app_repo_url("https://code.antopie.org/miraty/qr_ynh") - assert _is_app_repo_url("https://gitlab.domainepublic.net/Neutrinet/neutrinet_ynh/-/tree/unstable") + assert _is_app_repo_url( + "https://gitlab.domainepublic.net/Neutrinet/neutrinet_ynh/-/tree/unstable" + ) assert _is_app_repo_url("git@github.com:YunoHost-Apps/foobar_ynh.git") assert not _is_app_repo_url("github.com/YunoHost-Apps/foobar_ynh") @@ -50,7 +58,9 @@ def test_repo_url_definition(): assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_wat") assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh_wat") assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar/tree/testing") - assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh_wat/tree/testing") + assert not _is_app_repo_url( + "https://github.com/YunoHost-Apps/foobar_ynh_wat/tree/testing" + ) assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/") assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/pwet") assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/pwet_foo") diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 671f8768c..e89081abd 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -1151,7 +1151,11 @@ class Migration(object): os.system("systemctl stop slapd") cp("/etc/ldap", f"{backup_folder}/ldap_config", recursive=True) cp("/var/lib/ldap", f"{backup_folder}/ldap_db", recursive=True) - cp("/etc/yunohost/apps", f"{backup_folder}/apps_settings", recursive=True) + cp( + "/etc/yunohost/apps", + f"{backup_folder}/apps_settings", + recursive=True, + ) except Exception as e: raise YunohostError( "migration_ldap_can_not_backup_before_migration", error=str(e) @@ -1170,7 +1174,11 @@ class Migration(object): rm("/etc/ldap/slapd.d", force=True, recursive=True) cp(f"{backup_folder}/ldap_config", "/etc/ldap", recursive=True) cp(f"{backup_folder}/ldap_db", "/var/lib/ldap", recursive=True) - cp(f"{backup_folder}/apps_settings", "/etc/yunohost/apps", recursive=True) + cp( + f"{backup_folder}/apps_settings", + "/etc/yunohost/apps", + recursive=True, + ) os.system("systemctl start slapd") rm(backup_folder, force=True, recursive=True) logger.info(m18n.n("migration_ldap_rollback_success")) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index 7ae98bed8..87c163f1b 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -4,7 +4,13 @@ import glob from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_file, write_to_file, write_to_json, write_to_yaml, read_yaml +from moulinette.utils.filesystem import ( + read_file, + write_to_file, + write_to_json, + write_to_yaml, + read_yaml, +) from yunohost.user import user_list from yunohost.app import ( From b88074b00752f0af193253294e936d7ae53143eb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Oct 2021 20:23:23 +0200 Subject: [PATCH 077/142] Make linter happy --- src/yunohost/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index fd088bce9..a65ac76c7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -62,12 +62,12 @@ from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.filesystem import free_space_in_directory from yunohost.log import is_unit_operation, OperationLogger -from yunohost.app_catalog import ( +from yunohost.app_catalog import ( # noqa app_catalog, app_search, _load_apps_catalog, app_fetchlist, -) # noqa +) logger = getActionLogger("yunohost.app") From 4640d4577d5a9f146a9211acb65b6f69e674aa7d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Oct 2021 20:24:13 +0200 Subject: [PATCH 078/142] Moar mypy --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index a65ac76c7..4f9c3147f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2401,7 +2401,7 @@ def _make_environment_for_app_script( return env_dict -def _parse_app_instance_name(app_instance_name): +def _parse_app_instance_name(app_instance_name: str) -> Tuple[str, int]: """ Parse a Yunohost app instance name and extracts the original appid and the application instance number From 9c4ea1ccc6fa42f8ddb5905715bfce0254e7f9fc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 4 Oct 2021 01:16:20 +0200 Subject: [PATCH 079/142] Try to make mypy happy --- src/yunohost/app.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 4f9c3147f..f4dd2aa1f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2424,13 +2424,16 @@ def _parse_app_instance_name(app_instance_name: str) -> Tuple[str, int]: True """ match = re_app_instance_name.match(app_instance_name) - assert match, "Could not parse app instance name : %s" % app_instance_name + assert match, f"Could not parse app instance name : {app_instance_name}" appid = match.groupdict().get("appid") - app_instance_nb = ( - int(match.groupdict().get("appinstancenb")) - if match.groupdict().get("appinstancenb") is not None - else 1 - ) + app_instance_nb = match.groupdict().get("appinstancenb") or "1" + if not appid: + raise Exception(f"Could not parse app instance name : {app_instance_name}") + if not str(app_instance_nb).isdigit(): + raise Exception(f"Could not parse app instance name : {app_instance_name}") + else: + app_instance_nb = int(str(app_instance_nb)) + return (appid, app_instance_nb) From 42da17181910761dc993ab5cbee3bb9012d8c1bc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 4 Oct 2021 01:24:16 +0200 Subject: [PATCH 080/142] Add proper test for parse_app_instance_name --- src/yunohost/app.py | 24 ++++++++---------------- src/yunohost/tests/test_appurl.py | 14 +++++++++++++- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f4dd2aa1f..1c05ce20b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2406,22 +2406,14 @@ def _parse_app_instance_name(app_instance_name: str) -> Tuple[str, int]: Parse a Yunohost app instance name and extracts the original appid and the application instance number - >>> _parse_app_instance_name('yolo') == ('yolo', 1) - True - >>> _parse_app_instance_name('yolo1') == ('yolo1', 1) - True - >>> _parse_app_instance_name('yolo__0') == ('yolo__0', 1) - True - >>> _parse_app_instance_name('yolo__1') == ('yolo', 1) - True - >>> _parse_app_instance_name('yolo__23') == ('yolo', 23) - True - >>> _parse_app_instance_name('yolo__42__72') == ('yolo__42', 72) - True - >>> _parse_app_instance_name('yolo__23qdqsd') == ('yolo__23qdqsd', 1) - True - >>> _parse_app_instance_name('yolo__23qdqsd56') == ('yolo__23qdqsd56', 1) - True + 'yolo' -> ('yolo', 1) + 'yolo1' -> ('yolo1', 1) + 'yolo__0' -> ('yolo__0', 1) + 'yolo__1' -> ('yolo', 1) + 'yolo__23' -> ('yolo', 23) + 'yolo__42__72' -> ('yolo__42', 72) + 'yolo__23qdqsd' -> ('yolo__23qdqsd', 1) + 'yolo__23qdqsd56' -> ('yolo__23qdqsd56', 1) """ match = re_app_instance_name.match(app_instance_name) assert match, f"Could not parse app instance name : {app_instance_name}" diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index 186b76cdf..ca953dcf7 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -4,7 +4,7 @@ import os from .conftest import get_test_apps_dir from yunohost.utils.error import YunohostError -from yunohost.app import app_install, app_remove, _is_app_repo_url +from yunohost.app import app_install, app_remove, _is_app_repo_url, _parse_app_instance_name from yunohost.domain import _get_maindomain, domain_url_available from yunohost.permission import _validate_and_sanitize_permission_url @@ -28,6 +28,18 @@ def teardown_function(function): pass +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) + assert _parse_app_instance_name('yolo__1') == ('yolo', 1) + assert _parse_app_instance_name('yolo__23') == ('yolo', 23) + assert _parse_app_instance_name('yolo__42__72') == ('yolo__42', 72) + assert _parse_app_instance_name('yolo__23qdqsd') == ('yolo__23qdqsd', 1) + assert _parse_app_instance_name('yolo__23qdqsd56') == ('yolo__23qdqsd56', 1) + + def test_repo_url_definition(): assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh") assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh/") From 6ad07d2c837bc894f89671e1dcc1d4d815c597ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Thu, 30 Sep 2021 04:30:15 +0000 Subject: [PATCH 081/142] Translated using Weblate (Galician) Currently translated at 100.0% (707 of 707 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/gl/ --- locales/gl.json | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/locales/gl.json b/locales/gl.json index ebb65be02..d70d7a561 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -17,7 +17,7 @@ "app_argument_required": "Requírese o argumento '{name}'", "app_argument_password_no_default": "Erro ao procesar o argumento do contrasinal '{name}': o argumento do contrasinal non pode ter un valor por defecto por razón de seguridade", "app_argument_invalid": "Elixe un valor válido para o argumento '{name}': {error}", - "app_argument_choice_invalid": "Usa unha destas opcións '{choices}' para o argumento '{name}' no lugar de '{value}'", + "app_argument_choice_invalid": "Elixe un valor válido para o argumento '{name}': '{value}' non está entre as opcións dispoñibles ({choices})", "backup_archive_writing_error": "Non se puideron engadir os ficheiros '{source}' (chamados no arquivo '{dest}' para ser copiados dentro do arquivo comprimido '{archive}'", "backup_archive_system_part_not_available": "A parte do sistema '{part}' non está dispoñible nesta copia", "backup_archive_corrupted": "Semella que o arquivo de copia '{archive}' está estragado : {error}", @@ -102,7 +102,7 @@ "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", "backup_cant_mount_uncompress_archive": "Non se puido montar o arquivo sen comprimir porque está protexido contra escritura", - "backup_ask_for_copying_if_needed": "Queres realizar a copia de apoio utilizando temporalmente {size}MB? (Faise deste xeito porque algúns ficheiros non hai xeito de preparalos usando unha forma máis eficiente).", + "backup_ask_for_copying_if_needed": "Queres realizar a copia de apoio utilizando temporalmente {size}MB? (Faise deste xeito porque algúns ficheiros non hai xeito de preparalos usando unha forma máis eficiente.)", "backup_running_hooks": "Executando os ganchos da copia...", "backup_permission": "Permiso de copia para {app}", "backup_output_symlink_dir_broken": "O directorio de arquivo '{path}' é unha ligazón simbólica rota. Pode ser que esqueceses re/montar ou conectar o medio de almacenaxe ao que apunta.", @@ -455,7 +455,7 @@ "migration_0015_modified_files": "Ten en conta que os seguintes ficheiros semella que foron modificados manualmente e poderían ser sobrescritos na actualización: {manually_modified_files}", "migration_0015_problematic_apps_warning": "Ten en conta que se detectaron as seguintes apps que poderían ser problemáticas. Semella que non foron instaladas usando o catálogo de YunoHost, ou non están marcadas como 'funcionais'. En consecuencia, non se pode garantir que seguirán funcionando após a actualización: {problematic_apps}", "diagnosis_http_localdomain": "O dominio {domain}, cun TLD .local, non é de agardar que esté exposto ao exterior da rede local.", - "diagnosis_dns_specialusedomain": "O dominio {domain} baséase un dominio de nivel alto e uso especial (TLD) polo que non é de agardar que realmente teña rexistros DNS.", + "diagnosis_dns_specialusedomain": "O dominio {domain} baséase un dominio de nivel alto e uso especial (TLD) como .local ou .test polo que non é de agardar que realmente teña rexistros DNS.", "upnp_enabled": "UPnP activado", "upnp_disabled": "UPnP desactivado", "permission_creation_failed": "Non se creou o permiso '{permission}': {error}", @@ -675,5 +675,39 @@ "config_version_not_supported": "A versión do panel de configuración '{version}' non está soportada.", "file_extension_not_accepted": "Rexeitouse o ficheiro '{path}' porque a súa extensión non está entre as aceptadas: {accept}", "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}" -} \ No newline at end of file + "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.", + "domain_dns_conf_special_use_tld": "Este dominio baséase nun dominio de alto-nivel (TLD) de uso especial como .local ou .test e por isto non é de agardar que teña rexistros DNS asociados.", + "domain_dns_registrar_managed_in_parent_domain": "Este dominio é un subdominio de {parent_domain_link}. A configuración DNS debe xestionarse no panel de configuración de {parent_domain}'s.", + "domain_dns_registrar_not_supported": "YunoHost non é quen de detectar a rexistradora que xestiona o dominio. Debes configurar manualmente os seus rexistros DNS seguindo a documentación en https://yunohost.org/dns.", + "domain_dns_registrar_experimental": "Ata o momento, a interface coa API de **{registar}** aínda non foi comprobada e revisada pola comunidade YunoHost. O soporte é **moi experimental** - ten coidado!", + "domain_dns_push_failed_to_list": "Non se pode mostrar a lista actual de rexistros na API da rexistradora: {error}", + "domain_dns_push_already_up_to_date": "Rexistros ao día, nada que facer.", + "domain_dns_pushing": "Enviando rexistros DNS...", + "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)", + "domain_config_auth_secret": "Segreda de autenticación", + "domain_config_api_protocol": "Protocolo API", + "domain_config_auth_application_key": "Chave da aplicación", + "domain_config_auth_application_secret": "Chave segreda da aplicación", + "domain_config_auth_consumer_key": "Chave consumidora", + "log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'", + "other_available_options": "... e outras {n} opcións dispoñibles non mostradas", + "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost dyndns update')", + "domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )", + "domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.", + "domain_config_auth_token": "Token de autenticación", + "domain_config_auth_key": "Chave de autenticación", + "domain_config_auth_entrypoint": "Punto de entrada da API", + "domain_dns_push_failed_to_authenticate": "Fallou a autenticación na API da rexistradora do dominio '{domain}'. Comprobaches que sexan as credenciais correctas? (Erro: {error})", + "domain_registrar_is_not_configured": "A rexistradora non aínda non está configurada para o dominio {domain}.", + "domain_dns_push_not_applicable": "A función de rexistro DNS automático non é aplicable ao dominio {domain}. Debes configurar manualmente os teus rexistros DNS seguindo a documentación de https://yunohost.org/dns_config.", + "domain_dns_push_managed_in_parent_domain": "A función de rexistro DNS automático está xestionada polo dominio nai {parent_domain}.", + "ldap_attribute_already_exists": "Xa existe o atributo LDAP '{attribute}' con valor '{value}'", + "log_domain_config_set": "Actualizar configuración para o dominio '{}'" +} From 707866e222105a744403cb286078dea472f52be3 Mon Sep 17 00:00:00 2001 From: Tymofii-Lytvynenko Date: Thu, 30 Sep 2021 19:03:23 +0000 Subject: [PATCH 082/142] Translated using Weblate (Ukrainian) Currently translated at 96.1% (680 of 707 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/uk/ --- locales/uk.json | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/locales/uk.json b/locales/uk.json index 35923908f..814fa56a9 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -16,7 +16,7 @@ "app_argument_required": "Аргумент '{name}' необхідний", "app_argument_password_no_default": "Помилка під час розбору аргументу пароля '{name}': аргумент пароля не може мати типове значення з причин безпеки", "app_argument_invalid": "Виберіть правильне значення для аргументу '{name}': {error}", - "app_argument_choice_invalid": "Використовуйте один з цих варіантів '{choices}' для аргументу '{name}' замість '{value}'", + "app_argument_choice_invalid": "Виберіть дійсне значення для аргументу '{name}': '{value}' не є серед доступних варіантів ({choices})", "app_already_up_to_date": "{app} має найостаннішу версію", "app_already_installed_cant_change_url": "Цей застосунок уже встановлено. URL-адреса не може бути змінена тільки цією функцією. Перевірте в `app changeurl`, якщо вона доступна.", "app_already_installed": "{app} уже встановлено", @@ -482,7 +482,7 @@ "diagnosis_domain_expiration_not_found_details": "Відомості WHOIS для домену {domain} не містять даних про строк дії?", "diagnosis_domain_not_found_details": "Домен {domain} не існує в базі даних WHOIS або строк його дії сплив!", "diagnosis_domain_expiration_not_found": "Неможливо перевірити строк дії деяких доменів", - "diagnosis_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) і тому не очікується, що у нього будуть актуальні записи DNS.", + "diagnosis_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) такого як .local або .test і тому не очікується, що у нього будуть актуальні записи DNS.", "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}", @@ -504,7 +504,7 @@ "diagnosis_ip_connected_ipv4": "Сервер під'єднаний до Інтернету через IPv4!", "diagnosis_no_cache": "Для категорії «{category}» ще немає кеша діагностики", "diagnosis_failed": "Не вдалося отримати результат діагностики для категорії '{category}': {error}", - "diagnosis_everything_ok": "Усе виглядає добре для {category}!", + "diagnosis_everything_ok": "Здається, для категорії '{category}' все справно!", "diagnosis_found_warnings": "Знайдено {warnings} пунктів, які можна поліпшити для {category}.", "diagnosis_found_errors_and_warnings": "Знайдено {errors} істотний (і) питання (и) (і {warnings} попередження (я)), що відносяться до {category}!", "diagnosis_found_errors": "Знайдена {errors} важлива проблема (і), пов'язана з {category}!", @@ -675,5 +675,13 @@ "log_app_config_set": "Застосувати конфігурацію до застосунку '{}'", "service_not_reloading_because_conf_broken": "Неможливо перезавантажити/перезапустити службу '{name}', тому що її конфігурацію порушено: {errors}", "app_argument_password_help_optional": "Введіть один пробіл, щоб очистити пароль", - "app_argument_password_help_keep": "Натисніть Enter, щоб зберегти поточне значення" -} \ No newline at end of file + "app_argument_password_help_keep": "Натисніть Enter, щоб зберегти поточне значення", + "domain_registrar_is_not_configured": "Реєстратор ще не конфігуровано для домену {domain}.", + "domain_dns_push_not_applicable": "Функція автоматичної конфігурації DNS не застосовується до домену {domain}. Вам слід вручну конфігурувати записи DNS відповідно до документації за адресою https://yunohost.org/dns_config.", + "domain_dns_registrar_not_supported": "YunoHost не зміг автоматично виявити реєстратора, який обробляє цей домен. Вам слід вручну конфігурувати записи DNS відповідно до документації за адресою https://yunohost.org/dns.", + "diagnosis_http_special_use_tld": "Домен {domain} базується на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він буде відкритий за межами локальної мережі.", + "domain_dns_push_managed_in_parent_domain": "Функцією автоконфігурації DNS керує батьківський домен {parent_domain}.", + "domain_dns_registrar_managed_in_parent_domain": "Цей домен є піддоменом {parent_domain_link}. Конфігурацією реєстратора DNS слід керувати на панелі конфігурації {parent_domain}.", + "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost dyndns update')", + "domain_dns_conf_special_use_tld": "Цей домен засновано на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він матиме актуальні записи DNS." +} From fe82fafa6d4cf565903e206c5bf918f5de28a577 Mon Sep 17 00:00:00 2001 From: ppr Date: Sun, 3 Oct 2021 09:15:09 +0000 Subject: [PATCH 083/142] Translated using Weblate (French) Currently translated at 99.1% (700 of 706 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 29e6da673..ba2042ffb 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -4,7 +4,7 @@ "admin_password_change_failed": "Impossible de changer le mot de passe", "admin_password_changed": "Le mot de passe d'administration a été modifié", "app_already_installed": "{app} est déjà installé", - "app_argument_choice_invalid": "Choix invalide pour le paramètre '{name}'. Les valeurs acceptées sont {choices}, au lieu de '{value}'", + "app_argument_choice_invalid": "Choisir une valeur valide pour l'argument '{name}' : '{value}' ne fait pas partie des choix disponibles ({choices})", "app_argument_invalid": "Valeur invalide pour le paramètre '{name}' : {error}", "app_argument_required": "Le paramètre '{name}' est requis", "app_extraction_failed": "Impossible d'extraire les fichiers d'installation", @@ -632,7 +632,7 @@ "global_settings_setting_security_webadmin_allowlist": "Adresses IP autorisées à accéder à la webadmin. Elles doivent être séparées par une virgule.", "global_settings_setting_security_webadmin_allowlist_enabled": "Autoriser seulement certaines IP à accéder à la webadmin.", "diagnosis_http_localdomain": "Le domaine {domain}, avec un TLD .local, ne devrait pas être exposé en dehors du réseau local.", - "diagnosis_dns_specialusedomain": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial et ne devrait donc pas avoir d'enregistrements DNS réels.", + "diagnosis_dns_specialusedomain": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial comme .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.", "invalid_password": "Mot de passe incorrect", "ldap_server_is_down_restart_it": "Le service LDAP est en panne, essayez de le redémarrer...", "ldap_server_down": "Impossible d'atteindre le serveur LDAP", @@ -705,5 +705,7 @@ "domain_config_auth_application_secret": "Clé secrète de l'application", "ldap_attribute_already_exists": "L'attribut LDAP '{attribute}' existe déjà avec la valeur '{value}'", "log_domain_config_set": "Mettre à jour la configuration du domaine '{}'", - "log_domain_dns_push": "Pousser les enregistrements DNS pour le domaine '{}'" + "log_domain_dns_push": "Pousser les enregistrements DNS pour le domaine '{}'", + "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." } From 9019d75cb8281873b62b4d51aa0211f0734bca0a Mon Sep 17 00:00:00 2001 From: ppr Date: Sun, 3 Oct 2021 09:18:06 +0000 Subject: [PATCH 084/142] Translated using Weblate (French) Currently translated at 99.7% (704 of 706 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index ba2042ffb..10f05a848 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -593,7 +593,7 @@ "diagnosis_package_installed_from_sury": "Des paquets du système devraient être rétrogradé de version", "additional_urls_already_added": "URL supplémentaire '{url}' déjà ajoutée pour la permission '{permission}'", "unknown_main_domain_path": "Domaine ou chemin inconnu pour '{app}'. Vous devez spécifier un domaine et un chemin pour pouvoir spécifier une URL pour l'autorisation.", - "show_tile_cant_be_enabled_for_regex": "Vous ne pouvez pas activer 'show_tile' pour le moment, car l'URL de l'autorisation '{permission}' est une expression régulière", + "show_tile_cant_be_enabled_for_regex": "Vous ne pouvez pas activer 'show_tile' pour le moment, cela car l'URL de l'autorisation '{permission}' est une expression régulière", "show_tile_cant_be_enabled_for_url_not_defined": "Vous ne pouvez pas activer 'show_tile' pour le moment, car vous devez d'abord définir une URL pour l'autorisation '{permission}'", "regex_with_only_domain": "Vous ne pouvez pas utiliser une expression régulière pour le domaine, uniquement pour le chemin", "regex_incompatible_with_tile": "/!\\ Packagers ! La permission '{permission}' a 'show_tile' définie sur 'true' et vous ne pouvez donc pas définir une URL regex comme URL principale", @@ -678,7 +678,7 @@ "app_argument_password_help_optional": "Tapez un espace pour vider le mot de passe", "domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.", "domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.", - "domain_dns_registrar_yunohost": "Ce domaine est nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans autre configuration. (voir la commande 'yunohost dyndns update')", + "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost 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}.", @@ -707,5 +707,7 @@ "log_domain_config_set": "Mettre à jour la configuration du domaine '{}'", "log_domain_dns_push": "Pousser les enregistrements DNS pour le domaine '{}'", "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." + "domain_dns_conf_special_use_tld": "Ce domaine est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.", + "other_available_options": "... et {n} autres options disponibles non affichées", + "domain_config_auth_consumer_key": "Consumer key" } From 98c411ec9e09fe332346f7f68c208da170903045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= Date: Sun, 3 Oct 2021 10:03:26 +0000 Subject: [PATCH 085/142] Translated using Weblate (French) Currently translated at 100.0% (706 of 706 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 10f05a848..08224a2a0 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -685,7 +685,7 @@ "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.", "domain_dns_registrar_experimental": "Jusqu'à présent, l'interface avec l'API de **{registrar}** n'a pas été correctement testée et revue par la communauté YunoHost. L'assistance est **très expérimentale** - soyez prudent !", - "domain_dns_push_failed_to_authenticate": "Échec de l'authentification sur l'API du bureau d'enregistrement pour le domaine « {domain} ». Très probablement les informations d'identification sont incorrectes ? (Error: {error})", + "domain_dns_push_failed_to_authenticate": "Échec de l'authentification sur l'API du bureau d'enregistrement pour le domaine « {domain} ». Très probablement les informations d'identification sont incorrectes ? (Erreur : {error})", "domain_dns_push_failed_to_list": "Échec de la liste des enregistrements actuels à l'aide de l'API du registraire : {error}", "domain_dns_push_already_up_to_date": "Dossiers déjà à jour.", "domain_dns_pushing": "Transmission des enregistrements DNS...", From fb4d870e1f871764e65db267c7c6f55ddaab9c64 Mon Sep 17 00:00:00 2001 From: mifegui Date: Sun, 3 Oct 2021 15:56:23 +0000 Subject: [PATCH 086/142] Translated using Weblate (Portuguese) Currently translated at 29.4% (208 of 706 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pt/ --- locales/pt.json | 71 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index 534e0cb27..d285948be 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -109,7 +109,7 @@ "backup_output_directory_forbidden": "Escolha um diretório de saída diferente. Backups não podem ser criados nos subdiretórios /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", "app_already_installed_cant_change_url": "Este aplicativo já está instalado. A URL não pode ser alterada apenas por esta função. Confira em `app changeurl` se está disponível.", "app_already_up_to_date": "{app} já está atualizado", - "app_argument_choice_invalid": "Use uma das opções '{choices}' para o argumento '{name}' em vez de '{value}'", + "app_argument_choice_invalid": "Escolha um valor válido para o argumento '{name}' : '{value}' não está entre as opções disponíveis ({choices})", "app_argument_invalid": "Escolha um valor válido para o argumento '{name}': {error}", "app_argument_required": "O argumento '{name}' é obrigatório", "app_location_unavailable": "Esta url ou não está disponível ou está em conflito com outra(s) aplicação(ões) já instalada(s):\n{apps}", @@ -182,7 +182,7 @@ "backup_csv_creation_failed": "Não foi possível criar o arquivo CSV necessário para a restauração", "backup_csv_addition_failed": "Não foi possível adicionar os arquivos que estarão no backup ao arquivo CSV", "backup_create_size_estimation": "O arquivo irá conter cerca de {size} de dados.", - "backup_couldnt_bind": "Não foi possível vincular {src} ao {dest}", + "backup_couldnt_bind": "Não foi possível vincular {src} ao {dest}.", "certmanager_attempt_to_replace_valid_cert": "Você está tentando sobrescrever um certificado bom e válido para o domínio {domain}! (Use --force para prosseguir mesmo assim)", "backup_with_no_restore_script_for_app": "A aplicação {app} não tem um script de restauração, você não será capaz de automaticamente restaurar o backup dessa aplicação.", "backup_with_no_backup_script_for_app": "A aplicação '{app}' não tem um script de backup. Ignorando.", @@ -191,5 +191,68 @@ "backup_running_hooks": "Executando os hooks de backup...", "backup_permission": "Permissão de backup para {app}", "backup_output_symlink_dir_broken": "O diretório de seu arquivo '{path}' é um link simbólico quebrado. Talvez você tenha esquecido de re/montar ou conectar o dispositivo de armazenamento para onde o link aponta.", - "backup_output_directory_required": "Você deve especificar um diretório de saída para o backup" -} \ No newline at end of file + "backup_output_directory_required": "Você deve especificar um diretório de saída para o backup", + "diagnosis_description_apps": "Aplicações", + "diagnosis_apps_allgood": "Todos os apps instalados respeitam práticas básicas de empacotamento", + "diagnosis_apps_issue": "Um problema foi encontrado para o app {app}", + "diagnosis_apps_not_in_app_catalog": "Esta aplicação não está no catálogo de aplicações do YunoHost. Se estava no passado e foi removida, você deve considerar desinstalar este app já que ele não mais receberá atualizações e pode comprometer a integridade e segurança do seu sistema.", + "diagnosis_apps_broken": "Esta aplicação está atualmente marcada como quebrada no catálogo de apps do YunoHost. Isto pode ser um problema temporário enquanto os mantenedores consertam o problema. Enquanto isso, atualizar este app está desabilitado.", + "diagnosis_apps_bad_quality": "Esta aplicação está atualmente marcada como quebrada no catálogo de apps do YunoHost. Isto pode ser um problema temporário enquanto os mantenedores consertam o problema. Enquanto isso, atualizar este app está desabilitado.", + "diagnosis_apps_outdated_ynh_requirement": "A versão instalada deste app requer tão somente yunohost >= 2.x, o que tende a indicar que o app não está atualizado com as práticas de empacotamento recomendadas. Você deve considerar seriamente atualizá-lo.", + "diagnosis_apps_deprecated_practices": "A versão instalada deste app usa práticas de empacotamento extremamente velhas que não são mais usadas. Você deve considerar seriamente atualizá-lo.", + "certmanager_domain_http_not_working": "O domínio {domain} não parece estar acessível por HTTP. Por favor cheque a categoria 'Web' no diagnóstico para mais informações. (Se você sabe o que está fazendo, use '--no-checks' para desativar estas checagens.)", + "diagnosis_description_regenconf": "Configurações do sistema", + "diagnosis_description_services": "Cheque de status dos serviços", + "diagnosis_basesystem_hardware": "A arquitetura hardware do servidor é {virt} {arch}", + "diagnosis_description_web": "Web", + "diagnosis_basesystem_ynh_single_version": "Versão {package}: {version} ({repo})", + "diagnosis_basesystem_ynh_main_version": "O servidor está rodando YunoHost {main_version} ({repo})", + "app_config_unable_to_apply": "Falha ao aplicar valores do painel de configuração.", + "app_config_unable_to_read": "Falha ao ler valores do painel de configuração.", + "config_apply_failed": "Aplicar as novas configuração falhou: {error}", + "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", + "certmanager_cert_signing_failed": "Não foi possível assinar o novo certificado", + "certmanager_unable_to_parse_self_CA_name": "Não foi possível processar nome da autoridade de auto-assinatura (arquivo: {file})", + "confirm_app_install_warning": "Aviso: Pode ser que essa aplicação funcione, mas ela não está bem integrada ao YunoHost. Algumas funcionalidades como single sign-on e backup/restauração podem não estar disponíveis. Instalar mesmo assim? [{answers}] ", + "config_forbidden_keyword": "A palavra chave '{keyword}' é reservada, você não pode criar ou usar um painel de configuração com uma pergunta com esse id.", + "config_no_panel": "Painel de configuração não encontrado.", + "config_unknown_filter_key": "A chave de filtro '{filter_key}' está incorreta.", + "config_validate_color": "Deve ser uma cor RGB hexadecimal válida", + "config_validate_date": "Deve ser uma data válida como no formato AAAA-MM-DD", + "config_validate_email": "Deve ser um email válido", + "diagnosis_basesystem_kernel": "O servidor está rodando Linux kernel {kernel_version}", + "diagnosis_cache_still_valid": "(O cache para a categoria de diagnóstico {category} ainda é valido. Não será diagnosticada novamente ainda)", + "diagnosis_cant_run_because_of_dep": "Impossível fazer diagnóstico para {category} enquanto ainda existem problemas importantes relacionados a {dep}.", + "diagnosis_diskusage_low": "Unidade de armazenamento {mountpoint} (no dispositivo {device}_) tem somente {free} ({free_percent}%) de espaço restante (de {total}). Tenha cuidado.", + "diagnosis_description_ip": "Conectividade internet", + "diagnosis_description_dnsrecords": "Registros DNS", + "diagnosis_description_mail": "Email", + "certmanager_domain_not_diagnosed_yet": "Ainda não há resultado de diagnóstico para o domínio {domain}. Por favor re-execute um diagnóstico para as categorias 'Registros DNS' e 'Web' na seção de diagnósticos para checar se o domínio está pronto para o Let's Encrypt. (Ou, se você souber o que está fazendo, use '--no-checks' para desativar estas checagens.)", + "diagnosis_basesystem_host": "O Servidor está rodando Debian {debian_version}", + "diagnosis_description_systemresources": "Recursos do sistema", + "certmanager_acme_not_configured_for_domain": "O challenge ACME não pode ser realizado para {domain} porque o código correspondente na configuração do nginx está ausente... Por favor tenha certeza de que sua configuração do nginx está atualizada executando o comando `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_attempt_to_renew_nonLE_cert": "O certificado para o domínio '{domain}' não foi emitido pelo Let's Encrypt. Não é possível renová-lo automaticamente!", + "certmanager_attempt_to_renew_valid_cert": "O certificado para o domínio '{domain}' não esta prestes a expirar! (Você pode usar --force se saber o que está fazendo)", + "certmanager_cannot_read_cert": "Algo de errado aconteceu ao tentar abrir o atual certificado para o domínio {domain} (arquivo: {file}), motivo: {reason}", + "certmanager_cert_install_success": "Certificado Let's Encrypt foi instalado para o domínio '{domain}'", + "certmanager_cert_install_success_selfsigned": "Certificado autoassinado foi instalado para o domínio '{domain}'", + "certmanager_certificate_fetching_or_enabling_failed": "Tentativa de usar o novo certificado para o domínio {domain} não funcionou...", + "certmanager_domain_cert_not_selfsigned": "O certificado para o domínio {domain} não é autoassinado. Você tem certeza que quer substituí-lo? (Use '--force' para fazê-lo)", + "certmanager_domain_dns_ip_differs_from_public_ip": "O registro de DNS para o domínio '{domain}' é diferente do IP deste servidor. Por favor cheque a categoria 'Registros DNS' (básico) no diagnóstico para mais informações. Se você modificou recentemente o registro 'A', espere um tempo para ele se propagar (alguns serviços de checagem de propagação de DNS estão disponíveis online). (Se você sabe o que está fazendo, use '--no-checks' para desativar estas checagens.)", + "certmanager_hit_rate_limit": "Foram emitidos certificados demais para este conjunto de domínios {domain} recentemente. Por favor tente novamente mais tarde. Veja https://letsencrypt.org/docs/rate-limits/ para mais detalhes", + "certmanager_no_cert_file": "Não foi possível ler o arquivo de certificado para o domínio {domain} (arquivo: {file})", + "certmanager_self_ca_conf_file_not_found": "Não foi possível encontrar o arquivo de configuração para a autoridade de auto-assinatura (arquivo: {file})", + "confirm_app_install_danger": "ATENÇÃO! Sabe-se que esta aplicação ainda é experimental (isso se não que explicitamente não funciona)! Você provavelmente NÃO deve instalar ela a não ser que você saiba o que você está fazendo. NENHUM SUPORTE será fornecido se esta aplicação não funcionar ou quebrar o seu sistema... Se você está disposto a tomar esse rico de toda forma, digite '{answers}'", + "confirm_app_install_thirdparty": "ATENÇÃO! Essa aplicação não faz parte do catálogo do YunoHost. Instalar aplicações de terceiros pode comprometer a integridade e segurança do seu sistema. Você provavelmente NÃO deve instalá-la a não ser que você saiba o que você está fazendo. NENHUM SUPORTE será fornecido se este app não funcionar ou quebrar seu sistema... Se você está disposto a tomar este risco de toda forma, digite '{answers}'", + "diagnosis_description_ports": "Exposição de portas", + "diagnosis_basesystem_hardware_model": "O modelo do servidor é {model}", + "diagnosis_backports_in_sources_list": "Parece que o apt (o gerenciador de pacotes) está configurado para usar o repositório backport. A não ser que você saiba o que você esteá fazendo, desencorajamos fortemente a instalação de pacotes de backports porque é provável que crie instabilidades ou conflitos no seu sistema.", + "certmanager_cert_renew_success": "Certificado Let's Encrypt renovado para o domínio '{domain}'", + "certmanager_warning_subdomain_dns_record": "O subdomínio '{subdomain}' não resolve para o mesmo IP que '{domain}'. Algumas funcionalidades não estarão disponíveis até que você conserte isto e regenere o certificado." +} From 30ed2cd0b0d59328f7bb228d876406e7bac78ffe Mon Sep 17 00:00:00 2001 From: Tymofii-Lytvynenko Date: Sun, 3 Oct 2021 14:20:03 +0000 Subject: [PATCH 087/142] Translated using Weblate (Ukrainian) Currently translated at 100.0% (706 of 706 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/uk/ --- locales/uk.json | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/locales/uk.json b/locales/uk.json index 814fa56a9..b54d81fbd 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -683,5 +683,31 @@ "domain_dns_push_managed_in_parent_domain": "Функцією автоконфігурації DNS керує батьківський домен {parent_domain}.", "domain_dns_registrar_managed_in_parent_domain": "Цей домен є піддоменом {parent_domain_link}. Конфігурацією реєстратора DNS слід керувати на панелі конфігурації {parent_domain}.", "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost dyndns update')", - "domain_dns_conf_special_use_tld": "Цей домен засновано на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він матиме актуальні записи DNS." + "domain_dns_conf_special_use_tld": "Цей домен засновано на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він матиме актуальні записи DNS.", + "domain_dns_registrar_supported": "YunoHost автоматично визначив, що цей домен обслуговується реєстратором **{registrar}**. Якщо ви хочете, YunoHost автоматично налаштує цю DNS-зону, якщо ви надасте йому відповідні облікові дані API. Ви можете знайти документацію про те, як отримати реєстраційні дані API на цій сторінці: https://yunohost.org/registar_api_{registrar}. (Ви також можете вручну налаштувати свої DNS-записи, дотримуючись документації на https://yunohost.org/dns)", + "domain_dns_registrar_experimental": "Поки що інтерфейс з API **{registrar}** не був належним чином протестований і перевірений спільнотою YunoHost. Підтримка є **дуже експериментальною** - будьте обережні!", + "domain_dns_push_success": "Записи DNS оновлено!", + "domain_dns_push_failed": "Оновлення записів DNS зазнало невдачі.", + "domain_dns_push_partial_failure": "DNS-записи частково оновлено: повідомлялося про деякі попередження/помилки.", + "domain_config_mail_in": "Вхідні електронні листи", + "domain_config_mail_out": "Вихідні електронні листи", + "domain_config_auth_token": "Токен автентифікації", + "domain_config_auth_entrypoint": "Точка входу API", + "domain_config_auth_consumer_key": "Ключ споживача", + "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": "Секрет автентифікації", + "domain_config_api_protocol": "API-протокол", + "domain_config_auth_application_key": "Ключ застосунку", + "domain_config_auth_application_secret": "Таємний ключ застосунку", + "log_domain_config_set": "Оновлення конфігурації для домену '{}'", + "log_domain_dns_push": "Передавання записів DNS для домену '{}'", + "other_available_options": "...і {n} інших доступних опцій, які не показано", + "domain_dns_pushing": "Передання записів DNS...", + "ldap_attribute_already_exists": "Атрибут LDAP '{attribute}' вже існує зі значенням '{value}'", + "domain_dns_push_already_up_to_date": "Записи вже оновлені, нічого не потрібно робити." } From 592a998230eb44a3365bd8a795c53598dc76e35b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 4 Oct 2021 01:28:49 +0200 Subject: [PATCH 088/142] 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 08224a2a0..123270bd6 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -4,7 +4,7 @@ "admin_password_change_failed": "Impossible de changer le mot de passe", "admin_password_changed": "Le mot de passe d'administration a été modifié", "app_already_installed": "{app} est déjà installé", - "app_argument_choice_invalid": "Choisir une valeur valide pour l'argument '{name}' : '{value}' ne fait pas partie des choix disponibles ({choices})", + "app_argument_choice_invalid": "Choisissez une valeur valide pour l'argument '{name}' : '{value}' ne fait pas partie des choix disponibles ({choices})", "app_argument_invalid": "Valeur invalide pour le paramètre '{name}' : {error}", "app_argument_required": "Le paramètre '{name}' est requis", "app_extraction_failed": "Impossible d'extraire les fichiers d'installation", From a552700ca344f21fa390f03e67a5bfedd64bb4db Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 4 Oct 2021 01:34:16 +0200 Subject: [PATCH 089/142] Update changelog for 4.3.1.1 --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 84a29ed70..a10c83888 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (4.3.1.1) testing; urgency=low + + - [enh] app helpers: Update n version ([#1347](https://github.com/YunoHost/yunohost/pull/1347)) + - [enh] Misc app.py refactoring + Prevent change_url from being used to move a fulldomain app to a subpath ([#1346](https://github.com/YunoHost/yunohost/pull/1346)) + - [i18n] Translations updated for French, Galician, Portuguese, Ukrainian + + Thanks to all contributors <3 ! (Éric Gaspar, José M, mifegui, ppr, Tymofii-Lytvynenko) + + -- Alexandre Aubin Mon, 04 Oct 2021 01:33:22 +0200 + yunohost (4.3.1) testing; urgency=low - [fix] diagnosis: new app diagnosis grep reporing comments as issues ([#1333](https://github.com/YunoHost/yunohost/pull/1333)) From a68f98d8007be816d07695125502a8346d1dbbab Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Sun, 3 Oct 2021 23:56:11 +0000 Subject: [PATCH 090/142] [CI] Format code --- src/yunohost/tests/test_appurl.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index ca953dcf7..cf2c6c2c3 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -4,7 +4,12 @@ import os from .conftest import get_test_apps_dir from yunohost.utils.error import YunohostError -from yunohost.app import app_install, app_remove, _is_app_repo_url, _parse_app_instance_name +from yunohost.app import ( + app_install, + app_remove, + _is_app_repo_url, + _parse_app_instance_name, +) from yunohost.domain import _get_maindomain, domain_url_available from yunohost.permission import _validate_and_sanitize_permission_url @@ -30,14 +35,14 @@ 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) - assert _parse_app_instance_name('yolo__1') == ('yolo', 1) - assert _parse_app_instance_name('yolo__23') == ('yolo', 23) - assert _parse_app_instance_name('yolo__42__72') == ('yolo__42', 72) - assert _parse_app_instance_name('yolo__23qdqsd') == ('yolo__23qdqsd', 1) - assert _parse_app_instance_name('yolo__23qdqsd56') == ('yolo__23qdqsd56', 1) + 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) + assert _parse_app_instance_name("yolo__1") == ("yolo", 1) + assert _parse_app_instance_name("yolo__23") == ("yolo", 23) + assert _parse_app_instance_name("yolo__42__72") == ("yolo__42", 72) + assert _parse_app_instance_name("yolo__23qdqsd") == ("yolo__23qdqsd", 1) + assert _parse_app_instance_name("yolo__23qdqsd56") == ("yolo__23qdqsd56", 1) def test_repo_url_definition(): From 75b36f3b4a816e9410c909043a759649ff253f8a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 4 Oct 2021 03:40:41 +0200 Subject: [PATCH 091/142] tests: Try to fix mypy again /o\ --- src/yunohost/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 1c05ce20b..821ef06b0 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2418,13 +2418,13 @@ def _parse_app_instance_name(app_instance_name: str) -> Tuple[str, int]: match = re_app_instance_name.match(app_instance_name) assert match, f"Could not parse app instance name : {app_instance_name}" appid = match.groupdict().get("appid") - app_instance_nb = match.groupdict().get("appinstancenb") or "1" + app_instance_nb_ = match.groupdict().get("appinstancenb") or "1" if not appid: raise Exception(f"Could not parse app instance name : {app_instance_name}") - if not str(app_instance_nb).isdigit(): + if not str(app_instance_nb_).isdigit(): raise Exception(f"Could not parse app instance name : {app_instance_name}") else: - app_instance_nb = int(str(app_instance_nb)) + app_instance_nb = int(str(app_instance_nb_)) return (appid, app_instance_nb) From 2157576bf6446f95243a3a848bf0ec990ee015c0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 4 Oct 2021 03:47:12 +0200 Subject: [PATCH 092/142] i18n typos --- locales/gl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/gl.json b/locales/gl.json index d70d7a561..987093df8 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -680,7 +680,7 @@ "domain_dns_conf_special_use_tld": "Este dominio baséase nun dominio de alto-nivel (TLD) de uso especial como .local ou .test e por isto non é de agardar que teña rexistros DNS asociados.", "domain_dns_registrar_managed_in_parent_domain": "Este dominio é un subdominio de {parent_domain_link}. A configuración DNS debe xestionarse no panel de configuración de {parent_domain}'s.", "domain_dns_registrar_not_supported": "YunoHost non é quen de detectar a rexistradora que xestiona o dominio. Debes configurar manualmente os seus rexistros DNS seguindo a documentación en https://yunohost.org/dns.", - "domain_dns_registrar_experimental": "Ata o momento, a interface coa API de **{registar}** aínda non foi comprobada e revisada pola comunidade YunoHost. O soporte é **moi experimental** - ten coidado!", + "domain_dns_registrar_experimental": "Ata o momento, a interface coa API de **{registrar}** aínda non foi comprobada e revisada pola comunidade YunoHost. O soporte é **moi experimental** - ten coidado!", "domain_dns_push_failed_to_list": "Non se pode mostrar a lista actual de rexistros na API da rexistradora: {error}", "domain_dns_push_already_up_to_date": "Rexistros ao día, nada que facer.", "domain_dns_pushing": "Enviando rexistros DNS...", @@ -699,7 +699,7 @@ "log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'", "other_available_options": "... e outras {n} opcións dispoñibles non mostradas", "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost dyndns update')", - "domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )", + "domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registrar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registrar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )", "domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.", "domain_config_auth_token": "Token de autenticación", "domain_config_auth_key": "Chave de autenticación", From 4739859598cc7240a58a6175941f109cb78b8ab6 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Mon, 4 Oct 2021 11:52:12 +0200 Subject: [PATCH 093/142] fix app upgrade --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 821ef06b0..81b79ff48 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -507,7 +507,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False logger.warning(m18n.n("custom_app_url_required", app=app_instance_name)) continue elif app_dict["upgradable"] == "yes" or force: - new_app_src = app_dict["id"] + new_app_src = app_dict["manifest"]["id"] else: logger.success(m18n.n("app_already_up_to_date", app=app_instance_name)) continue From 9863263a284c933a29f8e001b73996db087f3629 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Mon, 4 Oct 2021 12:21:10 +0200 Subject: [PATCH 094/142] test to install/upgrade/remove an app from the manifest --- src/yunohost/tests/test_apps.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index 43125341b..db75aad02 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -189,6 +189,29 @@ def test_legacy_app_install_main_domain(): assert app_is_not_installed(main_domain, "legacy_app") +def test_app_from_catalog(): + main_domain = _get_maindomain() + + app_install("my_webapp", args="domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0") + app_map_ = app_map(raw=True) + assert main_domain in app_map_ + assert "/site" in app_map_[main_domain] + assert "id" in app_map_[main_domain]["/site"] + assert app_map_[main_domain]["/site"]["id"] == "my_webapp" + + assert app_is_installed(main_domain, "my_webapp") + assert app_is_exposed_on_http(main_domain, "/site", "Custom Web App") + + # Try upgrade, should do nothing + app_upgrade("my_webapp") + # Force upgrade, should upgrade to the same version + app_upgrade("my_webapp", force=True) + + app_remove("my_webapp") + + assert app_is_not_installed(main_domain, "my_webapp") + + def test_legacy_app_install_secondary_domain(secondary_domain): install_legacy_app(secondary_domain, "/legacy") From 96e7f1444d866d1603014373756d5dea74e2de12 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Mon, 4 Oct 2021 13:29:46 +0200 Subject: [PATCH 095/142] fix string in test_apps --- src/yunohost/tests/test_apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index db75aad02..bba769ec4 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -192,7 +192,7 @@ def test_legacy_app_install_main_domain(): def test_app_from_catalog(): main_domain = _get_maindomain() - app_install("my_webapp", args="domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0") + app_install("my_webapp", args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0") app_map_ = app_map(raw=True) assert main_domain in app_map_ assert "/site" in app_map_[main_domain] From 32998b118586259481b950f4b7874473e9f57fd5 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Mon, 4 Oct 2021 14:33:30 +0200 Subject: [PATCH 096/142] my_webapp is to clean too --- src/yunohost/tests/test_apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index bba769ec4..33af7256f 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -41,7 +41,7 @@ def clean(): os.system("mkdir -p /etc/ssowat/") app_ssowatconf() - test_apps = ["break_yo_system", "legacy_app", "legacy_app__2", "full_domain_app"] + test_apps = ["break_yo_system", "legacy_app", "legacy_app__2", "full_domain_app", "my_webapp"] for test_app in test_apps: From 27721749da10db3979b5629fdb3bbe83f3e00b89 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Mon, 4 Oct 2021 12:50:57 +0000 Subject: [PATCH 097/142] [CI] Format code --- src/yunohost/tests/test_apps.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index 33af7256f..22e18ec9a 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -41,7 +41,13 @@ def clean(): os.system("mkdir -p /etc/ssowat/") app_ssowatconf() - test_apps = ["break_yo_system", "legacy_app", "legacy_app__2", "full_domain_app", "my_webapp"] + test_apps = [ + "break_yo_system", + "legacy_app", + "legacy_app__2", + "full_domain_app", + "my_webapp", + ] for test_app in test_apps: @@ -192,7 +198,10 @@ def test_legacy_app_install_main_domain(): def test_app_from_catalog(): main_domain = _get_maindomain() - app_install("my_webapp", args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0") + app_install( + "my_webapp", + args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0", + ) app_map_ = app_map(raw=True) assert main_domain in app_map_ assert "/site" in app_map_[main_domain] From 54d901ad7820f3bb1895fba86ff651add08a048b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 5 Oct 2021 12:26:21 +0200 Subject: [PATCH 098/142] config: handle case where file quetion didnt get modified from webadmin, in which case self.value contains a path --- src/yunohost/utils/config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 27a9e1533..2a1159042 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -1019,10 +1019,9 @@ class FileQuestion(Question): FileQuestion.upload_dirs += [upload_dir] logger.debug(f"Saving file {self.name} for file question into {file_path}") - if Moulinette.interface.type != "api": + if Moulinette.interface.type != "api" or (self.value.startswith("/") and os.path.exists(self.value)): content = read_file(str(self.value), file_mode="rb") - - if Moulinette.interface.type == "api": + else: content = b64decode(self.value) write_to_file(file_path, content, file_mode="wb") From 753c4e34ed37889efc7846951042fff2c7836589 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 5 Oct 2021 12:32:23 +0200 Subject: [PATCH 099/142] Typo/wording --- data/helpers.d/config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/config b/data/helpers.d/config index d12316996..accedfce3 100644 --- a/data/helpers.d/config +++ b/data/helpers.d/config @@ -104,7 +104,7 @@ _ynh_app_config_apply_one() { cp "${!short_setting}" "$bind_file" fi ynh_store_file_checksum --file="$bind_file" --update_only - ynh_print_info --message="File '$bind_file' overwrited with ${!short_setting}" + ynh_print_info --message="File '$bind_file' overwritten with ${!short_setting}" fi # Save value in app settings @@ -124,7 +124,7 @@ _ynh_app_config_apply_one() { ynh_backup_if_checksum_is_different --file="$bind_file" echo "${!short_setting}" > "$bind_file" ynh_store_file_checksum --file="$bind_file" --update_only - ynh_print_info --message="File '$bind_file' overwrited with the content you provieded in '${short_setting}' question" + ynh_print_info --message="File '$bind_file' overwritten with the content provided in question '${short_setting}'" # Set value into a kind of key/value file else From 941cc29438e08a9a5f6579e099dd4fd7661c1758 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 5 Oct 2021 12:35:06 +0200 Subject: [PATCH 100/142] bind_key -> bind_key_ to prevent yunohost from redacting key names which leads to broken log metadata.yml somehow --- data/helpers.d/config | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/data/helpers.d/config b/data/helpers.d/config index accedfce3..3f856ffa4 100644 --- a/data/helpers.d/config +++ b/data/helpers.d/config @@ -51,15 +51,15 @@ _ynh_app_config_get_one() { then bind=":/etc/yunohost/apps/$app/settings.yml" fi - local bind_key="$(echo "$bind" | cut -d: -f1)" - bind_key=${bind_key:-$short_setting} - if [[ "$bind_key" == *">"* ]]; + local bind_key_="$(echo "$bind" | cut -d: -f1)" + bind_key_=${bind_key_:-$short_setting} + if [[ "$bind_key_" == *">"* ]]; then - bind_after="$(echo "${bind_key}" | cut -d'>' -f1)" - bind_key="$(echo "${bind_key}" | cut -d'>' -f2)" + bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" + bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" fi local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" - old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key}" --after="${bind_after}")" + old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key_}" --after="${bind_after}")" fi } @@ -129,22 +129,22 @@ _ynh_app_config_apply_one() { # Set value into a kind of key/value file else local bind_after="" - local bind_key="$(echo "$bind" | cut -d: -f1)" - bind_key=${bind_key:-$short_setting} - if [[ "$bind_key" == *">"* ]]; + local bind_key_="$(echo "$bind" | cut -d: -f1)" + bind_key_=${bind_key_:-$short_setting} + if [[ "$bind_key_" == *">"* ]]; then - bind_after="$(echo "${bind_key}" | cut -d'>' -f1)" - bind_key="$(echo "${bind_key}" | cut -d'>' -f2)" + bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" + bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" fi local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" ynh_backup_if_checksum_is_different --file="$bind_file" - ynh_write_var_in_file --file="${bind_file}" --key="${bind_key}" --value="${!short_setting}" --after="${bind_after}" + ynh_write_var_in_file --file="${bind_file}" --key="${bind_key_}" --value="${!short_setting}" --after="${bind_after}" ynh_store_file_checksum --file="$bind_file" --update_only # We stored the info in settings in order to be able to upgrade the app ynh_app_setting_set --app=$app --key=$short_setting --value="${!short_setting}" - ynh_print_info --message="Configuration key '$bind_key' edited into $bind_file" + ynh_print_info --message="Configuration key '$bind_key_' edited into $bind_file" fi fi From 61ec02c97cb6d39b7a5ce3c665cbe1700e7493fa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 5 Oct 2021 12:47:32 +0200 Subject: [PATCH 101/142] lint: Kill bare excepts --- src/yunohost/log.py | 2 +- src/yunohost/permission.py | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index c99c1bbc9..d73a62cd0 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -407,7 +407,7 @@ def is_unit_operation( if isinstance(value, IOBase): try: context[field] = value.name - except: + except Exception: context[field] = "IOBase" operation_logger = OperationLogger(op_key, related_to, args=context) diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 80d3b8602..1856046d6 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -474,7 +474,7 @@ def permission_create( protected=protected, sync_perm=sync_perm, ) - except: + except Exception: permission_delete(permission, force=True) raise diff --git a/tox.ini b/tox.ini index e79c70fec..733a0613d 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ deps = py37-mypy: mypy >= 0.900 commands = py37-lint: flake8 src doc data tests --ignore E402,E501,E203,W503 --exclude src/yunohost/vendor - py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F + py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F,E722 py37-black-check: black --check --diff src doc data tests py37-black-run: black src doc data tests py37-mypy: mypy --ignore-missing-import --install-types --non-interactive --follow-imports silent src/yunohost/ --exclude (acme_tiny|data_migrations) From b0e8a58b244037db65480450521f3fa3014a4915 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 5 Oct 2021 12:49:33 +0200 Subject: [PATCH 102/142] lint: Invalid escape sequences --- data/hooks/diagnosis/80-apps.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/hooks/diagnosis/80-apps.py b/data/hooks/diagnosis/80-apps.py index a75193a45..5aec48ed8 100644 --- a/data/hooks/diagnosis/80-apps.py +++ b/data/hooks/diagnosis/80-apps.py @@ -76,7 +76,7 @@ class AppDiagnoser(Diagnoser): for deprecated_helper in deprecated_helpers: if ( os.system( - f"grep -hr '{deprecated_helper}' {app['setting_path']}/scripts/ | grep -v -q '^\s*#'" + f"grep -hr '{deprecated_helper}' {app['setting_path']}/scripts/ | grep -v -q '^\\s*#'" ) == 0 ): diff --git a/tox.ini b/tox.ini index 733a0613d..267134e57 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ deps = py37-mypy: mypy >= 0.900 commands = py37-lint: flake8 src doc data tests --ignore E402,E501,E203,W503 --exclude src/yunohost/vendor - py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F,E722 + py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F,E722,W605 py37-black-check: black --check --diff src doc data tests py37-black-run: black src doc data tests py37-mypy: mypy --ignore-missing-import --install-types --non-interactive --follow-imports silent src/yunohost/ --exclude (acme_tiny|data_migrations) From de4b3825ab80a7b0f57639908812b0e00b7fc49d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 5 Oct 2021 12:50:25 +0200 Subject: [PATCH 103/142] Ambiguous var name --- src/yunohost/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 7d89af443..c9f70e152 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -677,7 +677,7 @@ def user_import(operation_logger, csvfile, update=False, delete=False): def to_list(str_list): L = str_list.split(",") if str_list else [] - L = [l.strip() for l in L] + L = [element.strip() for element in L] return L existing_users = user_list()["users"] From ef2a8c8dbd66b490f48348cbbfe2051e4ac221f5 Mon Sep 17 00:00:00 2001 From: ericgaspar Date: Tue, 5 Oct 2021 13:02:51 +0200 Subject: [PATCH 104/142] Update logrotate --- data/helpers.d/logrotate | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/helpers.d/logrotate b/data/helpers.d/logrotate index 2d9ab6b72..27803bafd 100644 --- a/data/helpers.d/logrotate +++ b/data/helpers.d/logrotate @@ -96,6 +96,10 @@ $logfile { EOF mkdir --parents $(dirname "$logfile") # Create the log directory, if not exist cat ${app}-logrotate | $customtee /etc/logrotate.d/$app > /dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee) + + ynh_user_exists "$app" || chown $app:$app "/var/log/$app" + chmod o-rwx "/var/log/$app" + } # Remove the app's logrotate config. From 423eef7a620192389170a158dfa77bc0469ccb12 Mon Sep 17 00:00:00 2001 From: ericgaspar Date: Tue, 5 Oct 2021 13:06:21 +0200 Subject: [PATCH 105/142] Update logrotate --- data/helpers.d/logrotate | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/logrotate b/data/helpers.d/logrotate index 27803bafd..e4b354e03 100644 --- a/data/helpers.d/logrotate +++ b/data/helpers.d/logrotate @@ -97,8 +97,8 @@ EOF mkdir --parents $(dirname "$logfile") # Create the log directory, if not exist cat ${app}-logrotate | $customtee /etc/logrotate.d/$app > /dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee) - ynh_user_exists "$app" || chown $app:$app "/var/log/$app" - chmod o-rwx "/var/log/$app" + ynh_user_exists "$app" || chown $app:$app "$logfile" + chmod o-rwx "$logfile" } From 1baebeba6d5d095e8c7f88cd785b668fe83941ef Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Tue, 5 Oct 2021 11:13:38 +0000 Subject: [PATCH 106/142] [CI] Format code --- src/yunohost/utils/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 2a1159042..2363545cf 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -1019,7 +1019,9 @@ class FileQuestion(Question): FileQuestion.upload_dirs += [upload_dir] logger.debug(f"Saving file {self.name} for file question into {file_path}") - if Moulinette.interface.type != "api" or (self.value.startswith("/") and os.path.exists(self.value)): + if Moulinette.interface.type != "api" or ( + self.value.startswith("/") and os.path.exists(self.value) + ): content = read_file(str(self.value), file_mode="rb") else: content = b64decode(self.value) From 4cd5e9b632113dc595940de766a83be13aebfb31 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 5 Oct 2021 13:46:06 +0200 Subject: [PATCH 107/142] app_info: return a new is_webapp info meant to be used by API --- src/yunohost/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 81b79ff48..2b8d71abf 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -168,6 +168,9 @@ def app_info(app, full=False): absolute_app_name, _ = _parse_app_instance_name(app) ret["from_catalog"] = _load_apps_catalog()["apps"].get(absolute_app_name, {}) ret["upgradable"] = _app_upgradable(ret) + + ret["is_webapp"] = ("domain" in settings and "path" in settings) + ret["supports_change_url"] = os.path.exists( os.path.join(setting_path, "scripts", "change_url") ) From 93a72a7b5fe4c7efbbecd9a1c6285ab5d9b97337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= <46165813+ericgaspar@users.noreply.github.com> Date: Tue, 5 Oct 2021 13:52:45 +0200 Subject: [PATCH 108/142] Update data/helpers.d/logrotate Co-authored-by: Kayou --- data/helpers.d/logrotate | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/logrotate b/data/helpers.d/logrotate index e4b354e03..a4548512d 100644 --- a/data/helpers.d/logrotate +++ b/data/helpers.d/logrotate @@ -97,7 +97,7 @@ EOF mkdir --parents $(dirname "$logfile") # Create the log directory, if not exist cat ${app}-logrotate | $customtee /etc/logrotate.d/$app > /dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee) - ynh_user_exists "$app" || chown $app:$app "$logfile" + ynh_user_exists --username="$app" || chown $app:$app "$logfile" chmod o-rwx "$logfile" } From 74256845525799c674df5984afe2ec18974dbcd0 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 6 Oct 2021 02:37:27 +0200 Subject: [PATCH 109/142] [enh] Add visible attribute support in cli --- src/yunohost/tests/test_questions.py | 96 ++++++++++++++ src/yunohost/utils/config.py | 182 ++++++++++++++++++++++++--- 2 files changed, 259 insertions(+), 19 deletions(-) diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index cf4e67733..b39990b73 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -15,6 +15,7 @@ from yunohost.utils.config import ( PathQuestion, BooleanQuestion, FileQuestion, + evaluate_simple_js_expression ) from yunohost.utils.error import YunohostError, YunohostValidationError @@ -2093,3 +2094,98 @@ def test_normalize_path(): assert PathQuestion.normalize("/macnuggets/") == "/macnuggets" assert PathQuestion.normalize("macnuggets/") == "/macnuggets" assert PathQuestion.normalize("////macnuggets///") == "/macnuggets" + +def test_simple_evaluate(): + context = { + 'a1': 1, + 'b2': 2, + 'c10': 10, + 'foo': 'bar', + 'comp': '1>2', + 'empty': '', + 'lorem': 'Lorem ipsum dolor et si qua met!', + 'warning': 'Warning! This sentence will fail!', + 'quote': "Je s'apelle Groot", + 'and_': '&&', + 'object': { 'a': 'Security risk' } + } + supported = { + '42': 42, + '9.5': 9.5, + "'bopbidibopbopbop'": 'bopbidibopbopbop', + 'true': True, + 'false': False, + 'null': None, + + # Math + '1 * (2 + 3 * (4 - 3))': 5, + '1 * (2 + 3 * (4 - 3)) > 10 - 2 || 3 * 2 > 9 - 2 * 3': True, + '(9 - 2) * 3 - 10': 11, + '12 - 2 * -2 + (3 - 4) * 3.1': 12.9, + '9 / 12 + 12 * 3 - 5': 31.75, + '9 / 12 + 12 * (3 - 5)': -23.25, + '12 > 13.1': False, + '12 < 14': True, + '12 <= 14': True, + '12 >= 14': False, + '12 == 14': False, + '12 % 5 > 3': False, + '12 != 14': True, + '9 - 1 > 10 && 3 * 5 > 10': False, + '9 - 1 > 10 || 3 * 5 > 10': True, + 'a1 > 0 || a1 < -12': True, + 'a1 > 0 && a1 < -12': False, + 'a1 + 1 > 0 && -a1 > -12': True, + '-(a1 + 1) < 0 || -(a1 + 2) > -12': True, + '-a1 * 2': -2, + '(9 - 2) * 3 - c10': 11, + '(9 - b2) * 3 - c10': 11, + 'c10 > b2': True, + + # String + "foo == 'bar'":True, + "foo != 'bar'":False, + 'foo == "bar" && 1 > 0':True, + '!!foo': True, + '!foo': False, + 'foo': 'bar', + '!(foo > "baa") || 1 > 2': False, + '!(foo > "baa") || 1 < 2': True, + 'empty == ""': True, + '1 == "1"': True, + '1.0 == "1"': True, + '1 == "aaa"': False, + "'I am ' + b2 + ' years'": 'I am 2 years', + "quote == 'Je s\\'apelle Groot'": True, + "lorem == 'Lorem ipsum dolor et si qua met!'": True, + "and_ == '&&'": True, + "warning == 'Warning! This sentence will fail!'": True, + + # Match + "match(lorem, '^Lorem [ia]psumE?')": bool, + "match(foo, '^Lorem [ia]psumE?')": None, + "match(lorem, '^Lorem [ia]psumE?') && 1 == 1": bool, + + # No code + "": False, + " ": False, + } + trigger_errors = { + "object.a": YunohostError, # Keep unsupported, for security reasons + 'a1 ** b2': YunohostError, # Keep unsupported, for security reasons + '().__class__.__bases__[0].__subclasses__()': YunohostError, # Very dangerous code + 'a1 > 11 ? 1 : 0': SyntaxError, + 'c10 > b2 == false': YunohostError, # JS and Python doesn't do the same thing for this situation + 'c10 > b2 == true': YunohostError, + } + + for expression, result in supported.items(): + if result == bool: + assert bool(evaluate_simple_js_expression(expression, context)), expression + else: + assert evaluate_simple_js_expression(expression, context) == result, expression + + for expression, error in trigger_errors.items(): + with pytest.raises(error): + evaluate_simple_js_expression(expression, context) + diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 2363545cf..0f18fad0d 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -24,6 +24,8 @@ 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 @@ -46,6 +48,138 @@ from yunohost.log import OperationLogger logger = getActionLogger("yunohost.config") CONFIG_PANEL_VERSION_SUPPORTED = 1.0 +# Those js-like evaluate functions are used to eval safely visible attributes +# The goal is to evaluate in the same way than js simple-evaluate +# https://github.com/shepherdwind/simple-evaluate +def evaluate_simple_ast(node, 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): # + values = node.values + 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: def __init__(self, config_path, save_path=None): @@ -466,11 +600,13 @@ class Question(object): hide_user_input_in_prompt = False pattern: Optional[Dict] = None - def __init__(self, question: Dict[str, Any]): + def __init__(self, question: Dict[str, Any], context: Dict[str, Any] = {}): self.name = question["name"] 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.context = context self.choices = question.get("choices", []) self.pattern = question.get("pattern", self.pattern) self.ask = question.get("ask", {"en": self.name}) @@ -512,6 +648,15 @@ class Question(object): ) 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 = None + return self.value + 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): @@ -577,7 +722,7 @@ class Question(object): # Prevent displaying a shitload of choices # (e.g. 100+ available users when choosing an app admin...) choices = ( - list(self.choices.values()) + list(self.choices.keys()) if isinstance(self.choices, dict) else self.choices ) @@ -710,8 +855,8 @@ class PasswordQuestion(Question): default_value = "" forbidden_chars = "{}" - def __init__(self, question): - super().__init__(question) + def __init__(self, question, context: Dict[str, Any] = {}): + super().__init__(question, context) self.redact = True if self.default is not None: raise YunohostValidationError( @@ -829,8 +974,8 @@ class BooleanQuestion(Question): choices="yes/no", ) - def __init__(self, question): - super().__init__(question) + def __init__(self, question, context: Dict[str, Any] = {}): + super().__init__(question, context) self.yes = question.get("yes", 1) self.no = question.get("no", 0) if self.default is None: @@ -850,10 +995,10 @@ class BooleanQuestion(Question): class DomainQuestion(Question): argument_type = "domain" - def __init__(self, question): + def __init__(self, question, context: Dict[str, Any] = {}): from yunohost.domain import domain_list, _get_maindomain - super().__init__(question) + super().__init__(question, context) if self.default is None: self.default = _get_maindomain() @@ -876,11 +1021,11 @@ class DomainQuestion(Question): class UserQuestion(Question): argument_type = "user" - def __init__(self, question): + def __init__(self, question, context: Dict[str, Any] = {}): from yunohost.user import user_list, user_info from yunohost.domain import _get_maindomain - super().__init__(question) + super().__init__(question, context) self.choices = list(user_list()["users"].keys()) if not self.choices: @@ -902,8 +1047,8 @@ class NumberQuestion(Question): argument_type = "number" default_value = None - def __init__(self, question): - super().__init__(question) + def __init__(self, question, context: Dict[str, Any] = {}): + super().__init__(question, context) self.min = question.get("min", None) self.max = question.get("max", None) self.step = question.get("step", None) @@ -954,8 +1099,8 @@ class DisplayTextQuestion(Question): argument_type = "display_text" readonly = True - def __init__(self, question): - super().__init__(question) + def __init__(self, question, context: Dict[str, Any] = {}): + super().__init__(question, context) self.optional = True self.style = question.get( @@ -989,8 +1134,8 @@ class FileQuestion(Question): if os.path.exists(upload_dir): shutil.rmtree(upload_dir) - def __init__(self, question): - super().__init__(question) + def __init__(self, question, context: Dict[str, Any] = {}): + super().__init__(question, context) self.accept = question.get("accept", "") def _prevalidate(self): @@ -1089,9 +1234,8 @@ def ask_questions_and_parse_answers( for question in questions: question_class = ARGUMENTS_TYPE_PARSERS[question.get("type", "string")] question["value"] = prefilled_answers.get(question["name"]) - question = question_class(question) - - question.ask_if_needed() + question = question_class(question, prefilled_answers) + prefilled_answers[question.name] = question.ask_if_needed() out.append(question) return out From eb8a59751ec112fc74526e53cb7c107be9b5228a Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Wed, 6 Oct 2021 00:57:04 +0000 Subject: [PATCH 110/142] [CI] Format code --- src/yunohost/app.py | 2 +- src/yunohost/tests/test_questions.py | 118 +++++++++++++-------------- src/yunohost/utils/config.py | 58 +++++++------ 3 files changed, 93 insertions(+), 85 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2b8d71abf..fe5281384 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -169,7 +169,7 @@ def app_info(app, full=False): ret["from_catalog"] = _load_apps_catalog()["apps"].get(absolute_app_name, {}) ret["upgradable"] = _app_upgradable(ret) - ret["is_webapp"] = ("domain" in settings and "path" in settings) + ret["is_webapp"] = "domain" in settings and "path" in settings ret["supports_change_url"] = os.path.exists( os.path.join(setting_path, "scripts", "change_url") diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index b39990b73..c21ff8c40 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -15,7 +15,7 @@ from yunohost.utils.config import ( PathQuestion, BooleanQuestion, FileQuestion, - evaluate_simple_js_expression + evaluate_simple_js_expression, ) from yunohost.utils.error import YunohostError, YunohostValidationError @@ -2095,97 +2095,95 @@ def test_normalize_path(): assert PathQuestion.normalize("macnuggets/") == "/macnuggets" assert PathQuestion.normalize("////macnuggets///") == "/macnuggets" + def test_simple_evaluate(): context = { - 'a1': 1, - 'b2': 2, - 'c10': 10, - 'foo': 'bar', - 'comp': '1>2', - 'empty': '', - 'lorem': 'Lorem ipsum dolor et si qua met!', - 'warning': 'Warning! This sentence will fail!', - 'quote': "Je s'apelle Groot", - 'and_': '&&', - 'object': { 'a': 'Security risk' } + "a1": 1, + "b2": 2, + "c10": 10, + "foo": "bar", + "comp": "1>2", + "empty": "", + "lorem": "Lorem ipsum dolor et si qua met!", + "warning": "Warning! This sentence will fail!", + "quote": "Je s'apelle Groot", + "and_": "&&", + "object": {"a": "Security risk"}, } supported = { - '42': 42, - '9.5': 9.5, - "'bopbidibopbopbop'": 'bopbidibopbopbop', - 'true': True, - 'false': False, - 'null': None, - + "42": 42, + "9.5": 9.5, + "'bopbidibopbopbop'": "bopbidibopbopbop", + "true": True, + "false": False, + "null": None, # Math - '1 * (2 + 3 * (4 - 3))': 5, - '1 * (2 + 3 * (4 - 3)) > 10 - 2 || 3 * 2 > 9 - 2 * 3': True, - '(9 - 2) * 3 - 10': 11, - '12 - 2 * -2 + (3 - 4) * 3.1': 12.9, - '9 / 12 + 12 * 3 - 5': 31.75, - '9 / 12 + 12 * (3 - 5)': -23.25, - '12 > 13.1': False, - '12 < 14': True, - '12 <= 14': True, - '12 >= 14': False, - '12 == 14': False, - '12 % 5 > 3': False, - '12 != 14': True, - '9 - 1 > 10 && 3 * 5 > 10': False, - '9 - 1 > 10 || 3 * 5 > 10': True, - 'a1 > 0 || a1 < -12': True, - 'a1 > 0 && a1 < -12': False, - 'a1 + 1 > 0 && -a1 > -12': True, - '-(a1 + 1) < 0 || -(a1 + 2) > -12': True, - '-a1 * 2': -2, - '(9 - 2) * 3 - c10': 11, - '(9 - b2) * 3 - c10': 11, - 'c10 > b2': True, - + "1 * (2 + 3 * (4 - 3))": 5, + "1 * (2 + 3 * (4 - 3)) > 10 - 2 || 3 * 2 > 9 - 2 * 3": True, + "(9 - 2) * 3 - 10": 11, + "12 - 2 * -2 + (3 - 4) * 3.1": 12.9, + "9 / 12 + 12 * 3 - 5": 31.75, + "9 / 12 + 12 * (3 - 5)": -23.25, + "12 > 13.1": False, + "12 < 14": True, + "12 <= 14": True, + "12 >= 14": False, + "12 == 14": False, + "12 % 5 > 3": False, + "12 != 14": True, + "9 - 1 > 10 && 3 * 5 > 10": False, + "9 - 1 > 10 || 3 * 5 > 10": True, + "a1 > 0 || a1 < -12": True, + "a1 > 0 && a1 < -12": False, + "a1 + 1 > 0 && -a1 > -12": True, + "-(a1 + 1) < 0 || -(a1 + 2) > -12": True, + "-a1 * 2": -2, + "(9 - 2) * 3 - c10": 11, + "(9 - b2) * 3 - c10": 11, + "c10 > b2": True, # String - "foo == 'bar'":True, - "foo != 'bar'":False, - 'foo == "bar" && 1 > 0':True, - '!!foo': True, - '!foo': False, - 'foo': 'bar', + "foo == 'bar'": True, + "foo != 'bar'": False, + 'foo == "bar" && 1 > 0': True, + "!!foo": True, + "!foo": False, + "foo": "bar", '!(foo > "baa") || 1 > 2': False, '!(foo > "baa") || 1 < 2': True, 'empty == ""': True, '1 == "1"': True, '1.0 == "1"': True, '1 == "aaa"': False, - "'I am ' + b2 + ' years'": 'I am 2 years', + "'I am ' + b2 + ' years'": "I am 2 years", "quote == 'Je s\\'apelle Groot'": True, "lorem == 'Lorem ipsum dolor et si qua met!'": True, "and_ == '&&'": True, "warning == 'Warning! This sentence will fail!'": True, - # Match "match(lorem, '^Lorem [ia]psumE?')": bool, "match(foo, '^Lorem [ia]psumE?')": None, "match(lorem, '^Lorem [ia]psumE?') && 1 == 1": bool, - # No code "": False, " ": False, } trigger_errors = { - "object.a": YunohostError, # Keep unsupported, for security reasons - 'a1 ** b2': YunohostError, # Keep unsupported, for security reasons - '().__class__.__bases__[0].__subclasses__()': YunohostError, # Very dangerous code - 'a1 > 11 ? 1 : 0': SyntaxError, - 'c10 > b2 == false': YunohostError, # JS and Python doesn't do the same thing for this situation - 'c10 > b2 == true': YunohostError, + "object.a": YunohostError, # Keep unsupported, for security reasons + "a1 ** b2": YunohostError, # Keep unsupported, for security reasons + "().__class__.__bases__[0].__subclasses__()": YunohostError, # Very dangerous code + "a1 > 11 ? 1 : 0": SyntaxError, + "c10 > b2 == false": YunohostError, # JS and Python doesn't do the same thing for this situation + "c10 > b2 == true": YunohostError, } for expression, result in supported.items(): if result == bool: assert bool(evaluate_simple_js_expression(expression, context)), expression else: - assert evaluate_simple_js_expression(expression, context) == result, expression + assert ( + evaluate_simple_js_expression(expression, context) == result + ), expression for expression, error in trigger_errors.items(): with pytest.raises(error): evaluate_simple_js_expression(expression, context) - diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 0f18fad0d..e38cfbb3a 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -55,24 +55,24 @@ def evaluate_simple_ast(node, 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.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 + ast.NotEq: op.ne, } - context['true'] = True - context['false'] = False - context['null'] = None + context["true"] = True + context["false"] = False + context["null"] = None # Variable - if isinstance(node, ast.Name): # Variable + if isinstance(node, ast.Name): # Variable return context[node.id] # Python <=3.7 String @@ -88,14 +88,16 @@ def evaluate_simple_ast(node, context={}): return node.value # + - * / % - elif isinstance(node, ast.BinOp) and type(node.op) in operators: # + 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 + 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 + elif type(left) != type(right): # support "111" - "1" -> 110 left = float(left) right = float(right) @@ -104,7 +106,9 @@ def evaluate_simple_ast(node, context={}): # 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: # + 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] @@ -116,11 +120,11 @@ def evaluate_simple_ast(node, context={}): return type(operator) == ast.NotEq try: return operators[type(operator)](left, right) - except TypeError: # support "e" > 1 -> False like in JS + except TypeError: # support "e" > 1 -> False like in JS return False # and / or - elif isinstance(node, ast.BoolOp): # + elif isinstance(node, ast.BoolOp): # values = node.values for value in node.values: value = evaluate_simple_ast(value, context) @@ -131,20 +135,22 @@ def evaluate_simple_ast(node, context={}): return isinstance(node.op, ast.And) # not / USub (it's negation number -\d) - elif isinstance(node, ast.UnaryOp): # e.g., -1 + 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': + 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] + 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) + raise YunohostError( + f"Unauthorize opcode '{opcode}' in visible attribute", raw_msg=True + ) + def js_to_python(expr): in_string = None @@ -163,7 +169,7 @@ def js_to_python(expr): # If we are not in a string, replace operators elif not in_string: - if char == "!" and expr[i +1] != "=": + if char == "!" and expr[i + 1] != "=": char = "not " elif char in "|&" and py_expr[-1:] == char: py_expr = py_expr[:-1] @@ -172,15 +178,17 @@ def js_to_python(expr): # Determine if next loop will be in escaped mode escaped = char == "\\" and not escaped py_expr += char - i+=1 + 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: def __init__(self, config_path, save_path=None): self.config_path = config_path @@ -649,7 +657,9 @@ class Question(object): def ask_if_needed(self): - if self.visible and not evaluate_simple_js_expression(self.visible, context=self.context): + 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 From 99d2637cfe55fdfe2c654710a3e331abf7d9a925 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 13:04:24 +0200 Subject: [PATCH 111/142] FileQuestion: self.value may not be an str --- src/yunohost/utils/config.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 0f18fad0d..e0fe1416b 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -1164,9 +1164,11 @@ class FileQuestion(Question): FileQuestion.upload_dirs += [upload_dir] logger.debug(f"Saving file {self.name} for file question into {file_path}") - if Moulinette.interface.type != "api" or ( - self.value.startswith("/") and os.path.exists(self.value) - ): + + 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) From 23bd32b3cdf435013ffc8fd4229ffa0c185daac0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 13:45:15 +0200 Subject: [PATCH 112/142] Fix linters --- src/yunohost/utils/config.py | 43 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index e0fe1416b..fbaecfbe0 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -121,7 +121,6 @@ def evaluate_simple_ast(node, context={}): # and / or elif isinstance(node, ast.BoolOp): # - values = node.values for value in node.values: value = evaluate_simple_ast(value, context) if isinstance(node.op, ast.And) and not value: @@ -600,7 +599,7 @@ class Question(object): hide_user_input_in_prompt = False pattern: Optional[Dict] = None - def __init__(self, question: Dict[str, Any], context: Dict[str, Any] = {}): + def __init__(self, question: Dict[str, Any], context: Mapping[str, Any] = {}): self.name = question["name"] self.type = question.get("type", "string") self.default = question.get("default", None) @@ -855,7 +854,7 @@ class PasswordQuestion(Question): default_value = "" forbidden_chars = "{}" - def __init__(self, question, context: Dict[str, Any] = {}): + def __init__(self, question, context: Mapping[str, Any] = {}): super().__init__(question, context) self.redact = True if self.default is not None: @@ -974,7 +973,7 @@ class BooleanQuestion(Question): choices="yes/no", ) - def __init__(self, question, context: Dict[str, Any] = {}): + def __init__(self, question, context: Mapping[str, Any] = {}): super().__init__(question, context) self.yes = question.get("yes", 1) self.no = question.get("no", 0) @@ -995,7 +994,7 @@ class BooleanQuestion(Question): class DomainQuestion(Question): argument_type = "domain" - def __init__(self, question, context: Dict[str, Any] = {}): + def __init__(self, question, context: Mapping[str, Any] = {}): from yunohost.domain import domain_list, _get_maindomain super().__init__(question, context) @@ -1021,7 +1020,7 @@ class DomainQuestion(Question): class UserQuestion(Question): argument_type = "user" - def __init__(self, question, context: Dict[str, Any] = {}): + def __init__(self, question, context: Mapping[str, Any] = {}): from yunohost.user import user_list, user_info from yunohost.domain import _get_maindomain @@ -1047,7 +1046,7 @@ class NumberQuestion(Question): argument_type = "number" default_value = None - def __init__(self, question, context: Dict[str, Any] = {}): + def __init__(self, question, context: Mapping[str, Any] = {}): super().__init__(question, context) self.min = question.get("min", None) self.max = question.get("max", None) @@ -1099,7 +1098,7 @@ class DisplayTextQuestion(Question): argument_type = "display_text" readonly = True - def __init__(self, question, context: Dict[str, Any] = {}): + def __init__(self, question, context: Mapping[str, Any] = {}): super().__init__(question, context) self.optional = True @@ -1134,7 +1133,7 @@ class FileQuestion(Question): if os.path.exists(upload_dir): shutil.rmtree(upload_dir) - def __init__(self, question, context: Dict[str, Any] = {}): + def __init__(self, question, context: Mapping[str, Any] = {}): super().__init__(question, context) self.accept = question.get("accept", "") @@ -1205,15 +1204,15 @@ ARGUMENTS_TYPE_PARSERS = { def ask_questions_and_parse_answers( - questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {} + raw_questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {} ) -> 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: - questions -- the arguments description store in yunohost - format from actions.json/toml, manifest.json/toml - or config_panel.json/toml + 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"} """ @@ -1224,20 +1223,22 @@ def ask_questions_and_parse_answers( # whereas parse.qs return list of values (which is useful for tags, etc) # For now, let's not migrate this piece of code to parse_qs # Because Aleks believes some bits of the app CI rely on overriding values (e.g. foo=foo&...&foo=bar) - prefilled_answers = dict( + answers = dict( urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True) ) + elif isinstance(prefilled_answers, Mapping): + answers = {**prefilled_answers} + else: + answers = {} - if not prefilled_answers: - prefilled_answers = {} out = [] - for question in questions: - question_class = ARGUMENTS_TYPE_PARSERS[question.get("type", "string")] - question["value"] = prefilled_answers.get(question["name"]) - question = question_class(question, prefilled_answers) - prefilled_answers[question.name] = question.ask_if_needed() + for raw_question in raw_questions: + question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")] + raw_question["value"] = answers.get(raw_question["name"]) + question = question_class(raw_question, context=answers) + answers[question.name] = question.ask_if_needed() out.append(question) return out From 344ed7252c996c7e907319853ba6decfabf90406 Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Wed, 6 Oct 2021 12:15:29 +0000 Subject: [PATCH 113/142] [CI] Format code --- src/yunohost/utils/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index 4ee944126..4ee62c6f7 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -1241,7 +1241,6 @@ def ask_questions_and_parse_answers( else: answers = {} - out = [] for raw_question in raw_questions: From 644cdd41d8aa3cd27915dccbe472084961b742cc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 15 Jun 2020 16:36:41 +0200 Subject: [PATCH 114/142] Allow to re-run ynh_install_app_dependencies multiple times --- data/helpers.d/apt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index c3439a583..aee022da7 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -210,6 +210,8 @@ ynh_package_install_from_equivs () { ynh_package_is_installed "$pkgname" } +YNH_INSTALL_APP_DEPENDENCIES_REPLACE="true" + # Define and install dependencies with a equivs control file # # This helper can/should only be called once per app @@ -248,6 +250,24 @@ ynh_install_app_dependencies () { dependencies="$(echo "$dependencies" | sed 's/\([^(\<=\>]\)\([\<=\>]\+\)\([^,]\+\)/\1 (\2 \3)/g')" fi + # The first time we run ynh_install_app_dependencies, we will replace the + # entire control file (This is in particular meant to cover the case of + # upgrade script where ynh_install_app_dependencies is called with this + # expected effect) Otherwise, any subsequent call will add dependencies + # to those already present in the equivs control file. + if [[ $YNH_INSTALL_APP_DEPENDENCIES_REPLACE == "true" ]] + then + YNH_INSTALL_APP_DEPENDENCIES_REPLACE="false" + else + local current_dependencies="" + if ynh_package_is_installed --package="${dep_app}-ynh-deps" + then + current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${dep_app}-ynh-deps) " + fi + current_dependencies=${current_dependencies// | /|} + dependencies="$current_dependencies $dependencies" + fi + # # Epic ugly hack to fix the goddamn dependency nightmare of sury # Sponsored by the "Djeezusse Fokin Kraiste Why Do Adminsys Has To Be So Fucking Complicated I Should Go Grow Potatoes Instead Of This Shit" collective @@ -284,6 +304,9 @@ EOF ynh_app_setting_set --app=$app --key=apt_dependencies --value="$dependencies" } + + + # Add dependencies to install with ynh_install_app_dependencies # # usage: ynh_add_app_dependencies --package=phpversion [--replace] From 76aaaab74e80a827fbbb2474676b110e04fb6ad5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 15 Jun 2020 16:47:51 +0200 Subject: [PATCH 115/142] Factorize sury repo configuration into ynh_add_sury --- data/helpers.d/apt | 19 ++++++++++++++++++- data/helpers.d/php | 3 +-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index aee022da7..8ba5bbe3e 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -283,7 +283,7 @@ ynh_install_app_dependencies () { if ! grep --recursive --quiet "^ *deb.*sury" /etc/apt/sources.list* then # Re-add sury - ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 + ynh_add_sury fi fi fi @@ -304,7 +304,24 @@ EOF ynh_app_setting_set --app=$app --key=apt_dependencies --value="$dependencies" } +# Add sury repository with adequate pin strategy +# +# [internal] +# +# usage: ynh_add_sury +# +ynh_add_sury() { + # Add an extra repository for those packages + ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 + + # Pin this extra repository after packages are installed to prevent sury of doing shit + for package_to_not_upgrade in "php" "php-fpm" "php-mysql" "php-xml" "php-zip" "php-mbstring" "php-ldap" "php-gd" "php-curl" "php-bz2" "php-json" "php-sqlite3" "php-intl" "openssl" "libssl1.1" "libssl-dev" + do + ynh_pin_repo --package="$package_to_not_upgrade" --pin="origin \"packages.sury.org\"" --priority="-1" --name=extra_php_version --append + done + +} # Add dependencies to install with ynh_install_app_dependencies diff --git a/data/helpers.d/php b/data/helpers.d/php index 7c91d89d2..6d47fdc13 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -348,8 +348,7 @@ ynh_install_php () { echo "$YNH_APP_INSTANCE_NAME:$phpversion" | tee --append "/etc/php/ynh_app_version" fi - # Add an extra repository for those packages - ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 + ynh_add_sury # Install requested dependencies from this extra repository. # Install PHP-FPM first, otherwise PHP will install apache as a dependency. From 040be532ad759b952c00a898136f5975d2e285ed Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 15 Jun 2020 17:39:16 +0200 Subject: [PATCH 116/142] During ynh_install_app_dependencies, if the dependency list contains specific php version stuff, add sury and other tweaks --- data/helpers.d/apt | 57 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 8ba5bbe3e..0eedfd601 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -250,6 +250,25 @@ ynh_install_app_dependencies () { dependencies="$(echo "$dependencies" | sed 's/\([^(\<=\>]\)\([\<=\>]\+\)\([^,]\+\)/\1 (\2 \3)/g')" fi + # Check for specific php dependencies which requires sury + # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni" + local specific_php_version=$(echo $dependencies | tr '-' ' ' | grep -o -E "\" | sed 's/php//g' | sort | uniq) + + # Ignore case where the php version found is the one available in debian vanilla + [[ "$specific_php_version" != "$YNH_DEFAULT_PHP_VERSION" ]] || specific_php_version="" + + if [[ -n "$specific_php_version" ]] + then + # Cover a small edge case where a packager could have specified "php7.4-pwet php5-gni" which is confusing + [[ $(echo $specific_php_version | wc -l) -eq 1 ]] \ + || ynh_die --message="Inconsistent php versions in dependencies ... found : $specific_php_version" + + dependencies+=", php${specific_php_version}, php${specific_php_version}-fpm, php${specific_php_version}-common" + + ynh_add_sury + fi + + # The first time we run ynh_install_app_dependencies, we will replace the # entire control file (This is in particular meant to cover the case of # upgrade script where ynh_install_app_dependencies is called with this @@ -263,9 +282,9 @@ ynh_install_app_dependencies () { if ynh_package_is_installed --package="${dep_app}-ynh-deps" then current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${dep_app}-ynh-deps) " + current_dependencies=${current_dependencies// | /|} fi - current_dependencies=${current_dependencies// | /|} - dependencies="$current_dependencies $dependencies" + dependencies="$current_dependencies, $dependencies" fi # @@ -301,7 +320,20 @@ EOF ynh_package_install_from_equivs /tmp/${dep_app}-ynh-deps.control \ || ynh_die --message="Unable to install dependencies" # Install the fake package and its dependencies rm /tmp/${dep_app}-ynh-deps.control + ynh_app_setting_set --app=$app --key=apt_dependencies --value="$dependencies" + + if [[ -n "$specific_php_version" ]] + then + # Set the default php version back as the default version for php-cli. + update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION + + # Store phpversion into the config of this app + ynh_app_setting_set $app phpversion $specific_php_version + + # Integrate new php-fpm service in yunohost + yunohost service add php${specific_php_version}-fpm --log "/var/log/php${phpversion}-fpm.log" + fi } # Add sury repository with adequate pin strategy @@ -315,7 +347,7 @@ ynh_add_sury() { # Add an extra repository for those packages ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 - # Pin this extra repository after packages are installed to prevent sury of doing shit + # Pin this extra repository after packages are installed to prevent sury from doing shit for package_to_not_upgrade in "php" "php-fpm" "php-mysql" "php-xml" "php-zip" "php-mbstring" "php-ldap" "php-gd" "php-curl" "php-bz2" "php-json" "php-sqlite3" "php-intl" "openssl" "libssl1.1" "libssl-dev" do ynh_pin_repo --package="$package_to_not_upgrade" --pin="origin \"packages.sury.org\"" --priority="-1" --name=extra_php_version --append @@ -365,7 +397,26 @@ ynh_add_app_dependencies () { # Requires YunoHost version 2.6.4 or higher. ynh_remove_app_dependencies () { local dep_app=${app//_/-} # Replace all '_' by '-' + + local current_dependencies="" + if ynh_package_is_installed --package="${dep_app}-ynh-deps" + then + current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${dep_app}-ynh-deps) " + current_dependencies=${current_dependencies// | /|} + fi + ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. + + # Check if this app used a specific php version ... in which case we check + # if the corresponding php-fpm is still there. Otherwise, we remove the + # service from yunohost as well + + local specific_php_version=$(echo $dependencies | tr '-' ' ' | grep -o -E "\" | sed 's/php//g' | sort | uniq) + [[ "$specific_php_version" != "$YNH_DEFAULT_PHP_VERSION" ]] || specific_php_version="" + if [[ -n "$specific_php_version" ]] && ! ynh_package_is_installed --package="php${specific_php_version}-fpm"; + then + yunohost service remove php${specific_php_version}-fpm + fi } # Install packages from an extra repository properly. From 76b60890c6f772f6a8e1aa105899f24877853dd3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 15 Jun 2020 18:23:32 +0200 Subject: [PATCH 117/142] Propagate changes on other apt/php helpers... --- data/helpers.d/apt | 19 ++------------- data/helpers.d/php | 59 ++++------------------------------------------ 2 files changed, 6 insertions(+), 72 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 0eedfd601..f662c58e4 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -360,7 +360,6 @@ ynh_add_sury() { # # usage: ynh_add_app_dependencies --package=phpversion [--replace] # | arg: -p, --package= - Packages to add as dependencies for the app. -# | arg: -r, --replace - Replace dependencies instead of adding to existing ones. # # Requires YunoHost version 3.8.1 or higher. ynh_add_app_dependencies () { @@ -368,24 +367,10 @@ ynh_add_app_dependencies () { local legacy_args=pr local -A args_array=( [p]=package= [r]=replace) local package - local replace # Manage arguments with getopts ynh_handle_getopts_args "$@" - replace=${replace:-0} - local current_dependencies="" - if [ $replace -eq 0 ] - then - local dep_app=${app//_/-} # Replace all '_' by '-' - if ynh_package_is_installed --package="${dep_app}-ynh-deps" - then - current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${dep_app}-ynh-deps) " - fi - - current_dependencies=${current_dependencies// | /|} - fi - - ynh_install_app_dependencies "${current_dependencies}${package}" + ynh_install_app_dependencies "${package}" } # Remove fake package and its dependencies @@ -450,7 +435,7 @@ ynh_install_extra_app_dependencies () { ynh_install_extra_repo --repo="$repo" $key --priority=995 --name=$name # Install requested dependencies from this extra repository. - ynh_add_app_dependencies --package="$package" + ynh_install_app_dependencies --package="$package" # Remove this extra repository after packages are installed ynh_remove_extra_repo --name=$app diff --git a/data/helpers.d/php b/data/helpers.d/php index 6d47fdc13..2191b0d22 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -111,7 +111,7 @@ ynh_add_fpm_config () { elif [ -n "$package" ] then # Install the additionnal packages from the default repository - ynh_add_app_dependencies --package="$package" + ynh_install_app_dependencies "$package" fi if [ $dedicated_service -eq 1 ] @@ -330,36 +330,13 @@ ynh_install_php () { ynh_handle_getopts_args "$@" package=${package:-} - # Store phpversion into the config of this app - ynh_app_setting_set $app phpversion $phpversion - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] then ynh_die --message="Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION" fi - # Create the file if doesn't exist already - touch /etc/php/ynh_app_version - - # Do not add twice the same line - if ! grep --quiet "$YNH_APP_INSTANCE_NAME:" "/etc/php/ynh_app_version" - then - # Store the ID of this app and the version of PHP requested for it - echo "$YNH_APP_INSTANCE_NAME:$phpversion" | tee --append "/etc/php/ynh_app_version" - fi - - ynh_add_sury - - # Install requested dependencies from this extra repository. - # Install PHP-FPM first, otherwise PHP will install apache as a dependency. - ynh_add_app_dependencies --package="php${phpversion}-fpm" - ynh_add_app_dependencies --package="php$phpversion php${phpversion}-common $package" - - # Set the default PHP version back as the default version for php-cli. - update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION - - # Advertise service in admin panel - yunohost service add php${phpversion}-fpm --log "/var/log/php${phpversion}-fpm.log" + ynh_install_app_dependencies "$package" + ynh_app_setting_set $app phpversion $phpversion } # Remove the specific version of PHP used by the app. @@ -370,35 +347,7 @@ ynh_install_php () { # # Requires YunoHost version 3.8.1 or higher. ynh_remove_php () { - # Get the version of PHP used by this app - local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) - - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] || [ -z "$phpversion" ] - then - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] - then - ynh_print_err "Do not use ynh_remove_php to remove php$YNH_DEFAULT_PHP_VERSION !" - fi - return 0 - fi - - # Create the file if doesn't exist already - touch /etc/php/ynh_app_version - - # Remove the line for this app - sed --in-place "/$YNH_APP_INSTANCE_NAME:$phpversion/d" "/etc/php/ynh_app_version" - - # If no other app uses this version of PHP, remove it. - if ! grep --quiet "$phpversion" "/etc/php/ynh_app_version" - then - # Remove the service from the admin panel - if ynh_package_is_installed --package="php${phpversion}-fpm"; then - yunohost service remove php${phpversion}-fpm - fi - - # Purge PHP dependencies for this version. - ynh_package_autopurge "php$phpversion php${phpversion}-fpm php${phpversion}-common" - fi + ynh_remove_app_dependencies } # Define the values to configure PHP-FPM From e07e1a95f4e223808d15fed8098d67f27de01653 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 16 Jun 2020 21:31:24 +0200 Subject: [PATCH 118/142] Apply suggestions from code review Co-authored-by: Kayou --- data/helpers.d/apt | 4 ++-- data/helpers.d/php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index f662c58e4..8b284d4fc 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -329,7 +329,7 @@ EOF update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION # Store phpversion into the config of this app - ynh_app_setting_set $app phpversion $specific_php_version + ynh_app_setting_set --app=$app --key=phpversion --value=$specific_php_version # Integrate new php-fpm service in yunohost yunohost service add php${specific_php_version}-fpm --log "/var/log/php${phpversion}-fpm.log" @@ -435,7 +435,7 @@ ynh_install_extra_app_dependencies () { ynh_install_extra_repo --repo="$repo" $key --priority=995 --name=$name # Install requested dependencies from this extra repository. - ynh_install_app_dependencies --package="$package" + ynh_install_app_dependencies "$package" # Remove this extra repository after packages are installed ynh_remove_extra_repo --name=$app diff --git a/data/helpers.d/php b/data/helpers.d/php index 2191b0d22..d383c1e4f 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -336,7 +336,7 @@ ynh_install_php () { fi ynh_install_app_dependencies "$package" - ynh_app_setting_set $app phpversion $phpversion + ynh_app_setting_set --app=$app --key=phpversion --value=$specific_php_version } # Remove the specific version of PHP used by the app. From 5054397a5a73d13eb3e50a8f52b01c693d3c871d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 15:11:09 +0200 Subject: [PATCH 119/142] helpers: Add deprecation warning to ynh_add_app_dependencies --- data/helpers.d/apt | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 8b284d4fc..b182edc6c 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -370,6 +370,7 @@ ynh_add_app_dependencies () { # Manage arguments with getopts ynh_handle_getopts_args "$@" + ynh_print_warn --message="Packagers: ynh_add_app_dependencies is deprecated and is now only an alias to ynh_install_app_dependencies" ynh_install_app_dependencies "${package}" } From 9ee631c1c451f1befe59fa3c5abeed325a4d1b5d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 15:16:18 +0200 Subject: [PATCH 120/142] helpers: Typo in ynh_remove_app_dependencies --- data/helpers.d/apt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index b182edc6c..6f1db90f8 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -397,7 +397,7 @@ ynh_remove_app_dependencies () { # if the corresponding php-fpm is still there. Otherwise, we remove the # service from yunohost as well - local specific_php_version=$(echo $dependencies | tr '-' ' ' | grep -o -E "\" | sed 's/php//g' | sort | uniq) + local specific_php_version=$(echo $current_dependencies | tr '-' ' ' | grep -o -E "\" | sed 's/php//g' | sort | uniq) [[ "$specific_php_version" != "$YNH_DEFAULT_PHP_VERSION" ]] || specific_php_version="" if [[ -n "$specific_php_version" ]] && ! ynh_package_is_installed --package="php${specific_php_version}-fpm"; then From 13d012bb4fe8ef05a1c6c8cfdab87a88b7b6cc87 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 15:34:26 +0200 Subject: [PATCH 121/142] helpers apt: save phpversion in settings even when using php default version --- data/helpers.d/apt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 6f1db90f8..235cc0067 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -293,7 +293,7 @@ ynh_install_app_dependencies () { # https://github.com/YunoHost/issues/issues/1407 # # If we require to install php dependency - if echo $dependencies | grep --quiet 'php' + if grep --quiet 'php' <<< "$dependencies" then # And we have packages from sury installed (7.0.33-10+weirdshiftafter instead of 7.0.33-0 on debian) if dpkg --list | grep "php7.0" | grep --quiet --invert-match "7.0.33-0+deb9" @@ -333,6 +333,9 @@ EOF # Integrate new php-fpm service in yunohost yunohost service add php${specific_php_version}-fpm --log "/var/log/php${phpversion}-fpm.log" + elif grep --quiet 'php' <<< "$dependencies" + # Store phpversion into the config of this app + ynh_app_setting_set --app=$app --key=phpversion --value=$YNH_DEFAULT_PHP_VERSION fi } From a98552ef0e3a39b0870414bad574b10cfe55da59 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 15:55:52 +0200 Subject: [PATCH 122/142] helpers: Fix weird 0 syntax which shfmt ain't happy with (dangling 0) --- data/helpers.d/logging | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/logging b/data/helpers.d/logging index 71998763e..d11fc578f 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -249,7 +249,7 @@ ynh_script_progression () { local weight_valuesB="$(grep --perl-regexp "^[^#]*ynh_script_progression.*-w " $0 | sed 's/.*-w[= ]\([[:digit:]]*\).*/\1/g')" # Each value will be on a different line. # Remove each 'end of line' and replace it by a '+' to sum the values. - local weight_values=$(( $(echo "$weight_valuesA" | tr '\n' '+') + $(echo "$weight_valuesB" | tr '\n' '+') 0 )) + local weight_values=$(( $(echo "$weight_valuesA" "$weight_valuesB" | grep -v -E '^\s*$' | tr '\n' '+' | sed 's/+$/+0/g') )) # max_progression is a total number of calls to this helper. # Less the number of calls with a weight value. From 93cc413f4aa22dde6f50b2dd3c4e5a8207ede442 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 16:01:53 +0200 Subject: [PATCH 123/142] helpers: lint/reformat with shfmt -bn -i 4 -w $FILE --- data/helpers.d/apt | 148 ++++++++++++------------- data/helpers.d/backup | 143 ++++++++++-------------- data/helpers.d/config | 172 +++++++++++------------------ data/helpers.d/fail2ban | 18 ++- data/helpers.d/getopts | 69 +++++------- data/helpers.d/hardware | 36 +++--- data/helpers.d/logging | 83 +++++++------- data/helpers.d/logrotate | 34 +++--- data/helpers.d/multimedia | 41 ++++--- data/helpers.d/mysql | 37 +++---- data/helpers.d/network | 38 +++---- data/helpers.d/nginx | 8 +- data/helpers.d/nodejs | 42 ++++--- data/helpers.d/permission | 95 ++++++---------- data/helpers.d/php | 177 ++++++++++++------------------ data/helpers.d/postgresql | 23 ++-- data/helpers.d/setting | 17 ++- data/helpers.d/string | 28 ++--- data/helpers.d/systemd | 55 ++++------ data/helpers.d/user | 37 +++---- data/helpers.d/utils | 223 ++++++++++++++++---------------------- 21 files changed, 627 insertions(+), 897 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index c3439a583..e77fb8a5c 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -12,31 +12,27 @@ ynh_wait_dpkg_free() { local try set +o xtrace # set +x # With seq 1 17, timeout will be almost 30 minutes - for try in `seq 1 17` - do + for try in $(seq 1 17); do # Check if /var/lib/dpkg/lock is used by another process - if lsof /var/lib/dpkg/lock > /dev/null - then + if lsof /var/lib/dpkg/lock >/dev/null; then echo "apt is already in use..." # Sleep an exponential time at each round - sleep $(( try * try )) + sleep $((try * try)) else # Check if dpkg hasn't been interrupted and is fully available. # See this for more information: https://sources.debian.org/src/apt/1.4.9/apt-pkg/deb/debsystem.cc/#L141-L174 local dpkg_dir="/var/lib/dpkg/updates/" # For each file in $dpkg_dir - while read dpkg_file <&9 - do + while read dpkg_file <&9; do # Check if the name of this file contains only numbers. - if echo "$dpkg_file" | grep --perl-regexp --quiet "^[[:digit:]]+$" - then + if echo "$dpkg_file" | grep --perl-regexp --quiet "^[[:digit:]]+$"; then # If so, that a remaining of dpkg. ynh_print_err "dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem." set -o xtrace # set -x return 1 fi - done 9<<< "$(ls -1 $dpkg_dir)" + done 9<<<"$(ls -1 $dpkg_dir)" set -o xtrace # set -x return 0 fi @@ -57,7 +53,7 @@ ynh_wait_dpkg_free() { ynh_package_is_installed() { # Declare an array to define the options of this helper. local legacy_args=p - local -A args_array=( [p]=package= ) + local -A args_array=([p]=package=) local package # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -79,13 +75,12 @@ ynh_package_is_installed() { ynh_package_version() { # Declare an array to define the options of this helper. local legacy_args=p - local -A args_array=( [p]=package= ) + local -A args_array=([p]=package=) local package # Manage arguments with getopts ynh_handle_getopts_args "$@" - if ynh_package_is_installed "$package" - then + if ynh_package_is_installed "$package"; then dpkg-query --show --showformat='${Version}' "$package" 2>/dev/null else echo '' @@ -166,14 +161,14 @@ ynh_package_autopurge() { # | arg: controlfile - path of the equivs control file # # Requires YunoHost version 2.2.4 or higher. -ynh_package_install_from_equivs () { +ynh_package_install_from_equivs() { local controlfile=$1 # retrieve package information - local pkgname=$(grep '^Package: ' $controlfile | cut --delimiter=' ' --fields=2) # Retrieve the name of the debian package - local pkgversion=$(grep '^Version: ' $controlfile | cut --delimiter=' ' --fields=2) # And its version number + local pkgname=$(grep '^Package: ' $controlfile | cut --delimiter=' ' --fields=2) # Retrieve the name of the debian package + local pkgversion=$(grep '^Version: ' $controlfile | cut --delimiter=' ' --fields=2) # And its version number [[ -z "$pkgname" || -z "$pkgversion" ]] \ - && ynh_die --message="Invalid control file" # Check if this 2 variables aren't empty. + && ynh_die --message="Invalid control file" # Check if this 2 variables aren't empty. # Update packages cache ynh_package_update @@ -182,7 +177,7 @@ ynh_package_install_from_equivs () { local TMPDIR=$(mktemp --directory) # Force the compatibility level at 10, levels below are deprecated - echo 10 > /usr/share/equivs/template/debian/compat + echo 10 >/usr/share/equivs/template/debian/compat # Note that the cd executes into a sub shell # Create a fake deb package with equivs-build and the given control file @@ -190,21 +185,24 @@ ynh_package_install_from_equivs () { # Install missing dependencies with ynh_package_install ynh_wait_dpkg_free cp "$controlfile" "${TMPDIR}/control" - (cd "$TMPDIR" - LC_ALL=C equivs-build ./control 1> /dev/null - LC_ALL=C dpkg --force-depends --install "./${pkgname}_${pkgversion}_all.deb" 2>&1 | tee ./dpkg_log) + ( + cd "$TMPDIR" + LC_ALL=C equivs-build ./control 1>/dev/null + LC_ALL=C dpkg --force-depends --install "./${pkgname}_${pkgversion}_all.deb" 2>&1 | tee ./dpkg_log + ) - ynh_package_install --fix-broken || \ - { # If the installation failed - # (the following is ran inside { } to not start a subshell otherwise ynh_die wouldnt exit the original process) - # Parse the list of problematic dependencies from dpkg's log ... - # (relevant lines look like: "foo-ynh-deps depends on bar; however:") - local problematic_dependencies="$(cat $TMPDIR/dpkg_log | grep -oP '(?<=-ynh-deps depends on ).*(?=; however)' | tr '\n' ' ')" - # Fake an install of those dependencies to see the errors - # The sed command here is, Print only from 'Reading state info' to the end. - [[ -n "$problematic_dependencies" ]] && ynh_package_install $problematic_dependencies --dry-run 2>&1 | sed --quiet '/Reading state info/,$p' | grep -v "fix-broken\|Reading state info" >&2 - ynh_die --message="Unable to install dependencies"; } - [[ -n "$TMPDIR" ]] && rm --recursive --force $TMPDIR # Remove the temp dir. + ynh_package_install --fix-broken \ + || { # If the installation failed + # (the following is ran inside { } to not start a subshell otherwise ynh_die wouldnt exit the original process) + # Parse the list of problematic dependencies from dpkg's log ... + # (relevant lines look like: "foo-ynh-deps depends on bar; however:") + local problematic_dependencies="$(cat $TMPDIR/dpkg_log | grep -oP '(?<=-ynh-deps depends on ).*(?=; however)' | tr '\n' ' ')" + # Fake an install of those dependencies to see the errors + # The sed command here is, Print only from 'Reading state info' to the end. + [[ -n "$problematic_dependencies" ]] && ynh_package_install $problematic_dependencies --dry-run 2>&1 | sed --quiet '/Reading state info/,$p' | grep -v "fix-broken\|Reading state info" >&2 + ynh_die --message="Unable to install dependencies" + } + [[ -n "$TMPDIR" ]] && rm --recursive --force $TMPDIR # Remove the temp dir. # check if the package is actually installed ynh_package_is_installed "$pkgname" @@ -221,7 +219,7 @@ ynh_package_install_from_equivs () { # | arg: "dep1|dep2|…" - You can specify alternatives. It will require to install (dep1 or dep2, etc). # # Requires YunoHost version 2.6.4 or higher. -ynh_install_app_dependencies () { +ynh_install_app_dependencies() { local dependencies=$@ # Add a comma for each space between packages. But not add a comma if the space separate a version specification. (See below) dependencies="$(echo "$dependencies" | sed 's/\([^\<=\>]\)\ \([^(]\)/\1, \2/g')" @@ -232,11 +230,10 @@ ynh_install_app_dependencies () { if [ -z "${version}" ] || [ "$version" == "null" ]; then version="1.0" fi - local dep_app=${app//_/-} # Replace all '_' by '-' + local dep_app=${app//_/-} # Replace all '_' by '-' # Handle specific versions - if [[ "$dependencies" =~ [\<=\>] ]] - then + if [[ "$dependencies" =~ [\<=\>] ]]; then # Replace version specifications by relationships syntax # https://www.debian.org/doc/debian-policy/ch-relationships.html # Sed clarification @@ -254,21 +251,18 @@ ynh_install_app_dependencies () { # https://github.com/YunoHost/issues/issues/1407 # # If we require to install php dependency - if echo $dependencies | grep --quiet 'php' - then + if echo $dependencies | grep --quiet 'php'; then # And we have packages from sury installed (7.0.33-10+weirdshiftafter instead of 7.0.33-0 on debian) - if dpkg --list | grep "php7.0" | grep --quiet --invert-match "7.0.33-0+deb9" - then + if dpkg --list | grep "php7.0" | grep --quiet --invert-match "7.0.33-0+deb9"; then # And sury ain't already in sources.lists - if ! grep --recursive --quiet "^ *deb.*sury" /etc/apt/sources.list* - then + if ! grep --recursive --quiet "^ *deb.*sury" /etc/apt/sources.list*; then # Re-add sury ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 fi fi fi - cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build + cat >/tmp/${dep_app}-ynh-deps.control < /dev/null + wget --timeout 900 --quiet "$key" --output-document=- | gpg --dearmor | $wget_append /etc/apt/trusted.gpg.d/$name.gpg >/dev/null fi # Update the list of package with the new repo @@ -449,10 +437,10 @@ ynh_install_extra_repo () { # | arg: -n, --name= - Name for the files for this repo, $app as default value. # # Requires YunoHost version 3.8.1 or higher. -ynh_remove_extra_repo () { +ynh_remove_extra_repo() { # Declare an array to define the options of this helper. local legacy_args=n - local -A args_array=( [n]=name= ) + local -A args_array=([n]=name=) local name # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -461,8 +449,8 @@ ynh_remove_extra_repo () { ynh_secure_remove --file="/etc/apt/sources.list.d/$name.list" # Sury pinning is managed by the regenconf in the core... [[ "$name" == "extra_php_version" ]] || ynh_secure_remove "/etc/apt/preferences.d/$name" - ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.gpg" > /dev/null - ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.asc" > /dev/null + ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.gpg" >/dev/null + ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.asc" >/dev/null # Update the list of package to exclude the old repo ynh_package_update @@ -484,10 +472,10 @@ ynh_remove_extra_repo () { # ynh_add_repo --uri=http://forge.yunohost.org/debian/ --suite=stretch --component=stable # # Requires YunoHost version 3.8.1 or higher. -ynh_add_repo () { +ynh_add_repo() { # Declare an array to define the options of this helper. local legacy_args=uscna - local -A args_array=( [u]=uri= [s]=suite= [c]=component= [n]=name= [a]=append ) + local -A args_array=([u]=uri= [s]=suite= [c]=component= [n]=name= [a]=append) local uri local suite local component @@ -498,8 +486,7 @@ ynh_add_repo () { name="${name:-$app}" append=${append:-0} - if [ $append -eq 1 ] - then + if [ $append -eq 1 ]; then append="tee --append" else append="tee" @@ -525,10 +512,10 @@ ynh_add_repo () { # See https://manpages.debian.org/stretch/apt/apt_preferences.5.en.html#How_APT_Interprets_Priorities for information about pinning. # # Requires YunoHost version 3.8.1 or higher. -ynh_pin_repo () { +ynh_pin_repo() { # Declare an array to define the options of this helper. local legacy_args=pirna - local -A args_array=( [p]=package= [i]=pin= [r]=priority= [n]=name= [a]=append ) + local -A args_array=([p]=package= [i]=pin= [r]=priority= [n]=name= [a]=append) local package local pin local priority @@ -541,8 +528,7 @@ ynh_pin_repo () { name="${name:-$app}" append=${append:-0} - if [ $append -eq 1 ] - then + if [ $append -eq 1 ]; then append="tee --append" else append="tee" @@ -556,5 +542,5 @@ ynh_pin_repo () { Pin: $pin Pin-Priority: $priority " \ - | $append "/etc/apt/preferences.d/$name" + | $append "/etc/apt/preferences.d/$name" } diff --git a/data/helpers.d/backup b/data/helpers.d/backup index 21ca2d7f0..27ffa015c 100644 --- a/data/helpers.d/backup +++ b/data/helpers.d/backup @@ -67,7 +67,7 @@ ynh_backup() { # Declare an array to define the options of this helper. local legacy_args=sdbm - local -A args_array=( [s]=src_path= [d]=dest_path= [b]=is_big [m]=not_mandatory ) + local -A args_array=([s]=src_path= [d]=dest_path= [b]=is_big [m]=not_mandatory) local src_path local dest_path local is_big @@ -83,10 +83,8 @@ ynh_backup() { # If backing up core only (used by ynh_backup_before_upgrade), # don't backup big data items - if [ $is_big -eq 1 ] && ( [ ${do_not_backup_data:-0} -eq 1 ] || [ $BACKUP_CORE_ONLY -eq 1 ] ) - then - if [ $BACKUP_CORE_ONLY -eq 1 ] - then + if [ $is_big -eq 1 ] && ([ ${do_not_backup_data:-0} -eq 1 ] || [ $BACKUP_CORE_ONLY -eq 1 ]); then + if [ $BACKUP_CORE_ONLY -eq 1 ]; then ynh_print_info --message="$src_path will not be saved, because 'BACKUP_CORE_ONLY' is set." else ynh_print_info --message="$src_path will not be saved, because 'do_not_backup_data' is set." @@ -98,14 +96,11 @@ ynh_backup() { # Format correctly source and destination paths # ============================================================================== # Be sure the source path is not empty - if [ ! -e "$src_path" ] - then + if [ ! -e "$src_path" ]; then ynh_print_warn --message="Source path '${src_path}' does not exist" - if [ "$not_mandatory" == "0" ] - then + if [ "$not_mandatory" == "0" ]; then # This is a temporary fix for fail2ban config files missing after the migration to stretch. - if echo "${src_path}" | grep --quiet "/etc/fail2ban" - then + if echo "${src_path}" | grep --quiet "/etc/fail2ban"; then touch "${src_path}" ynh_print_info --message="The missing file will be replaced by a dummy one for the backup !!!" else @@ -123,13 +118,11 @@ ynh_backup() { # If there is no destination path, initialize it with the source path # relative to "/". # eg: src_path=/etc/yunohost -> dest_path=etc/yunohost - if [[ -z "$dest_path" ]] - then + if [[ -z "$dest_path" ]]; then dest_path="${src_path#/}" else - if [[ "${dest_path:0:1}" == "/" ]] - then + if [[ "${dest_path:0:1}" == "/" ]]; then # If the destination path is an absolute path, transform it as a path # relative to the current working directory ($YNH_CWD) @@ -153,8 +146,7 @@ ynh_backup() { fi # Check if dest_path already exists in tmp archive - if [[ -e "${dest_path}" ]] - then + if [[ -e "${dest_path}" ]]; then ynh_print_err --message="Destination path '${dest_path}' already exist" return 1 fi @@ -171,7 +163,7 @@ ynh_backup() { # ============================================================================== local src=$(echo "${src_path}" | sed --regexp-extended 's/"/\"\"/g') local dest=$(echo "${dest_path}" | sed --regexp-extended 's/"/\"\"/g') - echo "\"${src}\",\"${dest}\"" >> "${YNH_BACKUP_CSV}" + echo "\"${src}\",\"${dest}\"" >>"${YNH_BACKUP_CSV}" # ============================================================================== @@ -185,19 +177,18 @@ ynh_backup() { # usage: ynh_restore # # Requires YunoHost version 2.6.4 or higher. -ynh_restore () { +ynh_restore() { # Deduce the relative path of $YNH_CWD local REL_DIR="${YNH_CWD#$YNH_BACKUP_DIR/}" REL_DIR="${REL_DIR%/}/" # For each destination path begining by $REL_DIR - cat ${YNH_BACKUP_CSV} | tr --delete $'\r' | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR.*\"$" | \ - while read line - do - local ORIGIN_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\"\K.*(?=\",\".*\"$)") - local ARCHIVE_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR\K.*(?=\"$)") - ynh_restore_file --origin_path="$ARCHIVE_PATH" --dest_path="$ORIGIN_PATH" - done + cat ${YNH_BACKUP_CSV} | tr --delete $'\r' | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR.*\"$" \ + | while read line; do + local ORIGIN_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\"\K.*(?=\",\".*\"$)") + local ARCHIVE_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR\K.*(?=\"$)") + ynh_restore_file --origin_path="$ARCHIVE_PATH" --dest_path="$ORIGIN_PATH" + done } # Return the path in the archive where has been stocked the origin path @@ -205,7 +196,7 @@ ynh_restore () { # [internal] # # usage: _get_archive_path ORIGIN_PATH -_get_archive_path () { +_get_archive_path() { # For security reasons we use csv python library to read the CSV python3 -c " import sys @@ -217,7 +208,7 @@ with open(sys.argv[1], 'r') as backup_file: print(row['dest']) sys.exit(0) raise Exception('Original path for %s not found' % sys.argv[2]) - " "${YNH_BACKUP_CSV}" "$1" + " "${YNH_BACKUP_CSV}" "$1" return $? } @@ -245,10 +236,10 @@ with open(sys.argv[1], 'r') as backup_file: # # Requires YunoHost version 2.6.4 or higher. # Requires YunoHost version 3.5.0 or higher for the argument --not_mandatory -ynh_restore_file () { +ynh_restore_file() { # Declare an array to define the options of this helper. local legacy_args=odm - local -A args_array=( [o]=origin_path= [d]=dest_path= [m]=not_mandatory ) + local -A args_array=([o]=origin_path= [d]=dest_path= [m]=not_mandatory) local origin_path local dest_path local not_mandatory @@ -261,10 +252,8 @@ ynh_restore_file () { local archive_path="$YNH_CWD${origin_path}" # If archive_path doesn't exist, search for a corresponding path in CSV - if [ ! -d "$archive_path" ] && [ ! -f "$archive_path" ] && [ ! -L "$archive_path" ] - then - if [ "$not_mandatory" == "0" ] - then + if [ ! -d "$archive_path" ] && [ ! -f "$archive_path" ] && [ ! -L "$archive_path" ]; then + if [ "$not_mandatory" == "0" ]; then archive_path="$YNH_BACKUP_DIR/$(_get_archive_path \"$origin_path\")" else return 0 @@ -272,14 +261,12 @@ ynh_restore_file () { fi # Move the old directory if it already exists - if [[ -e "${dest_path}" ]] - then + if [[ -e "${dest_path}" ]]; then # Check if the file/dir size is less than 500 Mo - if [[ $(du --summarize --bytes ${dest_path} | cut --delimiter="/" --fields=1) -le "500000000" ]] - then + if [[ $(du --summarize --bytes ${dest_path} | cut --delimiter="/" --fields=1) -le "500000000" ]]; then local backup_file="/home/yunohost.conf/backup/${dest_path}.backup.$(date '+%Y%m%d.%H%M%S')" mkdir --parents "$(dirname "$backup_file")" - mv "${dest_path}" "$backup_file" # Move the current file or directory + mv "${dest_path}" "$backup_file" # Move the current file or directory else ynh_secure_remove --file=${dest_path} fi @@ -289,10 +276,8 @@ ynh_restore_file () { mkdir --parents $(dirname "$dest_path") # Do a copy if it's just a mounting point - if mountpoint --quiet $YNH_BACKUP_DIR - then - if [[ -d "${archive_path}" ]] - then + if mountpoint --quiet $YNH_BACKUP_DIR; then + if [[ -d "${archive_path}" ]]; then archive_path="${archive_path}/." mkdir --parents "$dest_path" fi @@ -323,10 +308,10 @@ ynh_bind_or_cp() { # $app should be defined when calling this helper # # Requires YunoHost version 2.6.4 or higher. -ynh_store_file_checksum () { +ynh_store_file_checksum() { # Declare an array to define the options of this helper. local legacy_args=f - local -A args_array=( [f]=file= [u]=update_only ) + local -A args_array=([f]=file= [u]=update_only) local file local update_only update_only="${update_only:-0}" @@ -334,22 +319,21 @@ ynh_store_file_checksum () { # Manage arguments with getopts ynh_handle_getopts_args "$@" - local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' - + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + # If update only, we don't save the new checksum if no old checksum exist - if [ $update_only -eq 1 ] ; then + if [ $update_only -eq 1 ]; then local checksum_value=$(ynh_app_setting_get --app=$app --key=$checksum_setting_name) - if [ -z "${checksum_value}" ] ; then + if [ -z "${checksum_value}" ]; then unset backup_file_checksum return 0 fi fi - + ynh_app_setting_set --app=$app --key=$checksum_setting_name --value=$(md5sum "$file" | cut --delimiter=' ' --fields=1) # If backup_file_checksum isn't empty, ynh_backup_if_checksum_is_different has made a backup - if [ -n "${backup_file_checksum-}" ] - then + if [ -n "${backup_file_checksum-}" ]; then # Print the diff between the previous file and the new one. # diff return 1 if the files are different, so the || true diff --report-identical-files --unified --color=always $backup_file_checksum $file >&2 || true @@ -368,27 +352,25 @@ ynh_store_file_checksum () { # modified config files. # # Requires YunoHost version 2.6.4 or higher. -ynh_backup_if_checksum_is_different () { +ynh_backup_if_checksum_is_different() { # Declare an array to define the options of this helper. local legacy_args=f - local -A args_array=( [f]=file= ) + local -A args_array=([f]=file=) local file # Manage arguments with getopts ynh_handle_getopts_args "$@" - local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' local checksum_value=$(ynh_app_setting_get --app=$app --key=$checksum_setting_name) # backup_file_checksum isn't declare as local, so it can be reuse by ynh_store_file_checksum 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 + 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="/home/yunohost.conf/backup/$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 + 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 + echo "$backup_file_checksum" # Return the name of the backup file fi fi } @@ -401,15 +383,15 @@ ynh_backup_if_checksum_is_different () { # $app should be defined when calling this helper # # Requires YunoHost version 3.3.1 or higher. -ynh_delete_file_checksum () { +ynh_delete_file_checksum() { # Declare an array to define the options of this helper. local legacy_args=f - local -A args_array=( [f]=file= ) + local -A args_array=([f]=file=) local file # Manage arguments with getopts ynh_handle_getopts_args "$@" - local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' ynh_app_setting_delete --app=$app --key=$checksum_setting_name } @@ -417,7 +399,7 @@ ynh_delete_file_checksum () { # # [internal] # -ynh_backup_archive_exists () { +ynh_backup_archive_exists() { yunohost backup list --output-as json --quiet \ | jq -e --arg archive "$1" '.archives | index($archive)' >/dev/null } @@ -436,22 +418,19 @@ ynh_backup_archive_exists () { # ``` # # Requires YunoHost version 2.7.2 or higher. -ynh_backup_before_upgrade () { - if [ ! -e "/etc/yunohost/apps/$app/scripts/backup" ] - then +ynh_backup_before_upgrade() { + if [ ! -e "/etc/yunohost/apps/$app/scripts/backup" ]; then ynh_print_warn --message="This app doesn't have any backup script." return fi backup_number=1 local old_backup_number=2 - local app_bck=${app//_/-} # Replace all '_' by '-' + local app_bck=${app//_/-} # Replace all '_' by '-' NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0} - if [ "$NO_BACKUP_UPGRADE" -eq 0 ] - then + if [ "$NO_BACKUP_UPGRADE" -eq 0 ]; then # Check if a backup already exists with the prefix 1 - if ynh_backup_archive_exists "$app_bck-pre-upgrade1" - then + if ynh_backup_archive_exists "$app_bck-pre-upgrade1"; then # Prefix becomes 2 to preserve the previous backup backup_number=2 old_backup_number=1 @@ -459,13 +438,11 @@ ynh_backup_before_upgrade () { # Create backup BACKUP_CORE_ONLY=1 yunohost backup create --apps $app --name $app_bck-pre-upgrade$backup_number --debug - if [ "$?" -eq 0 ] - then + if [ "$?" -eq 0 ]; then # If the backup succeeded, remove the previous backup - if ynh_backup_archive_exists "$app_bck-pre-upgrade$old_backup_number" - then + if ynh_backup_archive_exists "$app_bck-pre-upgrade$old_backup_number"; then # Remove the previous backup only if it exists - yunohost backup delete $app_bck-pre-upgrade$old_backup_number > /dev/null + yunohost backup delete $app_bck-pre-upgrade$old_backup_number >/dev/null fi else ynh_die --message="Backup failed, the upgrade process was aborted." @@ -489,17 +466,15 @@ ynh_backup_before_upgrade () { # ``` # # Requires YunoHost version 2.7.2 or higher. -ynh_restore_upgradebackup () { +ynh_restore_upgradebackup() { ynh_print_err --message="Upgrade failed." - local app_bck=${app//_/-} # Replace all '_' by '-' + local app_bck=${app//_/-} # Replace all '_' by '-' NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0} - if [ "$NO_BACKUP_UPGRADE" -eq 0 ] - then + if [ "$NO_BACKUP_UPGRADE" -eq 0 ]; then # Check if an existing backup can be found before removing and restoring the application. - if ynh_backup_archive_exists "$app_bck-pre-upgrade$backup_number" - then + if ynh_backup_archive_exists "$app_bck-pre-upgrade$backup_number"; then # Remove the application then restore it yunohost app remove $app # Restore the backup diff --git a/data/helpers.d/config b/data/helpers.d/config index 3f856ffa4..247d12d6f 100644 --- a/data/helpers.d/config +++ b/data/helpers.d/config @@ -1,60 +1,49 @@ #!/bin/bash - _ynh_app_config_get_one() { local short_setting="$1" local type="$2" local bind="$3" local getter="get__${short_setting}" # Get value from getter if exists - if type -t $getter 2>/dev/null | grep -q '^function$' 2>/dev/null; - then + if type -t $getter 2>/dev/null | grep -q '^function$' 2>/dev/null; then old[$short_setting]="$($getter)" formats[${short_setting}]="yaml" - elif [[ "$bind" == *"("* ]] && type -t "get__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; - then + elif [[ "$bind" == *"("* ]] && type -t "get__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; then old[$short_setting]="$("get__${bind%%(*}" $short_setting $type $bind)" formats[${short_setting}]="yaml" - - elif [[ "$bind" == "null" ]] - then + + elif [[ "$bind" == "null" ]]; then old[$short_setting]="YNH_NULL" # Get value from app settings or from another file - elif [[ "$type" == "file" ]] - then - if [[ "$bind" == "settings" ]] - then + elif [[ "$type" == "file" ]]; then + if [[ "$bind" == "settings" ]]; then ynh_die --message="File '${short_setting}' can't be stored in settings" fi - old[$short_setting]="$(ls "$(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" 2> /dev/null || echo YNH_NULL)" + old[$short_setting]="$(ls "$(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" 2>/dev/null || echo YNH_NULL)" file_hash[$short_setting]="true" # Get multiline text from settings or from a full file - elif [[ "$type" == "text" ]] - then - if [[ "$bind" == "settings" ]] - then + elif [[ "$type" == "text" ]]; then + if [[ "$bind" == "settings" ]]; then old[$short_setting]="$(ynh_app_setting_get $app $short_setting)" - elif [[ "$bind" == *":"* ]] - then + elif [[ "$bind" == *":"* ]]; then ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" else - old[$short_setting]="$(cat $(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2> /dev/null || echo YNH_NULL)" + old[$short_setting]="$(cat $(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2>/dev/null || echo YNH_NULL)" fi # Get value from a kind of key/value file else local bind_after="" - if [[ "$bind" == "settings" ]] - then + if [[ "$bind" == "settings" ]]; then bind=":/etc/yunohost/apps/$app/settings.yml" fi local bind_key_="$(echo "$bind" | cut -d: -f1)" bind_key_=${bind_key_:-$short_setting} - if [[ "$bind_key_" == *">"* ]]; - then + if [[ "$bind_key_" == *">"* ]]; then bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" fi @@ -68,39 +57,31 @@ _ynh_app_config_apply_one() { local setter="set__${short_setting}" local bind="${binds[$short_setting]}" local type="${types[$short_setting]}" - if [ "${changed[$short_setting]}" == "true" ] - then + if [ "${changed[$short_setting]}" == "true" ]; then # Apply setter if exists - if type -t $setter 2>/dev/null | grep -q '^function$' 2>/dev/null; - then + if type -t $setter 2>/dev/null | grep -q '^function$' 2>/dev/null; then $setter - elif [[ "$bind" == *"("* ]] && type -t "set__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; - then + elif [[ "$bind" == *"("* ]] && type -t "set__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; then "set__${bind%%(*}" $short_setting $type $bind - - elif [[ "$bind" == "null" ]] - then + + elif [[ "$bind" == "null" ]]; then continue # Save in a file - elif [[ "$type" == "file" ]] - then - if [[ "$bind" == "settings" ]] - then + elif [[ "$type" == "file" ]]; then + if [[ "$bind" == "settings" ]]; then ynh_die --message="File '${short_setting}' can't be stored in settings" fi local bind_file="$(echo "$bind" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" - if [[ "${!short_setting}" == "" ]] - then + if [[ "${!short_setting}" == "" ]]; then ynh_backup_if_checksum_is_different --file="$bind_file" ynh_secure_remove --file="$bind_file" ynh_delete_file_checksum --file="$bind_file" --update_only ynh_print_info --message="File '$bind_file' removed" else ynh_backup_if_checksum_is_different --file="$bind_file" - if [[ "${!short_setting}" != "$bind_file" ]] - then + if [[ "${!short_setting}" != "$bind_file" ]]; then cp "${!short_setting}" "$bind_file" fi ynh_store_file_checksum --file="$bind_file" --update_only @@ -108,21 +89,18 @@ _ynh_app_config_apply_one() { fi # Save value in app settings - elif [[ "$bind" == "settings" ]] - then + elif [[ "$bind" == "settings" ]]; then ynh_app_setting_set --app=$app --key=$short_setting --value="${!short_setting}" ynh_print_info --message="Configuration key '$short_setting' edited in app settings" # Save multiline text in a file - elif [[ "$type" == "text" ]] - then - if [[ "$bind" == *":"* ]] - then + elif [[ "$type" == "text" ]]; then + if [[ "$bind" == *":"* ]]; then ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" fi local bind_file="$(echo "$bind" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" ynh_backup_if_checksum_is_different --file="$bind_file" - echo "${!short_setting}" > "$bind_file" + echo "${!short_setting}" >"$bind_file" ynh_store_file_checksum --file="$bind_file" --update_only ynh_print_info --message="File '$bind_file' overwritten with the content provided in question '${short_setting}'" @@ -131,8 +109,7 @@ _ynh_app_config_apply_one() { local bind_after="" local bind_key_="$(echo "$bind" | cut -d: -f1)" bind_key_=${bind_key_:-$short_setting} - if [[ "$bind_key_" == *">"* ]]; - then + if [[ "$bind_key_" == *">"* ]]; then bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" fi @@ -152,7 +129,8 @@ _ynh_app_config_apply_one() { _ynh_app_config_get() { # From settings local lines - lines=$(python3 << EOL + lines=$( + python3 </dev/null; - then + if type -t validate__$short_setting | grep -q '^function$' 2>/dev/null; then result="$(validate__$short_setting)" - elif [[ "$bind" == *"("* ]] && type -t "validate__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; - then + elif [[ "$bind" == *"("* ]] && type -t "validate__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; then "validate__${bind%%(*}" $short_setting fi - if [ -n "$result" ] - then + if [ -n "$result" ]; then # # Return a yaml such as: # @@ -287,8 +246,7 @@ _ynh_app_config_validate() { # # We use changes_validated to know if this is # the first validation error - if [[ "$changes_validated" == true ]] - then + if [[ "$changes_validated" == true ]]; then ynh_return "validation_errors:" fi ynh_return " ${short_setting}: \"$result\"" @@ -298,8 +256,7 @@ _ynh_app_config_validate() { # If validation failed, exit the script right now (instead of going into apply) # Yunohost core will pick up the errors returned via ynh_return previously - if [[ "$changes_validated" == "false" ]] - then + if [[ "$changes_validated" == "false" ]]; then exit 0 fi @@ -337,21 +294,20 @@ ynh_app_config_run() { declare -Ag formats=() case $1 in - show) - ynh_app_config_get - ynh_app_config_show - ;; - apply) - max_progression=4 - ynh_script_progression --message="Reading config panel description and current configuration..." - ynh_app_config_get + show) + ynh_app_config_get + ynh_app_config_show + ;; + apply) + max_progression=4 + ynh_script_progression --message="Reading config panel description and current configuration..." + ynh_app_config_get - ynh_app_config_validate + ynh_app_config_validate - ynh_script_progression --message="Applying the new configuration..." - ynh_app_config_apply - ynh_script_progression --message="Configuration of $app completed" --last - ;; + ynh_script_progression --message="Applying the new configuration..." + ynh_app_config_apply + ynh_script_progression --message="Configuration of $app completed" --last + ;; esac } - diff --git a/data/helpers.d/fail2ban b/data/helpers.d/fail2ban index 26c899d93..2b976cb8f 100644 --- a/data/helpers.d/fail2ban +++ b/data/helpers.d/fail2ban @@ -62,10 +62,10 @@ # ``` # # Requires YunoHost version 4.1.0 or higher. -ynh_add_fail2ban_config () { +ynh_add_fail2ban_config() { # Declare an array to define the options of this helper. local legacy_args=lrmptv - local -A args_array=( [l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template [v]=others_var=) + local -A args_array=([l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template [v]=others_var=) local logpath local failregex local max_retry @@ -81,8 +81,7 @@ ynh_add_fail2ban_config () { [[ -z "$others_var" ]] || ynh_print_warn --message="Packagers: using --others_var is unecessary since YunoHost 4.2" - if [ $use_template -ne 1 ] - then + if [ $use_template -ne 1 ]; then # Usage 1, no template. Build a config file from scratch. test -n "$logpath" || ynh_die --message="ynh_add_fail2ban_config expects a logfile path as first argument and received nothing." test -n "$failregex" || ynh_die --message="ynh_add_fail2ban_config expects a failure regex as second argument and received nothing." @@ -94,15 +93,15 @@ port = __PORTS__ filter = __APP__ logpath = __LOGPATH__ maxretry = __MAX_RETRY__ -" > $YNH_APP_BASEDIR/conf/f2b_jail.conf +" >$YNH_APP_BASEDIR/conf/f2b_jail.conf - echo " + echo " [INCLUDES] before = common.conf [Definition] failregex = __FAILREGEX__ ignoreregex = -" > $YNH_APP_BASEDIR/conf/f2b_filter.conf +" >$YNH_APP_BASEDIR/conf/f2b_filter.conf fi ynh_add_config --template="$YNH_APP_BASEDIR/conf/f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf" @@ -111,8 +110,7 @@ ignoreregex = ynh_systemd_action --service_name=fail2ban --action=reload --line_match="(Started|Reloaded) Fail2Ban Service" --log_path=systemd local fail2ban_error="$(journalctl --no-hostname --unit=fail2ban | tail --lines=50 | grep "WARNING.*$app.*")" - if [[ -n "$fail2ban_error" ]] - then + if [[ -n "$fail2ban_error" ]]; then ynh_print_err --message="Fail2ban failed to load the jail for $app" ynh_print_warn --message="${fail2ban_error#*WARNING}" fi @@ -123,7 +121,7 @@ ignoreregex = # usage: ynh_remove_fail2ban_config # # Requires YunoHost version 3.5.0 or higher. -ynh_remove_fail2ban_config () { +ynh_remove_fail2ban_config() { ynh_secure_remove --file="/etc/fail2ban/jail.d/$app.conf" ynh_secure_remove --file="/etc/fail2ban/filter.d/$app.conf" ynh_systemd_action --service_name=fail2ban --action=reload diff --git a/data/helpers.d/getopts b/data/helpers.d/getopts index 8d9e55826..e912220e4 100644 --- a/data/helpers.d/getopts +++ b/data/helpers.d/getopts @@ -45,11 +45,10 @@ # e.g. for `my_helper "val1" val2`, arg1 will be filled with val1, and arg2 with val2. # # Requires YunoHost version 3.2.2 or higher. -ynh_handle_getopts_args () { +ynh_handle_getopts_args() { # Manage arguments only if there's some provided set +o xtrace # set +x - if [ $# -ne 0 ] - then + if [ $# -ne 0 ]; then # Store arguments in an array to keep each argument separated local arguments=("$@") @@ -58,14 +57,12 @@ ynh_handle_getopts_args () { # ${!args_array[@]} is the list of all option_flags in the array (An option_flag is 'u' in [u]=user, user is a value) local getopts_parameters="" local option_flag="" - for option_flag in "${!args_array[@]}" - do + for option_flag in "${!args_array[@]}"; do # Concatenate each option_flags of the array to build the string of arguments for getopts # Will looks like 'abcd' for -a -b -c -d # If the value of an option_flag finish by =, it's an option with additionnal values. (e.g. --user bob or -u bob) # Check the last character of the value associate to the option_flag - if [ "${args_array[$option_flag]: -1}" = "=" ] - then + if [ "${args_array[$option_flag]: -1}" = "=" ]; then # For an option with additionnal values, add a ':' after the letter for getopts. getopts_parameters="${getopts_parameters}${option_flag}:" else @@ -74,8 +71,7 @@ ynh_handle_getopts_args () { # Check each argument given to the function local arg="" # ${#arguments[@]} is the size of the array - for arg in `seq 0 $(( ${#arguments[@]} - 1 ))` - do + for arg in $(seq 0 $((${#arguments[@]} - 1))); do # Escape options' values starting with -. Otherwise the - will be considered as another option. arguments[arg]="${arguments[arg]//--${args_array[$option_flag]}-/--${args_array[$option_flag]}\\TOBEREMOVED\\-}" # And replace long option (value of the option_flag) by the short option, the option_flag itself @@ -89,10 +85,9 @@ ynh_handle_getopts_args () { # Read and parse all the arguments # Use a function here, to use standart arguments $@ and be able to use shift. - parse_arg () { + parse_arg() { # Read all arguments, until no arguments are left - while [ $# -ne 0 ] - do + while [ $# -ne 0 ]; do # Initialize the index of getopts OPTIND=1 # Parse with getopts only if the argument begin by -, that means the argument is an option @@ -100,11 +95,9 @@ ynh_handle_getopts_args () { local parameter="" getopts ":$getopts_parameters" parameter || true - if [ "$parameter" = "?" ] - then + if [ "$parameter" = "?" ]; then ynh_die --message="Invalid argument: -${OPTARG:-}" - elif [ "$parameter" = ":" ] - then + elif [ "$parameter" = ":" ]; then ynh_die --message="-$OPTARG parameter requires an argument." else local shift_value=1 @@ -115,8 +108,7 @@ ynh_handle_getopts_args () { local option_var="${args_array[$parameter]%=}" # If this option doesn't take values # if there's a '=' at the end of the long option name, this option takes values - if [ "${args_array[$parameter]: -1}" != "=" ] - then + if [ "${args_array[$parameter]: -1}" != "=" ]; then # 'eval ${option_var}' will use the content of 'option_var' eval ${option_var}=1 else @@ -126,41 +118,35 @@ ynh_handle_getopts_args () { # If the first argument is longer than 2 characters, # There's a value attached to the option, in the same array cell - if [ ${#all_args[0]} -gt 2 ] - then + if [ ${#all_args[0]} -gt 2 ]; then # Remove the option and the space, so keep only the value itself. all_args[0]="${all_args[0]#-${parameter} }" # At this point, if all_args[0] start with "-", then the argument is not well formed - if [ "${all_args[0]:0:1}" == "-" ] - then + if [ "${all_args[0]:0:1}" == "-" ]; then ynh_die --message="Argument \"${all_args[0]}\" not valid! Did you use a single \"-\" instead of two?" fi # Reduce the value of shift, because the option has been removed manually - shift_value=$(( shift_value - 1 )) + shift_value=$((shift_value - 1)) fi # Declare the content of option_var as a variable. eval ${option_var}="" # Then read the array value per value local i - for i in `seq 0 $(( ${#all_args[@]} - 1 ))` - do + for i in $(seq 0 $((${#all_args[@]} - 1))); do # If this argument is an option, end here. - if [ "${all_args[$i]:0:1}" == "-" ] - then + if [ "${all_args[$i]:0:1}" == "-" ]; then # Ignore the first value of the array, which is the option itself if [ "$i" -ne 0 ]; then break fi else # Ignore empty parameters - if [ -n "${all_args[$i]}" ] - then + if [ -n "${all_args[$i]}" ]; then # Else, add this value to this option # Each value will be separated by ';' - if [ -n "${!option_var}" ] - then + if [ -n "${!option_var}" ]; then # If there's already another value for this option, add a ; before adding the new value eval ${option_var}+="\;" fi @@ -177,7 +163,7 @@ ynh_handle_getopts_args () { eval ${option_var}+='"${all_args[$i]}"' fi - shift_value=$(( shift_value + 1 )) + shift_value=$((shift_value + 1)) fi done fi @@ -190,24 +176,23 @@ ynh_handle_getopts_args () { # LEGACY MODE # Check if there's getopts arguments - if [ "${arguments[0]:0:1}" != "-" ] - then + if [ "${arguments[0]:0:1}" != "-" ]; then # If not, enter in legacy mode and manage the arguments as positionnal ones.. # Dot not echo, to prevent to go through a helper output. But print only in the log. - set -x; echo "! Helper used in legacy mode !" > /dev/null; set +x + set -x + echo "! Helper used in legacy mode !" >/dev/null + set +x local i - for i in `seq 0 $(( ${#arguments[@]} -1 ))` - do + for i in $(seq 0 $((${#arguments[@]} - 1))); do # Try to use legacy_args as a list of option_flag of the array args_array # Otherwise, fallback to getopts_parameters to get the option_flag. But an associative arrays isn't always sorted in the correct order... # Remove all ':' in getopts_parameters - getopts_parameters=${legacy_args:-${getopts_parameters//:}} + getopts_parameters=${legacy_args:-${getopts_parameters//:/}} # Get the option_flag from getopts_parameters, by using the option_flag according to the position of the argument. option_flag=${getopts_parameters:$i:1} - if [ -z "$option_flag" ] - then - ynh_print_warn --message="Too many arguments ! \"${arguments[$i]}\" will be ignored." - continue + if [ -z "$option_flag" ]; then + ynh_print_warn --message="Too many arguments ! \"${arguments[$i]}\" will be ignored." + continue fi # Use the long option, corresponding to the option_flag, as a variable # (e.g. for [u]=user, 'user' will be used as a variable) diff --git a/data/helpers.d/hardware b/data/helpers.d/hardware index 6d1c314fa..9f276b806 100644 --- a/data/helpers.d/hardware +++ b/data/helpers.d/hardware @@ -10,10 +10,10 @@ # | ret: the amount of free ram, in MB (MegaBytes) # # Requires YunoHost version 3.8.1 or higher. -ynh_get_ram () { +ynh_get_ram() { # Declare an array to define the options of this helper. local legacy_args=ftso - local -A args_array=( [f]=free [t]=total [s]=ignore_swap [o]=only_swap ) + local -A args_array=([f]=free [t]=total [s]=ignore_swap [o]=only_swap) local free local total local ignore_swap @@ -25,41 +25,34 @@ ynh_get_ram () { free=${free:-0} total=${total:-0} - if [ $free -eq $total ] - then + if [ $free -eq $total ]; then ynh_print_warn --message="You have to choose --free or --total when using ynh_get_ram" ram=0 # Use the total amount of ram - elif [ $free -eq 1 ] - then + elif [ $free -eq 1 ]; then local free_ram=$(vmstat --stats --unit M | grep "free memory" | awk '{print $1}') local free_swap=$(vmstat --stats --unit M | grep "free swap" | awk '{print $1}') - local free_ram_swap=$(( free_ram + free_swap )) + local free_ram_swap=$((free_ram + free_swap)) # Use the total amount of free ram local ram=$free_ram_swap - if [ $ignore_swap -eq 1 ] - then + if [ $ignore_swap -eq 1 ]; then # Use only the amount of free ram ram=$free_ram - elif [ $only_swap -eq 1 ] - then + elif [ $only_swap -eq 1 ]; then # Use only the amount of free swap ram=$free_swap fi - elif [ $total -eq 1 ] - then + elif [ $total -eq 1 ]; then local total_ram=$(vmstat --stats --unit M | grep "total memory" | awk '{print $1}') local total_swap=$(vmstat --stats --unit M | grep "total swap" | awk '{print $1}') - local total_ram_swap=$(( total_ram + total_swap )) + local total_ram_swap=$((total_ram + total_swap)) local ram=$total_ram_swap - if [ $ignore_swap -eq 1 ] - then + if [ $ignore_swap -eq 1 ]; then # Use only the amount of free ram ram=$total_ram - elif [ $only_swap -eq 1 ] - then + elif [ $only_swap -eq 1 ]; then # Use only the amount of free swap ram=$total_swap fi @@ -79,10 +72,10 @@ ynh_get_ram () { # | ret: 1 if the ram is under the requirement, 0 otherwise. # # Requires YunoHost version 3.8.1 or higher. -ynh_require_ram () { +ynh_require_ram() { # Declare an array to define the options of this helper. local legacy_args=rftso - local -A args_array=( [r]=required= [f]=free [t]=total [s]=ignore_swap [o]=only_swap ) + local -A args_array=([r]=required= [f]=free [t]=total [s]=ignore_swap [o]=only_swap) local required local free local total @@ -100,8 +93,7 @@ ynh_require_ram () { local ram=$(ynh_get_ram $free $total $ignore_swap $only_swap) - if [ $ram -lt $required ] - then + if [ $ram -lt $required ]; then return 1 else return 0 diff --git a/data/helpers.d/logging b/data/helpers.d/logging index d11fc578f..7a8be30e9 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -10,7 +10,7 @@ ynh_die() { # Declare an array to define the options of this helper. local legacy_args=mc - local -A args_array=( [m]=message= [c]=ret_code= ) + local -A args_array=([m]=message= [c]=ret_code=) local message local ret_code # Manage arguments with getopts @@ -30,7 +30,7 @@ ynh_die() { ynh_print_info() { # Declare an array to define the options of this helper. local legacy_args=m - local -A args_array=( [m]=message= ) + local -A args_array=([m]=message=) local message # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -62,7 +62,7 @@ ynh_no_log() { # [internal] # # Requires YunoHost version 3.2.0 or higher. -ynh_print_log () { +ynh_print_log() { echo -e "${1}" } @@ -72,10 +72,10 @@ ynh_print_log () { # | arg: -m, --message= - The text to print # # Requires YunoHost version 3.2.0 or higher. -ynh_print_warn () { +ynh_print_warn() { # Declare an array to define the options of this helper. local legacy_args=m - local -A args_array=( [m]=message= ) + local -A args_array=([m]=message=) local message # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -89,10 +89,10 @@ ynh_print_warn () { # | arg: -m, --message= - The text to print # # Requires YunoHost version 3.2.0 or higher. -ynh_print_err () { +ynh_print_err() { # Declare an array to define the options of this helper. local legacy_args=m - local -A args_array=( [m]=message= ) + local -A args_array=([m]=message=) local message # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -110,7 +110,7 @@ ynh_print_err () { # If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. # # Requires YunoHost version 3.2.0 or higher. -ynh_exec_err () { +ynh_exec_err() { ynh_print_err "$(eval $@)" } @@ -124,7 +124,7 @@ ynh_exec_err () { # If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. # # Requires YunoHost version 3.2.0 or higher. -ynh_exec_warn () { +ynh_exec_warn() { ynh_print_warn "$(eval $@)" } @@ -138,7 +138,7 @@ ynh_exec_warn () { # If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. # # Requires YunoHost version 3.2.0 or higher. -ynh_exec_warn_less () { +ynh_exec_warn_less() { eval $@ 2>&1 } @@ -152,8 +152,8 @@ ynh_exec_warn_less () { # If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. # # Requires YunoHost version 3.2.0 or higher. -ynh_exec_quiet () { - eval $@ > /dev/null +ynh_exec_quiet() { + eval $@ >/dev/null } # Execute a command and redirect stdout and stderr in /dev/null @@ -166,8 +166,8 @@ ynh_exec_quiet () { # If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. # # Requires YunoHost version 3.2.0 or higher. -ynh_exec_fully_quiet () { - eval $@ > /dev/null 2>&1 +ynh_exec_fully_quiet() { + eval $@ >/dev/null 2>&1 } # Remove any logs for all the following commands. @@ -177,7 +177,7 @@ ynh_exec_fully_quiet () { # WARNING: You should be careful with this helper, and never forget to use ynh_print_ON as soon as possible to restore the logging. # # Requires YunoHost version 3.2.0 or higher. -ynh_print_OFF () { +ynh_print_OFF() { exec {BASH_XTRACEFD}>/dev/null } @@ -186,10 +186,10 @@ ynh_print_OFF () { # usage: ynh_print_ON # # Requires YunoHost version 3.2.0 or higher. -ynh_print_ON () { +ynh_print_ON() { exec {BASH_XTRACEFD}>&1 # Print an echo only for the log, to be able to know that ynh_print_ON has been called. - echo ynh_print_ON > /dev/null + echo ynh_print_ON >/dev/null } # Initial definitions for ynh_script_progression @@ -214,11 +214,11 @@ base_time=$(date +%s) # | arg: -l, --last - Use for the last call of the helper, to fill the progression bar. # # Requires YunoHost version 3.5.0 or higher. -ynh_script_progression () { +ynh_script_progression() { set +o xtrace # set +x # Declare an array to define the options of this helper. local legacy_args=mwtl - local -A args_array=( [m]=message= [w]=weight= [t]=time [l]=last ) + local -A args_array=([m]=message= [w]=weight= [t]=time [l]=last) local message local weight local time @@ -232,12 +232,11 @@ ynh_script_progression () { last=${last:-0} # Get execution time since the last $base_time - local exec_time=$(( $(date +%s) - $base_time )) + local exec_time=$(($(date +%s) - $base_time)) base_time=$(date +%s) # Compute $max_progression (if we didn't already) - if [ "$max_progression" = -1 ] - then + if [ "$max_progression" = -1 ]; then # Get the number of occurrences of 'ynh_script_progression' in the script. Except those are commented. local helper_calls="$(grep --count "^[^#]*ynh_script_progression" $0)" # Get the number of call with a weight value @@ -249,23 +248,22 @@ ynh_script_progression () { local weight_valuesB="$(grep --perl-regexp "^[^#]*ynh_script_progression.*-w " $0 | sed 's/.*-w[= ]\([[:digit:]]*\).*/\1/g')" # Each value will be on a different line. # Remove each 'end of line' and replace it by a '+' to sum the values. - local weight_values=$(( $(echo "$weight_valuesA" "$weight_valuesB" | grep -v -E '^\s*$' | tr '\n' '+' | sed 's/+$/+0/g') )) + local weight_values=$(($(echo "$weight_valuesA" "$weight_valuesB" | grep -v -E '^\s*$' | tr '\n' '+' | sed 's/+$/+0/g'))) # max_progression is a total number of calls to this helper. # Less the number of calls with a weight value. # Plus the total of weight values - max_progression=$(( $helper_calls - $weight_calls + $weight_values )) + max_progression=$(($helper_calls - $weight_calls + $weight_values)) fi # Increment each execution of ynh_script_progression in this script by the weight of the previous call. - increment_progression=$(( $increment_progression + $previous_weight )) + increment_progression=$(($increment_progression + $previous_weight)) # Store the weight of the current call in $previous_weight for next call previous_weight=$weight # Reduce $increment_progression to the size of the scale - if [ $last -eq 0 ] - then - local effective_progression=$(( $increment_progression * $progress_scale / $max_progression )) + if [ $last -eq 0 ]; then + local effective_progression=$(($increment_progression * $progress_scale / $max_progression)) # If last is specified, fill immediately the progression_bar else local effective_progression=$progress_scale @@ -273,19 +271,17 @@ ynh_script_progression () { # Build $progression_bar from progress_string(0,1,2) according to $effective_progression and the weight of the current task # expected_progression is the progression expected after the current task - local expected_progression="$(( ( $increment_progression + $weight ) * $progress_scale / $max_progression - $effective_progression ))" - if [ $last -eq 1 ] - then + local expected_progression="$((($increment_progression + $weight) * $progress_scale / $max_progression - $effective_progression))" + if [ $last -eq 1 ]; then expected_progression=0 fi # left_progression is the progression not yet done - local left_progression="$(( $progress_scale - $effective_progression - $expected_progression ))" + local left_progression="$(($progress_scale - $effective_progression - $expected_progression))" # Build the progression bar with $effective_progression, work done, $expected_progression, current work and $left_progression, work to be done. 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 + if [ $time -eq 1 ]; then print_exec_time=" [$(date +%Hh%Mm,%Ss --date="0 + $exec_time sec")]" fi @@ -299,8 +295,8 @@ ynh_script_progression () { # usage: ynh_return somedata # # Requires YunoHost version 3.6.0 or higher. -ynh_return () { - echo "$1" >> "$YNH_STDRETURN" +ynh_return() { + echo "$1" >>"$YNH_STDRETURN" } # Debugger for app packagers @@ -310,12 +306,12 @@ ynh_return () { # | arg: -t, --trace= - Turn on or off the trace of the script. Usefull to trace nonly a small part of a script. # # Requires YunoHost version 3.5.0 or higher. -ynh_debug () { +ynh_debug() { # Disable set xtrace for the helper itself, to not pollute the debug log set +o xtrace # set +x # Declare an array to define the options of this helper. local legacy_args=mt - local -A args_array=( [m]=message= [t]=trace= ) + local -A args_array=([m]=message= [t]=trace=) local message local trace # Manage arguments with getopts @@ -325,13 +321,11 @@ ynh_debug () { message=${message:-} trace=${trace:-} - if [ -n "$message" ] - then + if [ -n "$message" ]; then ynh_print_log "[Debug] ${message}" >&2 fi - if [ "$trace" == "1" ] - then + if [ "$trace" == "1" ]; then ynh_debug --message="Enable debugging" set +o xtrace # set +x # Get the current file descriptor of xtrace @@ -343,8 +337,7 @@ ynh_debug () { # Force stdout to stderr exec 1>&2 fi - if [ "$trace" == "0" ] - then + if [ "$trace" == "0" ]; then ynh_debug --message="Disable debugging" set +o xtrace # set +x # Put xtrace back to its original fild descriptor @@ -366,6 +359,6 @@ ynh_debug () { # If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. # # Requires YunoHost version 3.5.0 or higher. -ynh_debug_exec () { +ynh_debug_exec() { ynh_debug --message="$(eval $@)" } diff --git a/data/helpers.d/logrotate b/data/helpers.d/logrotate index 2d9ab6b72..6f9726beb 100644 --- a/data/helpers.d/logrotate +++ b/data/helpers.d/logrotate @@ -15,10 +15,10 @@ # # Requires YunoHost version 2.6.4 or higher. # Requires YunoHost version 3.2.0 or higher for the argument `--specific_user` -ynh_use_logrotate () { +ynh_use_logrotate() { # Declare an array to define the options of this helper. local legacy_args=lnuya - local -A args_array=( [l]=logfile= [n]=nonappend [u]=specific_user= [y]=non [a]=append ) + local -A args_array=([l]=logfile= [n]=nonappend [u]=specific_user= [y]=non [a]=append) # [y]=non [a]=append are only for legacy purpose, to not fail on the old option '--non-append' local logfile local nonappend @@ -30,22 +30,18 @@ ynh_use_logrotate () { specific_user="${specific_user:-}" # LEGACY CODE - PRE GETOPTS - if [ $# -gt 0 ] && [ "$1" == "--non-append" ] - then + if [ $# -gt 0 ] && [ "$1" == "--non-append" ]; then nonappend=1 # Destroy this argument for the next command. shift - elif [ $# -gt 1 ] && [ "$2" == "--non-append" ] - then + elif [ $# -gt 1 ] && [ "$2" == "--non-append" ]; then nonappend=1 fi - if [ $# -gt 0 ] && [ "$(echo ${1:0:1})" != "-" ] - then + if [ $# -gt 0 ] && [ "$(echo ${1:0:1})" != "-" ]; then # If the given logfile parameter already exists as a file, or if it ends up with ".log", # we just want to manage a single file - if [ -f "$1" ] || [ "$(echo ${1##*.})" == "log" ] - then + if [ -f "$1" ] || [ "$(echo ${1##*.})" == "log" ]; then local logfile=$1 # Otherwise we assume we want to manage a directory and all its .log file inside else @@ -58,22 +54,20 @@ ynh_use_logrotate () { if [ "$nonappend" -eq 1 ]; then customtee="tee" fi - if [ -n "$logfile" ] - then - if [ ! -f "$1" ] && [ "$(echo ${logfile##*.})" != "log" ]; then # Keep only the extension to check if it's a logfile - local logfile="$logfile/*.log" # Else, uses the directory and all logfile into it. + if [ -n "$logfile" ]; then + if [ ! -f "$1" ] && [ "$(echo ${logfile##*.})" != "log" ]; then # Keep only the extension to check if it's a logfile + local logfile="$logfile/*.log" # Else, uses the directory and all logfile into it. fi else logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log fi local su_directive="" - if [[ -n $specific_user ]] - then + if [[ -n $specific_user ]]; then su_directive=" # Run logorotate as specific user - group su ${specific_user%/*} ${specific_user#*/}" fi - cat > ./${app}-logrotate << EOF # Build a config file for logrotate + cat >./${app}-logrotate < /dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee) + mkdir --parents $(dirname "$logfile") # Create the log directory, if not exist + cat ${app}-logrotate | $customtee /etc/logrotate.d/$app >/dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee) } # Remove the app's logrotate config. @@ -103,7 +97,7 @@ EOF # usage: ynh_remove_logrotate # # Requires YunoHost version 2.6.4 or higher. -ynh_remove_logrotate () { +ynh_remove_logrotate() { if [ -e "/etc/logrotate.d/$app" ]; then rm "/etc/logrotate.d/$app" fi diff --git a/data/helpers.d/multimedia b/data/helpers.d/multimedia index 552b8c984..abeb9ed2c 100644 --- a/data/helpers.d/multimedia +++ b/data/helpers.d/multimedia @@ -22,8 +22,7 @@ ynh_multimedia_build_main_dir() { mkdir -p "$MEDIA_DIRECTORY/share/eBook" ## Création des dossiers utilisateurs - for user in $(yunohost user list --output-as json | jq -r '.users | keys[]') - do + for user in $(yunohost user list --output-as json | jq -r '.users | keys[]'); do mkdir -p "$MEDIA_DIRECTORY/$user" mkdir -p "$MEDIA_DIRECTORY/$user/Music" mkdir -p "$MEDIA_DIRECTORY/$user/Picture" @@ -66,22 +65,22 @@ ynh_multimedia_addfolder() { # Declare an array to define the options of this helper. local legacy_args=sd - local -A args_array=( [s]=source_dir= [d]=dest_dir= ) - local source_dir - local dest_dir + local -A args_array=([s]=source_dir= [d]=dest_dir=) + local source_dir + local dest_dir # Manage arguments with getopts ynh_handle_getopts_args "$@" # Ajout d'un lien symbolique vers le dossier à partager - ln -sfn "$source_dir" "$MEDIA_DIRECTORY/$dest_dir" + ln -sfn "$source_dir" "$MEDIA_DIRECTORY/$dest_dir" - ## Application des droits étendus sur le dossier ajouté - # Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other: - setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir" - # Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers. - setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir" - # Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl. - setfacl -RL -m m::rwx "$source_dir" + ## Application des droits étendus sur le dossier ajouté + # Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other: + setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir" + # Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers. + setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir" + # Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl. + setfacl -RL -m m::rwx "$source_dir" } # Allow an user to have an write authorisation in multimedia directories @@ -91,14 +90,14 @@ ynh_multimedia_addfolder() { # | arg: -u, --user_name= - The name of the user which gain this access. # # Requires YunoHost version 4.2 or higher. -ynh_multimedia_addaccess () { - # Declare an array to define the options of this helper. +ynh_multimedia_addaccess() { + # Declare an array to define the options of this helper. local legacy_args=u - declare -Ar args_array=( [u]=user_name=) - local user_name - # Manage arguments with getopts - ynh_handle_getopts_args "$@" + declare -Ar args_array=([u]=user_name=) + local user_name + # Manage arguments with getopts + ynh_handle_getopts_args "$@" - groupadd -f multimedia - usermod -a -G multimedia $user_name + groupadd -f multimedia + usermod -a -G multimedia $user_name } diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index 091dfaf40..822159f27 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -15,7 +15,7 @@ ynh_mysql_connect_as() { # Declare an array to define the options of this helper. local legacy_args=upd - local -A args_array=( [u]=user= [p]=password= [d]=database= ) + local -A args_array=([u]=user= [p]=password= [d]=database=) local user local password local database @@ -36,19 +36,18 @@ ynh_mysql_connect_as() { ynh_mysql_execute_as_root() { # Declare an array to define the options of this helper. local legacy_args=sd - local -A args_array=( [s]=sql= [d]=database= ) + local -A args_array=([s]=sql= [d]=database=) local sql local database # Manage arguments with getopts ynh_handle_getopts_args "$@" database="${database:-}" - if [ -n "$database" ] - then + if [ -n "$database" ]; then database="--database=$database" fi - mysql -B "$database" <<< "$sql" + mysql -B "$database" <<<"$sql" } # Execute a command from a file as root user @@ -61,19 +60,18 @@ ynh_mysql_execute_as_root() { ynh_mysql_execute_file_as_root() { # Declare an array to define the options of this helper. local legacy_args=fd - local -A args_array=( [f]=file= [d]=database= ) + local -A args_array=([f]=file= [d]=database=) local file local database # Manage arguments with getopts ynh_handle_getopts_args "$@" database="${database:-}" - if [ -n "$database" ] - then + if [ -n "$database" ]; then database="--database=$database" fi - mysql -B "$database" < "$file" + mysql -B "$database" <"$file" } # Create a database and grant optionnaly privilegies to a user @@ -92,8 +90,7 @@ ynh_mysql_create_db() { local sql="CREATE DATABASE ${db};" # grant all privilegies to user - if [[ $# -gt 1 ]] - then + if [[ $# -gt 1 ]]; then sql+=" GRANT ALL PRIVILEGES ON ${db}.* TO '${2}'@'localhost'" if [[ -n ${3:-} ]]; then sql+=" IDENTIFIED BY '${3}'" @@ -131,7 +128,7 @@ ynh_mysql_drop_db() { ynh_mysql_dump_db() { # Declare an array to define the options of this helper. local legacy_args=d - local -A args_array=( [d]=database= ) + local -A args_array=([d]=database=) local database # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -160,17 +157,15 @@ ynh_mysql_create_user() { # | ret: 0 if the user exists, 1 otherwise. # # Requires YunoHost version 2.2.4 or higher. -ynh_mysql_user_exists() -{ +ynh_mysql_user_exists() { # Declare an array to define the options of this helper. local legacy_args=u - local -A args_array=( [u]=user= ) + local -A args_array=([u]=user=) local user # Manage arguments with getopts ynh_handle_getopts_args "$@" - if [[ -z $(ynh_mysql_execute_as_root --sql="SELECT User from mysql.user WHERE User = '$user';") ]] - then + if [[ -z $(ynh_mysql_execute_as_root --sql="SELECT User from mysql.user WHERE User = '$user';") ]]; then return 1 else return 0 @@ -200,10 +195,10 @@ ynh_mysql_drop_user() { # It will also be stored as "`mysqlpwd`" into the app settings. # # Requires YunoHost version 2.6.4 or higher. -ynh_mysql_setup_db () { +ynh_mysql_setup_db() { # Declare an array to define the options of this helper. local legacy_args=unp - local -A args_array=( [u]=db_user= [n]=db_name= [p]=db_pwd= ) + local -A args_array=([u]=db_user= [n]=db_name= [p]=db_pwd=) local db_user local db_name db_pwd="" @@ -226,10 +221,10 @@ ynh_mysql_setup_db () { # | arg: -n, --db_name= - Name of the database # # Requires YunoHost version 2.6.4 or higher. -ynh_mysql_remove_db () { +ynh_mysql_remove_db() { # Declare an array to define the options of this helper. local legacy_args=un - local -Ar args_array=( [u]=db_user= [n]=db_name= ) + local -Ar args_array=([u]=db_user= [n]=db_name=) local db_user local db_name # Manage arguments with getopts diff --git a/data/helpers.d/network b/data/helpers.d/network index 4e536a8db..d6c15060a 100644 --- a/data/helpers.d/network +++ b/data/helpers.d/network @@ -9,18 +9,17 @@ # example: port=$(ynh_find_port --port=8080) # # Requires YunoHost version 2.6.4 or higher. -ynh_find_port () { +ynh_find_port() { # Declare an array to define the options of this helper. local legacy_args=p - local -A args_array=( [p]=port= ) + local -A args_array=([p]=port=) local port # Manage arguments with getopts ynh_handle_getopts_args "$@" test -n "$port" || ynh_die --message="The argument of ynh_find_port must be a valid port." - while ! ynh_port_available --port=$port - do - port=$((port+1)) + while ! ynh_port_available --port=$port; do + port=$((port + 1)) done echo $port } @@ -34,28 +33,25 @@ ynh_find_port () { # example: ynh_port_available --port=1234 || ynh_die --message="Port 1234 is needs to be available for this app" # # Requires YunoHost version 3.8.0 or higher. -ynh_port_available () { +ynh_port_available() { # Declare an array to define the options of this helper. local legacy_args=p - local -A args_array=( [p]=port= ) + local -A args_array=([p]=port=) local port # Manage arguments with getopts ynh_handle_getopts_args "$@" # Check if the port is free - if ss --numeric --listening --tcp --udp | awk '{print$5}' | grep --quiet --extended-regexp ":$port$" - then + if ss --numeric --listening --tcp --udp | awk '{print$5}' | grep --quiet --extended-regexp ":$port$"; then return 1 # This is 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) - elif grep -q "port: '$port'" /etc/yunohost/apps/*/settings.yml - then + elif grep -q "port: '$port'" /etc/yunohost/apps/*/settings.yml; then return 1 else return 0 fi } - # Validate an IP address # # [internal] @@ -66,13 +62,12 @@ ynh_port_available () { # example: ynh_validate_ip 4 111.222.333.444 # # Requires YunoHost version 2.2.4 or higher. -ynh_validate_ip() -{ +ynh_validate_ip() { # http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python#319298 # Declare an array to define the options of this helper. local legacy_args=fi - local -A args_array=( [f]=family= [i]=ip_address= ) + local -A args_array=([f]=family= [i]=ip_address=) local family local ip_address # Manage arguments with getopts @@ -80,7 +75,7 @@ ynh_validate_ip() [ "$family" == "4" ] || [ "$family" == "6" ] || return 1 - python3 /dev/stdin << EOF + python3 /dev/stdin < "$YNH_APP_BASEDIR/conf/n.src" +SOURCE_SUM=d4da7ea91f680de0c9b5876e097e2a793e8234fcd0f7ca87a0599b925be087a3" >"$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) + ( + cd "$n_install_dir/git" + PREFIX=$N_PREFIX make install 2>&1 + ) } # Load the version of node for an app, and set variables. @@ -69,7 +71,7 @@ SOURCE_SUM=d4da7ea91f680de0c9b5876e097e2a793e8234fcd0f7ca87a0599b925be087a3" > " # - $nodejs_version: Just the version number of node for this app. Stored as 'nodejs_version' in settings.yml. # # Requires YunoHost version 2.7.12 or higher. -ynh_use_nodejs () { +ynh_use_nodejs() { nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version) # Get the absolute path of this version of node @@ -109,12 +111,12 @@ ynh_use_nodejs () { # Refer to `ynh_use_nodejs` for more information about available commands and variables # # Requires YunoHost version 2.7.12 or higher. -ynh_install_nodejs () { +ynh_install_nodejs() { # Use n, https://github.com/tj/n to manage the nodejs versions # Declare an array to define the options of this helper. local legacy_args=n - local -A args_array=( [n]=nodejs_version= ) + local -A args_array=([n]=nodejs_version=) local nodejs_version # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -132,11 +134,9 @@ ynh_install_nodejs () { 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 + 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 + elif dpkg --compare-versions "$($n_install_dir/bin/n --version)" lt $n_version; then ynh_install_n fi @@ -152,8 +152,7 @@ ynh_install_nodejs () { # Install the requested version of nodejs uname=$(uname --machine) - if [[ $uname =~ aarch64 || $uname =~ arm64 ]] - then + if [[ $uname =~ aarch64 || $uname =~ arm64 ]]; then n $nodejs_version --arch=arm64 else n $nodejs_version @@ -164,8 +163,7 @@ ynh_install_nodejs () { real_nodejs_version=$(basename $real_nodejs_version) # Create a symbolic link for this major version if the file doesn't already exist - if [ ! -e "$node_version_path/$nodejs_version" ] - then + if [ ! -e "$node_version_path/$nodejs_version" ]; then ln --symbolic --force --no-target-directory $node_version_path/$real_nodejs_version $node_version_path/$nodejs_version fi @@ -190,21 +188,19 @@ ynh_install_nodejs () { # - If no other app uses node, n will be also removed. # # Requires YunoHost version 2.7.12 or higher. -ynh_remove_nodejs () { +ynh_remove_nodejs() { nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version) # Remove the line for this app sed --in-place "/$YNH_APP_INSTANCE_NAME:$nodejs_version/d" "$n_install_dir/ynh_app_version" # If no other app uses this version of nodejs, remove it. - if ! grep --quiet "$nodejs_version" "$n_install_dir/ynh_app_version" - then + if ! grep --quiet "$nodejs_version" "$n_install_dir/ynh_app_version"; then $n_install_dir/bin/n rm $nodejs_version fi # If no other app uses n, remove n - if [ ! -s "$n_install_dir/ynh_app_version" ] - then + if [ ! -s "$n_install_dir/ynh_app_version" ]; then ynh_secure_remove --file="$n_install_dir" ynh_secure_remove --file="/usr/local/n" sed --in-place "/N_PREFIX/d" /root/.bashrc @@ -221,9 +217,9 @@ ynh_remove_nodejs () { # usage: ynh_cron_upgrade_node # # Requires YunoHost version 2.7.12 or higher. -ynh_cron_upgrade_node () { +ynh_cron_upgrade_node() { # Build the update script - cat > "$n_install_dir/node_update.sh" << EOF + cat >"$n_install_dir/node_update.sh" < "/etc/cron.daily/node_update" << EOF + cat >"/etc/cron.daily/node_update" <> $n_install_dir/node_update.log diff --git a/data/helpers.d/permission b/data/helpers.d/permission index c04b4145b..6c2fa7ef8 100644 --- a/data/helpers.d/permission +++ b/data/helpers.d/permission @@ -66,7 +66,7 @@ ynh_permission_create() { # Declare an array to define the options of this helper. local legacy_args=puAhaltP - local -A args_array=( [p]=permission= [u]=url= [A]=additional_urls= [h]=auth_header= [a]=allowed= [l]=label= [t]=show_tile= [P]=protected= ) + local -A args_array=([p]=permission= [u]=url= [A]=additional_urls= [h]=auth_header= [a]=allowed= [l]=label= [t]=show_tile= [P]=protected=) local permission local url local additional_urls @@ -84,13 +84,11 @@ ynh_permission_create() { show_tile=${show_tile:-} protected=${protected:-} - if [[ -n $url ]] - then + if [[ -n $url ]]; then url=",url='$url'" fi - if [[ -n $additional_urls ]] - then + if [[ -n $additional_urls ]]; then # Convert a list from getopts to python list # Note that getopts separate the args with ';' # By example: @@ -100,18 +98,15 @@ ynh_permission_create() { additional_urls=",additional_urls=['${additional_urls//;/\',\'}']" fi - if [[ -n $auth_header ]] - then - if [ $auth_header == "true" ] - then + if [[ -n $auth_header ]]; then + if [ $auth_header == "true" ]; then auth_header=",auth_header=True" else auth_header=",auth_header=False" fi fi - if [[ -n $allowed ]] - then + if [[ -n $allowed ]]; then # Convert a list from getopts to python list # Note that getopts separate the args with ';' # By example: @@ -127,20 +122,16 @@ ynh_permission_create() { label=",label='$permission'" fi - if [[ -n ${show_tile:-} ]] - then - if [ $show_tile == "true" ] - then + if [[ -n ${show_tile:-} ]]; then + if [ $show_tile == "true" ]; then show_tile=",show_tile=True" else show_tile=",show_tile=False" fi fi - if [[ -n ${protected:-} ]] - then - if [ $protected == "true" ] - then + if [[ -n ${protected:-} ]]; then + if [ $protected == "true" ]; then protected=",protected=True" else protected=",protected=False" @@ -161,7 +152,7 @@ ynh_permission_create() { ynh_permission_delete() { # Declare an array to define the options of this helper. local legacy_args=p - local -A args_array=( [p]=permission= ) + local -A args_array=([p]=permission=) local permission ynh_handle_getopts_args "$@" @@ -178,7 +169,7 @@ ynh_permission_delete() { ynh_permission_exists() { # Declare an array to define the options of this helper. local legacy_args=p - local -A args_array=( [p]=permission= ) + local -A args_array=([p]=permission=) local permission ynh_handle_getopts_args "$@" @@ -201,7 +192,7 @@ ynh_permission_exists() { ynh_permission_url() { # Declare an array to define the options of this helper. local legacy_args=puarhc - local -A args_array=( [p]=permission= [u]=url= [a]=add_url= [r]=remove_url= [h]=auth_header= [c]=clear_urls ) + local -A args_array=([p]=permission= [u]=url= [a]=add_url= [r]=remove_url= [h]=auth_header= [c]=clear_urls) local permission local url local add_url @@ -215,13 +206,11 @@ ynh_permission_url() { auth_header=${auth_header:-} clear_urls=${clear_urls:-} - if [[ -n $url ]] - then + if [[ -n $url ]]; then url=",url='$url'" fi - if [[ -n $add_url ]] - then + if [[ -n $add_url ]]; then # Convert a list from getopts to python list # Note that getopts separate the args with ';' # For example: @@ -231,8 +220,7 @@ ynh_permission_url() { add_url=",add_url=['${add_url//;/\',\'}']" fi - if [[ -n $remove_url ]] - then + if [[ -n $remove_url ]]; then # Convert a list from getopts to python list # Note that getopts separate the args with ';' # For example: @@ -242,25 +230,21 @@ ynh_permission_url() { remove_url=",remove_url=['${remove_url//;/\',\'}']" fi - if [[ -n $auth_header ]] - then - if [ $auth_header == "true" ] - then + if [[ -n $auth_header ]]; then + if [ $auth_header == "true" ]; then auth_header=",auth_header=True" else auth_header=",auth_header=False" fi fi - if [[ -n $clear_urls ]] && [ $clear_urls -eq 1 ] - then + if [[ -n $clear_urls ]] && [ $clear_urls -eq 1 ]; then clear_urls=",clear_urls=True" fi yunohost tools shell -c "from yunohost.permission import permission_url; permission_url('$app.$permission' $url $add_url $remove_url $auth_header $clear_urls)" } - # Update a permission for the app # # usage: ynh_permission_update --permission "permission" [--add="group" ["group" ...]] [--remove="group" ["group" ...]] @@ -276,7 +260,7 @@ ynh_permission_url() { ynh_permission_update() { # Declare an array to define the options of this helper. local legacy_args=parltP - local -A args_array=( [p]=permission= [a]=add= [r]=remove= [l]=label= [t]=show_tile= [P]=protected= ) + local -A args_array=([p]=permission= [a]=add= [r]=remove= [l]=label= [t]=show_tile= [P]=protected=) local permission local add local remove @@ -290,8 +274,7 @@ ynh_permission_update() { show_tile=${show_tile:-} protected=${protected:-} - if [[ -n $add ]] - then + if [[ -n $add ]]; then # Convert a list from getopts to python list # Note that getopts separate the args with ';' # For example: @@ -300,8 +283,7 @@ ynh_permission_update() { # add=['alice', 'bob'] add=",add=['${add//';'/"','"}']" fi - if [[ -n $remove ]] - then + if [[ -n $remove ]]; then # Convert a list from getopts to python list # Note that getopts separate the args with ';' # For example: @@ -311,15 +293,12 @@ ynh_permission_update() { remove=",remove=['${remove//';'/"','"}']" fi - if [[ -n $label ]] - then + if [[ -n $label ]]; then label=",label='$label'" fi - if [[ -n $show_tile ]] - then - if [ $show_tile == "true" ] - then + if [[ -n $show_tile ]]; then + if [ $show_tile == "true" ]; then show_tile=",show_tile=True" else show_tile=",show_tile=False" @@ -327,8 +306,7 @@ ynh_permission_update() { fi if [[ -n $protected ]]; then - if [ $protected == "true" ] - then + if [ $protected == "true" ]; then protected=",protected=True" else protected=",protected=False" @@ -351,23 +329,20 @@ ynh_permission_update() { ynh_permission_has_user() { local legacy_args=pu # Declare an array to define the options of this helper. - local -A args_array=( [p]=permission= [u]=user= ) + local -A args_array=([p]=permission= [u]=user=) local permission local user # Manage arguments with getopts ynh_handle_getopts_args "$@" - if ! ynh_permission_exists --permission=$permission - then + if ! ynh_permission_exists --permission=$permission; then return 1 fi # Check both allowed and corresponding_users sections in the json - for section in "allowed" "corresponding_users" - do + for section in "allowed" "corresponding_users"; do if yunohost user permission info "$app.$permission" --output-as json --quiet \ - | jq -e --arg user $user --arg section $section '.[$section] | index($user)' >/dev/null - then + | jq -e --arg user $user --arg section $section '.[$section] | index($user)' >/dev/null; then return 0 fi done @@ -381,9 +356,8 @@ ynh_permission_has_user() { # | exit: Return 1 if the permission doesn't exist, 0 otherwise # # Requires YunoHost version 4.1.2 or higher. -ynh_legacy_permissions_exists () { - for permission in "skipped" "unprotected" "protected" - do +ynh_legacy_permissions_exists() { + for permission in "skipped" "unprotected" "protected"; do if ynh_permission_exists --permission="legacy_${permission}_uris"; then return 0 fi @@ -402,9 +376,8 @@ ynh_legacy_permissions_exists () { # # You can recreate the required permissions here with ynh_permission_create # fi # Requires YunoHost version 4.1.2 or higher. -ynh_legacy_permissions_delete_all () { - for permission in "skipped" "unprotected" "protected" - do +ynh_legacy_permissions_delete_all() { + for permission in "skipped" "unprotected" "protected"; do if ynh_permission_exists --permission="legacy_${permission}_uris"; then ynh_permission_delete --permission="legacy_${permission}_uris" fi diff --git a/data/helpers.d/php b/data/helpers.d/php index 7c91d89d2..0aff50ee3 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -56,10 +56,10 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} # children ready to answer. # # Requires YunoHost version 4.1.0 or higher. -ynh_add_fpm_config () { +ynh_add_fpm_config() { # Declare an array to define the options of this helper. local legacy_args=vtufpd - local -A args_array=( [v]=phpversion= [t]=use_template [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service ) + local -A args_array=([v]=phpversion= [t]=use_template [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service) local phpversion local use_template local usage @@ -86,8 +86,7 @@ ynh_add_fpm_config () { local old_phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) # If the PHP version changed, remove the old fpm conf - if [ -n "$old_phpversion" ] && [ "$old_phpversion" != "$phpversion" ] - then + 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" @@ -97,25 +96,21 @@ ynh_add_fpm_config () { fi # If the requested PHP version is not the default version for YunoHost - if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] - then + if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ]; then # If the argument --package is used, add the packages to ynh_install_php to install them from sury - if [ -n "$package" ] - then + if [ -n "$package" ]; then local additionnal_packages="--package=$package" else local additionnal_packages="" fi # Install this specific version of PHP. ynh_install_php --phpversion="$phpversion" "$additionnal_packages" - elif [ -n "$package" ] - then + elif [ -n "$package" ]; then # Install the additionnal packages from the default repository ynh_add_app_dependencies --package="$package" fi - if [ $dedicated_service -eq 1 ] - then + if [ $dedicated_service -eq 1 ]; then local fpm_service="${app}-phpfpm" local fpm_config_dir="/etc/php/$phpversion/dedicated-fpm" else @@ -132,12 +127,10 @@ ynh_add_fpm_config () { ynh_app_setting_set --app=$app --key=phpversion --value=$phpversion # Migrate from mutual PHP service to dedicated one. - if [ $dedicated_service -eq 1 ] - then + if [ $dedicated_service -eq 1 ]; then local old_fpm_config_dir="/etc/php/$phpversion/fpm" # If a config file exist in the common pool, move it. - if [ -e "$old_fpm_config_dir/pool.d/$app.conf" ] - then + if [ -e "$old_fpm_config_dir/pool.d/$app.conf" ]; then ynh_print_info --message="Migrate to a dedicated php-fpm service for $app." # Create a backup of the old file before migration ynh_backup_if_checksum_is_different --file="$old_fpm_config_dir/pool.d/$app.conf" @@ -148,8 +141,7 @@ ynh_add_fpm_config () { fi fi - if [ $use_template -eq 1 ] - then + if [ $use_template -eq 1 ]; then # Usage 1, use the template in conf/php-fpm.conf local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf" # Make sure now that the template indeed exists @@ -181,49 +173,45 @@ pm = __PHP_PM__ pm.max_children = __PHP_MAX_CHILDREN__ pm.max_requests = 500 request_terminate_timeout = 1d -" > $phpfpm_path +" >$phpfpm_path - if [ "$php_pm" = "dynamic" ] - then + if [ "$php_pm" = "dynamic" ]; then echo " pm.start_servers = __PHP_START_SERVERS__ pm.min_spare_servers = __PHP_MIN_SPARE_SERVERS__ pm.max_spare_servers = __PHP_MAX_SPARE_SERVERS__ -" >> $phpfpm_path +" >>$phpfpm_path - elif [ "$php_pm" = "ondemand" ] - then + elif [ "$php_pm" = "ondemand" ]; then echo " pm.process_idle_timeout = 10s -" >> $phpfpm_path +" >>$phpfpm_path fi # Concatene the extra config. if [ -e $YNH_APP_BASEDIR/conf/extra_php-fpm.conf ]; then - cat $YNH_APP_BASEDIR/conf/extra_php-fpm.conf >> "$phpfpm_path" + cat $YNH_APP_BASEDIR/conf/extra_php-fpm.conf >>"$phpfpm_path" fi fi local finalphpconf="$fpm_config_dir/pool.d/$app.conf" ynh_add_config --template="$phpfpm_path" --destination="$finalphpconf" - if [ -e "$YNH_APP_BASEDIR/conf/php-fpm.ini" ] - then + if [ -e "$YNH_APP_BASEDIR/conf/php-fpm.ini" ]; then ynh_print_warn --message="Packagers ! Please do not use a separate php ini file, merge your directives in the pool file instead." ynh_add_config --template="$YNH_APP_BASEDIR/conf/php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini" fi - if [ $dedicated_service -eq 1 ] - then + if [ $dedicated_service -eq 1 ]; then # Create a dedicated php-fpm.conf for the service local globalphpconf=$fpm_config_dir/php-fpm-$app.conf -echo "[global] + echo "[global] pid = /run/php/php__PHPVERSION__-fpm-__APP__.pid error_log = /var/log/php/fpm-php.__APP__.log syslog.ident = php-fpm-__APP__ include = __FINALPHPCONF__ -" > $YNH_APP_BASEDIR/conf/php-fpm-$app.conf +" >$YNH_APP_BASEDIR/conf/php-fpm-$app.conf ynh_add_config --template="$YNH_APP_BASEDIR/conf/php-fpm-$app.conf" --destination="$globalphpconf" @@ -240,7 +228,7 @@ ExecReload=/bin/kill -USR2 \$MAINPID [Install] WantedBy=multi-user.target -" > $YNH_APP_BASEDIR/conf/$fpm_service +" >$YNH_APP_BASEDIR/conf/$fpm_service # Create this dedicated PHP-FPM service ynh_add_systemd_config --service=$fpm_service --template=$fpm_service @@ -252,8 +240,7 @@ WantedBy=multi-user.target ynh_systemd_action --service_name=$fpm_service --action=restart else # Validate that the new php conf doesn't break php-fpm entirely - if ! php-fpm${phpversion} --test 2>/dev/null - then + if ! php-fpm${phpversion} --test 2>/dev/null; then php-fpm${phpversion} --test || true ynh_secure_remove --file="$finalphpconf" ynh_die --message="The new configuration broke php-fpm?" @@ -267,7 +254,7 @@ WantedBy=multi-user.target # usage: ynh_remove_fpm_config # # Requires YunoHost version 2.7.2 or higher. -ynh_remove_fpm_config () { +ynh_remove_fpm_config() { local fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir) local fpm_service=$(ynh_app_setting_get --app=$app --key=fpm_service) local dedicated_service=$(ynh_app_setting_get --app=$app --key=fpm_dedicated_service) @@ -279,20 +266,17 @@ ynh_remove_fpm_config () { phpversion="${phpversion:-$YNH_DEFAULT_PHP_VERSION}" # Assume default PHP files if not set - if [ -z "$fpm_config_dir" ] - then + if [ -z "$fpm_config_dir" ]; then fpm_config_dir="/etc/php/$YNH_DEFAULT_PHP_VERSION/fpm" fpm_service="php$YNH_DEFAULT_PHP_VERSION-fpm" fi ynh_secure_remove --file="$fpm_config_dir/pool.d/$app.conf" - if [ -e $fpm_config_dir/conf.d/20-$app.ini ] - then + if [ -e $fpm_config_dir/conf.d/20-$app.ini ]; then ynh_secure_remove --file="$fpm_config_dir/conf.d/20-$app.ini" fi - if [ $dedicated_service -eq 1 ] - then + if [ $dedicated_service -eq 1 ]; then # Remove the dedicated service PHP-FPM service for the app ynh_remove_systemd_config --service=$fpm_service # Remove the global PHP-FPM conf @@ -304,8 +288,7 @@ ynh_remove_fpm_config () { fi # If the PHP version used is not the default version for YunoHost - if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] - then + if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ]; then # Remove this specific version of PHP ynh_remove_php fi @@ -320,10 +303,10 @@ ynh_remove_fpm_config () { # | arg: -p, --package= - Additionnal PHP packages to install # # Requires YunoHost version 3.8.1 or higher. -ynh_install_php () { +ynh_install_php() { # Declare an array to define the options of this helper. local legacy_args=vp - local -A args_array=( [v]=phpversion= [p]=package= ) + local -A args_array=([v]=phpversion= [p]=package=) local phpversion local package # Manage arguments with getopts @@ -333,8 +316,7 @@ ynh_install_php () { # Store phpversion into the config of this app ynh_app_setting_set $app phpversion $phpversion - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] - then + if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ]; then ynh_die --message="Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION" fi @@ -342,8 +324,7 @@ ynh_install_php () { touch /etc/php/ynh_app_version # Do not add twice the same line - if ! grep --quiet "$YNH_APP_INSTANCE_NAME:" "/etc/php/ynh_app_version" - then + if ! grep --quiet "$YNH_APP_INSTANCE_NAME:" "/etc/php/ynh_app_version"; then # Store the ID of this app and the version of PHP requested for it echo "$YNH_APP_INSTANCE_NAME:$phpversion" | tee --append "/etc/php/ynh_app_version" fi @@ -370,14 +351,12 @@ ynh_install_php () { # usage: ynh_install_php # # Requires YunoHost version 3.8.1 or higher. -ynh_remove_php () { +ynh_remove_php() { # Get the version of PHP used by this app local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] || [ -z "$phpversion" ] - then - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] - then + if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] || [ -z "$phpversion" ]; then + if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ]; then ynh_print_err "Do not use ynh_remove_php to remove php$YNH_DEFAULT_PHP_VERSION !" fi return 0 @@ -390,8 +369,7 @@ ynh_remove_php () { sed --in-place "/$YNH_APP_INSTANCE_NAME:$phpversion/d" "/etc/php/ynh_app_version" # If no other app uses this version of PHP, remove it. - if ! grep --quiet "$phpversion" "/etc/php/ynh_app_version" - then + if ! grep --quiet "$phpversion" "/etc/php/ynh_app_version"; then # Remove the service from the admin panel if ynh_package_is_installed --package="php${phpversion}-fpm"; then yunohost service remove php${phpversion}-fpm @@ -421,10 +399,10 @@ ynh_remove_php () { # high - High usage, frequently visited website. # # | arg: -p, --print - Print the result (intended for debug purpose only when packaging the app) -ynh_get_scalable_phpfpm () { +ynh_get_scalable_phpfpm() { local legacy_args=ufp # Declare an array to define the options of this helper. - local -A args_array=( [u]=usage= [f]=footprint= [p]=print ) + local -A args_array=([u]=usage= [f]=footprint= [p]=print) local usage local footprint local print @@ -435,38 +413,30 @@ ynh_get_scalable_phpfpm () { usage=${usage,,} print=${print:-0} - if [ "$footprint" = "low" ] - then + if [ "$footprint" = "low" ]; then footprint=20 - elif [ "$footprint" = "medium" ] - then + elif [ "$footprint" = "medium" ]; then footprint=35 - elif [ "$footprint" = "high" ] - then + elif [ "$footprint" = "high" ]; then footprint=50 fi # Define the factor to determine min_spare_servers # to avoid having too few children ready to start for heavy apps - if [ $footprint -le 20 ] - then + if [ $footprint -le 20 ]; then min_spare_servers_factor=8 - elif [ $footprint -le 35 ] - then + elif [ $footprint -le 35 ]; then min_spare_servers_factor=5 else min_spare_servers_factor=3 fi # Define the way the process manager handle child processes. - if [ "$usage" = "low" ] - then + if [ "$usage" = "low" ]; then php_pm=ondemand - elif [ "$usage" = "medium" ] - then + elif [ "$usage" = "medium" ]; then php_pm=dynamic - elif [ "$usage" = "high" ] - then + elif [ "$usage" = "high" ]; then php_pm=static else ynh_die --message="Does not recognize '$usage' as an usage value." @@ -477,8 +447,7 @@ ynh_get_scalable_phpfpm () { at_least_one() { # Do not allow value below 1 - if [ $1 -le 0 ] - then + if [ $1 -le 0 ]; then echo 1 else echo $1 @@ -488,20 +457,18 @@ ynh_get_scalable_phpfpm () { # Define pm.max_children # The value of pm.max_children is the total amount of ram divide by 2 and divide again by the footprint of a pool for this app. # So if PHP-FPM start the maximum of children, it won't exceed half of the ram. - php_max_children=$(( $max_ram / 2 / $footprint )) + php_max_children=$(($max_ram / 2 / $footprint)) # If process manager is set as static, use half less children. # Used as static, there's always as many children as the value of pm.max_children - if [ "$php_pm" = "static" ] - then - php_max_children=$(( $php_max_children / 2 )) + if [ "$php_pm" = "static" ]; then + php_max_children=$(($php_max_children / 2)) fi php_max_children=$(at_least_one $php_max_children) # To not overload the proc, limit the number of children to 4 times the number of cores. local core_number=$(nproc) - local max_proc=$(( $core_number * 4 )) - if [ $php_max_children -gt $max_proc ] - then + local max_proc=$(($core_number * 4)) + if [ $php_max_children -gt $max_proc ]; then php_max_children=$max_proc fi @@ -511,16 +478,15 @@ ynh_get_scalable_phpfpm () { php_max_children=$php_forced_max_children fi - if [ "$php_pm" = "dynamic" ] - then + if [ "$php_pm" = "dynamic" ]; then # Define pm.start_servers, pm.min_spare_servers and pm.max_spare_servers for a dynamic process manager - php_min_spare_servers=$(( $php_max_children / $min_spare_servers_factor )) + php_min_spare_servers=$(($php_max_children / $min_spare_servers_factor)) php_min_spare_servers=$(at_least_one $php_min_spare_servers) - php_max_spare_servers=$(( $php_max_children / 2 )) + php_max_spare_servers=$(($php_max_children / 2)) php_max_spare_servers=$(at_least_one $php_max_spare_servers) - php_start_servers=$(( $php_min_spare_servers + ( $php_max_spare_servers - $php_min_spare_servers ) /2 )) + php_start_servers=$(($php_min_spare_servers + ($php_max_spare_servers - $php_min_spare_servers) / 2)) php_start_servers=$(at_least_one $php_start_servers) else php_min_spare_servers=0 @@ -528,27 +494,22 @@ ynh_get_scalable_phpfpm () { php_start_servers=0 fi - if [ $print -eq 1 ] - then + if [ $print -eq 1 ]; then ynh_debug --message="Footprint=${footprint}Mb by pool." ynh_debug --message="Process manager=$php_pm" ynh_debug --message="Max RAM=${max_ram}Mb" - if [ "$php_pm" != "static" ] - then - ynh_debug --message="\nMax estimated footprint=$(( $php_max_children * $footprint ))" - ynh_debug --message="Min estimated footprint=$(( $php_min_spare_servers * $footprint ))" + if [ "$php_pm" != "static" ]; then + ynh_debug --message="\nMax estimated footprint=$(($php_max_children * $footprint))" + ynh_debug --message="Min estimated footprint=$(($php_min_spare_servers * $footprint))" fi - if [ "$php_pm" = "dynamic" ] - then - ynh_debug --message="Estimated average footprint=$(( $php_max_spare_servers * $footprint ))" - elif [ "$php_pm" = "static" ] - then - ynh_debug --message="Estimated footprint=$(( $php_max_children * $footprint ))" + if [ "$php_pm" = "dynamic" ]; then + ynh_debug --message="Estimated average footprint=$(($php_max_spare_servers * $footprint))" + elif [ "$php_pm" = "static" ]; then + ynh_debug --message="Estimated footprint=$(($php_max_children * $footprint))" fi ynh_debug --message="\nRaw php-fpm values:" ynh_debug --message="pm.max_children = $php_max_children" - if [ "$php_pm" = "dynamic" ] - then + if [ "$php_pm" = "dynamic" ]; then ynh_debug --message="pm.start_servers = $php_start_servers" ynh_debug --message="pm.min_spare_servers = $php_min_spare_servers" ynh_debug --message="pm.max_spare_servers = $php_max_spare_servers" @@ -569,10 +530,10 @@ YNH_COMPOSER_VERSION=${YNH_COMPOSER_VERSION:-$YNH_DEFAULT_COMPOSER_VERSION} # | arg: -c, --commands - Commands to execute. # # Requires YunoHost version 4.2 or higher. -ynh_composer_exec () { +ynh_composer_exec() { # Declare an array to define the options of this helper. local legacy_args=vwc - declare -Ar args_array=( [v]=phpversion= [w]=workdir= [c]=commands= ) + declare -Ar args_array=([v]=phpversion= [w]=workdir= [c]=commands=) local phpversion local workdir local commands @@ -595,10 +556,10 @@ ynh_composer_exec () { # | arg: -c, --composerversion - Composer version to install # # Requires YunoHost version 4.2 or higher. -ynh_install_composer () { +ynh_install_composer() { # 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=) + declare -Ar args_array=([v]=phpversion= [w]=workdir= [a]=install_args= [c]=composerversion=) local phpversion local workdir local install_args @@ -612,7 +573,7 @@ ynh_install_composer () { curl -sS https://getcomposer.org/installer \ | COMPOSER_HOME="$workdir/.composer" \ - php${phpversion} -- --quiet --install-dir="$workdir" --version=$composerversion \ + php${phpversion} -- --quiet --install-dir="$workdir" --version=$composerversion \ || ynh_die --message="Unable to install Composer." # install dependencies diff --git a/data/helpers.d/postgresql b/data/helpers.d/postgresql index 12738a922..992474dd5 100644 --- a/data/helpers.d/postgresql +++ b/data/helpers.d/postgresql @@ -46,8 +46,7 @@ ynh_psql_execute_as_root() { ynh_handle_getopts_args "$@" database="${database:-}" - if [ -n "$database" ] - then + if [ -n "$database" ]; then database="--database=$database" fi @@ -72,8 +71,7 @@ ynh_psql_execute_file_as_root() { ynh_handle_getopts_args "$@" database="${database:-}" - if [ -n "$database" ] - then + if [ -n "$database" ]; then database="--database=$database" fi @@ -175,8 +173,7 @@ ynh_psql_user_exists() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - if ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT rolname FROM pg_roles WHERE rolname='$user';" | grep --quiet "$user" - then + if ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT rolname FROM pg_roles WHERE rolname='$user';" | grep --quiet "$user"; then return 1 else return 0 @@ -198,8 +195,7 @@ ynh_psql_database_exists() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - if ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database" - then + if ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database"; then return 1 else return 0 @@ -269,16 +265,14 @@ ynh_psql_remove_db() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - if ynh_psql_database_exists --database=$db_name - then # Check if the database exists - ynh_psql_drop_db $db_name # Remove the database + if ynh_psql_database_exists --database=$db_name; then # Check if the database exists + ynh_psql_drop_db $db_name # Remove the database else ynh_print_warn --message="Database $db_name not found" fi # Remove psql user if it exists - if ynh_psql_user_exists --user=$db_user - then + if ynh_psql_user_exists --user=$db_user; then ynh_psql_drop_user $db_user else ynh_print_warn --message="User $db_user not found" @@ -310,8 +304,7 @@ ynh_psql_test_if_first_run() { # If this is the very first time, we define the root password # and configure a few things - if [ ! -f "$PSQL_ROOT_PWD_FILE" ] - then + if [ ! -f "$PSQL_ROOT_PWD_FILE" ]; then local pg_hba=/etc/postgresql/$PSQL_VERSION/main/pg_hba.conf local psql_root_password="$(ynh_string_random)" diff --git a/data/helpers.d/setting b/data/helpers.d/setting index 66bce9717..cd231c6ba 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -10,7 +10,7 @@ ynh_app_setting_get() { # Declare an array to define the options of this helper. local legacy_args=ak - local -A args_array=( [a]=app= [k]=key= ) + local -A args_array=([a]=app= [k]=key=) local app local key # Manage arguments with getopts @@ -34,7 +34,7 @@ ynh_app_setting_get() { ynh_app_setting_set() { # Declare an array to define the options of this helper. local legacy_args=akv - local -A args_array=( [a]=app= [k]=key= [v]=value= ) + local -A args_array=([a]=app= [k]=key= [v]=value=) local app local key local value @@ -58,7 +58,7 @@ ynh_app_setting_set() { ynh_app_setting_delete() { # Declare an array to define the options of this helper. local legacy_args=ak - local -A args_array=( [a]=app= [k]=key= ) + local -A args_array=([a]=app= [k]=key=) local app local key # Manage arguments with getopts @@ -76,8 +76,7 @@ ynh_app_setting_delete() { # # [internal] # -ynh_app_setting() -{ +ynh_app_setting() { set +o xtrace # set +x ACTION="$1" APP="$2" KEY="$3" VALUE="${4:-}" python3 - < /dev/null \ + dd if=/dev/urandom bs=1 count=1000 2>/dev/null \ | tr --complement --delete 'A-Za-z0-9' \ | sed --quiet 's/\(.\{'"$length"'\}\).*/\1/p' } @@ -34,10 +34,10 @@ ynh_string_random() { # sub-expressions can be used (see sed manual page for more information) # # Requires YunoHost version 2.6.4 or higher. -ynh_replace_string () { +ynh_replace_string() { # Declare an array to define the options of this helper. local legacy_args=mrf - local -A args_array=( [m]=match_string= [r]=replace_string= [f]=target_file= ) + local -A args_array=([m]=match_string= [r]=replace_string= [f]=target_file=) local match_string local replace_string local target_file @@ -65,10 +65,10 @@ ynh_replace_string () { # characters, you can't use some regular expressions and sub-expressions. # # Requires YunoHost version 2.7.7 or higher. -ynh_replace_special_string () { +ynh_replace_special_string() { # Declare an array to define the options of this helper. local legacy_args=mrf - local -A args_array=( [m]=match_string= [r]=replace_string= [f]=target_file= ) + local -A args_array=([m]=match_string= [r]=replace_string= [f]=target_file=) local match_string local replace_string local target_file @@ -97,10 +97,10 @@ ynh_replace_special_string () { # Underscorify the string (replace - and . by _) # # Requires YunoHost version 2.2.4 or higher. -ynh_sanitize_dbid () { +ynh_sanitize_dbid() { # Declare an array to define the options of this helper. local legacy_args=n - local -A args_array=( [n]=db_name= ) + local -A args_array=([n]=db_name=) local db_name # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -127,20 +127,20 @@ ynh_sanitize_dbid () { # | arg: -p, --path_url= - URL path to normalize before using it # # Requires YunoHost version 2.6.4 or higher. -ynh_normalize_url_path () { +ynh_normalize_url_path() { # Declare an array to define the options of this helper. local legacy_args=p - local -A args_array=( [p]=path_url= ) + local -A args_array=([p]=path_url=) local path_url # Manage arguments with getopts ynh_handle_getopts_args "$@" test -n "$path_url" || ynh_die --message="ynh_normalize_url_path expect a URL path as first argument and received nothing." - if [ "${path_url:0:1}" != "/" ]; then # If the first character is not a / - path_url="/$path_url" # Add / at begin of path variable + if [ "${path_url:0:1}" != "/" ]; then # If the first character is not a / + path_url="/$path_url" # Add / at begin of path variable fi - if [ "${path_url:${#path_url}-1}" == "/" ] && [ ${#path_url} -gt 1 ]; then # If the last character is a / and that not the only character. - path_url="${path_url:0:${#path_url}-1}" # Delete the last character + if [ "${path_url:${#path_url}-1}" == "/" ] && [ ${#path_url} -gt 1 ]; then # If the last character is a / and that not the only character. + path_url="${path_url:0:${#path_url}-1}" # Delete the last character fi echo $path_url } diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index d0f88b5f7..71b605181 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -12,10 +12,10 @@ # format and how placeholders are replaced with actual variables. # # Requires YunoHost version 4.1.0 or higher. -ynh_add_systemd_config () { +ynh_add_systemd_config() { # Declare an array to define the options of this helper. local legacy_args=stv - local -A args_array=( [s]=service= [t]=template= [v]=others_var=) + local -A args_array=([s]=service= [t]=template= [v]=others_var=) local service local template local others_var @@ -39,18 +39,17 @@ ynh_add_systemd_config () { # | arg: -s, --service= - Service name (optionnal, $app by default) # # Requires YunoHost version 2.7.2 or higher. -ynh_remove_systemd_config () { +ynh_remove_systemd_config() { # Declare an array to define the options of this helper. local legacy_args=s - local -A args_array=( [s]=service= ) + local -A args_array=([s]=service=) local service # Manage arguments with getopts ynh_handle_getopts_args "$@" local service="${service:-$app}" local finalsystemdconf="/etc/systemd/system/$service.service" - if [ -e "$finalsystemdconf" ] - then + if [ -e "$finalsystemdconf" ]; then ynh_systemd_action --service_name=$service --action=stop systemctl disable $service --quiet ynh_secure_remove --file="$finalsystemdconf" @@ -72,7 +71,7 @@ ynh_remove_systemd_config () { ynh_systemd_action() { # Declare an array to define the options of this helper. local legacy_args=nalpte - local -A args_array=( [n]=service_name= [a]=action= [l]=line_match= [p]=log_path= [t]=timeout= [e]=length= ) + local -A args_array=([n]=service_name= [a]=action= [l]=line_match= [p]=log_path= [t]=timeout= [e]=length=) local service_name local action local line_match @@ -89,25 +88,22 @@ ynh_systemd_action() { timeout=${timeout:-300} # Manage case of service already stopped - if [ "$action" == "stop" ] && ! systemctl is-active --quiet $service_name - then + if [ "$action" == "stop" ] && ! systemctl is-active --quiet $service_name; then return 0 fi # Start to read the log - if [[ -n "$line_match" ]] - then + if [[ -n "$line_match" ]]; then local templog="$(mktemp)" # Following the starting of the app in its log - if [ "$log_path" == "systemd" ] - then + if [ "$log_path" == "systemd" ]; then # Read the systemd journal - journalctl --unit=$service_name --follow --since=-0 --quiet > "$templog" & + journalctl --unit=$service_name --follow --since=-0 --quiet >"$templog" & # Get the PID of the journalctl command local pid_tail=$! else # Read the specified log file - tail --follow=name --retry --lines=0 "$log_path" > "$templog" 2>&1 & + tail --follow=name --retry --lines=0 "$log_path" >"$templog" 2>&1 & # Get the PID of the tail command local pid_tail=$! fi @@ -119,13 +115,11 @@ ynh_systemd_action() { fi # If the service fails to perform the action - if ! systemctl $action $service_name - then + if ! systemctl $action $service_name; then # Show syslog for this service ynh_exec_err journalctl --quiet --no-hostname --no-pager --lines=$length --unit=$service_name # If a log is specified for this service, show also the content of this log - if [ -e "$log_path" ] - then + if [ -e "$log_path" ]; then ynh_exec_err tail --lines=$length "$log_path" fi ynh_clean_check_starting @@ -133,15 +127,12 @@ ynh_systemd_action() { fi # Start the timeout and try to find line_match - if [[ -n "${line_match:-}" ]] - then + if [[ -n "${line_match:-}" ]]; then set +x local i=0 - for i in $(seq 1 $timeout) - do + 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 + 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 @@ -154,13 +145,11 @@ ynh_systemd_action() { if [ $i -ge 3 ]; then echo "" >&2 fi - if [ $i -eq $timeout ] - then + if [ $i -eq $timeout ]; then ynh_print_warn --message="The service $service_name didn't fully executed the action ${action} before the timeout." ynh_print_warn --message="Please find here an extract of the end of the log of the service $service_name:" ynh_exec_warn journalctl --quiet --no-hostname --no-pager --lines=$length --unit=$service_name - if [ -e "$log_path" ] - then + if [ -e "$log_path" ]; then ynh_print_warn --message="\-\-\-" ynh_exec_warn tail --lines=$length "$log_path" fi @@ -174,14 +163,12 @@ ynh_systemd_action() { # [internal] # # Requires YunoHost version 3.5.0 or higher. -ynh_clean_check_starting () { - if [ -n "${pid_tail:-}" ] - then +ynh_clean_check_starting() { + if [ -n "${pid_tail:-}" ]; then # Stop the execution of tail. kill -SIGTERM $pid_tail 2>&1 fi - if [ -n "${templog:-}" ] - then + if [ -n "${templog:-}" ]; then ynh_secure_remove --file="$templog" 2>&1 fi } diff --git a/data/helpers.d/user b/data/helpers.d/user index d5ede9f73..aecbd740e 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -12,7 +12,7 @@ ynh_user_exists() { # Declare an array to define the options of this helper. local legacy_args=u - local -A args_array=( [u]=username= ) + local -A args_array=([u]=username=) local username # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -33,7 +33,7 @@ ynh_user_exists() { ynh_user_get_info() { # Declare an array to define the options of this helper. local legacy_args=uk - local -A args_array=( [u]=username= [k]=key= ) + local -A args_array=([u]=username= [k]=key=) local username local key # Manage arguments with getopts @@ -64,7 +64,7 @@ ynh_user_list() { ynh_system_user_exists() { # Declare an array to define the options of this helper. local legacy_args=u - local -A args_array=( [u]=username= ) + local -A args_array=([u]=username=) local username # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -82,7 +82,7 @@ ynh_system_user_exists() { ynh_system_group_exists() { # Declare an array to define the options of this helper. local legacy_args=g - local -A args_array=( [g]=group= ) + local -A args_array=([g]=group=) local group # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -108,10 +108,10 @@ ynh_system_group_exists() { # ``` # # Requires YunoHost version 2.6.4 or higher. -ynh_system_user_create () { +ynh_system_user_create() { # Declare an array to define the options of this helper. local legacy_args=uhs - local -A args_array=( [u]=username= [h]=home_dir= [s]=use_shell [g]=groups= ) + local -A args_array=([u]=username= [h]=home_dir= [s]=use_shell [g]=groups=) local username local home_dir local use_shell @@ -123,17 +123,15 @@ ynh_system_user_create () { home_dir="${home_dir:-}" groups="${groups:-}" - if ! ynh_system_user_exists "$username" # Check if the user exists on the system - then # If the user doesn't exist - if [ -n "$home_dir" ] - then # If a home dir is mentioned + if ! ynh_system_user_exists "$username"; then # Check if the user exists on the system + # If the user doesn't exist + if [ -n "$home_dir" ]; then # If a home dir is mentioned local user_home_dir="--home-dir $home_dir" else local user_home_dir="--no-create-home" fi - if [ $use_shell -eq 1 ] - then # If we want a shell for the user - local shell="" # Use default shell + if [ $use_shell -eq 1 ]; then # If we want a shell for the user + local shell="" # Use default shell else local shell="--shell /usr/sbin/nologin" fi @@ -141,8 +139,7 @@ ynh_system_user_create () { fi local group - for group in $groups - do + for group in $groups; do usermod -a -G "$group" "$username" done } @@ -153,25 +150,23 @@ ynh_system_user_create () { # | arg: -u, --username= - Name of the system user that will be create # # Requires YunoHost version 2.6.4 or higher. -ynh_system_user_delete () { +ynh_system_user_delete() { # Declare an array to define the options of this helper. local legacy_args=u - local -A args_array=( [u]=username= ) + local -A args_array=([u]=username=) local username # Manage arguments with getopts ynh_handle_getopts_args "$@" # Check if the user exists on the system - if ynh_system_user_exists "$username" - then + if ynh_system_user_exists "$username"; then deluser $username else ynh_print_warn --message="The user $username was not found" fi # Check if the group exists on the system - if ynh_system_group_exists "$username" - then + if ynh_system_group_exists "$username"; then delgroup $username fi } diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 061ff324d..eb823b5f0 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -19,25 +19,25 @@ YNH_APP_BASEDIR=${YNH_APP_BASEDIR:-$(realpath ..)} # It prints a warning to inform that the script was failed, and execute the ynh_clean_setup function if used in the app script # # Requires YunoHost version 2.6.4 or higher. -ynh_exit_properly () { +ynh_exit_properly() { local exit_code=$? rm -rf "/var/cache/yunohost/download/" if [ "$exit_code" -eq 0 ]; then - exit 0 # Exit without error if the script ended correctly + exit 0 # Exit without error if the script ended correctly fi - trap '' EXIT # Ignore new exit signals + trap '' EXIT # Ignore new exit signals # Do not exit anymore if a command fail or if a variable is empty - set +o errexit # set +e - set +o nounset # set +u + set +o errexit # set +e + set +o nounset # set +u # Small tempo to avoid the next message being mixed up with other DEBUG messages sleep 0.5 - if type -t ynh_clean_setup > /dev/null; then # Check if the function exist in the app script. - ynh_clean_setup # Call the function to do specific cleaning for the app. + if type -t ynh_clean_setup >/dev/null; then # Check if the function exist in the app script. + ynh_clean_setup # Call the function to do specific cleaning for the app. fi # Exit with error status @@ -55,10 +55,10 @@ ynh_exit_properly () { # and a call to `ynh_clean_setup` is triggered if it has been defined by your script. # # Requires YunoHost version 2.6.4 or higher. -ynh_abort_if_errors () { - set -o errexit # set -e; Exit if a command fail - set -o nounset # set -u; And if a variable is used unset - trap ynh_exit_properly EXIT # Capturing exit signals on shell script +ynh_abort_if_errors() { + set -o errexit # set -e; Exit if a command fail + set -o nounset # set -u; And if a variable is used unset + trap ynh_exit_properly EXIT # Capturing exit signals on shell script } # Download, check integrity, uncompress and patch the source from app.src @@ -99,10 +99,10 @@ ynh_abort_if_errors () { # - Extra files in `sources/extra_files/$src_id` will be copied to dest_dir # # Requires YunoHost version 2.6.4 or higher. -ynh_setup_source () { +ynh_setup_source() { # Declare an array to define the options of this helper. local legacy_args=dsk - local -A args_array=( [d]=dest_dir= [s]=source_id= [k]=keep= ) + local -A args_array=([d]=dest_dir= [s]=source_id= [k]=keep=) local dest_dir local source_id local keep @@ -133,15 +133,13 @@ ynh_setup_source () { src_filename="${source_id}.${src_format}" 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}" mkdir -p /var/cache/yunohost/download/${YNH_APP_ID}/ src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${src_filename}" - if test -e "$local_src" - then + if test -e "$local_src"; then cp $local_src $src_filename else [ -n "$src_url" ] || ynh_die "Couldn't parse SOURCE_URL from $src_file_path ?" @@ -162,15 +160,12 @@ ynh_setup_source () { # Keep files to be backup/restored at the end of the helper # Assuming $dest_dir already exists rm -rf /var/cache/yunohost/files_to_keep_during_setup_source/ - if [ -n "$keep" ] && [ -e "$dest_dir" ] - then + if [ -n "$keep" ] && [ -e "$dest_dir" ]; then local keep_dir=/var/cache/yunohost/files_to_keep_during_setup_source/${YNH_APP_ID} mkdir -p $keep_dir local stuff_to_keep - for stuff_to_keep in $keep - do - if [ -e "$dest_dir/$stuff_to_keep" ] - then + for stuff_to_keep in $keep; do + if [ -e "$dest_dir/$stuff_to_keep" ]; then mkdir --parents "$(dirname "$keep_dir/$stuff_to_keep")" cp --archive "$dest_dir/$stuff_to_keep" "$keep_dir/$stuff_to_keep" fi @@ -180,20 +175,16 @@ ynh_setup_source () { # Extract source into the app dir mkdir --parents "$dest_dir" - if [ -n "${final_path:-}" ] && [ "$dest_dir" == "$final_path" ] - then + if [ -n "${final_path:-}" ] && [ "$dest_dir" == "$final_path" ]; then _ynh_apply_default_permissions $dest_dir fi - if ! "$src_extract" - then + if ! "$src_extract"; then mv $src_filename $dest_dir - elif [ "$src_format" = "zip" ] - then + elif [ "$src_format" = "zip" ]; then # Zip format # Using of a temp directory, because unzip doesn't manage --strip-components - if $src_in_subdir - then + if $src_in_subdir; then local tmp_dir=$(mktemp --directory) unzip -quo $src_filename -d "$tmp_dir" cp --archive $tmp_dir/*/. "$dest_dir" @@ -204,18 +195,15 @@ ynh_setup_source () { ynh_secure_remove --file="$src_filename" else local strip="" - if [ "$src_in_subdir" != "false" ] - then - if [ "$src_in_subdir" == "true" ] - then + if [ "$src_in_subdir" != "false" ]; then + if [ "$src_in_subdir" == "true" ]; then local sub_dirs=1 else local sub_dirs="$src_in_subdir" fi strip="--strip-components $sub_dirs" fi - if [[ "$src_format" =~ ^tar.gz|tar.bz2|tar.xz$ ]] - then + if [[ "$src_format" =~ ^tar.gz|tar.bz2|tar.xz$ ]]; then tar --extract --file=$src_filename --directory="$dest_dir" $strip else ynh_die --message="Archive format unrecognized." @@ -224,17 +212,16 @@ ynh_setup_source () { fi # Apply patches - if [ -d "$YNH_APP_BASEDIR/sources/patches/" ] - then + if [ -d "$YNH_APP_BASEDIR/sources/patches/" ]; then local patches_folder=$(realpath $YNH_APP_BASEDIR/sources/patches/) - if (( $(find $patches_folder -type f -name "${source_id}-*.patch" 2> /dev/null | wc --lines) > "0" )) - then - (cd "$dest_dir" - for p in $patches_folder/${source_id}-*.patch - do - echo $p - patch --strip=1 < $p - done) || ynh_die --message="Unable to apply patches" + if (($(find $patches_folder -type f -name "${source_id}-*.patch" 2>/dev/null | wc --lines) > "0")); then + ( + cd "$dest_dir" + for p in $patches_folder/${source_id}-*.patch; do + echo $p + patch --strip=1 <$p + done + ) || ynh_die --message="Unable to apply patches" fi fi @@ -245,14 +232,11 @@ ynh_setup_source () { # Keep files to be backup/restored at the end of the helper # Assuming $dest_dir already exists - if [ -n "$keep" ] - then + if [ -n "$keep" ]; then local keep_dir=/var/cache/yunohost/files_to_keep_during_setup_source/${YNH_APP_ID} local stuff_to_keep - for stuff_to_keep in $keep - do - if [ -e "$keep_dir/$stuff_to_keep" ] - then + for stuff_to_keep in $keep; do + if [ -e "$keep_dir/$stuff_to_keep" ]; then mkdir --parents "$(dirname "$dest_dir/$stuff_to_keep")" cp --archive "$keep_dir/$stuff_to_keep" "$dest_dir/$stuff_to_keep" fi @@ -276,7 +260,7 @@ ynh_setup_source () { # `$domain` and `$path_url` should be defined externally (and correspond to the domain.tld and the /path (of the app?)) # # Requires YunoHost version 2.6.4 or higher. -ynh_local_curl () { +ynh_local_curl() { # Define url of page to curl local local_page=$(ynh_normalize_url_path $1) local full_path=$path_url$local_page @@ -290,12 +274,10 @@ ynh_local_curl () { # Concatenate all other arguments with '&' to prepare POST data local POST_data="" local arg="" - for arg in "${@:2}" - do + for arg in "${@:2}"; do POST_data="${POST_data}${arg}&" done - if [ -n "$POST_data" ] - then + if [ -n "$POST_data" ]; then # Add --data arg and remove the last character, which is an unecessary '&' POST_data="--data ${POST_data::-1}" fi @@ -353,10 +335,10 @@ ynh_local_curl () { # into the app settings when configuration is done. # # Requires YunoHost version 4.1.0 or higher. -ynh_add_config () { +ynh_add_config() { # Declare an array to define the options of this helper. local legacy_args=tdv - local -A args_array=( [t]=template= [d]=destination= ) + local -A args_array=([t]=template= [d]=destination=) local template local destination # Manage arguments with getopts @@ -414,17 +396,16 @@ ynh_add_config () { # __VAR_2__ by $var_2 # # Requires YunoHost version 4.1.0 or higher. -ynh_replace_vars () { +ynh_replace_vars() { # Declare an array to define the options of this helper. local legacy_args=f - local -A args_array=( [f]=file= ) + local -A args_array=([f]=file=) local file # Manage arguments with getopts ynh_handle_getopts_args "$@" # Replace specific YunoHost variables - if test -n "${path_url:-}" - then + if test -n "${path_url:-}"; then # path_url_slash_less is path_url, or a blank value if path_url is only '/' local path_url_slash_less=${path_url%/} ynh_replace_string --match_string="__PATH__/" --replace_string="$path_url_slash_less/" --target_file="$file" @@ -448,12 +429,11 @@ ynh_replace_vars () { # Replace others variables # List other unique (__ __) variables in $file - local uniques_vars=( $(grep -oP '__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" )) + local uniques_vars=($(grep -oP '__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g")) # Do the replacement local delimit=@ - for one_var in "${uniques_vars[@]}" - do + for one_var in "${uniques_vars[@]}"; do # Validate that one_var is indeed defined # -v checks if the variable is defined, for example: # -v FOO tests if $FOO is defined @@ -509,7 +489,7 @@ ynh_replace_vars () { ynh_read_var_in_file() { # Declare an array to define the options of this helper. local legacy_args=fka - local -A args_array=( [f]=file= [k]=key= [a]=after=) + local -A args_array=([f]=file= [k]=key= [a]=after=) local file local key local after @@ -523,11 +503,9 @@ ynh_read_var_in_file() { # Get the line number after which we search for the variable local line_number=1 - if [[ -n "$after" ]]; - then + if [[ -n "$after" ]]; then line_number=$(grep -n $after $file | cut -d: -f1) - if [[ -z "$line_number" ]]; - then + if [[ -z "$line_number" ]]; then set -o xtrace # set -x return 1 fi @@ -545,7 +523,7 @@ ynh_read_var_in_file() { if [[ "$ext" =~ ^ini|env$ ]]; then comments="[;#]" fi - if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then + if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then comments="//" fi local list='\[\s*['$string']?\w+['$string']?\]' @@ -567,10 +545,10 @@ ynh_read_var_in_file() { local expression="$(echo "$expression_with_comment" | sed "s@$comments[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" local first_char="${expression:0:1}" - if [[ "$first_char" == '"' ]] ; then - echo "$expression" | grep -m1 -o -P '"\K([^"](\\")?)*[^\\](?=")' | head -n1 | sed 's/\\"/"/g' - elif [[ "$first_char" == "'" ]] ; then - echo "$expression" | grep -m1 -o -P "'\K([^'](\\\\')?)*[^\\\\](?=')" | head -n1 | sed "s/\\\\'/'/g" + if [[ "$first_char" == '"' ]]; then + echo "$expression" | grep -m1 -o -P '"\K([^"](\\")?)*[^\\](?=")' | head -n1 | sed 's/\\"/"/g' + elif [[ "$first_char" == "'" ]]; then + echo "$expression" | grep -m1 -o -P "'\K([^'](\\\\')?)*[^\\\\](?=')" | head -n1 | sed "s/\\\\'/'/g" else echo "$expression" fi @@ -588,7 +566,7 @@ ynh_read_var_in_file() { ynh_write_var_in_file() { # Declare an array to define the options of this helper. local legacy_args=fkva - local -A args_array=( [f]=file= [k]=key= [v]=value= [a]=after=) + local -A args_array=([f]=file= [k]=key= [v]=value= [a]=after=) local file local key local value @@ -603,11 +581,9 @@ ynh_write_var_in_file() { # Get the line number after which we search for the variable local line_number=1 - if [[ -n "$after" ]]; - then + if [[ -n "$after" ]]; then line_number=$(grep -n $after $file | cut -d: -f1) - if [[ -z "$line_number" ]]; - then + if [[ -z "$line_number" ]]; then set -o xtrace # set -x return 1 fi @@ -626,7 +602,7 @@ ynh_write_var_in_file() { if [[ "$ext" =~ ^ini|env$ ]]; then comments="[;#]" fi - if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then + if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then comments="//" fi local list='\[\s*['$string']?\w+['$string']?\]' @@ -650,22 +626,22 @@ ynh_write_var_in_file() { value="$(echo "$value" | sed 's/\\/\\\\/g')" local first_char="${expression:0:1}" delimiter=$'\001' - if [[ "$first_char" == '"' ]] ; then + if [[ "$first_char" == '"' ]]; then # \ and sed is quite complex you need 2 \\ to get one in a sed # So we need \\\\ to go through 2 sed value="$(echo "$value" | sed 's/"/\\\\"/g')" sed -ri "${range}s$delimiter"'(^'"${var_part}"'")([^"]|\\")*("[\s;,]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}"'"'"${endline}${delimiter}i" ${file} - elif [[ "$first_char" == "'" ]] ; then + elif [[ "$first_char" == "'" ]]; then # \ and sed is quite complex you need 2 \\ to get one in a sed # However double quotes implies to double \\ to # So we need \\\\\\\\ to go through 2 sed and 1 double quotes str value="$(echo "$value" | sed "s/'/\\\\\\\\'/g")" sed -ri "${range}s$delimiter(^${var_part}')([^']|\\')*('"'[\s,;]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}'${endline}${delimiter}i" ${file} else - if [[ "$value" == *"'"* ]] || [[ "$value" == *'"'* ]] || [[ "$ext" =~ ^php|py|json|js$ ]] ; then + if [[ "$value" == *"'"* ]] || [[ "$value" == *'"'* ]] || [[ "$ext" =~ ^php|py|json|js$ ]]; then value='\"'"$(echo "$value" | sed 's/"/\\\\"/g')"'\"' fi - if [[ "$ext" =~ ^yaml|yml$ ]] ; then + if [[ "$ext" =~ ^yaml|yml$ ]]; then value=" $value" fi sed -ri "${range}s$delimiter(^${var_part}).*\$$delimiter\1${value}${endline}${delimiter}i" ${file} @@ -673,7 +649,6 @@ ynh_write_var_in_file() { set -o xtrace # set -x } - # Render templates with Jinja2 # # [internal] @@ -691,7 +666,7 @@ ynh_render_template() { # Taken from https://stackoverflow.com/a/35009576 python3 -c 'import os, sys, jinja2; sys.stdout.write( jinja2.Template(sys.stdin.read() - ).render(os.environ));' < $template_path > $output_path + ).render(os.environ));' <$template_path >$output_path } # Fetch the Debian release codename @@ -700,7 +675,7 @@ ynh_render_template() { # | ret: The Debian release codename (i.e. jessie, stretch, ...) # # Requires YunoHost version 2.7.12 or higher. -ynh_get_debian_release () { +ynh_get_debian_release() { echo $(lsb_release --codename --short) } @@ -730,10 +705,10 @@ properly with chmod/chown." # | arg: -f, --file= - File or directory to remove # # Requires YunoHost version 2.6.4 or higher. -ynh_secure_remove () { +ynh_secure_remove() { # Declare an array to define the options of this helper. local legacy_args=f - local -A args_array=( [f]=file= ) + local -A args_array=([f]=file=) local file # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -743,24 +718,21 @@ ynh_secure_remove () { /var/www \ /home/yunohost.app" - if [ $# -ge 2 ] - then + if [ $# -ge 2 ]; then ynh_print_warn --message="/!\ Packager ! You provided more than one argument to ynh_secure_remove but it will be ignored... Use this helper with one argument at time." fi - if [[ -z "$file" ]] - then + if [[ -z "$file" ]]; then ynh_print_warn --message="ynh_secure_remove called with empty argument, ignoring." - elif [[ "$forbidden_path" =~ "$file" \ - # Match all paths or subpaths in $forbidden_path - || "$file" =~ ^/[[:alnum:]]+$ \ + elif [[ "$forbidden_path" =~ "$file" || + + "$file" =~ ^/[[:alnum:]]+$ || + + "${file:${#file}-1}" = "/" ]]; then # Match all paths or subpaths in $forbidden_path # Match all first level paths from / (Like /var, /root, etc...) - || "${file:${#file}-1}" = "/" ]] # Match if the path finishes by /. Because it seems there is an empty variable - then ynh_print_warn --message="Not deleting '$file' because it is not an acceptable path to delete." - elif [ -e "$file" ] - then + elif [ -e "$file" ]; then rm --recursive "$file" else ynh_print_info --message="'$file' wasn't deleted because it doesn't exist." @@ -781,16 +753,12 @@ ynh_get_plain_key() { # an info to be redacted by the core local key_=$1 shift - while read line - do - if [[ "$founded" == "1" ]] - then + while read line; do + if [[ "$founded" == "1" ]]; then [[ "$line" =~ ^${prefix}[^#] ]] && return echo $line - elif [[ "$line" =~ ^${prefix}${key_}$ ]] - then - if [[ -n "${1:-}" ]] - then + elif [[ "$line" =~ ^${prefix}${key_}$ ]]; then + if [[ -n "${1:-}" ]]; then prefix+="#" key_=$1 shift @@ -809,10 +777,10 @@ ynh_get_plain_key() { # | ret: the value associate to that key # # Requires YunoHost version 3.5.0 or higher. -ynh_read_manifest () { +ynh_read_manifest() { # Declare an array to define the options of this helper. local legacy_args=mk - local -A args_array=( [m]=manifest= [k]=manifest_key= ) + local -A args_array=([m]=manifest= [k]=manifest_key=) local manifest local manifest_key # Manage arguments with getopts @@ -839,20 +807,19 @@ ynh_read_manifest () { # For example, if the manifest contains `4.3-2~ynh3` the function will return `4.3-2` # # Requires YunoHost version 3.5.0 or higher. -ynh_app_upstream_version () { +ynh_app_upstream_version() { # Declare an array to define the options of this helper. local legacy_args=m - local -A args_array=( [m]=manifest= ) + local -A args_array=([m]=manifest=) local manifest # Manage arguments with getopts ynh_handle_getopts_args "$@" manifest="${manifest:-}" - if [[ "$manifest" != "" ]] && [[ -e "$manifest" ]]; - then - version_key_=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version") + if [[ "$manifest" != "" ]] && [[ -e "$manifest" ]]; then + version_key_=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version") else - version_key_=$YNH_APP_MANIFEST_VERSION + version_key_=$YNH_APP_MANIFEST_VERSION fi echo "${version_key_/~ynh*/}" @@ -869,10 +836,10 @@ ynh_app_upstream_version () { # For example, if the manifest contains `4.3-2~ynh3` the function will return `3` # # Requires YunoHost version 3.5.0 or higher. -ynh_app_package_version () { +ynh_app_package_version() { # Declare an array to define the options of this helper. local legacy_args=m - local -A args_array=( [m]=manifest= ) + local -A args_array=([m]=manifest=) local manifest # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -894,11 +861,10 @@ ynh_app_package_version () { # sudo yunohost app upgrade --force # ``` # Requires YunoHost version 3.5.0 or higher. -ynh_check_app_version_changed () { +ynh_check_app_version_changed() { local return_value=${YNH_APP_UPGRADE_TYPE} - if [ "$return_value" == "UPGRADE_FULL" ] || [ "$return_value" == "UPGRADE_FORCED" ] || [ "$return_value" == "DOWNGRADE_FORCED" ] - then + if [ "$return_value" == "UPGRADE_FULL" ] || [ "$return_value" == "UPGRADE_FORCED" ] || [ "$return_value" == "DOWNGRADE_FORCED" ]; then return_value="UPGRADE_APP" fi @@ -927,7 +893,7 @@ ynh_check_app_version_changed () { # Requires YunoHost version 3.8.0 or higher. ynh_compare_current_package_version() { local legacy_args=cv - declare -Ar args_array=( [c]=comparison= [v]=version= ) + declare -Ar args_array=([c]=comparison= [v]=version=) local version local comparison # Manage arguments with getopts @@ -936,8 +902,7 @@ ynh_compare_current_package_version() { local current_version=$YNH_APP_CURRENT_VERSION # Check the syntax of the versions - if [[ ! $version =~ '~ynh' ]] || [[ ! $current_version =~ '~ynh' ]] - then + if [[ ! $version =~ '~ynh' ]] || [[ ! $current_version =~ '~ynh' ]]; then ynh_die --message="Invalid argument for version." fi @@ -972,13 +937,11 @@ _ynh_apply_default_permissions() { local ynh_requirement=$(jq -r '.requirements.yunohost' $YNH_APP_BASEDIR/manifest.json | tr -d '>= ') - if [ -z "$ynh_requirement" ] || [ "$ynh_requirement" == "null" ] || dpkg --compare-versions $ynh_requirement ge 4.2 - then + if [ -z "$ynh_requirement" ] || [ "$ynh_requirement" == "null" ] || dpkg --compare-versions $ynh_requirement ge 4.2; then chmod o-rwx $target chmod g-w $target chown -R root:root $target - if ynh_system_user_exists $app - then + if ynh_system_user_exists $app; then chown $app:$app $target fi fi From 6048d1b0b35c7f1ad3618c5e4afbe5bf1ae4ff49 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 16:24:33 +0200 Subject: [PATCH 124/142] Typo ogod --- data/helpers.d/utils | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index eb823b5f0..0e38423f2 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -748,13 +748,13 @@ ynh_secure_remove() { # (Deprecated, use --output-as json and jq instead) ynh_get_plain_key() { local prefix="#" - local founded=0 + local found=0 # We call this key_ so that it's not caught as # an info to be redacted by the core local key_=$1 shift while read line; do - if [[ "$founded" == "1" ]]; then + if [[ "$found" == "1" ]]; then [[ "$line" =~ ^${prefix}[^#] ]] && return echo $line elif [[ "$line" =~ ^${prefix}${key_}$ ]]; then @@ -763,7 +763,7 @@ ynh_get_plain_key() { key_=$1 shift else - founded=1 + found=1 fi fi done From 5a7a719661424fe7d283d253438f0ab4bd988c65 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 16:29:14 +0200 Subject: [PATCH 125/142] Also lint/reformat core bash hooks --- .../backup/50-conf_manually_modified_files | 7 +- data/hooks/conf_regen/01-yunohost | 314 +++++++++--------- data/hooks/conf_regen/02-ssl | 97 +++--- data/hooks/conf_regen/06-slapd | 260 +++++++-------- data/hooks/conf_regen/09-nslcd | 16 +- data/hooks/conf_regen/10-apt | 15 +- data/hooks/conf_regen/12-metronome | 102 +++--- data/hooks/conf_regen/15-nginx | 226 +++++++------ data/hooks/conf_regen/19-postfix | 114 ++++--- data/hooks/conf_regen/25-dovecot | 80 ++--- data/hooks/conf_regen/31-rspamd | 88 ++--- data/hooks/conf_regen/34-mysql | 94 +++--- data/hooks/conf_regen/35-redis | 8 +- data/hooks/conf_regen/37-mdns | 63 ++-- data/hooks/conf_regen/43-dnsmasq | 111 +++---- data/hooks/conf_regen/46-nsswitch | 16 +- data/hooks/conf_regen/52-fail2ban | 24 +- data/hooks/post_user_create/ynh_multimedia | 2 +- data/hooks/restore/05-conf_ldap | 12 +- .../restore/50-conf_manually_modified_files | 3 +- 20 files changed, 819 insertions(+), 833 deletions(-) diff --git a/data/hooks/backup/50-conf_manually_modified_files b/data/hooks/backup/50-conf_manually_modified_files index 2cca11afb..bdea14113 100644 --- a/data/hooks/backup/50-conf_manually_modified_files +++ b/data/hooks/backup/50-conf_manually_modified_files @@ -6,13 +6,12 @@ YNH_CWD="${YNH_BACKUP_DIR%/}/conf/manually_modified_files" mkdir -p "$YNH_CWD" cd "$YNH_CWD" -yunohost tools shell -c "from yunohost.regenconf import manually_modified_files; print('\n'.join(manually_modified_files()))" > ./manually_modified_files_list +yunohost tools shell -c "from yunohost.regenconf import manually_modified_files; print('\n'.join(manually_modified_files()))" >./manually_modified_files_list ynh_backup --src_path="./manually_modified_files_list" -for file in $(cat ./manually_modified_files_list) -do +for file in $(cat ./manually_modified_files_list); do [[ -e $file ]] && ynh_backup --src_path="$file" done - + ynh_backup --src_path="/etc/ssowat/conf.json.persistent" diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index 14af66933..341efce9e 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -3,129 +3,128 @@ set -e do_init_regen() { - if [[ $EUID -ne 0 ]]; then - echo "You must be root to run this script" 1>&2 - exit 1 - fi + if [[ $EUID -ne 0 ]]; then + echo "You must be root to run this script" 1>&2 + exit 1 + fi - cd /usr/share/yunohost/templates/yunohost + cd /usr/share/yunohost/templates/yunohost - [[ -d /etc/yunohost ]] || mkdir -p /etc/yunohost + [[ -d /etc/yunohost ]] || mkdir -p /etc/yunohost - # set default current_host - [[ -f /etc/yunohost/current_host ]] \ - || echo "yunohost.org" > /etc/yunohost/current_host + # set default current_host + [[ -f /etc/yunohost/current_host ]] \ + || echo "yunohost.org" >/etc/yunohost/current_host - # copy default services and firewall - [[ -f /etc/yunohost/firewall.yml ]] \ - || cp firewall.yml /etc/yunohost/firewall.yml + # copy default services and firewall + [[ -f /etc/yunohost/firewall.yml ]] \ + || cp firewall.yml /etc/yunohost/firewall.yml - # allow users to access /media directory - [[ -d /etc/skel/media ]] \ - || (mkdir -p /media && ln -s /media /etc/skel/media) + # allow users to access /media directory + [[ -d /etc/skel/media ]] \ + || (mkdir -p /media && ln -s /media /etc/skel/media) - # Cert folders - mkdir -p /etc/yunohost/certs - chown -R root:ssl-cert /etc/yunohost/certs - chmod 750 /etc/yunohost/certs + # Cert folders + mkdir -p /etc/yunohost/certs + chown -R root:ssl-cert /etc/yunohost/certs + chmod 750 /etc/yunohost/certs - # App folders - mkdir -p /etc/yunohost/apps - chmod 700 /etc/yunohost/apps - mkdir -p /home/yunohost.app - chmod 755 /home/yunohost.app + # App folders + mkdir -p /etc/yunohost/apps + chmod 700 /etc/yunohost/apps + mkdir -p /home/yunohost.app + chmod 755 /home/yunohost.app - # Domain settings - mkdir -p /etc/yunohost/domains - chmod 700 /etc/yunohost/domains + # Domain settings + mkdir -p /etc/yunohost/domains + chmod 700 /etc/yunohost/domains - # Backup folders - mkdir -p /home/yunohost.backup/archives - chmod 750 /home/yunohost.backup/archives - chown root:root /home/yunohost.backup/archives # This is later changed to admin:root once admin user exists + # Backup folders + mkdir -p /home/yunohost.backup/archives + chmod 750 /home/yunohost.backup/archives + chown root:root /home/yunohost.backup/archives # This is later changed to admin:root once admin user exists - # Empty ssowat json persistent conf - echo "{}" > '/etc/ssowat/conf.json.persistent' - chmod 644 /etc/ssowat/conf.json.persistent - chown root:root /etc/ssowat/conf.json.persistent + # Empty ssowat json persistent conf + echo "{}" >'/etc/ssowat/conf.json.persistent' + chmod 644 /etc/ssowat/conf.json.persistent + chown root:root /etc/ssowat/conf.json.persistent - # Empty service conf - touch /etc/yunohost/services.yml + # Empty service conf + touch /etc/yunohost/services.yml - mkdir -p /var/cache/yunohost/repo - chown root:root /var/cache/yunohost - chmod 700 /var/cache/yunohost + mkdir -p /var/cache/yunohost/repo + chown root:root /var/cache/yunohost + chmod 700 /var/cache/yunohost - cp yunoprompt.service /etc/systemd/system/yunoprompt.service - cp dpkg-origins /etc/dpkg/origins/yunohost + cp yunoprompt.service /etc/systemd/system/yunoprompt.service + cp dpkg-origins /etc/dpkg/origins/yunohost - # Change dpkg vendor - # see https://wiki.debian.org/Derivatives/Guidelines#Vendor - readlink -f /etc/dpkg/origins/default | grep -q debian \ - && rm -f /etc/dpkg/origins/default \ - && ln -s /etc/dpkg/origins/yunohost /etc/dpkg/origins/default + # Change dpkg vendor + # see https://wiki.debian.org/Derivatives/Guidelines#Vendor + readlink -f /etc/dpkg/origins/default | grep -q debian \ + && rm -f /etc/dpkg/origins/default \ + && ln -s /etc/dpkg/origins/yunohost /etc/dpkg/origins/default } do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/yunohost + cd /usr/share/yunohost/templates/yunohost - # Legacy code that can be removed once on bullseye - touch /etc/yunohost/services.yml - yunohost tools shell -c "from yunohost.service import _get_services, _save_services; _save_services(_get_services())" + # Legacy code that can be removed once on bullseye + touch /etc/yunohost/services.yml + yunohost tools shell -c "from yunohost.service import _get_services, _save_services; _save_services(_get_services())" - mkdir -p $pending_dir/etc/systemd/system - mkdir -p $pending_dir/etc/cron.d/ - mkdir -p $pending_dir/etc/cron.daily/ + mkdir -p $pending_dir/etc/systemd/system + mkdir -p $pending_dir/etc/cron.d/ + mkdir -p $pending_dir/etc/cron.daily/ - # add cron job for diagnosis to be ran at 7h and 19h + a random delay between - # 0 and 20min, meant to avoid every instances running their diagnosis at - # exactly the same time, which may overload the diagnosis server. - cat > $pending_dir/etc/cron.d/yunohost-diagnosis << EOF + # add cron job for diagnosis to be ran at 7h and 19h + a random delay between + # 0 and 20min, meant to avoid every instances running their diagnosis at + # exactly the same time, which may overload the diagnosis server. + cat >$pending_dir/etc/cron.d/yunohost-diagnosis < /dev/null 2>/dev/null || echo "Running the automatic diagnosis failed miserably" EOF - # Cron job that upgrade the app list everyday - cat > $pending_dir/etc/cron.daily/yunohost-fetch-apps-catalog << EOF + # Cron job that upgrade the app list everyday + cat >$pending_dir/etc/cron.daily/yunohost-fetch-apps-catalog < /dev/null) & EOF - # Cron job that renew lets encrypt certificates if there's any that needs renewal - cat > $pending_dir/etc/cron.daily/yunohost-certificate-renew << EOF + # Cron job that renew lets encrypt certificates if there's any that needs renewal + cat >$pending_dir/etc/cron.daily/yunohost-certificate-renew </dev/null - then - cat > $pending_dir/etc/cron.d/yunohost-dyndns << EOF + # If we subscribed to a dyndns domain, add the corresponding cron + # - delay between 0 and 60 secs to spread the check over a 1 min window + # - do not run the command if some process already has the lock, to avoid queuing hundreds of commands... + if ls -l /etc/yunohost/dyndns/K*.private 2>/dev/null; then + cat >$pending_dir/etc/cron.d/yunohost-dyndns <> /dev/null EOF - fi + fi - # legacy stuff to avoid yunohost reporting etckeeper as manually modified - # (this make sure that the hash is null / file is flagged as to-delete) - mkdir -p $pending_dir/etc/etckeeper - touch $pending_dir/etc/etckeeper/etckeeper.conf + # legacy stuff to avoid yunohost reporting etckeeper as manually modified + # (this make sure that the hash is null / file is flagged as to-delete) + mkdir -p $pending_dir/etc/etckeeper + touch $pending_dir/etc/etckeeper/etckeeper.conf - # Skip ntp if inside a container (inspired from the conf of systemd-timesyncd) - mkdir -p ${pending_dir}/etc/systemd/system/ntp.service.d/ - echo " + # Skip ntp if inside a container (inspired from the conf of systemd-timesyncd) + mkdir -p ${pending_dir}/etc/systemd/system/ntp.service.d/ + echo " [Unit] ConditionCapability=CAP_SYS_TIME ConditionVirtualization=!container -" > ${pending_dir}/etc/systemd/system/ntp.service.d/ynh-override.conf +" >${pending_dir}/etc/systemd/system/ntp.service.d/ynh-override.conf - # Make nftable conflict with yunohost-firewall - mkdir -p ${pending_dir}/etc/systemd/system/nftables.service.d/ - cat > ${pending_dir}/etc/systemd/system/nftables.service.d/ynh-override.conf << EOF + # Make nftable conflict with yunohost-firewall + mkdir -p ${pending_dir}/etc/systemd/system/nftables.service.d/ + cat >${pending_dir}/etc/systemd/system/nftables.service.d/ynh-override.conf < ${pending_dir}/etc/systemd/logind.conf.d/ynh-override.conf << EOF + # Don't suspend computer on LidSwitch + mkdir -p ${pending_dir}/etc/systemd/logind.conf.d/ + cat >${pending_dir}/etc/systemd/logind.conf.d/ynh-override.conf </dev/null) - chmod 600 $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null) + # Misc configuration / state files + chown root:root $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null) + chmod 600 $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null) - # Apps folder, custom hooks folder - [[ ! -e /etc/yunohost/hooks.d ]] || (chown root /etc/yunohost/hooks.d && chmod 700 /etc/yunohost/hooks.d) - [[ ! -e /etc/yunohost/apps ]] || (chown root /etc/yunohost/apps && chmod 700 /etc/yunohost/apps) - [[ ! -e /etc/yunohost/domains ]] || (chown root /etc/yunohost/domains && chmod 700 /etc/yunohost/domains) + # Apps folder, custom hooks folder + [[ ! -e /etc/yunohost/hooks.d ]] || (chown root /etc/yunohost/hooks.d && chmod 700 /etc/yunohost/hooks.d) + [[ ! -e /etc/yunohost/apps ]] || (chown root /etc/yunohost/apps && chmod 700 /etc/yunohost/apps) + [[ ! -e /etc/yunohost/domains ]] || (chown root /etc/yunohost/domains && chmod 700 /etc/yunohost/domains) - # Create ssh.app and sftp.app groups if they don't exist yet - grep -q '^ssh.app:' /etc/group || groupadd ssh.app - grep -q '^sftp.app:' /etc/group || groupadd sftp.app + # Create ssh.app and sftp.app groups if they don't exist yet + grep -q '^ssh.app:' /etc/group || groupadd ssh.app + grep -q '^sftp.app:' /etc/group || groupadd sftp.app - # Propagates changes in systemd service config overrides - [[ ! "$regen_conf_files" =~ "ntp.service.d/ynh-override.conf" ]] || { systemctl daemon-reload; systemctl restart ntp; } - [[ ! "$regen_conf_files" =~ "nftables.service.d/ynh-override.conf" ]] || systemctl daemon-reload - [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || systemctl daemon-reload - if [[ "$regen_conf_files" =~ "yunoprompt.service" ]] - then - systemctl daemon-reload - action=$([[ -e /etc/systemd/system/yunoprompt.service ]] && echo 'enable' || echo 'disable') - systemctl $action yunoprompt --quiet --now - fi - if [[ "$regen_conf_files" =~ "proc-hidepid.service" ]] - then - systemctl daemon-reload - action=$([[ -e /etc/systemd/system/proc-hidepid.service ]] && echo 'enable' || echo 'disable') - systemctl $action proc-hidepid --quiet --now - fi + # Propagates changes in systemd service config overrides + [[ ! "$regen_conf_files" =~ "ntp.service.d/ynh-override.conf" ]] || { + systemctl daemon-reload + systemctl restart ntp + } + [[ ! "$regen_conf_files" =~ "nftables.service.d/ynh-override.conf" ]] || systemctl daemon-reload + [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || systemctl daemon-reload + if [[ "$regen_conf_files" =~ "yunoprompt.service" ]]; then + systemctl daemon-reload + action=$([[ -e /etc/systemd/system/yunoprompt.service ]] && echo 'enable' || echo 'disable') + systemctl $action yunoprompt --quiet --now + fi + if [[ "$regen_conf_files" =~ "proc-hidepid.service" ]]; then + systemctl daemon-reload + action=$([[ -e /etc/systemd/system/proc-hidepid.service ]] && echo 'enable' || echo 'disable') + systemctl $action proc-hidepid --quiet --now + fi - # Change dpkg vendor - # see https://wiki.debian.org/Derivatives/Guidelines#Vendor - readlink -f /etc/dpkg/origins/default | grep -q debian \ - && rm -f /etc/dpkg/origins/default \ - && ln -s /etc/dpkg/origins/yunohost /etc/dpkg/origins/default + # Change dpkg vendor + # see https://wiki.debian.org/Derivatives/Guidelines#Vendor + readlink -f /etc/dpkg/origins/default | grep -q debian \ + && rm -f /etc/dpkg/origins/default \ + && ln -s /etc/dpkg/origins/yunohost /etc/dpkg/origins/default } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl index 2b40c77a2..03478552c 100755 --- a/data/hooks/conf_regen/02-ssl +++ b/data/hooks/conf_regen/02-ssl @@ -23,7 +23,7 @@ regen_local_ca() { # (Update the serial so that it's specific to this very instance) # N.B. : the weird RANDFILE thing comes from: # https://stackoverflow.com/questions/94445/using-openssl-what-does-unable-to-write-random-state-mean - RANDFILE=.rnd openssl rand -hex 19 > serial + RANDFILE=.rnd openssl rand -hex 19 >serial rm -f index.txt touch index.txt cp /usr/share/yunohost/templates/ssl/openssl.cnf openssl.ca.cnf @@ -50,73 +50,72 @@ regen_local_ca() { do_init_regen() { - LOGFILE=/tmp/yunohost-ssl-init - echo "" > $LOGFILE - chown root:root $LOGFILE - chmod 640 $LOGFILE + LOGFILE=/tmp/yunohost-ssl-init + echo "" >$LOGFILE + chown root:root $LOGFILE + chmod 640 $LOGFILE - # Make sure this conf exists - mkdir -p ${ssl_dir} - cp /usr/share/yunohost/templates/ssl/openssl.cnf ${ssl_dir}/openssl.ca.cnf + # Make sure this conf exists + mkdir -p ${ssl_dir} + cp /usr/share/yunohost/templates/ssl/openssl.cnf ${ssl_dir}/openssl.ca.cnf - # create default certificates - if [[ ! -f "$ynh_ca" ]]; then - regen_local_ca yunohost.org >>$LOGFILE - fi + # create default certificates + if [[ ! -f "$ynh_ca" ]]; then + regen_local_ca yunohost.org >>$LOGFILE + fi - if [[ ! -f "$ynh_crt" ]]; then - echo -e "\n# Creating initial key and certificate \n" >>$LOGFILE + if [[ ! -f "$ynh_crt" ]]; then + echo -e "\n# Creating initial key and certificate \n" >>$LOGFILE - openssl req -new \ - -config "$openssl_conf" \ - -days 730 \ - -out "${ssl_dir}/certs/yunohost_csr.pem" \ - -keyout "${ssl_dir}/certs/yunohost_key.pem" \ - -nodes -batch &>>$LOGFILE + openssl req -new \ + -config "$openssl_conf" \ + -days 730 \ + -out "${ssl_dir}/certs/yunohost_csr.pem" \ + -keyout "${ssl_dir}/certs/yunohost_key.pem" \ + -nodes -batch &>>$LOGFILE - openssl ca \ - -config "$openssl_conf" \ - -days 730 \ - -in "${ssl_dir}/certs/yunohost_csr.pem" \ - -out "${ssl_dir}/certs/yunohost_crt.pem" \ - -batch &>>$LOGFILE + openssl ca \ + -config "$openssl_conf" \ + -days 730 \ + -in "${ssl_dir}/certs/yunohost_csr.pem" \ + -out "${ssl_dir}/certs/yunohost_crt.pem" \ + -batch &>>$LOGFILE - chmod 640 "${ssl_dir}/certs/yunohost_key.pem" - chmod 640 "${ssl_dir}/certs/yunohost_crt.pem" + chmod 640 "${ssl_dir}/certs/yunohost_key.pem" + chmod 640 "${ssl_dir}/certs/yunohost_crt.pem" - cp "${ssl_dir}/certs/yunohost_key.pem" "$ynh_key" - cp "${ssl_dir}/certs/yunohost_crt.pem" "$ynh_crt" - ln -sf "$ynh_crt" /etc/ssl/certs/yunohost_crt.pem - ln -sf "$ynh_key" /etc/ssl/private/yunohost_key.pem - fi + cp "${ssl_dir}/certs/yunohost_key.pem" "$ynh_key" + cp "${ssl_dir}/certs/yunohost_crt.pem" "$ynh_crt" + ln -sf "$ynh_crt" /etc/ssl/certs/yunohost_crt.pem + ln -sf "$ynh_key" /etc/ssl/private/yunohost_key.pem + fi - chown -R root:ssl-cert /etc/yunohost/certs/yunohost.org/ - chmod o-rwx /etc/yunohost/certs/yunohost.org/ + chown -R root:ssl-cert /etc/yunohost/certs/yunohost.org/ + chmod o-rwx /etc/yunohost/certs/yunohost.org/ - install -D -m 644 $openssl_conf "${ssl_dir}/openssl.cnf" + install -D -m 644 $openssl_conf "${ssl_dir}/openssl.cnf" } do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/ssl + cd /usr/share/yunohost/templates/ssl - install -D -m 644 openssl.cnf "${pending_dir}/${ssl_dir}/openssl.cnf" + install -D -m 644 openssl.cnf "${pending_dir}/${ssl_dir}/openssl.cnf" } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - current_local_ca_domain=$(openssl x509 -in $ynh_ca -text | tr ',' '\n' | grep Issuer | awk '{print $4}') - main_domain=$(cat /etc/yunohost/current_host) + current_local_ca_domain=$(openssl x509 -in $ynh_ca -text | tr ',' '\n' | grep Issuer | awk '{print $4}') + main_domain=$(cat /etc/yunohost/current_host) - if [[ "$current_local_ca_domain" != "$main_domain" ]] - then - regen_local_ca $main_domain - # Idk how useful this is, but this was in the previous python code (domain.main_domain()) - ln -sf /etc/yunohost/certs/$domain/crt.pem /etc/ssl/certs/yunohost_crt.pem - ln -sf /etc/yunohost/certs/$domain/key.pem /etc/ssl/private/yunohost_key.pem - fi + if [[ "$current_local_ca_domain" != "$main_domain" ]]; then + regen_local_ca $main_domain + # Idk how useful this is, but this was in the previous python code (domain.main_domain()) + ln -sf /etc/yunohost/certs/$domain/crt.pem /etc/ssl/certs/yunohost_crt.pem + ln -sf /etc/yunohost/certs/$domain/key.pem /etc/ssl/private/yunohost_key.pem + fi } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/06-slapd b/data/hooks/conf_regen/06-slapd index 49b1bf354..f7a7acf64 100755 --- a/data/hooks/conf_regen/06-slapd +++ b/data/hooks/conf_regen/06-slapd @@ -8,19 +8,19 @@ config="/usr/share/yunohost/templates/slapd/config.ldif" db_init="/usr/share/yunohost/templates/slapd/db_init.ldif" do_init_regen() { - if [[ $EUID -ne 0 ]]; then - echo "You must be root to run this script" 1>&2 - exit 1 - fi + if [[ $EUID -ne 0 ]]; then + echo "You must be root to run this script" 1>&2 + exit 1 + fi - do_pre_regen "" + do_pre_regen "" - # Drop current existing slapd data + # Drop current existing slapd data - rm -rf /var/backups/*.ldapdb - rm -rf /var/backups/slapd-* + rm -rf /var/backups/*.ldapdb + rm -rf /var/backups/slapd-* - debconf-set-selections << EOF + debconf-set-selections <&1 \ - | grep -v "none elapsed\|Closing DB" || true - chown -R openldap: /etc/ldap/slapd.d + rm -rf /etc/ldap/slapd.d + mkdir -p /etc/ldap/slapd.d + slapadd -F /etc/ldap/slapd.d -b cn=config -l "$config" 2>&1 \ + | grep -v "none elapsed\|Closing DB" || true + chown -R openldap: /etc/ldap/slapd.d - rm -rf /var/lib/ldap - mkdir -p /var/lib/ldap - slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org -l "$db_init" 2>&1 \ - | grep -v "none elapsed\|Closing DB" || true - chown -R openldap: /var/lib/ldap + rm -rf /var/lib/ldap + mkdir -p /var/lib/ldap + slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org -l "$db_init" 2>&1 \ + | grep -v "none elapsed\|Closing DB" || true + chown -R openldap: /var/lib/ldap - nscd -i group || true - nscd -i passwd || true + nscd -i group || true + nscd -i passwd || true - systemctl restart slapd + systemctl restart slapd - # We don't use mkhomedir_helper because 'admin' may not be recognized - # when this script is ran in a chroot (e.g. ISO install) - # We also refer to admin as uid 1007 for the same reason - if [ ! -d /home/admin ] - then - cp -r /etc/skel /home/admin - chown -R 1007:1007 /home/admin - fi + # We don't use mkhomedir_helper because 'admin' may not be recognized + # when this script is ran in a chroot (e.g. ISO install) + # We also refer to admin as uid 1007 for the same reason + if [ ! -d /home/admin ]; then + cp -r /etc/skel /home/admin + chown -R 1007:1007 /home/admin + fi } _regenerate_slapd_conf() { - # Validate the new slapd config - # To do so, we have to use the .ldif to generate the config directory - # so we use a temporary directory slapd_new.d - rm -Rf /etc/ldap/slapd_new.d - mkdir /etc/ldap/slapd_new.d - slapadd -b cn=config -l "$config" -F /etc/ldap/slapd_new.d/ 2>&1 \ - | grep -v "none elapsed\|Closing DB" || true - # Actual validation (-Q is for quiet, -u is for dry-run) - slaptest -Q -u -F /etc/ldap/slapd_new.d + # Validate the new slapd config + # To do so, we have to use the .ldif to generate the config directory + # so we use a temporary directory slapd_new.d + rm -Rf /etc/ldap/slapd_new.d + mkdir /etc/ldap/slapd_new.d + slapadd -b cn=config -l "$config" -F /etc/ldap/slapd_new.d/ 2>&1 \ + | grep -v "none elapsed\|Closing DB" || true + # Actual validation (-Q is for quiet, -u is for dry-run) + slaptest -Q -u -F /etc/ldap/slapd_new.d - # "Commit" / apply the new config (meaning we delete the old one and replace - # it with the new one) - rm -Rf /etc/ldap/slapd.d - mv /etc/ldap/slapd_new.d /etc/ldap/slapd.d + # "Commit" / apply the new config (meaning we delete the old one and replace + # it with the new one) + rm -Rf /etc/ldap/slapd.d + mv /etc/ldap/slapd_new.d /etc/ldap/slapd.d - chown -R openldap:openldap /etc/ldap/slapd.d/ + chown -R openldap:openldap /etc/ldap/slapd.d/ } do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - # remove temporary backup file - rm -f "$tmp_backup_dir_file" + # remove temporary backup file + rm -f "$tmp_backup_dir_file" - # Define if we need to migrate from hdb to mdb - curr_backend=$(grep '^database' /etc/ldap/slapd.conf 2>/dev/null | awk '{print $2}') - if [ -e /etc/ldap/slapd.conf ] && [ -n "$curr_backend" ] && \ - [ $curr_backend != 'mdb' ]; then - backup_dir="/var/backups/dc=yunohost,dc=org-${curr_backend}-$(date +%s)" - mkdir -p "$backup_dir" - slapcat -b dc=yunohost,dc=org -l "${backup_dir}/dc=yunohost-dc=org.ldif" - echo "$backup_dir" > "$tmp_backup_dir_file" - fi + # Define if we need to migrate from hdb to mdb + curr_backend=$(grep '^database' /etc/ldap/slapd.conf 2>/dev/null | awk '{print $2}') + if [ -e /etc/ldap/slapd.conf ] && [ -n "$curr_backend" ] \ + && [ $curr_backend != 'mdb' ]; then + backup_dir="/var/backups/dc=yunohost,dc=org-${curr_backend}-$(date +%s)" + mkdir -p "$backup_dir" + slapcat -b dc=yunohost,dc=org -l "${backup_dir}/dc=yunohost-dc=org.ldif" + echo "$backup_dir" >"$tmp_backup_dir_file" + fi - # create needed directories - ldap_dir="${pending_dir}/etc/ldap" - schema_dir="${ldap_dir}/schema" - mkdir -p "$ldap_dir" "$schema_dir" + # create needed directories + ldap_dir="${pending_dir}/etc/ldap" + schema_dir="${ldap_dir}/schema" + mkdir -p "$ldap_dir" "$schema_dir" - # remove legacy configuration file - [ ! -f /etc/ldap/slapd-yuno.conf ] || touch "${ldap_dir}/slapd-yuno.conf" - [ ! -f /etc/ldap/slapd.conf ] || touch "${ldap_dir}/slapd.conf" - [ ! -f /etc/ldap/schema/yunohost.schema ] || touch "${schema_dir}/yunohost.schema" + # remove legacy configuration file + [ ! -f /etc/ldap/slapd-yuno.conf ] || touch "${ldap_dir}/slapd-yuno.conf" + [ ! -f /etc/ldap/slapd.conf ] || touch "${ldap_dir}/slapd.conf" + [ ! -f /etc/ldap/schema/yunohost.schema ] || touch "${schema_dir}/yunohost.schema" - cd /usr/share/yunohost/templates/slapd + cd /usr/share/yunohost/templates/slapd - # copy configuration files - cp -a ldap.conf "$ldap_dir" - cp -a sudo.ldif mailserver.ldif permission.ldif "$schema_dir" + # copy configuration files + cp -a ldap.conf "$ldap_dir" + cp -a sudo.ldif mailserver.ldif permission.ldif "$schema_dir" - mkdir -p ${pending_dir}/etc/systemd/system/slapd.service.d/ - cp systemd-override.conf ${pending_dir}/etc/systemd/system/slapd.service.d/ynh-override.conf + mkdir -p ${pending_dir}/etc/systemd/system/slapd.service.d/ + cp systemd-override.conf ${pending_dir}/etc/systemd/system/slapd.service.d/ynh-override.conf - install -D -m 644 slapd.default "${pending_dir}/etc/default/slapd" + install -D -m 644 slapd.default "${pending_dir}/etc/default/slapd" } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - # fix some permissions - echo "Enforce permissions on ldap/slapd directories and certs ..." - # penldap user should be in the ssl-cert group to let it access the certificate for TLS - usermod -aG ssl-cert openldap - chown -R openldap:openldap /etc/ldap/schema/ - chown -R openldap:openldap /etc/ldap/slapd.d/ + # fix some permissions + echo "Enforce permissions on ldap/slapd directories and certs ..." + # penldap user should be in the ssl-cert group to let it access the certificate for TLS + usermod -aG ssl-cert openldap + chown -R openldap:openldap /etc/ldap/schema/ + chown -R openldap:openldap /etc/ldap/slapd.d/ - # If we changed the systemd ynh-override conf - if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/slapd.service.d/ynh-override.conf$" - then - systemctl daemon-reload - systemctl restart slapd - sleep 3 - fi + # If we changed the systemd ynh-override conf + if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/slapd.service.d/ynh-override.conf$"; then + systemctl daemon-reload + systemctl restart slapd + sleep 3 + fi - # For some reason, old setups don't have the admins group defined... - if ! slapcat | grep -q 'cn=admins,ou=groups,dc=yunohost,dc=org' - then - slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org <<< \ -"dn: cn=admins,ou=groups,dc=yunohost,dc=org + # For some reason, old setups don't have the admins group defined... + if ! slapcat | grep -q 'cn=admins,ou=groups,dc=yunohost,dc=org'; then + slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org <<< \ + "dn: cn=admins,ou=groups,dc=yunohost,dc=org cn: admins gidNumber: 4001 memberUid: admin objectClass: posixGroup objectClass: top" - chown -R openldap: /var/lib/ldap - systemctl restart slapd - nscd -i group - fi + chown -R openldap: /var/lib/ldap + systemctl restart slapd + nscd -i group + fi - [ -z "$regen_conf_files" ] && exit 0 + [ -z "$regen_conf_files" ] && exit 0 - # regenerate LDAP config directory from slapd.conf - echo "Regenerate LDAP config directory from config.ldif" - _regenerate_slapd_conf + # regenerate LDAP config directory from slapd.conf + echo "Regenerate LDAP config directory from config.ldif" + _regenerate_slapd_conf - # If there's a backup, re-import its data - backup_dir=$(cat "$tmp_backup_dir_file" 2>/dev/null || true) - if [[ -n "$backup_dir" && -f "${backup_dir}/dc=yunohost-dc=org.ldif" ]]; then - # regenerate LDAP config directory and import database as root - echo "Import the database using slapadd" - slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org -l "${backup_dir}/dc=yunohost-dc=org.ldif" - chown -R openldap:openldap /var/lib/ldap 2>&1 - fi + # If there's a backup, re-import its data + backup_dir=$(cat "$tmp_backup_dir_file" 2>/dev/null || true) + if [[ -n "$backup_dir" && -f "${backup_dir}/dc=yunohost-dc=org.ldif" ]]; then + # regenerate LDAP config directory and import database as root + echo "Import the database using slapadd" + slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org -l "${backup_dir}/dc=yunohost-dc=org.ldif" + chown -R openldap:openldap /var/lib/ldap 2>&1 + fi - echo "Running slapdindex" - su openldap -s "/bin/bash" -c "/usr/sbin/slapindex" + echo "Running slapdindex" + su openldap -s "/bin/bash" -c "/usr/sbin/slapindex" - echo "Reloading slapd" - systemctl force-reload slapd + echo "Reloading slapd" + systemctl force-reload slapd - # on slow hardware/vm this regen conf would exit before the admin user that - # is stored in ldap is available because ldap seems to slow to restart - # so we'll wait either until we are able to log as admin or until a timeout - # is reached - # we need to do this because the next hooks executed after this one during - # postinstall requires to run as admin thus breaking postinstall on slow - # hardware which mean yunohost can't be correctly installed on those hardware - # and this sucks - # wait a maximum time of 5 minutes - # yes, force-reload behave like a restart - number_of_wait=0 - while ! su admin -c '' && ((number_of_wait < 60)) - do - sleep 5 - ((number_of_wait += 1)) - done + # on slow hardware/vm this regen conf would exit before the admin user that + # is stored in ldap is available because ldap seems to slow to restart + # so we'll wait either until we are able to log as admin or until a timeout + # is reached + # we need to do this because the next hooks executed after this one during + # postinstall requires to run as admin thus breaking postinstall on slow + # hardware which mean yunohost can't be correctly installed on those hardware + # and this sucks + # wait a maximum time of 5 minutes + # yes, force-reload behave like a restart + number_of_wait=0 + while ! su admin -c '' && ((number_of_wait < 60)); do + sleep 5 + ((number_of_wait += 1)) + done } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/09-nslcd b/data/hooks/conf_regen/09-nslcd index cefd05cd3..ff1c05433 100755 --- a/data/hooks/conf_regen/09-nslcd +++ b/data/hooks/conf_regen/09-nslcd @@ -3,23 +3,23 @@ set -e do_init_regen() { - do_pre_regen "" - systemctl restart nslcd + do_pre_regen "" + systemctl restart nslcd } do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/nslcd + cd /usr/share/yunohost/templates/nslcd - install -D -m 644 nslcd.conf "${pending_dir}/etc/nslcd.conf" + install -D -m 644 nslcd.conf "${pending_dir}/etc/nslcd.conf" } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - [[ -z "$regen_conf_files" ]] \ - || systemctl restart nslcd + [[ -z "$regen_conf_files" ]] \ + || systemctl restart nslcd } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/10-apt b/data/hooks/conf_regen/10-apt index 1c80b6706..da0620e59 100755 --- a/data/hooks/conf_regen/10-apt +++ b/data/hooks/conf_regen/10-apt @@ -8,15 +8,14 @@ do_pre_regen() { mkdir --parents "${pending_dir}/etc/apt/preferences.d" packages_to_refuse_from_sury="php php-fpm php-mysql php-xml php-zip php-mbstring php-ldap php-gd php-curl php-bz2 php-json php-sqlite3 php-intl openssl libssl1.1 libssl-dev" - for package in $packages_to_refuse_from_sury - do + for package in $packages_to_refuse_from_sury; do echo " Package: $package Pin: origin \"packages.sury.org\" -Pin-Priority: -1" >> "${pending_dir}/etc/apt/preferences.d/extra_php_version" +Pin-Priority: -1" >>"${pending_dir}/etc/apt/preferences.d/extra_php_version" done - echo " + echo " # PLEASE READ THIS WARNING AND DON'T EDIT THIS FILE @@ -43,15 +42,15 @@ Pin-Priority: -1 Package: bind9 Pin: release * Pin-Priority: -1 -" >> "${pending_dir}/etc/apt/preferences.d/ban_packages" +" >>"${pending_dir}/etc/apt/preferences.d/ban_packages" } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - # Make sure php7.3 is the default version when using php in cli - update-alternatives --set php /usr/bin/php7.3 + # Make sure php7.3 is the default version when using php in cli + update-alternatives --set php /usr/bin/php7.3 } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/12-metronome b/data/hooks/conf_regen/12-metronome index ab9fca173..5dfa7b5dc 100755 --- a/data/hooks/conf_regen/12-metronome +++ b/data/hooks/conf_regen/12-metronome @@ -3,71 +3,71 @@ set -e do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/metronome + cd /usr/share/yunohost/templates/metronome - # create directories for pending conf - metronome_dir="${pending_dir}/etc/metronome" - metronome_conf_dir="${metronome_dir}/conf.d" - mkdir -p "$metronome_conf_dir" + # create directories for pending conf + metronome_dir="${pending_dir}/etc/metronome" + metronome_conf_dir="${metronome_dir}/conf.d" + mkdir -p "$metronome_conf_dir" - # retrieve variables - main_domain=$(cat /etc/yunohost/current_host) + # retrieve variables + main_domain=$(cat /etc/yunohost/current_host) - # install main conf file - cat metronome.cfg.lua \ - | sed "s/{{ main_domain }}/${main_domain}/g" \ - > "${metronome_dir}/metronome.cfg.lua" + # install main conf file + cat metronome.cfg.lua \ + | sed "s/{{ main_domain }}/${main_domain}/g" \ + >"${metronome_dir}/metronome.cfg.lua" - # add domain conf files - for domain in $YNH_DOMAINS; do - cat domain.tpl.cfg.lua \ - | sed "s/{{ domain }}/${domain}/g" \ - > "${metronome_conf_dir}/${domain}.cfg.lua" - done + # add domain conf files + for domain in $YNH_DOMAINS; do + cat domain.tpl.cfg.lua \ + | sed "s/{{ domain }}/${domain}/g" \ + >"${metronome_conf_dir}/${domain}.cfg.lua" + done - # remove old domain conf files - conf_files=$(ls -1 /etc/metronome/conf.d \ - | awk '/^[^\.]+\.[^\.]+.*\.cfg\.lua$/ { print $1 }') - for file in $conf_files; do - domain=${file%.cfg.lua} - [[ $YNH_DOMAINS =~ $domain ]] \ - || touch "${metronome_conf_dir}/${file}" - done + # remove old domain conf files + conf_files=$(ls -1 /etc/metronome/conf.d \ + | awk '/^[^\.]+\.[^\.]+.*\.cfg\.lua$/ { print $1 }') + for file in $conf_files; do + domain=${file%.cfg.lua} + [[ $YNH_DOMAINS =~ $domain ]] \ + || touch "${metronome_conf_dir}/${file}" + done } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - # retrieve variables - main_domain=$(cat /etc/yunohost/current_host) - - # FIXME : small optimization to do to avoid calling a yunohost command ... - # maybe another env variable like YNH_MAIN_DOMAINS idk - domain_list=$(yunohost domain list --exclude-subdomains --output-as plain --quiet) + # retrieve variables + main_domain=$(cat /etc/yunohost/current_host) - # create metronome directories for domains - for domain in $domain_list; do - mkdir -p "/var/lib/metronome/${domain//./%2e}/pep" - # http_upload directory must be writable by metronome and readable by nginx - mkdir -p "/var/xmpp-upload/${domain}/upload" - # sgid bit allows that file created in that dir will be owned by www-data - # despite the fact that metronome ain't in the www-data group - chmod g+s "/var/xmpp-upload/${domain}/upload" - done + # FIXME : small optimization to do to avoid calling a yunohost command ... + # maybe another env variable like YNH_MAIN_DOMAINS idk + domain_list=$(yunohost domain list --exclude-subdomains --output-as plain --quiet) - # fix some permissions - [ ! -e '/var/xmpp-upload' ] || chown -R metronome:www-data "/var/xmpp-upload/" - [ ! -e '/var/xmpp-upload' ] || chmod 750 "/var/xmpp-upload/" + # create metronome directories for domains + for domain in $domain_list; do + mkdir -p "/var/lib/metronome/${domain//./%2e}/pep" + # http_upload directory must be writable by metronome and readable by nginx + mkdir -p "/var/xmpp-upload/${domain}/upload" + # sgid bit allows that file created in that dir will be owned by www-data + # despite the fact that metronome ain't in the www-data group + chmod g+s "/var/xmpp-upload/${domain}/upload" + done - # metronome should be in ssl-cert group to let it access SSL certificates - usermod -aG ssl-cert metronome - chown -R metronome: /var/lib/metronome/ - chown -R metronome: /etc/metronome/conf.d/ + # fix some permissions + [ ! -e '/var/xmpp-upload' ] || chown -R metronome:www-data "/var/xmpp-upload/" + [ ! -e '/var/xmpp-upload' ] || chmod 750 "/var/xmpp-upload/" - [[ -z "$regen_conf_files" ]] \ - || systemctl restart metronome + # metronome should be in ssl-cert group to let it access SSL certificates + usermod -aG ssl-cert metronome + chown -R metronome: /var/lib/metronome/ + chown -R metronome: /etc/metronome/conf.d/ + + [[ -z "$regen_conf_files" ]] \ + || systemctl restart metronome } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/15-nginx b/data/hooks/conf_regen/15-nginx index c158ecd09..dd47651e8 100755 --- a/data/hooks/conf_regen/15-nginx +++ b/data/hooks/conf_regen/15-nginx @@ -5,148 +5,156 @@ set -e . /usr/share/yunohost/helpers do_init_regen() { - if [[ $EUID -ne 0 ]]; then - echo "You must be root to run this script" 1>&2 - exit 1 - fi + if [[ $EUID -ne 0 ]]; then + echo "You must be root to run this script" 1>&2 + exit 1 + fi - cd /usr/share/yunohost/templates/nginx + cd /usr/share/yunohost/templates/nginx - nginx_dir="/etc/nginx" - nginx_conf_dir="${nginx_dir}/conf.d" - mkdir -p "$nginx_conf_dir" + nginx_dir="/etc/nginx" + nginx_conf_dir="${nginx_dir}/conf.d" + mkdir -p "$nginx_conf_dir" - # install plain conf files - cp plain/* "$nginx_conf_dir" + # install plain conf files + cp plain/* "$nginx_conf_dir" - # probably run with init: just disable default site, restart NGINX and exit - rm -f "${nginx_dir}/sites-enabled/default" + # probably run with init: just disable default site, restart NGINX and exit + rm -f "${nginx_dir}/sites-enabled/default" - export compatibility="intermediate" - ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc" - ynh_render_template "yunohost_admin.conf" "${nginx_conf_dir}/yunohost_admin.conf" - ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc" - ynh_render_template "yunohost_api.conf.inc" "${nginx_conf_dir}/yunohost_api.conf.inc" + export compatibility="intermediate" + ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc" + ynh_render_template "yunohost_admin.conf" "${nginx_conf_dir}/yunohost_admin.conf" + ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc" + ynh_render_template "yunohost_api.conf.inc" "${nginx_conf_dir}/yunohost_api.conf.inc" - mkdir -p $nginx_conf_dir/default.d/ - cp "redirect_to_admin.conf" $nginx_conf_dir/default.d/ + mkdir -p $nginx_conf_dir/default.d/ + cp "redirect_to_admin.conf" $nginx_conf_dir/default.d/ - # Restart nginx if conf looks good, otherwise display error and exit unhappy - nginx -t 2>/dev/null || { nginx -t; exit 1; } - systemctl restart nginx || { journalctl --no-pager --lines=10 -u nginx >&2; exit 1; } + # Restart nginx if conf looks good, otherwise display error and exit unhappy + nginx -t 2>/dev/null || { + nginx -t + exit 1 + } + systemctl restart nginx || { + journalctl --no-pager --lines=10 -u nginx >&2 + exit 1 + } - exit 0 + exit 0 } do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/nginx + cd /usr/share/yunohost/templates/nginx - nginx_dir="${pending_dir}/etc/nginx" - nginx_conf_dir="${nginx_dir}/conf.d" - mkdir -p "$nginx_conf_dir" + nginx_dir="${pending_dir}/etc/nginx" + nginx_conf_dir="${nginx_dir}/conf.d" + mkdir -p "$nginx_conf_dir" - # install / update plain conf files - cp plain/* "$nginx_conf_dir" - # remove the panel overlay if this is specified in settings - panel_overlay=$(yunohost settings get 'ssowat.panel_overlay.enabled') - if [ "$panel_overlay" == "false" ] || [ "$panel_overlay" == "False" ] - then - echo "#" > "${nginx_conf_dir}/yunohost_panel.conf.inc" - fi + # install / update plain conf files + cp plain/* "$nginx_conf_dir" + # remove the panel overlay if this is specified in settings + panel_overlay=$(yunohost settings get 'ssowat.panel_overlay.enabled') + if [ "$panel_overlay" == "false" ] || [ "$panel_overlay" == "False" ]; then + echo "#" >"${nginx_conf_dir}/yunohost_panel.conf.inc" + fi - # retrieve variables - main_domain=$(cat /etc/yunohost/current_host) + # retrieve variables + main_domain=$(cat /etc/yunohost/current_host) - # Support different strategy for security configurations - export redirect_to_https="$(yunohost settings get 'security.nginx.redirect_to_https')" - export compatibility="$(yunohost settings get 'security.nginx.compatibility')" - export experimental="$(yunohost settings get 'security.experimental.enabled')" - ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc" + # Support different strategy for security configurations + export redirect_to_https="$(yunohost settings get 'security.nginx.redirect_to_https')" + export compatibility="$(yunohost settings get 'security.nginx.compatibility')" + export experimental="$(yunohost settings get 'security.experimental.enabled')" + ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc" - cert_status=$(yunohost domain cert status --json) + cert_status=$(yunohost domain cert status --json) - # add domain conf files - for domain in $YNH_DOMAINS; do - domain_conf_dir="${nginx_conf_dir}/${domain}.d" - mkdir -p "$domain_conf_dir" - mail_autoconfig_dir="${pending_dir}/var/www/.well-known/${domain}/autoconfig/mail/" - mkdir -p "$mail_autoconfig_dir" + # add domain conf files + for domain in $YNH_DOMAINS; do + domain_conf_dir="${nginx_conf_dir}/${domain}.d" + mkdir -p "$domain_conf_dir" + mail_autoconfig_dir="${pending_dir}/var/www/.well-known/${domain}/autoconfig/mail/" + mkdir -p "$mail_autoconfig_dir" - # NGINX server configuration - export domain - export domain_cert_ca=$(echo $cert_status \ - | jq ".certificates.\"$domain\".CA_type" \ - | tr -d '"') + # NGINX server configuration + export domain + export domain_cert_ca=$(echo $cert_status \ + | jq ".certificates.\"$domain\".CA_type" \ + | tr -d '"') - ynh_render_template "server.tpl.conf" "${nginx_conf_dir}/${domain}.conf" - ynh_render_template "autoconfig.tpl.xml" "${mail_autoconfig_dir}/config-v1.1.xml" + ynh_render_template "server.tpl.conf" "${nginx_conf_dir}/${domain}.conf" + ynh_render_template "autoconfig.tpl.xml" "${mail_autoconfig_dir}/config-v1.1.xml" - touch "${domain_conf_dir}/yunohost_local.conf" # Clean legacy conf files + touch "${domain_conf_dir}/yunohost_local.conf" # Clean legacy conf files - done + done - export webadmin_allowlist_enabled=$(yunohost settings get security.webadmin.allowlist.enabled) - if [ "$webadmin_allowlist_enabled" == "True" ] - then - export webadmin_allowlist=$(yunohost settings get security.webadmin.allowlist) - fi - ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc" - ynh_render_template "yunohost_api.conf.inc" "${nginx_conf_dir}/yunohost_api.conf.inc" - ynh_render_template "yunohost_admin.conf" "${nginx_conf_dir}/yunohost_admin.conf" - mkdir -p $nginx_conf_dir/default.d/ - cp "redirect_to_admin.conf" $nginx_conf_dir/default.d/ + export webadmin_allowlist_enabled=$(yunohost settings get security.webadmin.allowlist.enabled) + if [ "$webadmin_allowlist_enabled" == "True" ]; then + export webadmin_allowlist=$(yunohost settings get security.webadmin.allowlist) + fi + ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc" + ynh_render_template "yunohost_api.conf.inc" "${nginx_conf_dir}/yunohost_api.conf.inc" + ynh_render_template "yunohost_admin.conf" "${nginx_conf_dir}/yunohost_admin.conf" + mkdir -p $nginx_conf_dir/default.d/ + cp "redirect_to_admin.conf" $nginx_conf_dir/default.d/ - # remove old domain conf files - conf_files=$(ls -1 /etc/nginx/conf.d \ - | awk '/^[^\.]+\.[^\.]+.*\.conf$/ { print $1 }') - for file in $conf_files; do - domain=${file%.conf} - [[ $YNH_DOMAINS =~ $domain ]] \ - || touch "${nginx_conf_dir}/${file}" - done + # remove old domain conf files + conf_files=$(ls -1 /etc/nginx/conf.d \ + | awk '/^[^\.]+\.[^\.]+.*\.conf$/ { print $1 }') + for file in $conf_files; do + domain=${file%.conf} + [[ $YNH_DOMAINS =~ $domain ]] \ + || touch "${nginx_conf_dir}/${file}" + done - # remove old mail-autoconfig files - autoconfig_files=$(ls -1 /var/www/.well-known/*/autoconfig/mail/config-v1.1.xml 2>/dev/null || true) - for file in $autoconfig_files; do - domain=$(basename $(readlink -f $(dirname $file)/../..)) - [[ $YNH_DOMAINS =~ $domain ]] \ - || (mkdir -p "$(dirname ${pending_dir}/${file})" && touch "${pending_dir}/${file}") - done + # remove old mail-autoconfig files + autoconfig_files=$(ls -1 /var/www/.well-known/*/autoconfig/mail/config-v1.1.xml 2>/dev/null || true) + for file in $autoconfig_files; do + domain=$(basename $(readlink -f $(dirname $file)/../..)) + [[ $YNH_DOMAINS =~ $domain ]] \ + || (mkdir -p "$(dirname ${pending_dir}/${file})" && touch "${pending_dir}/${file}") + done - # disable default site - mkdir -p "${nginx_dir}/sites-enabled" - touch "${nginx_dir}/sites-enabled/default" + # disable default site + mkdir -p "${nginx_dir}/sites-enabled" + touch "${nginx_dir}/sites-enabled/default" } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - [ -z "$regen_conf_files" ] && exit 0 + [ -z "$regen_conf_files" ] && exit 0 - # create NGINX conf directories for domains - for domain in $YNH_DOMAINS; do - mkdir -p "/etc/nginx/conf.d/${domain}.d" - done + # create NGINX conf directories for domains + for domain in $YNH_DOMAINS; do + mkdir -p "/etc/nginx/conf.d/${domain}.d" + done - # Get rid of legacy lets encrypt snippets - for domain in $YNH_DOMAINS; do - # If the legacy letsencrypt / acme-challenge domain-specific snippet is still there - if [ -e /etc/nginx/conf.d/${domain}.d/000-acmechallenge.conf ] - then - # And if we're effectively including the new domain-independant snippet now - if grep -q "include /etc/nginx/conf.d/acme-challenge.conf.inc;" /etc/nginx/conf.d/${domain}.conf - then - # Delete the old domain-specific snippet - rm /etc/nginx/conf.d/${domain}.d/000-acmechallenge.conf - fi - fi - done + # Get rid of legacy lets encrypt snippets + for domain in $YNH_DOMAINS; do + # If the legacy letsencrypt / acme-challenge domain-specific snippet is still there + if [ -e /etc/nginx/conf.d/${domain}.d/000-acmechallenge.conf ]; then + # And if we're effectively including the new domain-independant snippet now + if grep -q "include /etc/nginx/conf.d/acme-challenge.conf.inc;" /etc/nginx/conf.d/${domain}.conf; then + # Delete the old domain-specific snippet + rm /etc/nginx/conf.d/${domain}.d/000-acmechallenge.conf + fi + fi + done - # Reload nginx if conf looks good, otherwise display error and exit unhappy - nginx -t 2>/dev/null || { nginx -t; exit 1; } - pgrep nginx && systemctl reload nginx || { journalctl --no-pager --lines=10 -u nginx >&2; exit 1; } + # Reload nginx if conf looks good, otherwise display error and exit unhappy + nginx -t 2>/dev/null || { + nginx -t + exit 1 + } + pgrep nginx && systemctl reload nginx || { + journalctl --no-pager --lines=10 -u nginx >&2 + exit 1 + } } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/19-postfix b/data/hooks/conf_regen/19-postfix index c569e1ca1..7865cd312 100755 --- a/data/hooks/conf_regen/19-postfix +++ b/data/hooks/conf_regen/19-postfix @@ -5,78 +5,76 @@ set -e . /usr/share/yunohost/helpers do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/postfix + cd /usr/share/yunohost/templates/postfix - postfix_dir="${pending_dir}/etc/postfix" - mkdir -p "$postfix_dir" + postfix_dir="${pending_dir}/etc/postfix" + mkdir -p "$postfix_dir" - default_dir="${pending_dir}/etc/default/" - mkdir -p "$default_dir" + default_dir="${pending_dir}/etc/default/" + mkdir -p "$default_dir" - # install plain conf files - cp plain/* "$postfix_dir" + # install plain conf files + cp plain/* "$postfix_dir" - # prepare main.cf conf file - main_domain=$(cat /etc/yunohost/current_host) + # prepare main.cf conf file + main_domain=$(cat /etc/yunohost/current_host) - # Support different strategy for security configurations - export compatibility="$(yunohost settings get 'security.postfix.compatibility')" - - # Add possibility to specify a relay - # Could be useful with some isp with no 25 port open or more complex setup - export relay_port="" - export relay_user="" - export relay_host="$(yunohost settings get 'smtp.relay.host')" - if [ -n "${relay_host}" ] - then - relay_port="$(yunohost settings get 'smtp.relay.port')" - relay_user="$(yunohost settings get 'smtp.relay.user')" - relay_password="$(yunohost settings get 'smtp.relay.password')" - - # Avoid to display "Relay account paswword" to other users - touch ${postfix_dir}/sasl_passwd - chmod 750 ${postfix_dir}/sasl_passwd - # Avoid "postmap: warning: removing zero-length database file" - chown postfix ${pending_dir}/etc/postfix - chown postfix ${pending_dir}/etc/postfix/sasl_passwd + # Support different strategy for security configurations + export compatibility="$(yunohost settings get 'security.postfix.compatibility')" - cat <<< "[${relay_host}]:${relay_port} ${relay_user}:${relay_password}" > ${postfix_dir}/sasl_passwd - postmap ${postfix_dir}/sasl_passwd - fi - export main_domain - export domain_list="$YNH_DOMAINS" - ynh_render_template "main.cf" "${postfix_dir}/main.cf" + # Add possibility to specify a relay + # Could be useful with some isp with no 25 port open or more complex setup + export relay_port="" + export relay_user="" + export relay_host="$(yunohost settings get 'smtp.relay.host')" + if [ -n "${relay_host}" ]; then + relay_port="$(yunohost settings get 'smtp.relay.port')" + relay_user="$(yunohost settings get 'smtp.relay.user')" + relay_password="$(yunohost settings get 'smtp.relay.password')" - cat postsrsd \ - | sed "s/{{ main_domain }}/${main_domain}/g" \ - | sed "s/{{ domain_list }}/${YNH_DOMAINS}/g" \ - > "${default_dir}/postsrsd" + # Avoid to display "Relay account paswword" to other users + touch ${postfix_dir}/sasl_passwd + chmod 750 ${postfix_dir}/sasl_passwd + # Avoid "postmap: warning: removing zero-length database file" + chown postfix ${pending_dir}/etc/postfix + chown postfix ${pending_dir}/etc/postfix/sasl_passwd - # adapt it for IPv4-only hosts - ipv6="$(yunohost settings get 'smtp.allow_ipv6')" - if [ "$ipv6" == "False" ] || [ ! -f /proc/net/if_inet6 ]; then - sed -i \ - 's/ \[::ffff:127.0.0.0\]\/104 \[::1\]\/128//g' \ - "${postfix_dir}/main.cf" - sed -i \ - 's/inet_interfaces = all/&\ninet_protocols = ipv4/' \ - "${postfix_dir}/main.cf" - fi + cat <<<"[${relay_host}]:${relay_port} ${relay_user}:${relay_password}" >${postfix_dir}/sasl_passwd + postmap ${postfix_dir}/sasl_passwd + fi + export main_domain + export domain_list="$YNH_DOMAINS" + ynh_render_template "main.cf" "${postfix_dir}/main.cf" + + cat postsrsd \ + | sed "s/{{ main_domain }}/${main_domain}/g" \ + | sed "s/{{ domain_list }}/${YNH_DOMAINS}/g" \ + >"${default_dir}/postsrsd" + + # adapt it for IPv4-only hosts + ipv6="$(yunohost settings get 'smtp.allow_ipv6')" + if [ "$ipv6" == "False" ] || [ ! -f /proc/net/if_inet6 ]; then + sed -i \ + 's/ \[::ffff:127.0.0.0\]\/104 \[::1\]\/128//g' \ + "${postfix_dir}/main.cf" + sed -i \ + 's/inet_interfaces = all/&\ninet_protocols = ipv4/' \ + "${postfix_dir}/main.cf" + fi } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - if [ -e /etc/postfix/sasl_passwd ] - then - chmod 750 /etc/postfix/sasl_passwd* - chown postfix:root /etc/postfix/sasl_passwd* - fi + if [ -e /etc/postfix/sasl_passwd ]; then + chmod 750 /etc/postfix/sasl_passwd* + chown postfix:root /etc/postfix/sasl_passwd* + fi - [[ -z "$regen_conf_files" ]] \ - || { systemctl restart postfix && systemctl restart postsrsd; } + [[ -z "$regen_conf_files" ]] \ + || { systemctl restart postfix && systemctl restart postsrsd; } } diff --git a/data/hooks/conf_regen/25-dovecot b/data/hooks/conf_regen/25-dovecot index a0663a4a6..e95816604 100755 --- a/data/hooks/conf_regen/25-dovecot +++ b/data/hooks/conf_regen/25-dovecot @@ -5,62 +5,62 @@ set -e . /usr/share/yunohost/helpers do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/dovecot + cd /usr/share/yunohost/templates/dovecot - dovecot_dir="${pending_dir}/etc/dovecot" - mkdir -p "${dovecot_dir}/global_script" + dovecot_dir="${pending_dir}/etc/dovecot" + mkdir -p "${dovecot_dir}/global_script" - # copy simple conf files - cp dovecot-ldap.conf "${dovecot_dir}/dovecot-ldap.conf" - cp dovecot.sieve "${dovecot_dir}/global_script/dovecot.sieve" + # copy simple conf files + cp dovecot-ldap.conf "${dovecot_dir}/dovecot-ldap.conf" + cp dovecot.sieve "${dovecot_dir}/global_script/dovecot.sieve" - export pop3_enabled="$(yunohost settings get 'pop3.enabled')" - export main_domain=$(cat /etc/yunohost/current_host) + export pop3_enabled="$(yunohost settings get 'pop3.enabled')" + export main_domain=$(cat /etc/yunohost/current_host) - ynh_render_template "dovecot.conf" "${dovecot_dir}/dovecot.conf" + ynh_render_template "dovecot.conf" "${dovecot_dir}/dovecot.conf" - # adapt it for IPv4-only hosts - if [ ! -f /proc/net/if_inet6 ]; then - sed -i \ - 's/^\(listen =\).*/\1 */' \ - "${dovecot_dir}/dovecot.conf" - fi + # adapt it for IPv4-only hosts + if [ ! -f /proc/net/if_inet6 ]; then + sed -i \ + 's/^\(listen =\).*/\1 */' \ + "${dovecot_dir}/dovecot.conf" + fi - mkdir -p "${dovecot_dir}/yunohost.d" - cp pre-ext.conf "${dovecot_dir}/yunohost.d" - cp post-ext.conf "${dovecot_dir}/yunohost.d" + mkdir -p "${dovecot_dir}/yunohost.d" + cp pre-ext.conf "${dovecot_dir}/yunohost.d" + cp post-ext.conf "${dovecot_dir}/yunohost.d" } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - mkdir -p "/etc/dovecot/yunohost.d/pre-ext.d" - mkdir -p "/etc/dovecot/yunohost.d/post-ext.d" + mkdir -p "/etc/dovecot/yunohost.d/pre-ext.d" + mkdir -p "/etc/dovecot/yunohost.d/post-ext.d" - # create vmail user - id vmail > /dev/null 2>&1 \ - || adduser --system --ingroup mail --uid 500 vmail --home /var/vmail --no-create-home + # create vmail user + id vmail >/dev/null 2>&1 \ + || adduser --system --ingroup mail --uid 500 vmail --home /var/vmail --no-create-home - # Delete legacy home for vmail that existed in the past but was empty, poluting /home/ - [ ! -e /home/vmail ] || rmdir --ignore-fail-on-non-empty /home/vmail + # Delete legacy home for vmail that existed in the past but was empty, poluting /home/ + [ ! -e /home/vmail ] || rmdir --ignore-fail-on-non-empty /home/vmail - # fix permissions - chown -R vmail:mail /etc/dovecot/global_script - chmod 770 /etc/dovecot/global_script - chown root:mail /var/mail - chmod 1775 /var/mail - - [ -z "$regen_conf_files" ] && exit 0 - - # compile sieve script - [[ "$regen_conf_files" =~ dovecot\.sieve ]] && { - sievec /etc/dovecot/global_script/dovecot.sieve + # fix permissions chown -R vmail:mail /etc/dovecot/global_script - } + chmod 770 /etc/dovecot/global_script + chown root:mail /var/mail + chmod 1775 /var/mail - systemctl restart dovecot + [ -z "$regen_conf_files" ] && exit 0 + + # compile sieve script + [[ "$regen_conf_files" =~ dovecot\.sieve ]] && { + sievec /etc/dovecot/global_script/dovecot.sieve + chown -R vmail:mail /etc/dovecot/global_script + } + + systemctl restart dovecot } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/31-rspamd b/data/hooks/conf_regen/31-rspamd index da9b35dfe..72a35fdcc 100755 --- a/data/hooks/conf_regen/31-rspamd +++ b/data/hooks/conf_regen/31-rspamd @@ -3,60 +3,60 @@ set -e do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/rspamd + cd /usr/share/yunohost/templates/rspamd - install -D -m 644 metrics.local.conf \ - "${pending_dir}/etc/rspamd/local.d/metrics.conf" - install -D -m 644 dkim_signing.conf \ - "${pending_dir}/etc/rspamd/local.d/dkim_signing.conf" - install -D -m 644 rspamd.sieve \ - "${pending_dir}/etc/dovecot/global_script/rspamd.sieve" + install -D -m 644 metrics.local.conf \ + "${pending_dir}/etc/rspamd/local.d/metrics.conf" + install -D -m 644 dkim_signing.conf \ + "${pending_dir}/etc/rspamd/local.d/dkim_signing.conf" + install -D -m 644 rspamd.sieve \ + "${pending_dir}/etc/dovecot/global_script/rspamd.sieve" } do_post_regen() { - ## - ## DKIM key generation - ## + ## + ## DKIM key generation + ## - # create DKIM directory with proper permission - mkdir -p /etc/dkim - chown _rspamd /etc/dkim + # create DKIM directory with proper permission + mkdir -p /etc/dkim + chown _rspamd /etc/dkim - # create DKIM key for domains - for domain in $YNH_DOMAINS; do - domain_key="/etc/dkim/${domain}.mail.key" - [ ! -f "$domain_key" ] && { - # We use a 1024 bit size because nsupdate doesn't seem to be able to - # handle 2048... - opendkim-genkey --domain="$domain" \ - --selector=mail --directory=/etc/dkim -b 1024 - mv /etc/dkim/mail.private "$domain_key" - mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt" + # create DKIM key for domains + for domain in $YNH_DOMAINS; do + domain_key="/etc/dkim/${domain}.mail.key" + [ ! -f "$domain_key" ] && { + # We use a 1024 bit size because nsupdate doesn't seem to be able to + # handle 2048... + opendkim-genkey --domain="$domain" \ + --selector=mail --directory=/etc/dkim -b 1024 + mv /etc/dkim/mail.private "$domain_key" + mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt" + } + done + + # fix DKIM keys permissions + chown _rspamd /etc/dkim/*.mail.key + chmod 400 /etc/dkim/*.mail.key + + [ ! -e /var/log/rspamd ] || chown -R _rspamd:_rspamd /var/log/rspamd + + regen_conf_files=$1 + [ -z "$regen_conf_files" ] && exit 0 + + # compile sieve script + [[ "$regen_conf_files" =~ rspamd\.sieve ]] && { + sievec /etc/dovecot/global_script/rspamd.sieve + chown -R vmail:mail /etc/dovecot/global_script + systemctl restart dovecot } - done - # fix DKIM keys permissions - chown _rspamd /etc/dkim/*.mail.key - chmod 400 /etc/dkim/*.mail.key - - [ ! -e /var/log/rspamd ] || chown -R _rspamd:_rspamd /var/log/rspamd - - regen_conf_files=$1 - [ -z "$regen_conf_files" ] && exit 0 - - # compile sieve script - [[ "$regen_conf_files" =~ rspamd\.sieve ]] && { - sievec /etc/dovecot/global_script/rspamd.sieve - chown -R vmail:mail /etc/dovecot/global_script - systemctl restart dovecot - } - - # Restart rspamd due to the upgrade - # https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html - systemctl -q restart rspamd.service + # Restart rspamd due to the upgrade + # https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html + systemctl -q restart rspamd.service } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/34-mysql b/data/hooks/conf_regen/34-mysql index 41afda110..8b4d59288 100755 --- a/data/hooks/conf_regen/34-mysql +++ b/data/hooks/conf_regen/34-mysql @@ -4,69 +4,65 @@ set -e . /usr/share/yunohost/helpers do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/mysql + cd /usr/share/yunohost/templates/mysql - install -D -m 644 my.cnf "${pending_dir}/etc/mysql/my.cnf" + install -D -m 644 my.cnf "${pending_dir}/etc/mysql/my.cnf" } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - if [[ ! -d /var/lib/mysql/mysql ]] - then - # dpkg-reconfigure will initialize mysql (if it ain't already) - # It enabled auth_socket for root, so no need to define any root password... - # c.f. : cat /var/lib/dpkg/info/mariadb-server-10.3.postinst | grep install_db -C3 - MYSQL_PKG="$(dpkg --list | sed -ne 's/^ii \(mariadb-server-[[:digit:].]\+\) .*$/\1/p')" - dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1 + if [[ ! -d /var/lib/mysql/mysql ]]; then + # dpkg-reconfigure will initialize mysql (if it ain't already) + # It enabled auth_socket for root, so no need to define any root password... + # c.f. : cat /var/lib/dpkg/info/mariadb-server-10.3.postinst | grep install_db -C3 + MYSQL_PKG="$(dpkg --list | sed -ne 's/^ii \(mariadb-server-[[:digit:].]\+\) .*$/\1/p')" + dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1 - systemctl -q is-active mariadb.service \ - || systemctl start mariadb + systemctl -q is-active mariadb.service \ + || systemctl start mariadb - sleep 5 + sleep 5 - echo "" | mysql && echo "Can't connect to mysql using unix_socket auth ... something went wrong during initial configuration of mysql !?" >&2 - fi + echo "" | mysql && echo "Can't connect to mysql using unix_socket auth ... something went wrong during initial configuration of mysql !?" >&2 + fi - # Legacy code to get rid of /etc/yunohost/mysql ... - # Nowadays, we can simply run mysql while being run as root of unix_socket/auth_socket is enabled... - if [ -f /etc/yunohost/mysql ]; then + # Legacy code to get rid of /etc/yunohost/mysql ... + # Nowadays, we can simply run mysql while being run as root of unix_socket/auth_socket is enabled... + if [ -f /etc/yunohost/mysql ]; then - # This is a trick to check if we're able to use mysql without password - # Expect instances installed in stretch to already have unix_socket - #configured, but not old instances from the jessie/wheezy era - if ! echo "" | mysql 2>/dev/null - then - password="$(cat /etc/yunohost/mysql)" - # Enable plugin unix_socket for root on localhost - mysql -u root -p"$password" <<< "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED WITH unix_socket WITH GRANT OPTION;" - fi + # This is a trick to check if we're able to use mysql without password + # Expect instances installed in stretch to already have unix_socket + #configured, but not old instances from the jessie/wheezy era + if ! echo "" | mysql 2>/dev/null; then + password="$(cat /etc/yunohost/mysql)" + # Enable plugin unix_socket for root on localhost + mysql -u root -p"$password" <<<"GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED WITH unix_socket WITH GRANT OPTION;" + fi - # If now we're able to login without password, drop the mysql password - if echo "" | mysql 2>/dev/null - then - rm /etc/yunohost/mysql - else - echo "Can't connect to mysql using unix_socket auth ... something went wrong while trying to get rid of mysql password !?" >&2 - fi - fi + # If now we're able to login without password, drop the mysql password + if echo "" | mysql 2>/dev/null; then + rm /etc/yunohost/mysql + else + echo "Can't connect to mysql using unix_socket auth ... something went wrong while trying to get rid of mysql password !?" >&2 + fi + fi - # mysql is supposed to be an alias to mariadb... but in some weird case is not - # c.f. https://forum.yunohost.org/t/mysql-ne-fonctionne-pas/11661 - # Playing with enable/disable allows to recreate the proper symlinks. - if [ ! -e /etc/systemd/system/mysql.service ] - then - systemctl stop mysql -q - systemctl disable mysql -q - systemctl disable mariadb -q - systemctl enable mariadb -q - systemctl is-active mariadb -q || systemctl start mariadb - fi + # mysql is supposed to be an alias to mariadb... but in some weird case is not + # c.f. https://forum.yunohost.org/t/mysql-ne-fonctionne-pas/11661 + # Playing with enable/disable allows to recreate the proper symlinks. + if [ ! -e /etc/systemd/system/mysql.service ]; then + systemctl stop mysql -q + systemctl disable mysql -q + systemctl disable mariadb -q + systemctl enable mariadb -q + systemctl is-active mariadb -q || systemctl start mariadb + fi - [[ -z "$regen_conf_files" ]] \ - || systemctl restart mysql + [[ -z "$regen_conf_files" ]] \ + || systemctl restart mysql } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/35-redis b/data/hooks/conf_regen/35-redis index da5eac4c9..ac486f373 100755 --- a/data/hooks/conf_regen/35-redis +++ b/data/hooks/conf_regen/35-redis @@ -1,13 +1,13 @@ #!/bin/bash do_pre_regen() { - : + : } do_post_regen() { - # Enforce these damn permissions because for some reason in some weird cases - # they are spontaneously replaced by root:root -_- - chown -R redis:adm /var/log/redis + # Enforce these damn permissions because for some reason in some weird cases + # they are spontaneously replaced by root:root -_- + chown -R redis:adm /var/log/redis } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/37-mdns b/data/hooks/conf_regen/37-mdns index b8112a6c1..8cb364084 100755 --- a/data/hooks/conf_regen/37-mdns +++ b/data/hooks/conf_regen/37-mdns @@ -3,55 +3,52 @@ set -e _generate_config() { - echo "domains:" - echo " - yunohost.local" - for domain in $YNH_DOMAINS - do - # Only keep .local domains (don't keep - [[ "$domain" =~ [^.]+\.[^.]+\.local$ ]] && echo "Subdomain $domain cannot be handled by Bonjour/Zeroconf/mDNS" >&2 - [[ "$domain" =~ ^[^.]+\.local$ ]] || continue - echo " - $domain" - done + echo "domains:" + echo " - yunohost.local" + for domain in $YNH_DOMAINS; do + # Only keep .local domains (don't keep + [[ "$domain" =~ [^.]+\.[^.]+\.local$ ]] && echo "Subdomain $domain cannot be handled by Bonjour/Zeroconf/mDNS" >&2 + [[ "$domain" =~ ^[^.]+\.local$ ]] || continue + echo " - $domain" + done } do_init_regen() { - do_pre_regen - do_post_regen /etc/systemd/system/yunomdns.service - systemctl enable yunomdns + do_pre_regen + do_post_regen /etc/systemd/system/yunomdns.service + systemctl enable yunomdns } do_pre_regen() { - pending_dir="$1" + pending_dir="$1" - cd /usr/share/yunohost/templates/mdns - mkdir -p ${pending_dir}/etc/systemd/system/ - cp yunomdns.service ${pending_dir}/etc/systemd/system/ + cd /usr/share/yunohost/templates/mdns + mkdir -p ${pending_dir}/etc/systemd/system/ + cp yunomdns.service ${pending_dir}/etc/systemd/system/ - getent passwd mdns &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group mdns + getent passwd mdns &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group mdns - mkdir -p ${pending_dir}/etc/yunohost - _generate_config > ${pending_dir}/etc/yunohost/mdns.yml + mkdir -p ${pending_dir}/etc/yunohost + _generate_config >${pending_dir}/etc/yunohost/mdns.yml } do_post_regen() { - regen_conf_files="$1" + regen_conf_files="$1" - chown mdns:mdns /etc/yunohost/mdns.yml + chown mdns:mdns /etc/yunohost/mdns.yml - # If we changed the systemd ynh-override conf - if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/yunomdns.service$" - then - systemctl daemon-reload - fi + # If we changed the systemd ynh-override conf + if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/yunomdns.service$"; then + systemctl daemon-reload + fi - # Legacy stuff to enable the new yunomdns service on legacy systems - if [[ -e /etc/avahi/avahi-daemon.conf ]] && grep -q 'yunohost' /etc/avahi/avahi-daemon.conf - then - systemctl enable yunomdns - fi + # Legacy stuff to enable the new yunomdns service on legacy systems + if [[ -e /etc/avahi/avahi-daemon.conf ]] && grep -q 'yunohost' /etc/avahi/avahi-daemon.conf; then + systemctl enable yunomdns + fi - [[ -z "$regen_conf_files" ]] \ - || systemctl restart yunomdns + [[ -z "$regen_conf_files" ]] \ + || systemctl restart yunomdns } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/43-dnsmasq b/data/hooks/conf_regen/43-dnsmasq index f3bed7b04..0c016bcb9 100755 --- a/data/hooks/conf_regen/43-dnsmasq +++ b/data/hooks/conf_regen/43-dnsmasq @@ -4,80 +4,77 @@ set -e . /usr/share/yunohost/helpers do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/dnsmasq + cd /usr/share/yunohost/templates/dnsmasq - # create directory for pending conf - dnsmasq_dir="${pending_dir}/etc/dnsmasq.d" - mkdir -p "$dnsmasq_dir" - etcdefault_dir="${pending_dir}/etc/default" - mkdir -p "$etcdefault_dir" + # create directory for pending conf + dnsmasq_dir="${pending_dir}/etc/dnsmasq.d" + mkdir -p "$dnsmasq_dir" + etcdefault_dir="${pending_dir}/etc/default" + mkdir -p "$etcdefault_dir" - # add general conf files - cp plain/etcdefault ${pending_dir}/etc/default/dnsmasq - cp plain/dnsmasq.conf ${pending_dir}/etc/dnsmasq.conf + # add general conf files + cp plain/etcdefault ${pending_dir}/etc/default/dnsmasq + cp plain/dnsmasq.conf ${pending_dir}/etc/dnsmasq.conf - # add resolver file - cat plain/resolv.dnsmasq.conf | grep "^nameserver" | shuf > ${pending_dir}/etc/resolv.dnsmasq.conf + # add resolver file + cat plain/resolv.dnsmasq.conf | grep "^nameserver" | shuf >${pending_dir}/etc/resolv.dnsmasq.conf - # retrieve variables - ipv4=$(curl -s -4 https://ip.yunohost.org 2>/dev/null || true) - ynh_validate_ip4 "$ipv4" || ipv4='127.0.0.1' - ipv6=$(curl -s -6 https://ip6.yunohost.org 2>/dev/null || true) - ynh_validate_ip6 "$ipv6" || ipv6='' + # retrieve variables + ipv4=$(curl -s -4 https://ip.yunohost.org 2>/dev/null || true) + ynh_validate_ip4 "$ipv4" || ipv4='127.0.0.1' + ipv6=$(curl -s -6 https://ip6.yunohost.org 2>/dev/null || true) + ynh_validate_ip6 "$ipv6" || ipv6='' - export ipv4 - export ipv6 + export ipv4 + export ipv6 - # add domain conf files - for domain in $YNH_DOMAINS; do - export domain - ynh_render_template "domain.tpl" "${dnsmasq_dir}/${domain}" - done + # add domain conf files + for domain in $YNH_DOMAINS; do + export domain + ynh_render_template "domain.tpl" "${dnsmasq_dir}/${domain}" + done - # remove old domain conf files - conf_files=$(ls -1 /etc/dnsmasq.d \ - | awk '/^[^\.]+\.[^\.]+.*$/ { print $1 }') - for domain in $conf_files; do - [[ $YNH_DOMAINS =~ $domain ]] \ - || touch "${dnsmasq_dir}/${domain}" - done + # remove old domain conf files + conf_files=$(ls -1 /etc/dnsmasq.d \ + | awk '/^[^\.]+\.[^\.]+.*$/ { print $1 }') + for domain in $conf_files; do + [[ $YNH_DOMAINS =~ $domain ]] \ + || touch "${dnsmasq_dir}/${domain}" + done } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - # Fuck it, those domain/search entries from dhclient are usually annoying - # lying shit from the ISP trying to MiTM - if grep -q -E "^ *(domain|search)" /run/resolvconf/resolv.conf - then - if grep -q -E "^ *(domain|search)" /run/resolvconf/interface/*.dhclient 2>/dev/null - then - sed -E "s/^(domain|search)/#\1/g" -i /run/resolvconf/interface/*.dhclient - fi + # Fuck it, those domain/search entries from dhclient are usually annoying + # lying shit from the ISP trying to MiTM + if grep -q -E "^ *(domain|search)" /run/resolvconf/resolv.conf; then + if grep -q -E "^ *(domain|search)" /run/resolvconf/interface/*.dhclient 2>/dev/null; then + sed -E "s/^(domain|search)/#\1/g" -i /run/resolvconf/interface/*.dhclient + fi - grep -q '^supersede domain-name "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede domain-name "";' >> /etc/dhcp/dhclient.conf - grep -q '^supersede domain-search "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede domain-search "";' >> /etc/dhcp/dhclient.conf - grep -q '^supersede name "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede name "";' >> /etc/dhcp/dhclient.conf - systemctl restart resolvconf - fi + grep -q '^supersede domain-name "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede domain-name "";' >>/etc/dhcp/dhclient.conf + grep -q '^supersede domain-search "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede domain-search "";' >>/etc/dhcp/dhclient.conf + grep -q '^supersede name "";' /etc/dhcp/dhclient.conf 2>/dev/null || echo 'supersede name "";' >>/etc/dhcp/dhclient.conf + systemctl restart resolvconf + fi - # Some stupid things like rabbitmq-server used by onlyoffice won't work if - # the *short* hostname doesn't exists in /etc/hosts -_- - short_hostname=$(hostname -s) - grep -q "127.0.0.1.*$short_hostname" /etc/hosts || echo -e "\n127.0.0.1\t$short_hostname" >>/etc/hosts + # Some stupid things like rabbitmq-server used by onlyoffice won't work if + # the *short* hostname doesn't exists in /etc/hosts -_- + short_hostname=$(hostname -s) + grep -q "127.0.0.1.*$short_hostname" /etc/hosts || echo -e "\n127.0.0.1\t$short_hostname" >>/etc/hosts - [[ -n "$regen_conf_files" ]] || return + [[ -n "$regen_conf_files" ]] || return - # Remove / disable services likely to conflict with dnsmasq - for SERVICE in systemd-resolved bind9 - do - systemctl is-enabled $SERVICE &>/dev/null && systemctl disable $SERVICE 2>/dev/null - systemctl is-active $SERVICE &>/dev/null && systemctl stop $SERVICE - done + # Remove / disable services likely to conflict with dnsmasq + for SERVICE in systemd-resolved bind9; do + systemctl is-enabled $SERVICE &>/dev/null && systemctl disable $SERVICE 2>/dev/null + systemctl is-active $SERVICE &>/dev/null && systemctl stop $SERVICE + done - systemctl restart dnsmasq + systemctl restart dnsmasq } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/46-nsswitch b/data/hooks/conf_regen/46-nsswitch index be5cb2b86..2c984a905 100755 --- a/data/hooks/conf_regen/46-nsswitch +++ b/data/hooks/conf_regen/46-nsswitch @@ -3,23 +3,23 @@ set -e do_init_regen() { - do_pre_regen "" - systemctl restart unscd + do_pre_regen "" + systemctl restart unscd } do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/nsswitch + cd /usr/share/yunohost/templates/nsswitch - install -D -m 644 nsswitch.conf "${pending_dir}/etc/nsswitch.conf" + install -D -m 644 nsswitch.conf "${pending_dir}/etc/nsswitch.conf" } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - [[ -z "$regen_conf_files" ]] \ - || systemctl restart unscd + [[ -z "$regen_conf_files" ]] \ + || systemctl restart unscd } do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/52-fail2ban b/data/hooks/conf_regen/52-fail2ban index 7aef72ebc..6cbebbfb1 100755 --- a/data/hooks/conf_regen/52-fail2ban +++ b/data/hooks/conf_regen/52-fail2ban @@ -5,26 +5,26 @@ set -e . /usr/share/yunohost/helpers do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/fail2ban + cd /usr/share/yunohost/templates/fail2ban - fail2ban_dir="${pending_dir}/etc/fail2ban" - mkdir -p "${fail2ban_dir}/filter.d" - mkdir -p "${fail2ban_dir}/jail.d" + fail2ban_dir="${pending_dir}/etc/fail2ban" + mkdir -p "${fail2ban_dir}/filter.d" + mkdir -p "${fail2ban_dir}/jail.d" - cp yunohost.conf "${fail2ban_dir}/filter.d/yunohost.conf" - cp jail.conf "${fail2ban_dir}/jail.conf" + cp yunohost.conf "${fail2ban_dir}/filter.d/yunohost.conf" + cp jail.conf "${fail2ban_dir}/jail.conf" - export ssh_port="$(yunohost settings get 'security.ssh.port')" - ynh_render_template "yunohost-jails.conf" "${fail2ban_dir}/jail.d/yunohost-jails.conf" + export ssh_port="$(yunohost settings get 'security.ssh.port')" + ynh_render_template "yunohost-jails.conf" "${fail2ban_dir}/jail.d/yunohost-jails.conf" } do_post_regen() { - regen_conf_files=$1 + regen_conf_files=$1 - [[ -z "$regen_conf_files" ]] \ - || systemctl reload fail2ban + [[ -z "$regen_conf_files" ]] \ + || systemctl reload fail2ban } do_$1_regen ${@:2} diff --git a/data/hooks/post_user_create/ynh_multimedia b/data/hooks/post_user_create/ynh_multimedia index 26282cdc9..5b4b31b88 100644 --- a/data/hooks/post_user_create/ynh_multimedia +++ b/data/hooks/post_user_create/ynh_multimedia @@ -1,7 +1,7 @@ #!/bin/bash user=$1 - + readonly MEDIA_GROUP=multimedia readonly MEDIA_DIRECTORY=/home/yunohost.multimedia diff --git a/data/hooks/restore/05-conf_ldap b/data/hooks/restore/05-conf_ldap index c2debe018..a9eb10b1c 100644 --- a/data/hooks/restore/05-conf_ldap +++ b/data/hooks/restore/05-conf_ldap @@ -14,11 +14,11 @@ die() { # Restore saved configuration and database [[ $state -ge 1 ]] \ - && (rm -rf /etc/ldap/slapd.d && - mv "${TMPDIR}/slapd.d" /etc/ldap/slapd.d) + && (rm -rf /etc/ldap/slapd.d \ + && mv "${TMPDIR}/slapd.d" /etc/ldap/slapd.d) [[ $state -ge 2 ]] \ - && (rm -rf /var/lib/ldap && - mv "${TMPDIR}/ldap" /var/lib/ldap) + && (rm -rf /var/lib/ldap \ + && mv "${TMPDIR}/ldap" /var/lib/ldap) chown -R openldap: /etc/ldap/slapd.d /var/lib/ldap systemctl start slapd @@ -38,7 +38,7 @@ cp -a "${backup_dir}/ldap.conf" /etc/ldap/ldap.conf || cp -a "${backup_dir}/slapd.conf" /etc/ldap/slapd.conf slapadd -F /etc/ldap/slapd.d -b cn=config \ -l "${backup_dir}/cn=config.master.ldif" \ - || die 1 "Unable to restore LDAP configuration" + || die 1 "Unable to restore LDAP configuration" chown -R openldap: /etc/ldap/slapd.d # Restore the database @@ -46,7 +46,7 @@ mv /var/lib/ldap "$TMPDIR" mkdir -p /var/lib/ldap slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org \ -l "${backup_dir}/dc=yunohost-dc=org.ldif" \ - || die 2 "Unable to restore LDAP database" + || die 2 "Unable to restore LDAP database" chown -R openldap: /var/lib/ldap systemctl start slapd diff --git a/data/hooks/restore/50-conf_manually_modified_files b/data/hooks/restore/50-conf_manually_modified_files index 2d0943043..b23b95ec9 100644 --- a/data/hooks/restore/50-conf_manually_modified_files +++ b/data/hooks/restore/50-conf_manually_modified_files @@ -5,8 +5,7 @@ ynh_abort_if_errors YNH_CWD="${YNH_BACKUP_DIR%/}/conf/manually_modified_files" cd "$YNH_CWD" -for file in $(cat ./manually_modified_files_list) -do +for file in $(cat ./manually_modified_files_list); do ynh_restore_file --origin_path="$file" --not_mandatory done From 28b1bbcc86ab1557c500c22636137b003585e538 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 16:36:08 +0200 Subject: [PATCH 126/142] Also lint/reformat debian scripts --- debian/postinst | 59 ++++++++++++++++++++++++------------------------- debian/postrm | 6 ++--- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/debian/postinst b/debian/postinst index ceeed3cdf..0dd1dedd0 100644 --- a/debian/postinst +++ b/debian/postinst @@ -3,36 +3,35 @@ set -e do_configure() { - rm -rf /var/cache/moulinette/* + rm -rf /var/cache/moulinette/* - mkdir -p /usr/share/moulinette/actionsmap/ - ln -sf /usr/share/yunohost/actionsmap/yunohost.yml /usr/share/moulinette/actionsmap/yunohost.yml + mkdir -p /usr/share/moulinette/actionsmap/ + ln -sf /usr/share/yunohost/actionsmap/yunohost.yml /usr/share/moulinette/actionsmap/yunohost.yml - if [ ! -f /etc/yunohost/installed ]; then - # If apps/ is not empty, we're probably already installed in the past and - # something funky happened ... - if [ -d /etc/yunohost/apps/ ] && ls /etc/yunohost/apps/* >/dev/null 2>&1 - then - echo "Sounds like /etc/yunohost/installed mysteriously disappeared ... You should probably contact the Yunohost support ..." - else - bash /usr/share/yunohost/hooks/conf_regen/01-yunohost init - bash /usr/share/yunohost/hooks/conf_regen/02-ssl init - bash /usr/share/yunohost/hooks/conf_regen/09-nslcd init - bash /usr/share/yunohost/hooks/conf_regen/46-nsswitch init - bash /usr/share/yunohost/hooks/conf_regen/06-slapd init - bash /usr/share/yunohost/hooks/conf_regen/15-nginx init - bash /usr/share/yunohost/hooks/conf_regen/37-mdns init - fi - else - echo "Regenerating configuration, this might take a while..." - yunohost tools regen-conf --output-as none + if [ ! -f /etc/yunohost/installed ]; then + # If apps/ is not empty, we're probably already installed in the past and + # something funky happened ... + if [ -d /etc/yunohost/apps/ ] && ls /etc/yunohost/apps/* >/dev/null 2>&1; then + echo "Sounds like /etc/yunohost/installed mysteriously disappeared ... You should probably contact the Yunohost support ..." + else + bash /usr/share/yunohost/hooks/conf_regen/01-yunohost init + bash /usr/share/yunohost/hooks/conf_regen/02-ssl init + bash /usr/share/yunohost/hooks/conf_regen/09-nslcd init + bash /usr/share/yunohost/hooks/conf_regen/46-nsswitch init + bash /usr/share/yunohost/hooks/conf_regen/06-slapd init + bash /usr/share/yunohost/hooks/conf_regen/15-nginx init + bash /usr/share/yunohost/hooks/conf_regen/37-mdns init + fi + else + echo "Regenerating configuration, this might take a while..." + yunohost tools regen-conf --output-as none - echo "Launching migrations..." - yunohost tools migrations run --auto + echo "Launching migrations..." + yunohost tools migrations run --auto - echo "Re-diagnosing server health..." - yunohost diagnosis run --force - fi + echo "Re-diagnosing server health..." + yunohost diagnosis run --force + fi } @@ -50,13 +49,13 @@ do_configure() { case "$1" in configure) do_configure - ;; - abort-upgrade|abort-remove|abort-deconfigure) - ;; + ;; + abort-upgrade | abort-remove | abort-deconfigure) ;; + *) echo "postinst called with unknown argument \`$1'" >&2 exit 1 - ;; + ;; esac #DEBHELPER# diff --git a/debian/postrm b/debian/postrm index 63e42b4d4..ceadd5bce 100644 --- a/debian/postrm +++ b/debian/postrm @@ -6,12 +6,12 @@ set -e if [ "$1" = "purge" ]; then - update-rc.d yunohost-firewall remove >/dev/null - rm -f /etc/yunohost/installed + update-rc.d yunohost-firewall remove >/dev/null + rm -f /etc/yunohost/installed fi if [ "$1" = "remove" ]; then - rm -f /etc/yunohost/installed + rm -f /etc/yunohost/installed fi # Reset dpkg vendor to debian From c9cdfc6b0f9417578704c1cfd32c69d5460129ff Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 16:56:47 +0200 Subject: [PATCH 127/142] Typo :| --- data/helpers.d/apt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 235cc0067..f563757fb 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -333,7 +333,7 @@ EOF # Integrate new php-fpm service in yunohost yunohost service add php${specific_php_version}-fpm --log "/var/log/php${phpversion}-fpm.log" - elif grep --quiet 'php' <<< "$dependencies" + elif grep --quiet 'php' <<< "$dependencies"; then # Store phpversion into the config of this app ynh_app_setting_set --app=$app --key=phpversion --value=$YNH_DEFAULT_PHP_VERSION fi From fd1cc5e6ce7544e6d1f7bf51e70e73e6b2399bdf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 18:56:17 +0200 Subject: [PATCH 128/142] Fix stuff reported by shellcheck --- data/helpers.d/config | 2 +- data/helpers.d/utils | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/config b/data/helpers.d/config index 247d12d6f..5999387db 100644 --- a/data/helpers.d/config +++ b/data/helpers.d/config @@ -66,7 +66,7 @@ _ynh_app_config_apply_one() { "set__${bind%%(*}" $short_setting $type $bind elif [[ "$bind" == "null" ]]; then - continue + return # Save in a file elif [[ "$type" == "file" ]]; then diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 0e38423f2..a4afdaf88 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -542,7 +542,7 @@ ynh_read_var_in_file() { fi # Remove comments if needed - local expression="$(echo "$expression_with_comment" | sed "s@$comments[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" + local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" local first_char="${expression:0:1}" if [[ "$first_char" == '"' ]]; then @@ -620,7 +620,7 @@ ynh_write_var_in_file() { fi # Remove comments if needed - local expression="$(echo "$expression_with_comment" | sed "s@$comments[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" + local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" endline=${expression_with_comment#"$expression"} endline="$(echo "$endline" | sed 's/\\/\\\\/g')" value="$(echo "$value" | sed 's/\\/\\\\/g')" From 1d2e4e78f24b891e6cbe2a6d6e2f7a437f280759 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Oct 2021 18:57:56 +0200 Subject: [PATCH 129/142] Safer, clearer ynh_secure_remove --- data/helpers.d/utils | 49 +++++++------ tests/test_helpers.d/ynhtest_secure_remove.sh | 71 +++++++++++++++++++ 2 files changed, 100 insertions(+), 20 deletions(-) create mode 100644 tests/test_helpers.d/ynhtest_secure_remove.sh diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 061ff324d..a2d7855b9 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -724,6 +724,28 @@ properly with chmod/chown." echo $TMP_DIR } +_acceptable_path_to_delete() { + local file=$1 + + 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" ]] + then + forbidden_paths=$(echo "$forbidden_paths" | grep -v "/home/$app") + fi + + # Use realpath to normalize the path .. + # i.e convert ///foo//bar//..///baz//// to /foo/baz + file=$(realpath --no-symlinks $file) + if [ -z "$file" ] || grep -q -x -F "$file" <<< "$forbidden_paths"; then + return 1 + else + return 0 + fi +} + + # Remove a file or a directory securely # # usage: ynh_secure_remove --file=path_to_remove @@ -739,31 +761,18 @@ ynh_secure_remove () { ynh_handle_getopts_args "$@" set +o xtrace # set +x - local forbidden_path=" \ - /var/www \ - /home/yunohost.app" - - if [ $# -ge 2 ] - then + if [ $# -ge 2 ]; then ynh_print_warn --message="/!\ Packager ! You provided more than one argument to ynh_secure_remove but it will be ignored... Use this helper with one argument at time." fi - if [[ -z "$file" ]] - then + if [[ -z "$file" ]]; then ynh_print_warn --message="ynh_secure_remove called with empty argument, ignoring." - elif [[ "$forbidden_path" =~ "$file" \ - # Match all paths or subpaths in $forbidden_path - || "$file" =~ ^/[[:alnum:]]+$ \ - # Match all first level paths from / (Like /var, /root, etc...) - || "${file:${#file}-1}" = "/" ]] - # Match if the path finishes by /. Because it seems there is an empty variable - then - ynh_print_warn --message="Not deleting '$file' because it is not an acceptable path to delete." - elif [ -e "$file" ] - then - rm --recursive "$file" - else + elif [[ ! -e $file ]]; then ynh_print_info --message="'$file' wasn't deleted because it doesn't exist." + elif ! _acceptable_path_to_delete "$file"; then + ynh_print_warn --message="Not deleting '$file' because it is not an acceptable path to delete." + else + rm --recursive "$file" fi set -o xtrace # set -x diff --git a/tests/test_helpers.d/ynhtest_secure_remove.sh b/tests/test_helpers.d/ynhtest_secure_remove.sh new file mode 100644 index 000000000..04d85fa7a --- /dev/null +++ b/tests/test_helpers.d/ynhtest_secure_remove.sh @@ -0,0 +1,71 @@ +ynhtest_acceptable_path_to_delete() { + + mkdir -p /home/someuser + mkdir -p /home/$app + mkdir -p /home/yunohost.app/$app + mkdir -p /var/www/$app + touch /var/www/$app/bar + touch /etc/cron.d/$app + + ! _acceptable_path_to_delete / + ! _acceptable_path_to_delete //// + ! _acceptable_path_to_delete " //// " + ! _acceptable_path_to_delete /var + ! _acceptable_path_to_delete /var/www + ! _acceptable_path_to_delete /var/cache + ! _acceptable_path_to_delete /usr + ! _acceptable_path_to_delete /usr/bin + ! _acceptable_path_to_delete /home + ! _acceptable_path_to_delete /home/yunohost.backup + ! _acceptable_path_to_delete /home/yunohost.app + ! _acceptable_path_to_delete /home/yunohost.app/ + ! _acceptable_path_to_delete ///home///yunohost.app/// + ! _acceptable_path_to_delete /home/yunohost.app/$app/.. + ! _acceptable_path_to_delete ///home///yunohost.app///$app///..// + ! _acceptable_path_to_delete /home/yunohost.app/../$app/.. + ! _acceptable_path_to_delete /home/someuser + ! _acceptable_path_to_delete /home/yunohost.app//../../$app + ! _acceptable_path_to_delete " /home/yunohost.app/// " + ! _acceptable_path_to_delete /etc/cron.d/ + ! _acceptable_path_to_delete /etc/yunohost/ + + _acceptable_path_to_delete /home/yunohost.app/$app + _acceptable_path_to_delete /home/yunohost.app/$app/bar + _acceptable_path_to_delete /etc/cron.d/$app + _acceptable_path_to_delete /var/www/$app/bar + _acceptable_path_to_delete /var/www/$app + + rm /var/www/$app/bar + rm /etc/cron.d/$app + rmdir /home/yunohost.app/$app + rmdir /home/$app + rmdir /home/someuser + rmdir /var/www/$app +} + +ynhtest_secure_remove() { + + mkdir -p /home/someuser + mkdir -p /home/yunohost.app/$app + mkdir -p /var/www/$app + mkdir -p /var/whatever + touch /var/www/$app/bar + touch /etc/cron.d/$app + + ! ynh_secure_remove --file="/home/someuser" + ! ynh_secure_remove --file="/home/yunohost.app/" + ! ynh_secure_remove --file="/var/whatever" + ynh_secure_remove --file="/home/yunohost.app/$app" + ynh_secure_remove --file="/var/www/$app" + ynh_secure_remove --file="/etc/cron.d/$app" + + test -e /home/someuser + test -e /home/yunohost.app + test -e /var/whatever + ! test -e /home/yunohost.app/$app + ! test -e /var/www/$app + ! test -e /etc/cron.d/$app + + rmdir /home/someuser + rmdir /var/whatever +} From e563a366ef5eb85ed9e90dc39fea6a2bbb670018 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 7 Oct 2021 01:46:09 +0200 Subject: [PATCH 130/142] helpers apt: Use smarter grep with lookbehind to extract php version from dependency list Co-authored-by: Florent --- data/helpers.d/apt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index f563757fb..46b769804 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -252,7 +252,8 @@ ynh_install_app_dependencies () { # Check for specific php dependencies which requires sury # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni" - local specific_php_version=$(echo $dependencies | tr '-' ' ' | grep -o -E "\" | sed 's/php//g' | sort | uniq) + # The (?<=php) syntax corresponds to lookbehind ;) + local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>)' | sort -u) # Ignore case where the php version found is the one available in debian vanilla [[ "$specific_php_version" != "$YNH_DEFAULT_PHP_VERSION" ]] || specific_php_version="" From 560162dd96e47aca8e49bbe0bbd7351a5c772cf3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 7 Oct 2021 10:29:03 +0200 Subject: [PATCH 131/142] Sury pinning is managed in the core, c.f. 346728e5 --- data/helpers.d/apt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 46b769804..2f5df175c 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -351,12 +351,6 @@ ynh_add_sury() { # Add an extra repository for those packages ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 - # Pin this extra repository after packages are installed to prevent sury from doing shit - for package_to_not_upgrade in "php" "php-fpm" "php-mysql" "php-xml" "php-zip" "php-mbstring" "php-ldap" "php-gd" "php-curl" "php-bz2" "php-json" "php-sqlite3" "php-intl" "openssl" "libssl1.1" "libssl-dev" - do - ynh_pin_repo --package="$package_to_not_upgrade" --pin="origin \"packages.sury.org\"" --priority="-1" --name=extra_php_version --append - done - } From bde5590783f1e9a81b5b9ae33b1101c217fcf000 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 7 Oct 2021 10:33:33 +0200 Subject: [PATCH 132/142] Update data/helpers.d/logrotate --- data/helpers.d/logrotate | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/logrotate b/data/helpers.d/logrotate index a4548512d..1844cc5c7 100644 --- a/data/helpers.d/logrotate +++ b/data/helpers.d/logrotate @@ -97,8 +97,10 @@ EOF mkdir --parents $(dirname "$logfile") # Create the log directory, if not exist cat ${app}-logrotate | $customtee /etc/logrotate.d/$app > /dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee) - ynh_user_exists --username="$app" || chown $app:$app "$logfile" - chmod o-rwx "$logfile" + if ynh_user_exists --username="$app"; then + chown $app:$app "$logfile" + chmod o-rwx "$logfile" + fi } From f769c40f9602d9ae85eb4c99d332ee6da9781f95 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 7 Oct 2021 10:37:05 +0200 Subject: [PATCH 133/142] Double quotes to prevent bash apocalypse --- data/helpers.d/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index a2d7855b9..1e1930010 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -737,7 +737,7 @@ _acceptable_path_to_delete() { # Use realpath to normalize the path .. # i.e convert ///foo//bar//..///baz//// to /foo/baz - file=$(realpath --no-symlinks $file) + file=$(realpath --no-symlinks "$file") if [ -z "$file" ] || grep -q -x -F "$file" <<< "$forbidden_paths"; then return 1 else From c8e14133d5757cefec3236a41c2914cb0a4feddd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 7 Oct 2021 10:46:18 +0200 Subject: [PATCH 134/142] Update changelog for 4.3.1.2 --- debian/changelog | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/debian/changelog b/debian/changelog index a10c83888..f778e0e5d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,17 @@ +yunohost (4.3.1.2) testing; urgency=low + + - [fix] apps: upgrade was broken because of typo ([#1350](https://github.com/YunoHost/yunohost/pull/1350)) + - [enh] apps: in app_info, return a new is_webapp info meant to be used by API/webadmin (4cd5e9b6) + - [fix] configpanel: handle case where file question didnt get modified from webadmin, in which case self.value contains a path (54d901ad) + - [fix] configpanel: bind_key -> bind_key_ to prevent yunohost from redacting key names which leads to broken log metadata.yml somehow (941cc294) + - [enh] questions: Add visible attribute support in cli (74256845) + - [enh] helpers: Simplify apt/php dependencies helpers ([#1018](https://github.com/YunoHost/yunohost/pull/1018)) + - [enh] helpers: In logrotate helper, enforce decent permissions on log file if app user exists ([#1352](https://github.com/YunoHost/yunohost/pull/1352)) + + Thanks to all contributors <3 ! (Éric Gaspar, Kay0u, ljf) + + -- Alexandre Aubin Thu, 07 Oct 2021 10:42:06 +0200 + yunohost (4.3.1.1) testing; urgency=low - [enh] app helpers: Update n version ([#1347](https://github.com/YunoHost/yunohost/pull/1347)) From 38cff4a98e9d52668a7360b53e0e685cfd77b612 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 7 Oct 2021 11:24:46 +0200 Subject: [PATCH 135/142] Fix app url regex, branch names may contain dots --- src/yunohost/app.py | 2 +- src/yunohost/tests/test_appurl.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index fe5281384..fb544cab2 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -79,7 +79,7 @@ re_app_instance_name = re.compile( ) APP_REPO_URL = re.compile( - r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?tree/[a-zA-Z0-9-_]+)?(\.git)?/?$" + r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?tree/[a-zA-Z0-9-_.]+)?(\.git)?/?$" ) APP_FILES_TO_COPY = [ diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index cf2c6c2c3..28f33d998 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -68,6 +68,7 @@ def test_repo_url_definition(): assert _is_app_repo_url( "https://gitlab.domainepublic.net/Neutrinet/neutrinet_ynh/-/tree/unstable" ) + assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar/tree/1.23.4") assert _is_app_repo_url("git@github.com:YunoHost-Apps/foobar_ynh.git") assert not _is_app_repo_url("github.com/YunoHost-Apps/foobar_ynh") From 0b2ef5d16fa2f3be229bdd3ae211390ff388ae7b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 7 Oct 2021 18:31:59 +0200 Subject: [PATCH 136/142] Update changelog for 4.3.1.3 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index f778e0e5d..def37d6b2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (4.3.1.3) testing; urgency=low + + - [fix] app: repo url branch names may contain dots (38cff4a9) + + -- Alexandre Aubin Thu, 07 Oct 2021 18:31:09 +0200 + yunohost (4.3.1.2) testing; urgency=low - [fix] apps: upgrade was broken because of typo ([#1350](https://github.com/YunoHost/yunohost/pull/1350)) From df02f898ee2ee5fe9260d0875e40dc490ec72926 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 9 Oct 2021 00:32:49 +0200 Subject: [PATCH 137/142] [enh] Don't generate dnsmasq conf for .local domains --- data/hooks/conf_regen/43-dnsmasq | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/hooks/conf_regen/43-dnsmasq b/data/hooks/conf_regen/43-dnsmasq index f3bed7b04..687fc704f 100755 --- a/data/hooks/conf_regen/43-dnsmasq +++ b/data/hooks/conf_regen/43-dnsmasq @@ -32,6 +32,7 @@ do_pre_regen() { # add domain conf files for domain in $YNH_DOMAINS; do + [[ ! $domain =~ \.local$ ]] || continue export domain ynh_render_template "domain.tpl" "${dnsmasq_dir}/${domain}" done @@ -40,8 +41,10 @@ do_pre_regen() { conf_files=$(ls -1 /etc/dnsmasq.d \ | awk '/^[^\.]+\.[^\.]+.*$/ { print $1 }') for domain in $conf_files; do - [[ $YNH_DOMAINS =~ $domain ]] \ - || touch "${dnsmasq_dir}/${domain}" + if [[ ! $YNH_DOMAINS =~ $domain ]] && [[ ! $domain =~ \.local$ ]] + then + touch "${dnsmasq_dir}/${domain}" + fi done } From e521fef23d204570735b2cf97382deb9f72902a5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 9 Oct 2021 02:20:08 +0200 Subject: [PATCH 138/142] Fix typo in tests @_@ --- src/yunohost/tests/test_appurl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index 28f33d998..7b4c6e2e3 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -68,7 +68,7 @@ def test_repo_url_definition(): assert _is_app_repo_url( "https://gitlab.domainepublic.net/Neutrinet/neutrinet_ynh/-/tree/unstable" ) - assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar/tree/1.23.4") + assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh/tree/1.23.4") assert _is_app_repo_url("git@github.com:YunoHost-Apps/foobar_ynh.git") assert not _is_app_repo_url("github.com/YunoHost-Apps/foobar_ynh") From dab3dc6f370e262ed427a3459aebde35f4f92da6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Oct 2021 19:42:39 +0200 Subject: [PATCH 139/142] dovecot: add conf snippet to get rid of stupid stats-writer errors in mail.log --- data/templates/dovecot/dovecot.conf | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/data/templates/dovecot/dovecot.conf b/data/templates/dovecot/dovecot.conf index ee8511f83..c7e937979 100644 --- a/data/templates/dovecot/dovecot.conf +++ b/data/templates/dovecot/dovecot.conf @@ -78,6 +78,20 @@ service quota-warning { } } +service stats { + unix_listener stats-reader { + user = vmail + group = mail + mode = 0660 + } + + unix_listener stats-writer { + user = vmail + group = mail + mode = 0660 + } +} + plugin { sieve = /var/mail/sievescript/%n/.dovecot.sieve sieve_dir = /var/mail/sievescript/%n/scripts/ From 910364f9c4a3309e0411a87b15e8987996bd4cfc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 12 Oct 2021 15:49:08 +0200 Subject: [PATCH 140/142] helpers: Drop obsolete/unused/weird logging helpers --- data/helpers.d/logging | 86 ------------------------------------------ data/helpers.d/php | 24 ++++++------ 2 files changed, 12 insertions(+), 98 deletions(-) diff --git a/data/helpers.d/logging b/data/helpers.d/logging index 71998763e..9075fc7aa 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -38,25 +38,6 @@ ynh_print_info() { echo "$message" >&$YNH_STDINFO } -# Ignore the yunohost-cli log to prevent errors with conditional commands -# -# [internal] -# -# usage: ynh_no_log COMMAND -# -# Simply duplicate the log, execute the yunohost command and replace the log without the result of this command -# It's a very badly hack... -# -# Requires YunoHost version 2.6.4 or higher. -ynh_no_log() { - local ynh_cli_log=/var/log/yunohost/yunohost-cli.log - cp --archive ${ynh_cli_log} ${ynh_cli_log}-move - eval $@ - local exit_code=$? - mv ${ynh_cli_log}-move ${ynh_cli_log} - return $exit_code -} - # Main printer, just in case in the future we have to change anything about that. # # [internal] @@ -302,70 +283,3 @@ ynh_script_progression () { ynh_return () { echo "$1" >> "$YNH_STDRETURN" } - -# Debugger for app packagers -# -# usage: ynh_debug [--message=message] [--trace=1/0] -# | arg: -m, --message= - The text to print -# | arg: -t, --trace= - Turn on or off the trace of the script. Usefull to trace nonly a small part of a script. -# -# Requires YunoHost version 3.5.0 or higher. -ynh_debug () { - # Disable set xtrace for the helper itself, to not pollute the debug log - set +o xtrace # set +x - # Declare an array to define the options of this helper. - local legacy_args=mt - local -A args_array=( [m]=message= [t]=trace= ) - local message - local trace - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - # Re-disable xtrace, ynh_handle_getopts_args set it back - set +o xtrace # set +x - message=${message:-} - trace=${trace:-} - - if [ -n "$message" ] - then - ynh_print_log "[Debug] ${message}" >&2 - fi - - if [ "$trace" == "1" ] - then - ynh_debug --message="Enable debugging" - set +o xtrace # set +x - # Get the current file descriptor of xtrace - old_bash_xtracefd=$BASH_XTRACEFD - # Add the current file name and the line number of any command currently running while tracing. - PS4='$(basename ${BASH_SOURCE[0]})-L${LINENO}: ' - # Force xtrace to stderr - BASH_XTRACEFD=2 - # Force stdout to stderr - exec 1>&2 - fi - if [ "$trace" == "0" ] - then - ynh_debug --message="Disable debugging" - set +o xtrace # set +x - # Put xtrace back to its original fild descriptor - BASH_XTRACEFD=$old_bash_xtracefd - # Restore stdout - exec 1>&1 - fi - # Renable set xtrace - set -o xtrace # set -x -} - -# Execute a command and print the result as debug -# -# usage: ynh_debug_exec "your_command [ | other_command ]" -# | arg: command - command to execute -# -# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. -# -# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. -# -# Requires YunoHost version 3.5.0 or higher. -ynh_debug_exec () { - ynh_debug --message="$(eval $@)" -} diff --git a/data/helpers.d/php b/data/helpers.d/php index d383c1e4f..63f3af653 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -478,28 +478,28 @@ ynh_get_scalable_phpfpm () { if [ $print -eq 1 ] then - ynh_debug --message="Footprint=${footprint}Mb by pool." - ynh_debug --message="Process manager=$php_pm" - ynh_debug --message="Max RAM=${max_ram}Mb" + ynh_print_warn --message="Footprint=${footprint}Mb by pool." + ynh_print_warn --message="Process manager=$php_pm" + ynh_print_warn --message="Max RAM=${max_ram}Mb" if [ "$php_pm" != "static" ] then - ynh_debug --message="\nMax estimated footprint=$(( $php_max_children * $footprint ))" - ynh_debug --message="Min estimated footprint=$(( $php_min_spare_servers * $footprint ))" + ynh_print_warn --message="\nMax estimated footprint=$(( $php_max_children * $footprint ))" + ynh_print_warn --message="Min estimated footprint=$(( $php_min_spare_servers * $footprint ))" fi if [ "$php_pm" = "dynamic" ] then - ynh_debug --message="Estimated average footprint=$(( $php_max_spare_servers * $footprint ))" + ynh_print_warn --message="Estimated average footprint=$(( $php_max_spare_servers * $footprint ))" elif [ "$php_pm" = "static" ] then - ynh_debug --message="Estimated footprint=$(( $php_max_children * $footprint ))" + ynh_print_warn --message="Estimated footprint=$(( $php_max_children * $footprint ))" fi - ynh_debug --message="\nRaw php-fpm values:" - ynh_debug --message="pm.max_children = $php_max_children" + ynh_print_warn --message="\nRaw php-fpm values:" + ynh_print_warn --message="pm.max_children = $php_max_children" if [ "$php_pm" = "dynamic" ] then - ynh_debug --message="pm.start_servers = $php_start_servers" - ynh_debug --message="pm.min_spare_servers = $php_min_spare_servers" - ynh_debug --message="pm.max_spare_servers = $php_max_spare_servers" + ynh_print_warn --message="pm.start_servers = $php_start_servers" + ynh_print_warn --message="pm.min_spare_servers = $php_min_spare_servers" + ynh_print_warn --message="pm.max_spare_servers = $php_max_spare_servers" fi fi } From a3c0ca4da69525eee6045c63b3dfaecbf9f4f032 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 12 Oct 2021 15:49:46 +0200 Subject: [PATCH 141/142] helpers: Don't use eval in ynh_exec_* helpers to prevent issues with special chars --- data/helpers.d/logging | 85 ++++++++++++++++------- tests/test_helpers.d/ynhtest_logging.sh | 92 +++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 25 deletions(-) create mode 100644 tests/test_helpers.d/ynhtest_logging.sh diff --git a/data/helpers.d/logging b/data/helpers.d/logging index 9075fc7aa..b8deef26e 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -83,72 +83,107 @@ ynh_print_err () { # Execute a command and print the result as an error # -# usage: ynh_exec_err "your_command [ | other_command ]" +# usage: ynh_exec_err your command and args # | arg: command - command to execute # -# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. -# -# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. +# Note that you should NOT quote the command but only prefix it with ynh_exec_err # # Requires YunoHost version 3.2.0 or higher. ynh_exec_err () { - ynh_print_err "$(eval $@)" + # Boring legacy handling for when people calls ynh_exec_* wrapping the command in quotes, + # (because in the past eval was used) ... + # we detect this by checking that there's no 2nd arg, and $1 contains a space + if [[ "$#" -eq 1 ]] && [[ "$1" == *" "* ]] + then + ynh_print_err "$(eval $@)" + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + ynh_print_err "$("$@")" + fi } # Execute a command and print the result as a warning # -# usage: ynh_exec_warn "your_command [ | other_command ]" +# usage: ynh_exec_warn your command and args # | arg: command - command to execute # -# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. -# -# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. +# Note that you should NOT quote the command but only prefix it with ynh_exec_warn # # Requires YunoHost version 3.2.0 or higher. ynh_exec_warn () { - ynh_print_warn "$(eval $@)" + # Boring legacy handling for when people calls ynh_exec_* wrapping the command in quotes, + # (because in the past eval was used) ... + # we detect this by checking that there's no 2nd arg, and $1 contains a space + if [[ "$#" -eq 1 ]] && [[ "$1" == *" "* ]] + then + ynh_print_warn "$(eval $@)" + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + ynh_print_warn "$("$@")" + fi } # Execute a command and force the result to be printed on stdout # -# usage: ynh_exec_warn_less "your_command [ | other_command ]" +# usage: ynh_exec_warn_less your command and args # | arg: command - command to execute # -# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. -# -# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. +# Note that you should NOT quote the command but only prefix it with ynh_exec_warn # # Requires YunoHost version 3.2.0 or higher. ynh_exec_warn_less () { - eval $@ 2>&1 + # Boring legacy handling for when people calls ynh_exec_* wrapping the command in quotes, + # (because in the past eval was used) ... + # we detect this by checking that there's no 2nd arg, and $1 contains a space + if [[ "$#" -eq 1 ]] && [[ "$1" == *" "* ]] + then + eval $@ 2>&1 + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + "$@" 2>&1 + fi } # Execute a command and redirect stdout in /dev/null # -# usage: ynh_exec_quiet "your_command [ | other_command ]" +# usage: ynh_exec_quiet your command and args # | arg: command - command to execute # -# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. -# -# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. +# Note that you should NOT quote the command but only prefix it with ynh_exec_warn # # Requires YunoHost version 3.2.0 or higher. ynh_exec_quiet () { - eval $@ > /dev/null + # Boring legacy handling for when people calls ynh_exec_* wrapping the command in quotes, + # (because in the past eval was used) ... + # we detect this by checking that there's no 2nd arg, and $1 contains a space + if [[ "$#" -eq 1 ]] && [[ "$1" == *" "* ]] + then + eval $@ > /dev/null + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + "$@" > /dev/null + fi } # Execute a command and redirect stdout and stderr in /dev/null # -# usage: ynh_exec_fully_quiet "your_command [ | other_command ]" +# usage: ynh_exec_quiet your command and args # | arg: command - command to execute # -# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. -# -# If the command to execute uses double quotes, they have to be escaped or they will be interpreted and removed. +# Note that you should NOT quote the command but only prefix it with ynh_exec_quiet # # Requires YunoHost version 3.2.0 or higher. ynh_exec_fully_quiet () { - eval $@ > /dev/null 2>&1 + # Boring legacy handling for when people calls ynh_exec_* wrapping the command in quotes, + # (because in the past eval was used) ... + # we detect this by checking that there's no 2nd arg, and $1 contains a space + if [[ "$#" -eq 1 ]] && [[ "$1" == *" "* ]] + then + eval $@ > /dev/null 2>&1 + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + "$@" > /dev/null 2>&1 + fi } # Remove any logs for all the following commands. diff --git a/tests/test_helpers.d/ynhtest_logging.sh b/tests/test_helpers.d/ynhtest_logging.sh new file mode 100644 index 000000000..bb1241614 --- /dev/null +++ b/tests/test_helpers.d/ynhtest_logging.sh @@ -0,0 +1,92 @@ +ynhtest_exec_warn_less() { + + FOO='foo' + bar="" + BAR='$bar' + FOOBAR="foo bar" + + # These looks like stupid edge case + # but in fact happens when dealing with passwords + # (which could also contain bash chars like [], {}, ...) + # or urls containing &, ... + FOOANDBAR="foo&bar" + FOO1QUOTEBAR="foo'bar" + FOO2QUOTEBAR="foo\"bar" + + ynh_exec_warn_less uptime + + test ! -e $FOO + ynh_exec_warn_less touch $FOO + test -e $FOO + rm $FOO + + test ! -e $FOO1QUOTEBAR + ynh_exec_warn_less touch $FOO1QUOTEBAR + test -e $FOO1QUOTEBAR + rm $FOO1QUOTEBAR + + test ! -e $FOO2QUOTEBAR + ynh_exec_warn_less touch $FOO2QUOTEBAR + test -e $FOO2QUOTEBAR + rm $FOO2QUOTEBAR + + test ! -e $BAR + ynh_exec_warn_less touch $BAR + test -e $BAR + rm $BAR + + test ! -e "$FOOBAR" + ynh_exec_warn_less touch "$FOOBAR" + test -e "$FOOBAR" + rm "$FOOBAR" + + test ! -e "$FOOANDBAR" + ynh_exec_warn_less touch $FOOANDBAR + test -e "$FOOANDBAR" + rm "$FOOANDBAR" + + ########################### + # Legacy stuff using eval # + ########################### + + test ! -e $FOO + ynh_exec_warn_less "touch $FOO" + test -e $FOO + rm $FOO + + test ! -e $FOO1QUOTEBAR + ynh_exec_warn_less "touch \"$FOO1QUOTEBAR\"" + # (this works but expliciy *double* quotes have to be provided) + test -e $FOO1QUOTEBAR + rm $FOO1QUOTEBAR + + #test ! -e $FOO2QUOTEBAR + #ynh_exec_warn_less "touch \'$FOO2QUOTEBAR\'" + ## (this doesn't work with simple or double quotes) + #test -e $FOO2QUOTEBAR + #rm $FOO2QUOTEBAR + + test ! -e $BAR + ynh_exec_warn_less 'touch $BAR' + # That one works because $BAR is only interpreted during eval + test -e $BAR + rm $BAR + + #test ! -e $BAR + #ynh_exec_warn_less "touch $BAR" + # That one doesn't work because $bar gets interpreted as empty var by eval... + #test -e $BAR + #rm $BAR + + test ! -e "$FOOBAR" + ynh_exec_warn_less "touch \"$FOOBAR\"" + # (works but requires explicit double quotes otherwise eval would interpret 'foo bar' as two separate args..) + test -e "$FOOBAR" + rm "$FOOBAR" + + test ! -e "$FOOANDBAR" + ynh_exec_warn_less "touch \"$FOOANDBAR\"" + # (works but requires explicit double quotes otherwise eval would interpret '&' as a "run command in background" and also bar is not a valid command) + test -e "$FOOANDBAR" + rm "$FOOANDBAR" +} From fe959bd7faee506c4691ac78813256f1e2ed1e68 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 12 Oct 2021 16:53:01 +0200 Subject: [PATCH 142/142] helpers: Flag ynh_print_ON/OFF as internal to not advertise them in the doc --- data/helpers.d/logging | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/helpers.d/logging b/data/helpers.d/logging index 7a8be30e9..72403e7e1 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -174,6 +174,8 @@ ynh_exec_fully_quiet() { # # usage: ynh_print_OFF # +# [internal] +# # WARNING: You should be careful with this helper, and never forget to use ynh_print_ON as soon as possible to restore the logging. # # Requires YunoHost version 3.2.0 or higher. @@ -185,6 +187,8 @@ ynh_print_OFF() { # # usage: ynh_print_ON # +# [internal] +# # Requires YunoHost version 3.2.0 or higher. ynh_print_ON() { exec {BASH_XTRACEFD}>&1