diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 000000000..453396f07 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,24 @@ +--- +version: "2" +plugins: + duplication: + enabled: true + config: + languages: + python: + python_version: 3 + shellcheck: + enabled: true + pep8: + enabled: true + fixme: + enabled: true + sonar-python: + enabled: true + config: + tests_patterns: + - bin/* + - data/** + - doc/* + - src/** + - tests/** \ No newline at end of file diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..fe22c8381 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[report] +omit=src/tests/*,src/vendor/*,/usr/lib/moulinette/yunohost/* diff --git a/.gitignore b/.gitignore index 75f4ae6ea..eae46b4c5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ pip-log.txt .mr.developer.cfg # moulinette lib -src/yunohost/locales +src/locales # Test -src/yunohost/tests/apps +src/tests/apps diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d1cb36b73..4d0f30679 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ stages: - build - install - - tests + - test - lint - doc - translation @@ -13,12 +13,25 @@ default: # All jobs are interruptible by default interruptible: true +code_quality: + tags: + - docker + +code_quality_html: + extends: code_quality + variables: + REPORT_FORMAT: html + artifacts: + paths: [gl-code-quality-report.html] + # see: https://docs.gitlab.com/ee/ci/yaml/#switch-between-branch-pipelines-and-merge-request-pipelines workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" # If we move to gitlab one day - if: $CI_PIPELINE_SOURCE == "external_pull_request_event" # For github PR - if: $CI_COMMIT_TAG # For tags + - if: $CI_COMMIT_REF_NAME == "ci-format-$CI_DEFAULT_BRANCH" # Ignore black formatting branch created by the CI + when: never - if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" # If it's not the default branch and if it's a push, then do not trigger a build when: never - when: always @@ -27,4 +40,5 @@ variables: YNH_BUILD_DIR: "ynh-build" include: + - template: Code-Quality.gitlab-ci.yml - local: .gitlab/ci/*.gitlab-ci.yml diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index 717a5ee73..db691b9d2 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -5,11 +5,13 @@ YNH_SOURCE: "https://github.com/yunohost" before_script: - mkdir -p $YNH_BUILD_DIR + - DEBIAN_FRONTEND=noninteractive apt update artifacts: paths: - $YNH_BUILD_DIR/*.deb .build_script: &build_script + - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" install devscripts --no-install-recommends - cd $YNH_BUILD_DIR/$PACKAGE - VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null) - VERSION_NIGHTLY="${VERSION}+$(date +%Y%m%d%H%M)" diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index 9c48bd912..2c2bdcc1d 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -3,31 +3,29 @@ ######################################## # later we must fix lint and format-check jobs and remove "allow_failure" ---- -lint37: +lint39: stage: lint image: "before-install" needs: [] allow_failure: true script: - - tox -e py37-lint + - tox -e py39-lint -invalidcode37: +invalidcode39: stage: lint image: "before-install" needs: [] script: - - tox -e py37-invalidcode + - tox -e py39-invalidcode -format-check: +mypy: stage: lint image: "before-install" - allow_failure: true needs: [] script: - - tox -e py37-black-check + - tox -e py39-mypy -format-run: +black: stage: lint image: "before-install" needs: [] @@ -40,11 +38,11 @@ format-run: script: # create a local branch that will overwrite distant one - git checkout -b "ci-format-${CI_COMMIT_REF_NAME}" --no-track - - tox -e py37-black-run + - tox -e py39-black-run - '[ $(git diff | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit - - git commit -am "[CI] Format code" || true + - git commit -am "[CI] Format code with Black" || true - git push -f origin "ci-format-${CI_COMMIT_REF_NAME}":"ci-format-${CI_COMMIT_REF_NAME}" - - hub pull-request -m "[CI] Format code" -b Yunohost:dev -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd + - hub pull-request -m "[CI] Format code with Black" -b Yunohost:$CI_COMMIT_REF_NAME -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd only: - refs: - - dev + variables: + - $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index e0e0e001a..27b9b4913 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -1,9 +1,10 @@ .install_debs: &install_debs - apt-get update -o Acquire::Retries=3 - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ./$YNH_BUILD_DIR/*.deb + - pip3 install -U mock pip pytest pytest-cov pytest-mock pytest-sugar requests-mock tox ansi2html black jinja2 .test-stage: - stage: tests + stage: test image: "after-install" variables: PYTEST_ADDOPTS: "--color=yes" @@ -11,7 +12,7 @@ - *install_debs cache: paths: - - src/yunohost/tests/apps + - src/tests/apps key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG" needs: - job: build-yunohost @@ -22,13 +23,12 @@ artifacts: true - job: upgrade - ######################################## # TESTS ######################################## full-tests: - stage: tests + stage: test image: "before-install" variables: PYTEST_ADDOPTS: "--color=yes" @@ -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/tests/ src/diagnosers/ --junitxml=report.xml - cd tests - bash test_helpers.sh needs: @@ -50,31 +50,13 @@ full-tests: reports: junit: report.xml -test-i18n-keys: - extends: .test-stage - script: - - python3 -m pytest tests/test_i18n_keys.py - only: - changes: - - locales/en.json - - src/yunohost/*.py - - data/hooks/diagnosis/*.py - -test-translation-format-consistency: - extends: .test-stage - script: - - python3 -m pytest tests/test_translation_format_consistency.py - only: - changes: - - locales/* - test-actionmap: extends: .test-stage script: - - python3 -m pytest tests tests/test_actionmap.py + - python3 -m pytest tests/test_actionmap.py only: changes: - - data/actionsmap/*.yml + - share/actionsmap.yml test-helpers: extends: .test-stage @@ -83,112 +65,126 @@ test-helpers: - bash test_helpers.sh only: changes: - - data/helpers.d/* + - helpers/* + +test-domains: + extends: .test-stage + script: + - python3 -m pytest src/tests/test_domains.py + only: + changes: + - src/domain.py + +test-dns: + extends: .test-stage + script: + - python3 -m pytest src/tests/test_dns.py + only: + changes: + - src/dns.py + - src/utils/dns.py test-apps: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_apps.py + - python3 -m pytest src/tests/test_apps.py only: changes: - - src/yunohost/app.py + - src/app.py test-appscatalog: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_appscatalog.py + - python3 -m pytest src/tests/test_app_catalog.py only: changes: - - src/yunohost/app.py + - src/app_calalog.py test-appurl: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_appurl.py + - python3 -m pytest src/tests/test_appurl.py only: changes: - - src/yunohost/app.py + - src/app.py -test-apps-arguments-parsing: +test-questions: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_apps_arguments_parsing.py + - python3 -m pytest src/tests/test_questions.py only: changes: - - src/yunohost/app.py + - src/utils/config.py + +test-app-config: + extends: .test-stage + script: + - python3 -m pytest src/tests/test_app_config.py + only: + changes: + - src/app.py + - src/utils/config.py test-changeurl: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_changeurl.py + - python3 -m pytest src/tests/test_changeurl.py only: changes: - - src/yunohost/app.py + - src/app.py test-backuprestore: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_backuprestore.py + - python3 -m pytest src/tests/test_backuprestore.py only: changes: - - src/yunohost/backup.py + - src/backup.py test-permission: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_permission.py + - python3 -m pytest src/tests/test_permission.py only: changes: - - src/yunohost/permission.py + - src/permission.py test-settings: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_settings.py + - python3 -m pytest src/tests/test_settings.py only: changes: - - src/yunohost/settings.py + - src/settings.py test-user-group: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_user-group.py + - python3 -m pytest src/tests/test_user-group.py only: changes: - - src/yunohost/user.py + - src/user.py test-regenconf: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_regenconf.py + - python3 -m pytest src/tests/test_regenconf.py only: changes: - - src/yunohost/regenconf.py + - src/regenconf.py test-service: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_service.py + - python3 -m pytest src/tests/test_service.py only: changes: - - src/yunohost/service.py + - src/service.py test-ldapauth: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_ldapauth.py + - python3 -m pytest src/tests/test_ldapauth.py only: changes: - - src/yunohost/authenticators/*.py + - src/authenticators/*.py diff --git a/.gitlab/ci/translation.gitlab-ci.yml b/.gitlab/ci/translation.gitlab-ci.yml index e6365adbc..b6c683f57 100644 --- a/.gitlab/ci/translation.gitlab-ci.yml +++ b/.gitlab/ci/translation.gitlab-ci.yml @@ -1,6 +1,15 @@ ######################################## # TRANSLATION ######################################## +test-i18n-keys: + stage: translation + script: + - python3 maintenance/missing_i18n_keys.py --check + only: + changes: + - locales/en.json + - src/*.py + - src/diagnosers/*.py autofix-translated-strings: stage: translation @@ -10,18 +19,17 @@ autofix-translated-strings: - apt-get update -y && apt-get install git hub -y - git config --global user.email "yunohost@yunohost.org" - git config --global user.name "$GITHUB_USER" - - git remote set-url origin https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git + - hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo + - cd github_repo script: - - cd tests # Maybe move this script location to another folder? # create a local branch that will overwrite distant one - git checkout -b "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}" --no-track - - python3 remove_stale_translated_strings.py - - python3 autofix_locale_format.py - - python3 reformat_locales.py - - '[ $(git diff -w | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit + - python3 maintenance/missing_i18n_keys.py --fix + - python3 maintenance/autofix_locale_format.py + - '[ $(git diff | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit - git commit -am "[CI] Reformat / remove stale translated strings" || true - - git push -f origin "ci-remove-stale-translated-strings-${CI_COMMIT_REF_NAME}":"ci-remove-stale-translated-strings-${CI_COMMIT_REF_NAME}" - - hub pull-request -m "[CI] Reformat / remove stale translated strings" -b Yunohost:dev -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd + - git push -f origin "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}":"ci-remove-stale-translated-strings-${CI_COMMIT_REF_NAME}" + - hub pull-request -m "[CI] Reformat / remove stale translated strings" -b Yunohost:$CI_COMMIT_REF_NAME -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd only: variables: - $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH diff --git a/.lgtm.yml b/.lgtm.yml new file mode 100644 index 000000000..8fd57e49e --- /dev/null +++ b/.lgtm.yml @@ -0,0 +1,4 @@ +extraction: + python: + python_setup: + version: "3" \ No newline at end of file diff --git a/README.md b/README.md index 9fc93740d..969651eee 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,10 @@
+![Version](https://img.shields.io/github/v/tag/yunohost/yunohost?label=version&sort=semver) [![Build status](https://shields.io/gitlab/pipeline/yunohost/yunohost/dev)](https://gitlab.com/yunohost/yunohost/-/pipelines) +![Test coverage](https://img.shields.io/gitlab/coverage/yunohost/yunohost/dev) +[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/YunoHost/yunohost.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/YunoHost/yunohost/context:python) [![GitHub license](https://img.shields.io/github/license/YunoHost/yunohost)](https://github.com/YunoHost/yunohost/blob/dev/LICENSE) [![Mastodon Follow](https://img.shields.io/mastodon/follow/28084)](https://mastodon.social/@yunohost) diff --git a/bin/yunohost b/bin/yunohost index 0220c5f09..8cebdee8e 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -4,45 +4,48 @@ import os import sys import argparse - -sys.path.insert(0, "/usr/lib/moulinette/") import yunohost def _parse_cli_args(): """Parse additional arguments for the cli""" parser = argparse.ArgumentParser(add_help=False) - parser.add_argument('--output-as', - choices=['json', 'plain', 'none'], default=None, - help="Output result in another format" + parser.add_argument( + "--output-as", + choices=["json", "plain", "none"], + default=None, + help="Output result in another format", ) - parser.add_argument('--debug', - action='store_true', default=False, - help="Log and print debug messages" + parser.add_argument( + "--debug", + action="store_true", + default=False, + help="Log and print debug messages", ) - parser.add_argument('--quiet', - action='store_true', default=False, - help="Don't produce any output" + parser.add_argument( + "--quiet", action="store_true", default=False, help="Don't produce any output" ) - parser.add_argument('--timeout', - type=int, default=None, - help="Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock" + parser.add_argument( + "--timeout", + type=int, + default=None, + help="Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock", ) # deprecated arguments - parser.add_argument('--plain', - action='store_true', default=False, help=argparse.SUPPRESS + parser.add_argument( + "--plain", action="store_true", default=False, help=argparse.SUPPRESS ) - parser.add_argument('--json', - action='store_true', default=False, help=argparse.SUPPRESS + parser.add_argument( + "--json", action="store_true", default=False, help=argparse.SUPPRESS ) opts, args = parser.parse_known_args() # output compatibility if opts.plain: - opts.output_as = 'plain' + opts.output_as = "plain" elif opts.json: - opts.output_as = 'json' + opts.output_as = "json" return (parser, opts, args) @@ -54,10 +57,12 @@ if os.environ["PATH"] != default_path: # Main action ---------------------------------------------------------- -if __name__ == '__main__': +if __name__ == "__main__": if os.geteuid() != 0: - sys.stderr.write("\033[1;31mError:\033[0m yunohost command must be " - "run as root or with sudo.\n") + sys.stderr.write( + "\033[1;31mError:\033[0m yunohost command must be " + "run as root or with sudo.\n" + ) sys.exit(1) parser, opts, args = _parse_cli_args() @@ -69,5 +74,5 @@ if __name__ == '__main__': output_as=opts.output_as, timeout=opts.timeout, args=args, - parser=parser + parser=parser, ) diff --git a/bin/yunohost-api b/bin/yunohost-api index b3ed3a817..8cf9d4f26 100755 --- a/bin/yunohost-api +++ b/bin/yunohost-api @@ -1,44 +1,53 @@ #! /usr/bin/python3 # -*- coding: utf-8 -*- -import sys import argparse - -sys.path.insert(0, "/usr/lib/moulinette/") import yunohost # Default server configuration -DEFAULT_HOST = 'localhost' +DEFAULT_HOST = "localhost" DEFAULT_PORT = 6787 def _parse_api_args(): """Parse main arguments for the api""" - parser = argparse.ArgumentParser(add_help=False, + parser = argparse.ArgumentParser( + add_help=False, description="Run the YunoHost API to manage your server.", ) - srv_group = parser.add_argument_group('server configuration') - srv_group.add_argument('-h', '--host', - action='store', default=DEFAULT_HOST, + srv_group = parser.add_argument_group("server configuration") + srv_group.add_argument( + "-h", + "--host", + action="store", + default=DEFAULT_HOST, help="Host to listen on (default: %s)" % DEFAULT_HOST, ) - srv_group.add_argument('-p', '--port', - action='store', default=DEFAULT_PORT, type=int, + srv_group.add_argument( + "-p", + "--port", + action="store", + default=DEFAULT_PORT, + type=int, help="Port to listen on (default: %d)" % DEFAULT_PORT, ) - glob_group = parser.add_argument_group('global arguments') - glob_group.add_argument('--debug', - action='store_true', default=False, + glob_group = parser.add_argument_group("global arguments") + glob_group.add_argument( + "--debug", + action="store_true", + default=False, help="Set log level to DEBUG", ) - glob_group.add_argument('--help', - action='help', help="Show this help message and exit", + glob_group.add_argument( + "--help", + action="help", + help="Show this help message and exit", ) return parser.parse_args() -if __name__ == '__main__': +if __name__ == "__main__": opts = _parse_api_args() # Run the server yunohost.api(debug=opts.debug, host=opts.host, port=opts.port) diff --git a/bin/yunomdns b/bin/yunomdns index 862a1f477..1bdcf88ca 100755 --- a/bin/yunomdns +++ b/bin/yunomdns @@ -4,160 +4,173 @@ 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 -from zeroconf import Zeroconf, ServiceInfo +import ifaddr +from ipaddress import ip_address +from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser -# 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 - - 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 ip_address(ip.ip).is_private + and not ip_address(ip.ip).is_link_local + ], + "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" } + return interfaces - return devices -if __name__ == '__main__': +# 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: ### # 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 {} - updated = False - 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("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.') - sys.exit(0) + if "interfaces" not in config: + config["interfaces"] = [ + interface + for interface, local_ips in interfaces.items() + if local_ips["ipv4"] + ] - if 'yunohost.local' not in config['domains']: - config['domains'].append('yunohost.local') + if "ban_interfaces" in config: + config["interfaces"] = [ + interface + for interface in config["interfaces"] + if interface not in config["ban_interfaces"] + ] - zcs = {} - interfaces = get_network_interfaces() - for interface in config['interfaces']: - infos = [] # List of ServiceInfo objects, to feed Zeroconf - ips = [] # Human-readable IPs - b_ips = [] # Binary-convered IPs + # 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() - ipv4 = interfaces[interface]['ipv4'].split('/')[0] - if ipv4: - ips.append(ipv4) - b_ips.append(socket.inet_pton(socket.AF_INET, ipv4)) + # Always attempt to publish yunohost.local + if "yunohost.local" not in config["domains"]: + config["domains"].append("yunohost.local") - ipv6 = interfaces[interface]['ipv6'].split('/')[0] - if ipv6: - ips.append(ipv6) - b_ips.append(socket.inet_pton(socket.AF_INET6, ipv6)) + def find_domain_not_already_published(domain): + + # 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]] = {} + + for interface in config["interfaces"]: + + if interface not in interfaces: + print( + f"Interface {interface} listed in config file is not present on system." + ) + continue + + # 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 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...") 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.") @@ -168,6 +181,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) diff --git a/conf/dnsmasq/dnsmasq.conf.tpl b/conf/dnsmasq/dnsmasq.conf.tpl new file mode 100644 index 000000000..eece530dc --- /dev/null +++ b/conf/dnsmasq/dnsmasq.conf.tpl @@ -0,0 +1,10 @@ +domain-needed +expand-hosts +localise-queries + +{% set interfaces = wireless_interfaces.strip().split(' ') %} +{% for interface in interfaces %} +interface={{ interface }} +{% endfor %} +resolv-file=/etc/resolv.dnsmasq.conf +cache-size=256 diff --git a/data/templates/dnsmasq/domain.tpl b/conf/dnsmasq/domain.tpl similarity index 60% rename from data/templates/dnsmasq/domain.tpl rename to conf/dnsmasq/domain.tpl index c4bb56d1d..50b946176 100644 --- a/data/templates/dnsmasq/domain.tpl +++ b/conf/dnsmasq/domain.tpl @@ -1,5 +1,8 @@ -host-record={{ domain }},{{ ipv4 }} -host-record=xmpp-upload.{{ domain }},{{ ipv4 }} +{% set interfaces_list = interfaces.split(' ') %} +{% for interface in interfaces_list %} +interface-name={{ domain }},{{ interface }} +interface-name=xmpp-upload.{{ domain }},{{ interface }} +{% endfor %} {% if ipv6 %} host-record={{ domain }},{{ ipv6 }} host-record=xmpp-upload.{{ domain }},{{ ipv6 }} diff --git a/data/templates/dnsmasq/plain/etcdefault b/conf/dnsmasq/plain/etcdefault similarity index 100% rename from data/templates/dnsmasq/plain/etcdefault rename to conf/dnsmasq/plain/etcdefault diff --git a/data/templates/dnsmasq/plain/resolv.dnsmasq.conf b/conf/dnsmasq/plain/resolv.dnsmasq.conf similarity index 100% rename from data/templates/dnsmasq/plain/resolv.dnsmasq.conf rename to conf/dnsmasq/plain/resolv.dnsmasq.conf diff --git a/data/templates/dovecot/dovecot-ldap.conf b/conf/dovecot/dovecot-ldap.conf similarity index 100% rename from data/templates/dovecot/dovecot-ldap.conf rename to conf/dovecot/dovecot-ldap.conf diff --git a/data/templates/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf similarity index 84% rename from data/templates/dovecot/dovecot.conf rename to conf/dovecot/dovecot.conf index ee8511f83..72fd71c4d 100644 --- a/data/templates/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -21,9 +21,14 @@ ssl = required ssl_cert = /path/to/dhparam -ssl_dh = /path/to/dhparam.pem # not actually 1024 bits, this applies to all DHE >= 1024 bits -smtpd_tls_dh1024_param_file = /usr/share/yunohost/other/ffdhe2048.pem +smtpd_tls_dh1024_param_file = /usr/share/yunohost/ffdhe2048.pem tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 {% else %} diff --git a/data/templates/postfix/plain/header_checks b/conf/postfix/plain/header_checks similarity index 100% rename from data/templates/postfix/plain/header_checks rename to conf/postfix/plain/header_checks diff --git a/data/templates/postfix/plain/ldap-accounts.cf b/conf/postfix/plain/ldap-accounts.cf similarity index 100% rename from data/templates/postfix/plain/ldap-accounts.cf rename to conf/postfix/plain/ldap-accounts.cf diff --git a/data/templates/postfix/plain/ldap-aliases.cf b/conf/postfix/plain/ldap-aliases.cf similarity index 100% rename from data/templates/postfix/plain/ldap-aliases.cf rename to conf/postfix/plain/ldap-aliases.cf diff --git a/data/templates/postfix/plain/ldap-domains.cf b/conf/postfix/plain/ldap-domains.cf similarity index 100% rename from data/templates/postfix/plain/ldap-domains.cf rename to conf/postfix/plain/ldap-domains.cf diff --git a/data/templates/postfix/plain/master.cf b/conf/postfix/plain/master.cf similarity index 100% rename from data/templates/postfix/plain/master.cf rename to conf/postfix/plain/master.cf diff --git a/data/templates/postfix/plain/sender_canonical b/conf/postfix/plain/sender_canonical similarity index 100% rename from data/templates/postfix/plain/sender_canonical rename to conf/postfix/plain/sender_canonical diff --git a/data/templates/postfix/plain/smtp_reply_filter b/conf/postfix/plain/smtp_reply_filter similarity index 100% rename from data/templates/postfix/plain/smtp_reply_filter rename to conf/postfix/plain/smtp_reply_filter diff --git a/data/templates/postfix/postsrsd b/conf/postfix/postsrsd similarity index 100% rename from data/templates/postfix/postsrsd rename to conf/postfix/postsrsd diff --git a/conf/postfix/sni b/conf/postfix/sni new file mode 100644 index 000000000..29ed2e043 --- /dev/null +++ b/conf/postfix/sni @@ -0,0 +1,2 @@ +{% for domain in domain_list.split() %}{{ domain }} /etc/yunohost/certs/{{ domain }}/key.pem /etc/yunohost/certs/{{ domain }}/crt.pem +{% endfor %} \ No newline at end of file diff --git a/data/templates/rspamd/dkim_signing.conf b/conf/rspamd/dkim_signing.conf similarity index 100% rename from data/templates/rspamd/dkim_signing.conf rename to conf/rspamd/dkim_signing.conf diff --git a/data/templates/rspamd/metrics.local.conf b/conf/rspamd/metrics.local.conf similarity index 100% rename from data/templates/rspamd/metrics.local.conf rename to conf/rspamd/metrics.local.conf diff --git a/data/templates/rspamd/milter_headers.conf b/conf/rspamd/milter_headers.conf similarity index 100% rename from data/templates/rspamd/milter_headers.conf rename to conf/rspamd/milter_headers.conf diff --git a/data/templates/rspamd/rspamd.sieve b/conf/rspamd/rspamd.sieve similarity index 100% rename from data/templates/rspamd/rspamd.sieve rename to conf/rspamd/rspamd.sieve diff --git a/data/templates/slapd/config.ldif b/conf/slapd/config.ldif similarity index 100% rename from data/templates/slapd/config.ldif rename to conf/slapd/config.ldif diff --git a/data/templates/slapd/db_init.ldif b/conf/slapd/db_init.ldif similarity index 100% rename from data/templates/slapd/db_init.ldif rename to conf/slapd/db_init.ldif diff --git a/data/templates/slapd/ldap.conf b/conf/slapd/ldap.conf similarity index 100% rename from data/templates/slapd/ldap.conf rename to conf/slapd/ldap.conf diff --git a/data/templates/slapd/mailserver.ldif b/conf/slapd/mailserver.ldif similarity index 100% rename from data/templates/slapd/mailserver.ldif rename to conf/slapd/mailserver.ldif diff --git a/data/templates/slapd/permission.ldif b/conf/slapd/permission.ldif similarity index 100% rename from data/templates/slapd/permission.ldif rename to conf/slapd/permission.ldif diff --git a/data/templates/slapd/slapd.default b/conf/slapd/slapd.default similarity index 100% rename from data/templates/slapd/slapd.default rename to conf/slapd/slapd.default diff --git a/data/templates/slapd/sudo.ldif b/conf/slapd/sudo.ldif similarity index 100% rename from data/templates/slapd/sudo.ldif rename to conf/slapd/sudo.ldif diff --git a/data/templates/slapd/systemd-override.conf b/conf/slapd/systemd-override.conf similarity index 100% rename from data/templates/slapd/systemd-override.conf rename to conf/slapd/systemd-override.conf diff --git a/data/templates/ssh/sshd_config b/conf/ssh/sshd_config similarity index 90% rename from data/templates/ssh/sshd_config rename to conf/ssh/sshd_config index 1c2854f73..b6d4111ee 100644 --- a/data/templates/ssh/sshd_config +++ b/conf/ssh/sshd_config @@ -2,6 +2,8 @@ # by YunoHost Protocol 2 +# PLEASE: if you wish to change the ssh port properly in YunoHost, use this command: +# yunohost settings set security.ssh.port -v Port {{ port }} {% if ipv6_enabled == "true" %}ListenAddress ::{% endif %} @@ -53,9 +55,13 @@ PermitEmptyPasswords no ChallengeResponseAuthentication no UsePAM yes -# Change to no to disable tunnelled clear text passwords -# (i.e. everybody will need to authenticate using ssh keys) +# PLEASE: if you wish to force everybody to authenticate using ssh keys, run this command: +# yunohost settings set security.ssh.password_authentication -v no +{% if password_authentication == "False" %} +PasswordAuthentication no +{% else %} #PasswordAuthentication yes +{% endif %} # Post-login stuff Banner /etc/issue.net diff --git a/data/templates/ssl/openssl.cnf b/conf/ssl/openssl.cnf similarity index 98% rename from data/templates/ssl/openssl.cnf rename to conf/ssl/openssl.cnf index 3ef7d80c3..a19a9c3df 100644 --- a/data/templates/ssl/openssl.cnf +++ b/conf/ssl/openssl.cnf @@ -5,7 +5,7 @@ # This definition stops the following lines choking if HOME isn't # defined. -HOME = /usr/share/yunohost/yunohost-config/ssl +HOME = /usr/share/yunohost/ssl RANDFILE = $ENV::HOME/.rnd # Extra OBJECT IDENTIFIER info: @@ -34,7 +34,7 @@ default_ca = Yunohost # The default ca section #################################################################### [ Yunohost ] -dir = /usr/share/yunohost/yunohost-config/ssl/yunoCA # Where everything is kept +dir = /usr/share/yunohost/ssl # Where everything is kept certs = $dir/certs # Where the issued certs are kept crl_dir = $dir/crl # Where the issued crl are kept database = $dir/index.txt # database index file. diff --git a/data/other/dpkg-origins/yunohost b/conf/yunohost/dpkg-origins similarity index 100% rename from data/other/dpkg-origins/yunohost rename to conf/yunohost/dpkg-origins diff --git a/data/templates/yunohost/firewall.yml b/conf/yunohost/firewall.yml similarity index 100% rename from data/templates/yunohost/firewall.yml rename to conf/yunohost/firewall.yml diff --git a/data/templates/yunohost/proc-hidepid.service b/conf/yunohost/proc-hidepid.service similarity index 100% rename from data/templates/yunohost/proc-hidepid.service rename to conf/yunohost/proc-hidepid.service diff --git a/data/templates/yunohost/services.yml b/conf/yunohost/services.yml similarity index 77% rename from data/templates/yunohost/services.yml rename to conf/yunohost/services.yml index c781d54aa..45621876e 100644 --- a/data/templates/yunohost/services.yml +++ b/conf/yunohost/services.yml @@ -12,24 +12,31 @@ metronome: log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err] needs_exposed_ports: [5222, 5269] category: xmpp + ignore_if_package_is_not_installed: metronome mysql: log: [/var/log/mysql.log,/var/log/mysql.err,/var/log/mysql/error.log] actual_systemd_service: mariadb category: database + ignore_if_package_is_not_installed: mariadb-server nginx: log: /var/log/nginx test_conf: nginx -t needs_exposed_ports: [80, 443] category: web -php7.3-fpm: - log: /var/log/php7.3-fpm.log - test_conf: php-fpm7.3 --test - category: web +# Yunohost will dynamically add installed php-fpm services (7.3, 7.4, 8.0, ...) in services.py +#php7.4-fpm: +# log: /var/log/php7.4-fpm.log +# test_conf: php-fpm7.4 --test +# category: web postfix: log: [/var/log/mail.log,/var/log/mail.err] actual_systemd_service: postfix@- needs_exposed_ports: [25, 587] category: email +postgresql: + actual_systemd_service: 'postgresql@13-main' + category: database + ignore_if_package_is_not_installed: postgresql-13 redis-server: log: /var/log/redis/redis-server.log category: database @@ -68,5 +75,6 @@ spamassassin: null rmilter: null php5-fpm: null php7.0-fpm: null +php7.3-fpm: null nslcd: null avahi-daemon: null diff --git a/debian/yunohost-api.service b/conf/yunohost/yunohost-api.service similarity index 59% rename from debian/yunohost-api.service rename to conf/yunohost/yunohost-api.service index 850255127..aa429ec7a 100644 --- a/debian/yunohost-api.service +++ b/conf/yunohost/yunohost-api.service @@ -4,9 +4,7 @@ After=network.target [Service] Type=simple -Environment=DAEMON_OPTS= -EnvironmentFile=-/etc/default/yunohost-api -ExecStart=/usr/bin/yunohost-api $DAEMON_OPTS +ExecStart=/usr/bin/yunohost-api Restart=always RestartSec=5 TimeoutStopSec=30 diff --git a/debian/yunohost-firewall.service b/conf/yunohost/yunohost-firewall.service similarity index 100% rename from debian/yunohost-firewall.service rename to conf/yunohost/yunohost-firewall.service diff --git a/data/other/yunoprompt.service b/conf/yunohost/yunoprompt.service similarity index 100% rename from data/other/yunoprompt.service rename to conf/yunohost/yunoprompt.service diff --git a/data/bash-completion.d/yunohost b/data/bash-completion.d/yunohost deleted file mode 100644 index 2572a391d..000000000 --- a/data/bash-completion.d/yunohost +++ /dev/null @@ -1,3 +0,0 @@ -# This file is automatically generated -# during Debian's package build by the script -# data/actionsmap/yunohost_completion.py diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost deleted file mode 100755 index dd018e8f1..000000000 --- a/data/hooks/conf_regen/01-yunohost +++ /dev/null @@ -1,226 +0,0 @@ -#!/bin/bash - -set -e - -do_init_regen() { - 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 - - [[ -d /etc/yunohost ]] || mkdir -p /etc/yunohost - - # 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 - - # 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 - - # App folders - mkdir -p /etc/yunohost/apps - chmod 700 /etc/yunohost/apps - mkdir -p /home/yunohost.app - chmod 755 /home/yunohost.app - - # 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 service conf - touch /etc/yunohost/services.yml - - mkdir -p /var/cache/yunohost/repo - chown root:root /var/cache/yunohost - chmod 700 /var/cache/yunohost -} - -do_pre_regen() { - pending_dir=$1 - - 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())" - - 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 -SHELL=/bin/bash -0 7,19 * * * root : YunoHost Automatic Diagnosis; sleep \$((RANDOM\\%1200)); yunohost diagnosis run --email > /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 -#!/bin/bash -(sleep \$((RANDOM%3600)); yunohost tools update --apps > /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 -#!/bin/bash -yunohost domain cert-renew --email -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 << EOF -SHELL=/bin/bash -*/10 * * * * root : YunoHost DynDNS update; sleep \$((RANDOM\\%60)); test -e /var/run/moulinette_yunohost.lock || yunohost dyndns update >> /dev/null -EOF - 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 - - # 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 - - # 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 -[Unit] -# yunohost-firewall and nftables conflict with each other -Conflicts=yunohost-firewall.service -ConditionFileIsExecutable=!/etc/init.d/yunohost-firewall -ConditionPathExists=!/etc/systemd/system/multi-user.target.wants/yunohost-firewall.service -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 << EOF -[Login] -HandleLidSwitch=ignore -HandleLidSwitchDocked=ignore -HandleLidSwitchExternalPower=ignore -EOF - - mkdir -p ${pending_dir}/etc/systemd/ - if [[ "$(yunohost settings get 'security.experimental.enabled')" == "True" ]] - then - cp proc-hidepid.service ${pending_dir}/etc/systemd/system/proc-hidepid.service - else - touch ${pending_dir}/etc/systemd/system/proc-hidepid.service - fi - -} - -do_post_regen() { - regen_conf_files=$1 - - ###################### - # Enfore permissions # - ###################### - - chmod 750 /home/admin - chmod 750 /home/yunohost.conf - chmod 750 /home/yunohost.backup - chmod 750 /home/yunohost.backup/archives - chown root:root /home/yunohost.conf - chown admin:root /home/yunohost.backup - chown admin:root /home/yunohost.backup/archives - - # Certs - # We do this with find because there could be a lot of them... - chown -R root:ssl-cert /etc/yunohost/certs - chmod 750 /etc/yunohost/certs - find /etc/yunohost/certs/ -type f -exec chmod 640 {} \; - find /etc/yunohost/certs/ -type d -exec chmod 750 {} \; - - find /etc/cron.*/yunohost-* -type f -exec chmod 755 {} \; - find /etc/cron.d/yunohost-* -type f -exec chmod 644 {} \; - find /etc/cron.*/yunohost-* -type f -exec chown root:root {} \; - - chown root:root /var/cache/yunohost - chmod 700 /var/cache/yunohost - chown root:root /var/cache/moulinette - chmod 700 /var/cache/moulinette - - setfacl -m g:all_users:--- /var/www - setfacl -m g:all_users:--- /var/log/nginx - setfacl -m g:all_users:--- /etc/yunohost - setfacl -m g:all_users:--- /etc/ssowat - - for USER in $(yunohost user list --quiet --output-as json | jq -r '.users | .[] | .username') - do - [ ! -e "/home/$USER" ] || setfacl -m g:all_users:--- /home/$USER - done - - - # 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) - - # 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" =~ "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 -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - init) - do_init_regen - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl deleted file mode 100755 index 6536e7280..000000000 --- a/data/hooks/conf_regen/02-ssl +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/bash - -set -e - -ssl_dir="/usr/share/yunohost/yunohost-config/ssl/yunoCA" -ynh_ca="/etc/yunohost/certs/yunohost.org/ca.pem" -ynh_crt="/etc/yunohost/certs/yunohost.org/crt.pem" -ynh_key="/etc/yunohost/certs/yunohost.org/key.pem" -openssl_conf="/usr/share/yunohost/templates/ssl/openssl.cnf" - -regen_local_ca() { - - domain="$1" - - echo -e "\n# Creating local certification authority with domain=$domain\n" - - # create certs and SSL directories - mkdir -p "/etc/yunohost/certs/yunohost.org" - mkdir -p "${ssl_dir}/"{ca,certs,crl,newcerts} - - pushd ${ssl_dir} - - # (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 - rm -f index.txt - touch index.txt - cp /usr/share/yunohost/templates/ssl/openssl.cnf openssl.ca.cnf - sed -i "s/yunohost.org/${domain}/g" openssl.ca.cnf - openssl req -x509 \ - -new \ - -config openssl.ca.cnf \ - -days 3650 \ - -out ca/cacert.pem \ - -keyout ca/cakey.pem \ - -nodes \ - -batch \ - -subj /CN=${domain}/O=${domain%.*} 2>&1 - - chmod 640 ca/cacert.pem - chmod 640 ca/cakey.pem - - cp ca/cacert.pem $ynh_ca - ln -sf "$ynh_ca" /etc/ssl/certs/ca-yunohost_crt.pem - update-ca-certificates - - popd -} - - - -do_init_regen() { - - 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 - - # 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 - - 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 - - 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 - - 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" -} - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/templates/ssl - - install -D -m 644 openssl.cnf "${pending_dir}/${ssl_dir}/openssl.cnf" -} - -do_post_regen() { - 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) - - 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 -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - init) - do_init_regen - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/06-slapd b/data/hooks/conf_regen/06-slapd deleted file mode 100755 index b2439dcf9..000000000 --- a/data/hooks/conf_regen/06-slapd +++ /dev/null @@ -1,221 +0,0 @@ -#!/bin/bash - -set -e - -tmp_backup_dir_file="/root/slapd-backup-dir.txt" - -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 - - do_pre_regen "" - - # Drop current existing slapd data - - rm -rf /var/backups/*.ldapdb - rm -rf /var/backups/slapd-* - - debconf-set-selections << EOF -slapd slapd/password1 password yunohost -slapd slapd/password2 password yunohost -slapd slapd/domain string yunohost.org -slapd shared/organization string yunohost.org -slapd slapd/allow_ldap_v2 boolean false -slapd slapd/invalid_config boolean true -slapd slapd/backend select MDB -slapd slapd/move_old_database boolean true -slapd slapd/no_configuration boolean false -slapd slapd/purge_database boolean false -EOF - - DEBIAN_FRONTEND=noninteractive dpkg-reconfigure slapd -u - - # Enforce permissions - chown -R openldap:openldap /etc/ldap/schema/ - usermod -aG ssl-cert openldap - - # (Re-)init data according to default ldap entries - echo ' Initializing LDAP with YunoHost DB structure' - - 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 - - nscd -i group || true - nscd -i passwd || true - - systemctl restart slapd - - # We don't use mkhomedir_helper because 'admin' may not be recognized - # when this script is ran in a chroot (e.g. ISO install) - # We also refer to admin as uid 1007 for the same reason - if [ ! -d /home/admin ] - then - cp -r /etc/skel /home/admin - chown -R 1007:1007 /home/admin - fi -} - -_regenerate_slapd_conf() { - - # 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 - - chown -R openldap:openldap /etc/ldap/slapd.d/ -} - -do_pre_regen() { - pending_dir=$1 - - # 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 - - # 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" - - 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" - - 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" -} - -do_post_regen() { - 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/ - - # 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 -cn: admins -gidNumber: 4001 -memberUid: admin -objectClass: posixGroup -objectClass: top" - chown -R openldap: /var/lib/ldap - systemctl restart slapd - nscd -i group - fi - - [ -z "$regen_conf_files" ] && exit 0 - - # 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 - - echo "Running slapdindex" - su openldap -s "/bin/bash" -c "/usr/sbin/slapindex" - - 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 -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - init) - do_init_regen - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/09-nslcd b/data/hooks/conf_regen/09-nslcd deleted file mode 100755 index 2e911b328..000000000 --- a/data/hooks/conf_regen/09-nslcd +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -set -e - -do_init_regen() { - do_pre_regen "" - systemctl restart nslcd -} - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/templates/nslcd - - install -D -m 644 nslcd.conf "${pending_dir}/etc/nslcd.conf" -} - -do_post_regen() { - regen_conf_files=$1 - - [[ -z "$regen_conf_files" ]] \ - || systemctl restart nslcd -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - init) - do_init_regen - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/10-apt b/data/hooks/conf_regen/10-apt deleted file mode 100755 index d2977ee92..000000000 --- a/data/hooks/conf_regen/10-apt +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash - -set -e - -do_pre_regen() { - pending_dir=$1 - - 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 - echo " -Package: $package -Pin: origin \"packages.sury.org\" -Pin-Priority: -1" >> "${pending_dir}/etc/apt/preferences.d/extra_php_version" - done - - echo " - -# PLEASE READ THIS WARNING AND DON'T EDIT THIS FILE - -# You are probably reading this file because you tried to install apache2 or -# bind9. These 2 packages conflict with YunoHost. - -# Installing apache2 will break nginx and break the entire YunoHost ecosystem -# on your server, therefore don't remove those lines! - -# You have been warned. - -Package: apache2 -Pin: release * -Pin-Priority: -1 - -Package: apache2-bin -Pin: release * -Pin-Priority: -1 - -# Also bind9 will conflict with dnsmasq. -# Same story as for apache2. -# Don't install it, don't remove those lines. - -Package: bind9 -Pin: release * -Pin-Priority: -1 -" >> "${pending_dir}/etc/apt/preferences.d/ban_packages" - -} - -do_post_regen() { - 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 -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/12-metronome b/data/hooks/conf_regen/12-metronome deleted file mode 100755 index 9820f9881..000000000 --- a/data/hooks/conf_regen/12-metronome +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -set -e - -do_pre_regen() { - pending_dir=$1 - - 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" - - # 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" - - # 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 -} - -do_post_regen() { - 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) - - # 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 - - # fix some permissions - [ ! -e '/var/xmpp-upload' ] || chown -R metronome:www-data "/var/xmpp-upload/" - [ ! -e '/var/xmpp-upload' ] || chmod 750 "/var/xmpp-upload/" - - # 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 -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/15-nginx b/data/hooks/conf_regen/15-nginx deleted file mode 100755 index a2d8f1259..000000000 --- a/data/hooks/conf_regen/15-nginx +++ /dev/null @@ -1,170 +0,0 @@ -#!/bin/bash - -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 - - cd /usr/share/yunohost/templates/nginx - - 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" - - # 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" - - 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; } - - exit 0 -} - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/templates/nginx - - 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 - - # retrieve variables - main_domain=$(cat /etc/yunohost/current_host) - - # Support different strategy for security configurations - 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) - - # 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 '"') - - 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 - - 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/ - - # 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 - - # disable default site - mkdir -p "${nginx_dir}/sites-enabled" - touch "${nginx_dir}/sites-enabled/default" -} - -do_post_regen() { - regen_conf_files=$1 - - [ -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 - - # 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; } -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - init) - do_init_regen - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/19-postfix b/data/hooks/conf_regen/19-postfix deleted file mode 100755 index 166b5d5e9..000000000 --- a/data/hooks/conf_regen/19-postfix +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash - -set -e - -. /usr/share/yunohost/helpers - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/templates/postfix - - postfix_dir="${pending_dir}/etc/postfix" - mkdir -p "$postfix_dir" - - default_dir="${pending_dir}/etc/default/" - mkdir -p "$default_dir" - - # install plain conf files - cp plain/* "$postfix_dir" - - # 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 - - 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 - - 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; } - -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/25-dovecot b/data/hooks/conf_regen/25-dovecot deleted file mode 100755 index 916b88c35..000000000 --- a/data/hooks/conf_regen/25-dovecot +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash - -set -e - -. /usr/share/yunohost/helpers - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/templates/dovecot - - 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" - - 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" - - # 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" -} - -do_post_regen() { - regen_conf_files=$1 - - 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 - - # 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 - chown -R vmail:mail /etc/dovecot/global_script - } - - systemctl restart dovecot -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/31-rspamd b/data/hooks/conf_regen/31-rspamd deleted file mode 100755 index 87ed722a7..000000000 --- a/data/hooks/conf_regen/31-rspamd +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -set -e - -do_pre_regen() { - pending_dir=$1 - - 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" -} - -do_post_regen() { - - ## - ## DKIM key generation - ## - - # 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" - } - 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 -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/34-mysql b/data/hooks/conf_regen/34-mysql deleted file mode 100755 index d5180949e..000000000 --- a/data/hooks/conf_regen/34-mysql +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/bash - -set -e -. /usr/share/yunohost/helpers - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/templates/mysql - - install -D -m 644 my.cnf "${pending_dir}/etc/mysql/my.cnf" -} - -do_post_regen() { - 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 - - systemctl -q is-active mariadb.service \ - || systemctl start mariadb - - sleep 5 - - 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 - - # 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 - - # 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 -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/35-redis b/data/hooks/conf_regen/35-redis deleted file mode 100755 index 10358cefc..000000000 --- a/data/hooks/conf_regen/35-redis +++ /dev/null @@ -1,29 +0,0 @@ -#!/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 -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/37-mdns b/data/hooks/conf_regen/37-mdns deleted file mode 100755 index 1d7381e26..000000000 --- a/data/hooks/conf_regen/37-mdns +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash - -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 "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() { - do_pre_regen - do_post_regen /etc/systemd/system/yunomdns.service - systemctl enable yunomdns -} - -do_pre_regen() { - pending_dir="$1" - - 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 - - mkdir -p ${pending_dir}/etc/yunohost - _generate_config > ${pending_dir}/etc/yunohost/mdns.yml -} - -do_post_regen() { - regen_conf_files="$1" - - 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 - - # 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 -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - init) - do_init_regen - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/43-dnsmasq b/data/hooks/conf_regen/43-dnsmasq deleted file mode 100755 index e7b0531e8..000000000 --- a/data/hooks/conf_regen/43-dnsmasq +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash - -set -e -. /usr/share/yunohost/helpers - -do_pre_regen() { - pending_dir=$1 - - 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" - - # 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 - - # 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 - - # 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 -} - -do_post_regen() { - 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 - - 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 - - [[ -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 - - systemctl restart dnsmasq -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/46-nsswitch b/data/hooks/conf_regen/46-nsswitch deleted file mode 100755 index e6d998094..000000000 --- a/data/hooks/conf_regen/46-nsswitch +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -set -e - -do_init_regen() { - do_pre_regen "" - systemctl restart unscd -} - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/templates/nsswitch - - install -D -m 644 nsswitch.conf "${pending_dir}/etc/nsswitch.conf" -} - -do_post_regen() { - regen_conf_files=$1 - - [[ -z "$regen_conf_files" ]] \ - || systemctl restart unscd -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - init) - do_init_regen - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/hooks/conf_regen/52-fail2ban b/data/hooks/conf_regen/52-fail2ban deleted file mode 100755 index c96940c94..000000000 --- a/data/hooks/conf_regen/52-fail2ban +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -set -e - -. /usr/share/yunohost/helpers - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/templates/fail2ban - - 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" - - 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 - - [[ -z "$regen_conf_files" ]] \ - || systemctl reload fail2ban -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/templates/dnsmasq/plain/dnsmasq.conf b/data/templates/dnsmasq/plain/dnsmasq.conf deleted file mode 100644 index 12a14048a..000000000 --- a/data/templates/dnsmasq/plain/dnsmasq.conf +++ /dev/null @@ -1,6 +0,0 @@ -domain-needed -expand-hosts - -listen-address=127.0.0.1 -resolv-file=/etc/resolv.dnsmasq.conf -cache-size=256 diff --git a/data/templates/mysql/my.cnf b/data/templates/mysql/my.cnf deleted file mode 100644 index 429596cf5..000000000 --- a/data/templates/mysql/my.cnf +++ /dev/null @@ -1,92 +0,0 @@ -# Example MySQL config file for small systems. -# -# This is for a system with little memory (<= 64M) where MySQL is only used -# from time to time and it's important that the mysqld daemon -# doesn't use much resources. -# -# MySQL programs look for option files in a set of -# locations which depend on the deployment platform. -# You can copy this option file to one of those -# locations. For information about these locations, see: -# http://dev.mysql.com/doc/mysql/en/option-files.html -# -# In this file, you can use all long options that a program supports. -# If you want to know which options a program supports, run the program -# with the "--help" option. - -# The following options will be passed to all MySQL clients -[client] -#password = your_password -port = 3306 -socket = /var/run/mysqld/mysqld.sock - -# Here follows entries for some specific programs - -# The MySQL server -[mysqld] -port = 3306 -socket = /var/run/mysqld/mysqld.sock -skip-external-locking -key_buffer_size = 16K -max_allowed_packet = 16M -table_open_cache = 4 -sort_buffer_size = 256K -read_buffer_size = 256K -read_rnd_buffer_size = 256K -net_buffer_length = 2K -thread_stack = 128K - -# to avoid corruption on powerfailure -default-storage-engine=innodb - -# Don't listen on a TCP/IP port at all. This can be a security enhancement, -# if all processes that need to connect to mysqld run on the same host. -# All interaction with mysqld must be made via Unix sockets or named pipes. -# Note that using this option without enabling named pipes on Windows -# (using the "enable-named-pipe" option) will render mysqld useless! -# -#skip-networking -server-id = 1 - -# Uncomment the following if you want to log updates -#log-bin=mysql-bin - -# binary logging format - mixed recommended -#binlog_format=mixed - -# Causes updates to non-transactional engines using statement format to be -# written directly to binary log. Before using this option make sure that -# there are no dependencies between transactional and non-transactional -# tables such as in the statement INSERT INTO t_myisam SELECT * FROM -# t_innodb; otherwise, slaves may diverge from the master. -#binlog_direct_non_transactional_updates=TRUE - -# Uncomment the following if you are using InnoDB tables -#innodb_data_home_dir = /var/lib/mysql -#innodb_data_file_path = ibdata1:10M:autoextend -#innodb_log_group_home_dir = /var/lib/mysql -# You can set .._buffer_pool_size up to 50 - 80 % -# of RAM but beware of setting memory usage too high -#innodb_buffer_pool_size = 16M -#innodb_additional_mem_pool_size = 2M -# Set .._log_file_size to 25 % of buffer pool size -#innodb_log_file_size = 5M -#innodb_log_buffer_size = 8M -#innodb_flush_log_at_trx_commit = 1 -#innodb_lock_wait_timeout = 50 - -[mysqldump] -quick -max_allowed_packet = 16M - -[mysql] -no-auto-rehash -# Remove the next comment character if you are not familiar with SQL -#safe-updates - -[myisamchk] -key_buffer_size = 8M -sort_buffer_size = 8M - -[mysqlhotcopy] -interactive-timeout diff --git a/debian/changelog b/debian/changelog index 48f3bbdca..fc43389f4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,365 @@ +yunohost (11.0.6) testing; urgency=low + + - [fix] configpanel: the config panel was not modifying the configuration of the correct app in certain situations ([#1449](http://github.com/YunoHost/yunohost/pull/1449)) + - [fix] debian package: fix for openssl conflict (ec41b697) + - [i18n] Translations updated for Arabic, Basque, Finnish, French, Galician, German, Kabyle, Polish + + Thanks to all contributors <3 ! (3ole, Alexandre Aubin, Baloo, Bartłomiej Garbiec, José M, Kayou, ljf, Mico Hauataluoma, punkrockgirl, Selyan Slimane Amiri, Tagada) + + -- Kay0u Tue, 29 Mar 2022 14:13:40 +0200 + +yunohost (11.0.5) testing; urgency=low + + - [mod] configpanel: improve 'filter' mechanism in AppQuestion ([#1429](https://github.com/YunoHost/yunohost/pull/1429)) + - [fix] postinstall: migrate_to_bullseye should be skipped on bullseye (de684425) + - [enh] security: Enable proc-hidepid by default ([#1433](https://github.com/YunoHost/yunohost/pull/1433)) + - [enh] nodejs: Update n to 8.0.2 ([#1435](https://github.com/YunoHost/yunohost/pull/1435)) + - [fix] postfix: sni tls_server_chain_sni_maps -> tls_server_sni_maps ([#1438](https://github.com/YunoHost/yunohost/pull/1438)) + - [fix] ynh_get_ram: Avoid grep issue with vmstat command ([#1440](https://github.com/YunoHost/yunohost/pull/1440)) + - [fix] ynh_exec_*: ensure the arg message is used ([#1442](https://github.com/YunoHost/yunohost/pull/1442)) + - [enh] helpers: Always activate --time when running inside CI tests ([#1444](https://github.com/YunoHost/yunohost/pull/1444)) + - [fix] helpers: unbound variable in ynh_script_progression (676973a1) + - [mod] quality: Several FIXME fix ([#1441](https://github.com/YunoHost/yunohost/pull/1441)) + + Thanks to all contributors <3 ! (ericgaspar, ewilly, Kayou, Melchisedech, Tagadda) + + -- Alexandre Aubin Tue, 08 Mar 2022 13:01:06 +0100 + +yunohost (11.0.4) testing; urgency=low + + - [mod] certificate: drop unused 'staging' LE mode (4b78e8e3) + - [fix] cli: bash_completion was broken ([#1423](https://github.com/YunoHost/yunohost/pull/1423)) + - [enh] mdns: Wait for network to be fully up to start the service ([#1425](https://github.com/YunoHost/yunohost/pull/1425)) + - [fix] regenconf: make some systemctl enable/disable quiet (bccff1b4, 345e50ae) + - [fix] configpanels: Compute choices for the yunohost admin when installing an app ([#1427](https://github.com/YunoHost/yunohost/pull/1427)) + - [fix] configpanels: optimize _get_toml for domains to not load the whole DNS section stuff when just getting a simple info from another section (bf6252ac) + - [fix] configpanel: oopsies, could only change the default app for domain configs :P (0a59f863) + - [fix] php73_to_php74: another search&replace for synapse (f0a01ba2) + - [fix] php73_to_php74: stopping php7.3 before starting 7.4 should be more robust in case confs are conflicting (9ae7ec59) + - [i18n] Translations updated for French, Ukrainian + + Thanks to all contributors <3 ! (Éric Gaspar, Kay0u, Tagadda, tituspijean, Tymofii-Lytvynenko) + + -- Alexandre Aubin Sat, 29 Jan 2022 19:19:44 +0100 + +yunohost (11.0.3) testing; urgency=low + + - [enh] mail: Add SNI support for postfix and dovecot ([#1413](https://github.com/YunoHost/yunohost/pull/1413)) + - [fix] services: fix a couple edge cases (4571c5b2) + - [fix] services: Do not save php-fpm services in services.yml (5d0f8021) + - [fix] diagnosis: diagnosers were run in a funky order ([#1418](https://github.com/YunoHost/yunohost/pull/1418)) + - [fix] configpanels: config_get should return possible choices for domain, user questions (and other dynamic-choices questions) ([#1420](https://github.com/YunoHost/yunohost/pull/1420)) + - [enh] apps/domain: Clarify the default app mecanism, handle it fron domain config panel ([#1406](https://github.com/YunoHost/yunohost/pull/1406)) + - [fix] apps: When no main app permission found, fallback to default label instead of having a 'None' label to prevent the webadmin from displaying an empty app list (07396b8b) + - [i18n] Translations updated for Galician + + Thanks to all contributors <3 ! (José M, Kay0u, Tagadda, tituspijean) + + -- Alexandre Aubin Tue, 25 Jan 2022 13:06:10 +0100 + +yunohost (11.0.2) testing; urgency=low + + - [mod] Various tweaks for Python 3.9, PHP 7.4, PostgreSQL 13, and other changes related to Buster->Bullseye ecosystem + - [mod] debian: Moved mysql, php, and metronome from Depends to Recommends ([#1369](https://github.com/YunoHost/yunohost/pull/1369)) + - [mod] apt: **Add sury by default** ([#1369](https://github.com/YunoHost/yunohost/pull/1369)) + - [enh] mysql: **Drop super old mysql config, now rely on Debian default** ([44c972f...144126f](https://github.com/YunoHost/yunohost/compare/44c972f2dd65...144126f56a3d)) + - [enh] regenconf/helpers: Better integration for postgresql ([#1369](https://github.com/YunoHost/yunohost/pull/1369)) + - [mod] quality: **Rework repository code architecture** ([#1377](https://github.com/YunoHost/yunohost/pull/1377)) + - [mod] quality: **Rework where yunohost files are deployed** (yunohost now a much closer to a python lib with files in /usr/lib/python3/dist-packages/yunohost/, and other "common" files are in /usr/share/yunohost) ([#1377](https://github.com/YunoHost/yunohost/pull/1377)) + - [enh] upgrade: Try to implement **a smarter self-upgrade mechanism to prevent/limit API downtime and related UX issues** ([#1374](https://github.com/YunoHost/yunohost/pull/1374)) + - [mod] regenconf: store tmp files in /var/cache/yunohost/ instead of the misleading /home/yunohost.conf folder (00d535a6) + - [mod] dyndns: rewrite tsig keygen + nsupdate using full python, now that dnssec-keygen doesnt support hmacsha512 anymore (63a84f53) + - [mod] app: During app scripts (and all stuff run in hook_exec), do not inject the HOME variable if it exists. This aims to prevent inconsistencies between CLI (where HOME usually is defined) and API (where HOME doesnt exists) (f43e567b) + - [mod] quality: **Drop legacy commands or arguments** listed below + - Drop `--other_vars` options in ynh_add_fail2ban_config and systemd_config helpers + - Drop deprecated/superold `ynh_bind_or_cp`, `ynh_mkdir_tmp`, `ynh_get_plain_key` helpers + - Drop obsolete `yunohost-reset-ldap-password` command + - Drop obsolete `yunohost dyndns installcron` and `removecron` commands + - Drop deprecated `yunohost service regen-conf` command (see `tools regen-conf` instead) + - Drop deprecated `yunohost app fetchlist` command + - Drop obsolete `yunohost app add/remove/clearaccess` commands + - Drop deprecated `--installed` and `--filter` options in `yunohost app list` + - Drop deprecated `--apps` and `--system` options in `yunohost tools update/upgrade` (no double dashes anymore) + - Drop deprecated `--status` and `--log_type` options in `yunohost service add` + - Drop deprecated `--mail` option in `yunohost user create` + + -- Alexandre Aubin Wed, 19 Jan 2022 20:52:39 +0100 + +yunohost (4.4.1) testing; urgency=low + + - [fix] php helpers: prevent epic catastrophies when the app changes php version (31d3719b) + + Thanks to all contributors <3 ! (Alexandre Aubin) + + -- Kay0u Tue, 29 Mar 2022 14:03:52 +0200 + +yunohost (4.4.0) testing; urgency=low + + - [enh] Add buster->bullseye migration + + -- Alexandre Aubin Wed, 19 Jan 2022 20:45:22 +0100 + +yunohost (4.3.6.3) stable; urgency=low + + - [fix] debian package: backport fix for openssl conflict (1693c831) + + Thanks to all contributors <3 ! (Kay0u) + + -- Kay0u Tue, 29 Mar 2022 13:52:58 +0200 + +yunohost (4.3.6.2) stable; urgency=low + + - [fix] apt helpers: fix bug when var is empty... (7920cc62) + + -- Alexandre Aubin Wed, 19 Jan 2022 20:30:25 +0100 + +yunohost (4.3.6.1) stable; urgency=low + + - [fix] dnsmasq: ensure interface is up ([#1410](https://github.com/YunoHost/yunohost/pull/1410)) + - [fix] apt helpers: fix ynh_install_app_dependencies when an app change his default phpversion (6ea32728) + - [fix] certificates: fix edge case where None is returned, triggering 'NoneType has no attribute get' (019839db) + - [i18n] Translations updated for German + + Thanks to all contributors <3 ! (Gregor, Kay0u) + + -- Alexandre Aubin Wed, 19 Jan 2022 20:05:13 +0100 + +yunohost (4.3.6) stable; urgency=low + + - [enh] ssh: add a new setting to manage PasswordAuthentication in sshd_config ([#1388](https://github.com/YunoHost/yunohost/pull/1388)) + - [enh] upgrades: filter more boring apt messages (3cc1a0a5) + - [fix] ynh_add_config: crons should be owned by root, otherwise they probably don't run? (0973301b) + - [fix] domains: force cert install during domain_add ([#1404](https://github.com/YunoHost/yunohost/pull/1404)) + - [fix] logs: remove 'args' for metadata, may contain unredacted secrets in edge cases + - [fix] helpers, apt: upgrade apt dependencies from extra repos ([#1407](https://github.com/YunoHost/yunohost/pull/1407)) + - [fix] diagnosis: incorrect dns check (relative vs absolute) for CNAME on subdomain (d81b85a4) + - [i18n] Translations updated for Dutch, French, Galician, German, Spanish, Ukrainian + + Thanks to all contributors <3 ! (Boudewijn, Christian Wehrli, Éric Gaspar, Germain Edy, José M, Kay0u, Kayou, ljf, Tagada, Tymofii-Lytvynenko) + + -- Alexandre Aubin Fri, 14 Jan 2022 01:29:58 +0100 + +yunohost (4.3.5) stable; urgency=low + + - [fix] backup: bug in backup_delete when compress_tar_archives is True ([#1381](https://github.com/YunoHost/yunohost/pull/1381)) + - [fix] helpers logrorate: remove permission tweak .. code was not working as expected. To be re-addressed some day ... (0fc209ac) + - [fix] i18n: consistency for deprecation for --apps in 'yunohost tools update/upgrade' ([#1392](https://github.com/YunoHost/yunohost/pull/1392)) + - [fix] apps: typo when deleting superfluous question keys ([#1393](https://github.com/YunoHost/yunohost/pull/1393)) + - [fix] diagnosis: typo in dns record diagnoser (a615528c) + - [fix] diagnosis: tweak treshold for suspiciously high number of auth failure because too many people getting report about it idk (76abbf03) + - [enh] quality: apply pyupgrade ([#1395](https://github.com/YunoHost/yunohost/pull/1395)) + - [enh] quality: add lgtm/code quality badge ([#1396](https://github.com/YunoHost/yunohost/pull/1396)) + - [i18n] Translations updated for Dutch, French, Galician, German, Indonesian, Russian, Spanish, Ukrainian + + Thanks to all contributors <3 ! (Boudewijn, Bram, Christian Wehrli, Colin Wawrik, Éric Gaspar, Ilya, José M, Juan Alberto González, Kay0u, liimee, Moutonjr Geoff, tituspijean, Tymofii Lytvynenko, Valentin von Guttenberg) + + -- Alexandre Aubin Wed, 29 Dec 2021 01:01:33 +0100 + +yunohost (4.3.4.2) stable; urgency=low + + - [fix] yunomdns: Ignore ipv4 link-local addresses (6854f23c) + - [fix] backup: Fix path for multimedia restore ([#1386](https://github.com/YunoHost/yunohost/pull/1386)) + - [fix] helpers apt/php: typo in extra php-fpm yunohost service integration (47f3c00d) + - [enh] helpers: Update n to 8.0.1 (d1ab1f67) + + Thanks to all contributors <3 ! (ericgaspar, Kayou) + + -- Alexandre Aubin Wed, 08 Dec 2021 22:04:04 +0100 + +yunohost (4.3.4.1) stable; urgency=low + + - [fix] regenconf: Force permission on /etc/resolv.dnsmasq.conf to fix an issue on some setup with umask=027 (5881938c) + - [fix] regenconf: Typo in custom mdns alias regen conf (b3df36dd) + - [fix] regenconf: Try to fix the return line bug in dnsmasq conf ([#1385](https://github.com/YunoHost/yunohost/pull/1385)) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Sat, 27 Nov 2021 21:15:29 +0100 + +yunohost (4.3.4) stable; urgency=low + + - [fix] apps: Allow tilde in username/organization for repo URLs ([#1382](https://github.com/YunoHost/yunohost/pull/1382)) + - [fix] misc: /etc/yunohost permissions broken on some setups (6488b4f6) + - [fix] mdns: Don't add yunohost.local in config if it's already among the yunohost domains (c4962834) + - [enh] dnsmasq: Tweak conf for better support of some stuff like the hotspot app ([#1383](https://github.com/YunoHost/yunohost/pull/1383)) + + Thanks to all contributors <3 ! (ljf, tituspijean) + + -- Alexandre Aubin Sat, 27 Nov 2021 00:53:16 +0100 + +yunohost (4.3.3) stable; urgency=low + + - [fix] log: fix dump_script_log_extract_for_debugging displaying wrong log snippet during failed upgrade ([#1376](https://github.com/YunoHost/yunohost/pull/1376)) + - [fix] certificate: fix stupid certificate/diagnosis issue with subdomains of ynh domains (7c569d16) + - [fix] diagnosis: Read DNS Blacklist answer and compare it against list of non-BL codes ([#1375](https://github.com/YunoHost/yunohost/pull/1375)) + - [enh] helpers: Update n to 8.0.0 ([#1372](https://github.com/YunoHost/yunohost/pull/1372)) + - [fix] helpers: Make ynh_add_fpm_config more robust to some edge cases (51d5dca0) + - [fix] backup: conf_ynh_settings backup/restore hook, /etc/yunohost/domains may not exist (38f5352f) + - [i18n] Translations updated for Basque, Chinese (Simplified), Indonesian, Italian, Ukrainian + + Thanks to all contributors <3 ! (dagangtie, ericgaspar, Félix Piédallu, Flavio Cristoforetti, liimee, punkrockgirl, Romain Thouvenin, Tommi, Tymofii-Lytvynenko) + + -- Alexandre Aubin Sun, 14 Nov 2021 22:55:16 +0100 + +yunohost (4.3.2.2) stable; urgency=low + + - [fix] nginx: Try to fix again the webadmin cache hell (74e2a51e) + + -- Alexandre Aubin Sat, 06 Nov 2021 17:39:58 +0100 + +yunohost (4.3.2.1) stable; urgency=low + + - [enh] mdns: Add possibility to manually add .local aliases via /etc/yunohost/mdns.aliases (meant for internetcube) (3da2df6e) + - [fix] debian: Fix conflict with redis-server (6558b23d) + - [fix] nginx: Refine experimental CSP header (in the end still gotta enable unsafe-inline and unsafe-eval for a bunch of things, but better than no policy at all...) (1cc3e440) + + -- Alexandre Aubin Sat, 06 Nov 2021 16:58:07 +0100 + +yunohost (4.3.2) stable; urgency=low + + - Release as stable + - [i18n] Translations updated for Basque, Occitan + + Thanks to all contributors <3 ! (punkrockgirl, Quentí) + + -- Alexandre Aubin Fri, 05 Nov 2021 02:32:56 +0100 + +yunohost (4.3.1.8) testing; urgency=low + + - [enh] dyndns: Drop some YAGNI + improve IPv6-only support + resilience w.r.t. ns0 / ns1 being down (a61d0231, [#1367](https://github.com/YunoHost/yunohost/pull/1367)) + - [fix] helpers: improve composer debug when it can't install dependencies (4ebcaf8d) + - [enh] helpers: allow to get/set/delete app settings without explicitly passing app id everytime... (fcd2ef9d) + - [fix] helpers: Don't say the 'app was restored' when restore failed after failed upgrade (019d207c) + - [enh] helpers: temporarily auto-add visitors during ynh_local_curl if needed ([#1370](https://github.com/YunoHost/yunohost/pull/1370)) + - [enh] apps: Add YNH_ARCH to app script env for easier debugging and arch check in script (85eb43a7) + - [mod] misc fixes/enh (2687121f, 146fba7d, 86a9cb37, 4e917b5e, 974ea71f, edc5295d, ba489bfc) + - [i18n] Translations updated for Basque, French, Spanish + + Thanks to all contributors <3 ! (ljf, Page Asgardius, ppr, punkrockgirl) + + -- Alexandre Aubin Wed, 03 Nov 2021 18:35:18 +0100 + +yunohost (4.3.1.7) testing; urgency=low + + - [fix] configpanel: Misc technical fixes ... (341059d0, 9c22329e) + - [i18n] Translations updated for Basque, French + + Thanks to all contributors <3 ! (ljf, ppr, punkrockgirl) + + -- Alexandre Aubin Tue, 19 Oct 2021 15:30:50 +0200 + +yunohost (4.3.1.6) testing; urgency=low + + - [fix] configpanel: Various technical fixes (07c1ddce, eae826b2, ff69067d) + - [i18n] Translations updated for Basque, Galician, German, Russian, Ukrainian + + Thanks to all contributors <3 ! (Colin Wawrik, Daniel, José M, ljf, punkrockgirl, Semen Turchikhin, Tymofii-Lytvynenko) + + -- Alexandre Aubin Mon, 18 Oct 2021 18:50:00 +0200 + +yunohost (4.3.1.5) testing; urgency=low + + - [enh] configpanel: Add hook mecanism between questions (9f7fb61b) + - [fix] configpanel: Issue with visible-if context missing between section + - [mod] Force-disable old avahi-daemon (af3d6dd7, 3a07a780) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Sun, 17 Oct 2021 20:44:33 +0200 + +yunohost (4.3.1.4) testing; urgency=low + + - [mod] codequality: Safer, clearer ynh_secure_remove ([#1357](https://github.com/YunoHost/yunohost/pull/1357)) + - [mod] codequality: Lint/autoformat helpers, hooks and debian scripts ([#1356](https://github.com/YunoHost/yunohost/pull/1356)) + - [mod] helpers: Flag ynh_print_ON/OFF as internal to not advertise them in the doc (fe959bd7) + - [fix] helpers: Eval mecanism in ynh_exec_* lead to epic bugs ([#1358](https://github.com/YunoHost/yunohost/pull/1358)) + - [enh] dyndns: validate that we're connected to the internet before triggering yunohost dyndns update (55bacd74) + - [enh] regenconf/dyndns: Delete dyndns cron in regenconf if no dyndns domain found (cb835a2d) + - [fix] regenconf/dovecot: add conf snippet to get rid of stupid stats-writer errors in mail.log (dab3dc6f) + - [enh] regenconf/dnsmasq: Don't generate dnsmasq conf for .local domains (df02f898) + + -- Alexandre Aubin Wed, 13 Oct 2021 15:41:21 +0200 + +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)) + - [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)) + - [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)) + - [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)) + - [domain] Add mDNS for .local domains / replace avahi-daemon ([#1112](https://github.com/YunoHost/yunohost/pull/1112)) + - [settings] new setting to enable experimental security features ([#1290](https://github.com/YunoHost/yunohost/pull/1290)) + - [settings] new setting to handle https redirect ([#1304](https://github.com/YunoHost/yunohost/pull/1304)) + - [diagnosis] add an "app" section to check that app are in catalog with good quality, check for deprecated practices ([#1217](https://github.com/YunoHost/yunohost/pull/1217)) + - [diagnosis] report suspiciously high number of auth failures ([#1292](https://github.com/YunoHost/yunohost/pull/1292)) + - [refactor] Rework the authentication system ([#1183](https://github.com/YunoHost/yunohost/pull/1183)) + - [enh] New config-panel mechanism ([#987](https://github.com/YunoHost/yunohost/pull/987)) + - [enh] Add backup for multimedia files (88063dc7) + - [enh] Configure automatically the DNS records using lexicon ([#1315](https://github.com/YunoHost/yunohost/pull/1315)) + - also brings domain settings, domain config panel, subdomain awareness, improvements in dns recommended conf + - [i18n] Translations updated for Catalan, Chinese (Simplified), Czech, Esperanto, French, Galician, German, Italian, Occitan, Persian, Portuguese, Spanish, Ukrainian + + Thanks to all contributors <3 ! (Corentin Mercier, Daniel, Éric Gaspar, Flavio Cristoforetti, Gregor Lenz, José M, Kay0u, ljf, MercierCorentin, mifegui, Paco, Parviz Homayun, ppr, tituspijean, Tymofii-Lytvynenko) + + -- Alexandre Aubin Sun, 19 Sep 2021 23:55:21 +0200 + +yunohost (4.2.8.3) stable; urgency=low + + - [fix] mysql: Another bump for sort_buffer_size to make Nextcloud 22 work (34e9246b) + + Thanks to all contributors <3 ! (ljf (zamentur)) + + -- Kay0u Fri, 10 Sep 2021 10:40:38 +0200 + +yunohost (4.2.8.2) stable; urgency=low + + - [fix] mysql: Bump sort_buffer_size to 256K to fix Nextcloud 22 installation (d8c49619) + + Thanks to all contributors <3 ! (ericg) + + -- Alexandre Aubin Tue, 07 Sep 2021 23:23:18 +0200 + yunohost (4.2.8.1) stable; urgency=low - [fix] Safer location for slapd backup during hdb/mdb migration (3c646b3d) diff --git a/debian/compat b/debian/compat deleted file mode 100644 index ec635144f..000000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/debian/control b/debian/control index 2e101dca3..0760e2cde 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: yunohost Section: utils Priority: extra Maintainer: YunoHost Contributors -Build-Depends: debhelper (>=9), dh-systemd, dh-python, python3-all (>= 3.7), python3-yaml, python3-jinja2 +Build-Depends: debhelper (>=9), debhelper-compat (= 13), dh-python, python3-all (>= 3.7), python3-yaml, python3-jinja2 Standards-Version: 3.9.6 Homepage: https://yunohost.org/ @@ -10,15 +10,15 @@ Package: yunohost Essential: yes Architecture: all Depends: ${python3:Depends}, ${misc:Depends} - , moulinette (>= 4.2), ssowat (>= 4.0) + , moulinette (>= 11.0), ssowat (>= 11.0) , python3-psutil, python3-requests, python3-dnspython, python3-openssl , python3-miniupnpc, python3-dbus, python3-jinja2 - , python3-toml, python3-packaging, python3-publicsuffix, - , python3-ldap, python3-zeroconf, + , python3-toml, python3-packaging, python3-publicsuffix2 + , python3-ldap, python3-zeroconf (>= 0.36), python3-lexicon, + , python-is-python3 + , nginx, nginx-extras (>=1.18) , apt, apt-transport-https, apt-utils, dirmngr - , php7.3-common, php7.3-fpm, php7.3-ldap, php7.3-intl - , mariadb-server, php7.3-mysql - , openssh-server, iptables, fail2ban, dnsutils, bind9utils + , openssh-server, iptables, fail2ban, bind9-dnsutils , openssl, ca-certificates, netcat-openbsd, iproute2 , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd , dnsmasq, resolvconf, libnss-myhostname @@ -26,28 +26,29 @@ Depends: ${python3:Depends}, ${misc:Depends} , dovecot-core, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved, dovecot-antispam , rspamd, opendkim-tools, postsrsd, procmail, mailutils , redis-server - , metronome (>=3.14.0) , acl , git, curl, wget, cron, unzip, jq, bc, at , lsb-release, haveged, fake-hwclock, equivs, lsof, whois Recommends: yunohost-admin , ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog - , php7.3-gd, php7.3-curl, php-gettext + , php7.4-common, php7.4-fpm, php7.4-ldap, php7.4-intl + , mariadb-server, php7.4-mysql + , php7.4-gd, php7.4-curl, php-php-gettext , python3-pip , unattended-upgrades , libdbd-ldap-perl, libnet-dns-perl -Suggests: htop, vim, rsync, acpi-support-base, udisks2 + , metronome (>=3.14.0) Conflicts: iptables-persistent , apache2 , bind9 - , nginx-extras (>= 1.16) - , openssl (>= 1.1.1g) - , slapd (>= 2.4.49) - , dovecot-core (>= 1:2.3.7) - , redis-server (>= 5:5.0.7) - , fail2ban (>= 0.11) - , iptables (>= 1.8.3) + , nginx-extras (>= 1.19) + , openssl (>= 1.1.1o-0) + , slapd (>= 2.4.58) + , dovecot-core (>= 1:2.3.14) + , redis-server (>= 5:6.1) + , fail2ban (>= 0.11.3) + , iptables (>= 1.8.8) Description: manageable and configured self-hosting server YunoHost aims to make self-hosting accessible to everyone. It configures an email, Web and IM server alongside a LDAP base. It also provides diff --git a/debian/install b/debian/install index 1691a4849..5169d0b62 100644 --- a/debian/install +++ b/debian/install @@ -1,18 +1,10 @@ bin/* /usr/bin/ -sbin/* /usr/sbin/ -data/bash-completion.d/yunohost /etc/bash_completion.d/ +share/* /usr/share/yunohost/ +hooks/* /usr/share/yunohost/hooks/ +helpers/* /usr/share/yunohost/helpers.d/ +conf/* /usr/share/yunohost/conf/ +locales/* /usr/share/yunohost/locales/ doc/yunohost.8.gz /usr/share/man/man8/ -data/actionsmap/* /usr/share/moulinette/actionsmap/ -data/hooks/* /usr/share/yunohost/hooks/ -data/other/yunoprompt.service /etc/systemd/system/ -data/other/password/* /usr/share/yunohost/other/password/ -data/other/dpkg-origins/yunohost /etc/dpkg/origins -data/other/dnsbl_list.yml /usr/share/yunohost/other/ -data/other/ffdhe2048.pem /usr/share/yunohost/other/ -data/other/* /usr/share/yunohost/yunohost-config/moulinette/ -data/templates/* /usr/share/yunohost/templates/ -data/helpers /usr/share/yunohost/ -data/helpers.d/* /usr/share/yunohost/helpers.d/ -lib/metronome/modules/* /usr/lib/metronome/modules/ -locales/* /usr/lib/moulinette/yunohost/locales/ -src/yunohost /usr/lib/moulinette +doc/bash_completion.d/* /etc/bash_completion.d/ +conf/metronome/modules/* /usr/lib/metronome/modules/ +src/* /usr/lib/python3/dist-packages/yunohost/ diff --git a/debian/postinst b/debian/postinst index 8fc00bbe2..e93845e88 100644 --- a/debian/postinst +++ b/debian/postinst @@ -3,42 +3,43 @@ set -e do_configure() { - rm -rf /var/cache/moulinette/* - 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 - # 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 - - # Yunoprompt - systemctl enable yunoprompt.service + # Trick to let yunohost handle the restart of the API, + # to prevent the webadmin from cutting the branch it's sitting on + if systemctl is-enabled yunohost-api --quiet + then + if [[ "${YUNOHOST_API_RESTART_WILL_BE_HANDLED_BY_YUNOHOST:-}" != "yes" ]]; + then + systemctl restart yunohost-api + else + echo "(Delaying the restart of yunohost-api, this should automatically happen after the end of this upgrade)" + fi + fi } # summary of how this script can be called: @@ -55,13 +56,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 diff --git a/debian/rules b/debian/rules index 3790c0ef2..5cf1d9bee 100755 --- a/debian/rules +++ b/debian/rules @@ -1,26 +1,10 @@ #!/usr/bin/make -f # -*- makefile -*- -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - %: - dh ${@} --with=python3,systemd + dh ${@} --with python3 override_dh_auto_build: # Generate bash completion file - python3 data/actionsmap/yunohost_completion.py + python3 doc/generate_bash_completion.py python3 doc/generate_manpages.py --gzip --output doc/yunohost.8.gz - -override_dh_installinit: - dh_installinit -pyunohost --name=yunohost-api --restart-after-upgrade - dh_installinit -pyunohost --name=yunohost-firewall --noscripts - -override_dh_systemd_enable: - dh_systemd_enable --name=yunohost-api \ - yunohost-api.service - dh_systemd_enable --name=yunohost-firewall --no-enable \ - yunohost-firewall.service - -#override_dh_systemd_start: -# dh_systemd_start --restart-after-upgrade yunohost-api.service diff --git a/debian/yunohost-api.default b/debian/yunohost-api.default deleted file mode 100644 index b6a9e5a99..000000000 --- a/debian/yunohost-api.default +++ /dev/null @@ -1,4 +0,0 @@ -# Override yunohost-api options. -# Example to log debug: DAEMON_OPTS="--debug" -# -#DAEMON_OPTS="" diff --git a/data/actionsmap/yunohost_completion.py b/doc/generate_bash_completion.py similarity index 96% rename from data/actionsmap/yunohost_completion.py rename to doc/generate_bash_completion.py index 3891aee9c..d55973010 100644 --- a/data/actionsmap/yunohost_completion.py +++ b/doc/generate_bash_completion.py @@ -12,8 +12,9 @@ import os import yaml THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -ACTIONSMAP_FILE = THIS_SCRIPT_DIR + "/yunohost.yml" -BASH_COMPLETION_FILE = THIS_SCRIPT_DIR + "/../bash-completion.d/yunohost" +ACTIONSMAP_FILE = THIS_SCRIPT_DIR + "/../share/actionsmap.yml" +BASH_COMPLETION_FOLDER = THIS_SCRIPT_DIR + "/bash_completion.d" +BASH_COMPLETION_FILE = BASH_COMPLETION_FOLDER + "/yunohost" def get_dict_actions(OPTION_SUBTREE, category): @@ -61,6 +62,8 @@ with open(ACTIONSMAP_FILE, "r") as stream: OPTION_TREE[category]["subcategories"], subcategory ) + os.makedirs(BASH_COMPLETION_FOLDER, exist_ok=True) + with open(BASH_COMPLETION_FILE, "w") as generated_file: # header of the file diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index f2d5bf444..371e8899b 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -107,7 +107,7 @@ class Parser: else: # We're getting out of a comment bloc, we should find # the name of the function - assert len(line.split()) >= 1, "Malformed line %s in %s" % ( + assert len(line.split()) >= 1, "Malformed line {} in {}".format( i, self.filename, ) @@ -217,7 +217,7 @@ def malformed_error(line_number): def main(): - helper_files = sorted(glob.glob("../data/helpers.d/*")) + helper_files = sorted(glob.glob("../helpers/*")) helpers = [] for helper_file in helper_files: diff --git a/doc/generate_manpages.py b/doc/generate_manpages.py index fa042fb17..bdb1fcaee 100644 --- a/doc/generate_manpages.py +++ b/doc/generate_manpages.py @@ -22,7 +22,7 @@ template = Template(open(os.path.join(base_path, "manpage.template")).read()) THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -ACTIONSMAP_FILE = os.path.join(THIS_SCRIPT_DIR, "../data/actionsmap/yunohost.yml") +ACTIONSMAP_FILE = os.path.join(THIS_SCRIPT_DIR, "../share/actionsmap.yml") def ordered_yaml_load(stream): diff --git a/doc/helper_doc_template.md b/doc/helper_doc_template.md index cf88e10ac..ac5d455fb 100644 --- a/doc/helper_doc_template.md +++ b/doc/helper_doc_template.md @@ -10,11 +10,10 @@ routes: Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/doc/generate_helper_doc.py) on {{data.date}} (YunoHost version {{data.version}}) {% for category, helpers in data.helpers %} -### {{ category.upper() }} +## {{ category.upper() }} {% for h in helpers %} -**{{ h.name }}**
+#### {{ h.name }} [details summary="{{ h.brief }}" class="helper-card-subtitle text-muted"] -

**Usage**: `{{ h.usage }}` {%- if h.args %} @@ -53,7 +52,7 @@ Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{{ {{ h.details }} {%- endif %} -[Dude, show me the code!](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/data/helpers.d/{{ category }}#L{{ h.line + 1 }}) +[Dude, show me the code!](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/helpers/{{ category }}#L{{ h.line + 1 }}) [/details] ---------------- {% endfor %} diff --git a/data/helpers.d/apt b/helpers/apt similarity index 66% rename from data/helpers.d/apt rename to helpers/apt index c3439a583..61f3c0c2d 100644 --- a/data/helpers.d/apt +++ b/helpers/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 @@ -181,8 +176,9 @@ ynh_package_install_from_equivs () { # Build and install the package local TMPDIR=$(mktemp --directory) - # Force the compatibility level at 10, levels below are deprecated - echo 10 > /usr/share/equivs/template/debian/compat + # Make sure to delete the legacy compat file + # It's now handle somewhat magically through the control file + rm -f /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,26 +186,31 @@ 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 2>&1 + 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" } +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 @@ -221,7 +222,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 +233,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 @@ -248,27 +248,41 @@ ynh_install_app_dependencies () { dependencies="$(echo "$dependencies" | sed 's/\([^(\<=\>]\)\([\<=\>]\+\)\([^,]\+\)/\1 (\2 \3)/g')" 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 - # https://github.com/YunoHost/issues/issues/1407 - # - # If we require to install php dependency - if echo $dependencies | grep --quiet 'php' + # Check for specific php dependencies which requires sury + # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni" + # The (?<=php) syntax corresponds to lookbehind ;) + local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>)' | sort -u) + + if [[ -n "$specific_php_version" ]] 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 - # And sury ain't already in sources.lists - 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 + # 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" fi - cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build + local psql_installed="$(ynh_package_is_installed "postgresql-$PSQL_VERSION" && echo yes || echo no)" + + # 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) " + current_dependencies=${current_dependencies// | /|} + fi + dependencies="$current_dependencies, $dependencies" + fi + + cat >/tmp/${dep_app}-ynh-deps.control <" | 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. @@ -337,10 +389,10 @@ ynh_remove_app_dependencies () { # | arg: -n, --name= - Name for the files for this repo, $app as default value. # # Requires YunoHost version 3.8.1 or higher. -ynh_install_extra_app_dependencies () { +ynh_install_extra_app_dependencies() { # Declare an array to define the options of this helper. local legacy_args=rpkn - local -A args_array=( [r]=repo= [p]=package= [k]=key= [n]=name= ) + local -A args_array=([r]=repo= [p]=package= [k]=key= [n]=name=) local repo local package local key @@ -351,15 +403,20 @@ ynh_install_extra_app_dependencies () { key=${key:-} # Set a key only if asked - if [ -n "$key" ] - then + if [ -n "$key" ]; then key="--key=$key" fi # Add an extra repository for those packages 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" + + # Force to upgrade to the last version... + # Without doing apt install, an already installed dep is not upgraded + local apps_auto_installed="$(apt-mark showauto $package)" + ynh_package_install "$package" + [ -z "$apps_auto_installed" ] || apt-mark auto $apps_auto_installed # Remove this extra repository after packages are installed ynh_remove_extra_repo --name=$app @@ -377,10 +434,10 @@ ynh_install_extra_app_dependencies () { # | arg: -a, --append - Do not overwrite existing files. # # Requires YunoHost version 3.8.1 or higher. -ynh_install_extra_repo () { +ynh_install_extra_repo() { # Declare an array to define the options of this helper. local legacy_args=rkpna - local -A args_array=( [r]=repo= [k]=key= [p]=priority= [n]=name= [a]=append ) + local -A args_array=([r]=repo= [k]=key= [p]=priority= [n]=name= [a]=append) local repo local key local priority @@ -393,8 +450,7 @@ ynh_install_extra_repo () { key=${key:-} priority=${priority:-} - if [ $append -eq 1 ] - then + if [ $append -eq 1 ]; then append="--append" wget_append="tee --append" else @@ -423,18 +479,16 @@ ynh_install_extra_repo () { local pin="${uri#*://}" pin="${pin%%/*}" # Set a priority only if asked - if [ -n "$priority" ] - then + if [ -n "$priority" ]; then priority="--priority=$priority" fi ynh_pin_repo --package="*" --pin="origin \"$pin\"" $priority --name="$name" $append # Get the public key for the repo - if [ -n "$key" ] - then + if [ -n "$key" ]; then mkdir --parents "/etc/apt/trusted.gpg.d" # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) - wget --timeout 900 --quiet "$key" --output-document=- | gpg --dearmor | $wget_append /etc/apt/trusted.gpg.d/$name.gpg > /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 +503,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 +515,14 @@ 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 + if [ -e /etc/apt/trusted.gpg.d/$name.gpg ]; then + ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.gpg" + fi + + # (Do we even create a .asc file anywhere ...?) + if [ -e /etc/apt/trusted.gpg.d/$name.asc ]; then + ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.asc" + fi # Update the list of package to exclude the old repo ynh_package_update @@ -484,10 +544,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 +558,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 +584,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 +600,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 +614,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/helpers/backup similarity index 80% rename from data/helpers.d/backup rename to helpers/backup index ae746a37b..01b51d5a1 100644 --- a/data/helpers.d/backup +++ b/helpers/backup @@ -9,7 +9,6 @@ CAN_BIND=${CAN_BIND:-1} # | arg: -d, --dest_path= - destination file or directory inside the backup dir # | arg: -b, --is_big - Indicate data are big (mail, video, image ...) # | arg: -m, --not_mandatory - Indicate that if the file is missing, the backup can ignore it. -# | arg: arg - Deprecated arg # # This helper can be used both in a system backup hook, and in an app backup script # @@ -67,7 +66,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 +82,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 +95,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 +117,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 +145,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 +162,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 +176,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 +195,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 +207,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 $? } @@ -236,7 +226,7 @@ with open(sys.argv[1], 'r') as backup_file: # ynh_restore_file -o "conf/nginx.conf" # # If `DEST_PATH` already exists and is lighter than 500 Mo, a backup will be made in -# `/home/yunohost.conf/backup/`. Otherwise, the existing file is removed. +# `/var/cache/yunohost/appconfbackup/`. Otherwise, the existing file is removed. # # if `apps/$app/etc/nginx/conf.d/$domain.d/$app.conf` exists, restore it into # `/etc/nginx/conf.d/$domain.d/$app.conf` @@ -245,10 +235,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 +251,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 +260,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 - local backup_file="/home/yunohost.conf/backup/${dest_path}.backup.$(date '+%Y%m%d.%H%M%S')" + if [[ $(du --summarize --bytes ${dest_path} | cut --delimiter="/" --fields=1) -le "500000000" ]]; then + local backup_file="/var/cache/yunohost/appconfbackup/${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 +275,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 @@ -303,18 +287,6 @@ ynh_restore_file () { fi } -# Deprecated helper since it's a dangerous one! -# -# [internal] -# -ynh_bind_or_cp() { - local AS_ROOT=${3:-0} - local NO_ROOT=0 - [[ "${AS_ROOT}" = "1" ]] || NO_ROOT=1 - ynh_print_warn --message="This helper is deprecated, you should use ynh_backup instead" - ynh_backup "$1" "$2" 1 -} - # Calculate and store a file checksum into the app settings # # usage: ynh_store_file_checksum --file=file @@ -323,20 +295,32 @@ 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= ) + local -A args_array=([f]=file= [u]=update_only) local file + local update_only + update_only="${update_only:-0}" + # 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 + local checksum_value=$(ynh_app_setting_get --app=$app --key=$checksum_setting_name) + 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 @@ -355,27 +339,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 - backup_file_checksum="/home/yunohost.conf/backup/$file.backup.$(date '+%Y%m%d.%H%M%S')" + if [ -n "$checksum_value" ]; then # Proceed only if a value was stored into the app settings + if [ -e $file ] && ! echo "$checksum_value $file" | md5sum --check --status; then # If the checksum is now different + backup_file_checksum="/var/cache/yunohost/appconfbackup/$file.backup.$(date '+%Y%m%d.%H%M%S')" mkdir --parents "$(dirname "$backup_file_checksum")" - cp --archive "$file" "$backup_file_checksum" # Backup the current file + 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 } @@ -388,15 +370,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 } @@ -404,7 +386,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 } @@ -423,22 +405,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 @@ -446,13 +425,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." @@ -476,22 +453,25 @@ 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 yunohost backup restore $app_bck-pre-upgrade$backup_number --apps $app --force --debug - ynh_die --message="The app was restored to the way it was before the failed upgrade." + if [[ -d /etc/yunohost/apps/$app ]] + then + ynh_die --message="The app was restored to the way it was before the failed upgrade." + else + ynh_die --message="Uhoh ... Yunohost failed to restore the app to the way it was before the failed upgrade :|" + fi fi else ynh_print_warn --message="\$NO_BACKUP_UPGRADE is set, that means there's no backup to restore. You have to fix this upgrade by yourself !" diff --git a/helpers/config b/helpers/config new file mode 100644 index 000000000..9c7272b85 --- /dev/null +++ b/helpers/config @@ -0,0 +1,313 @@ +#!/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 + return + + # 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' overwritten 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' overwritten with the content provided in question '${short_setting}'" + + # 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 + lines=$( + python3 </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 + # + # Return a yaml such as: + # + # validation_errors: + # some_key: "An error message" + # some_other_key: "Another error message" + # + # We use changes_validated to know if this is + # the first validation error + if [[ "$changes_validated" == true ]]; then + ynh_return "validation_errors:" + fi + ynh_return " ${short_setting}: \"$result\"" + changes_validated=false + fi + done + + # 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 + exit 0 + fi + +} + +ynh_app_config_get_one() { + _ynh_app_config_get_one $1 $2 $3 +} + +ynh_app_config_get() { + _ynh_app_config_get +} + +ynh_app_config_show() { + _ynh_app_config_show +} + +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 +} + +ynh_app_config_run() { + declare -Ag old=() + declare -Ag changed=() + declare -Ag file_hash=() + declare -Ag binds=() + declare -Ag types=() + 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 + + 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 + ;; + esac +} diff --git a/data/helpers.d/fail2ban b/helpers/fail2ban similarity index 85% rename from data/helpers.d/fail2ban rename to helpers/fail2ban index 26c899d93..21177fa8d 100644 --- a/data/helpers.d/fail2ban +++ b/helpers/fail2ban @@ -10,9 +10,8 @@ # # ----------------------------------------------------------------------------- # -# usage 2: ynh_add_fail2ban_config --use_template [--others_var="list of others variables to replace"] +# usage 2: ynh_add_fail2ban_config --use_template # | arg: -t, --use_template - Use this helper in template mode -# | arg: -v, --others_var= - List of others variables to replace separeted by a space for example : 'var_1 var_2 ...' # # This will use a template in `../conf/f2b_jail.conf` and `../conf/f2b_filter.conf` # See the documentation of `ynh_add_config` for a description of the template @@ -62,27 +61,22 @@ # ``` # # 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) local logpath local failregex local max_retry local ports - local others_var local use_template # Manage arguments with getopts ynh_handle_getopts_args "$@" max_retry=${max_retry:-3} ports=${ports:-http,https} - others_var="${others_var:-}" use_template="${use_template:-0}" - [[ -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 +88,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 +105,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 +116,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/helpers/getopts similarity index 87% rename from data/helpers.d/getopts rename to helpers/getopts index 8d9e55826..e912220e4 100644 --- a/data/helpers.d/getopts +++ b/helpers/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/helpers/hardware similarity index 72% rename from data/helpers.d/hardware rename to helpers/hardware index 6d1c314fa..337630fa8 100644 --- a/data/helpers.d/hardware +++ b/helpers/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 - 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 )) + elif [ $free -eq 1 ]; then + local free_ram=$(LANG=C vmstat --stats --unit M | grep "free memory" | awk '{print $1}') + local free_swap=$(LANG=C vmstat --stats --unit M | grep "free swap" | awk '{print $1}') + local free_ram_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 - 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 )) + elif [ $total -eq 1 ]; then + local total_ram=$(LANG=C vmstat --stats --unit M | grep "total memory" | awk '{print $1}') + local total_swap=$(LANG=C vmstat --stats --unit M | grep "total swap" | awk '{print $1}') + local total_ram_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/helpers/logging similarity index 56% rename from data/helpers.d/logging rename to helpers/logging index 71998763e..4601e0b39 100644 --- a/data/helpers.d/logging +++ b/helpers/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 "$@" @@ -38,31 +38,12 @@ 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] # # Requires YunoHost version 3.2.0 or higher. -ynh_print_log () { +ynh_print_log() { echo -e "${1}" } @@ -72,10 +53,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 +70,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 "$@" @@ -102,82 +83,119 @@ 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 $@)" +ynh_exec_err() { + # 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 --message="$(eval $@)" + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + ynh_print_err --message="$("$@")" + 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 $@)" +ynh_exec_warn() { + # 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 --message="$(eval $@)" + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + ynh_print_warn --message="$("$@")" + 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 +ynh_exec_warn_less() { + # 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 +ynh_exec_quiet() { + # 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 +ynh_exec_fully_quiet() { + # 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. # # 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. -ynh_print_OFF () { +ynh_print_OFF() { exec {BASH_XTRACEFD}>/dev/null } @@ -185,11 +203,13 @@ ynh_print_OFF () { # # usage: ynh_print_ON # +# [internal] +# # 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 +234,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 @@ -228,16 +248,22 @@ ynh_script_progression () { # Re-disable xtrace, ynh_handle_getopts_args set it back set +o xtrace # set +x weight=${weight:-1} - time=${time:-0} + + # Always activate time when running inside CI tests + if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then + time=${time:-1} + else + time=${time:-0} + fi + 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 +275,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" | 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. # 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 +298,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,73 +322,6 @@ ynh_script_progression () { # usage: ynh_return somedata # # Requires YunoHost version 3.6.0 or higher. -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 $@)" +ynh_return() { + echo "$1" >>"$YNH_STDRETURN" } diff --git a/data/helpers.d/logrotate b/helpers/logrotate similarity index 75% rename from data/helpers.d/logrotate rename to helpers/logrotate index 2d9ab6b72..6f9726beb 100644 --- a/data/helpers.d/logrotate +++ b/helpers/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/helpers/multimedia similarity index 76% rename from data/helpers.d/multimedia rename to helpers/multimedia index 552b8c984..abeb9ed2c 100644 --- a/data/helpers.d/multimedia +++ b/helpers/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/helpers/mysql similarity index 90% rename from data/helpers.d/mysql rename to helpers/mysql index 091dfaf40..822159f27 100644 --- a/data/helpers.d/mysql +++ b/helpers/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/helpers/network similarity index 86% rename from data/helpers.d/network rename to helpers/network index 4e536a8db..d6c15060a 100644 --- a/data/helpers.d/network +++ b/helpers/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 </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/helpers/php similarity index 71% rename from data/helpers.d/php rename to helpers/php index 7c91d89d2..05e0939c8 100644 --- a/data/helpers.d/php +++ b/helpers/php @@ -1,6 +1,6 @@ #!/bin/bash -readonly YNH_DEFAULT_PHP_VERSION=7.3 +readonly YNH_DEFAULT_PHP_VERSION=7.4 # Declare the actual PHP version to use. # A packager willing to use another version of PHP can override the variable into its _common.sh. YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} @@ -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,36 +86,24 @@ 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" - ynh_backup_if_checksum_is_different --file="$old_php_finalphpconf" - - ynh_remove_fpm_config - fi - - # If the requested PHP version is not the default version for YunoHost - 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" ] + if [[ -f "$old_php_finalphpconf" ]] then - local additionnal_packages="--package=$package" - else - local additionnal_packages="" + ynh_backup_if_checksum_is_different --file="$old_php_finalphpconf" + ynh_remove_fpm_config fi - # Install this specific version of PHP. - ynh_install_php --phpversion="$phpversion" "$additionnal_packages" - 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 + # Legacy args (packager should just list their php dependency as regular apt dependencies... + if [ -n "$package" ]; then + # Install the additionnal packages from the default repository + ynh_install_app_dependencies "$package" + fi + + if [ $dedicated_service -eq 1 ]; then local fpm_service="${app}-phpfpm" local fpm_config_dir="/etc/php/$phpversion/dedicated-fpm" else @@ -132,12 +120,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 +134,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 +166,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 +221,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 +233,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 +247,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 +259,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,10 +281,11 @@ ynh_remove_fpm_config () { fi # If the PHP version used is not the default version for YunoHost - if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] - then - # Remove this specific version of PHP - ynh_remove_php + # The second part with YNH_APP_PURGE is an ugly hack to guess that we're inside the remove script + # (we don't actually care about its value, we just check its not empty hence it exists) + if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] && [ -n "${YNH_APP_PURGE:-}" ]; then + # Remove app dependencies ... but ideally should happen via an explicit call from packager + ynh_remove_app_dependencies fi } @@ -315,91 +293,41 @@ ynh_remove_fpm_config () { # # [internal] # +# Legacy, to be remove on bullseye +# # usage: ynh_install_php --phpversion=phpversion [--package=packages] # | arg: -v, --phpversion= - Version of PHP to install. # | arg: -p, --package= - Additionnal PHP packages to install # # Requires YunoHost version 3.8.1 or higher. -ynh_install_php () { +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 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 + 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 - - # 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 - - # 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" } # Remove the specific version of PHP used by the app. # # [internal] # -# usage: ynh_install_php +# Legacy, to be remove on bullseye +# +# usage: ynh_remove_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 @@ -421,10 +349,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 +363,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 +397,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 +407,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 +428,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,30 +444,25 @@ ynh_get_scalable_phpfpm () { php_start_servers=0 fi - 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 [ $print -eq 1 ]; then + 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_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 ))" - elif [ "$php_pm" = "static" ] - then - ynh_debug --message="Estimated footprint=$(( $php_max_children * $footprint ))" + if [ "$php_pm" = "dynamic" ]; then + ynh_print_warn --message="Estimated average footprint=$(($php_max_spare_servers * $footprint))" + elif [ "$php_pm" = "static" ]; then + 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" - 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="\nRaw php-fpm values:" + ynh_print_warn --message="pm.max_children = $php_max_children" + if [ "$php_pm" = "dynamic" ]; then + 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 } @@ -569,10 +480,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 @@ -583,7 +494,7 @@ ynh_composer_exec () { COMPOSER_HOME="$workdir/.composer" COMPOSER_MEMORY_LIMIT=-1 \ php${phpversion} "$workdir/composer.phar" $commands \ - -d "$workdir" --quiet --no-interaction + -d "$workdir" --no-interaction --no-ansi 2>&1 } # Install and initialize Composer in the given directory @@ -595,10 +506,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 +523,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/helpers/postgresql similarity index 79% rename from data/helpers.d/postgresql rename to helpers/postgresql index 12738a922..92a70a166 100644 --- a/data/helpers.d/postgresql +++ b/helpers/postgresql @@ -1,7 +1,7 @@ #!/bin/bash PSQL_ROOT_PWD_FILE=/etc/yunohost/psql -PSQL_VERSION=11 +PSQL_VERSION=13 # Open a connection as a user # @@ -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" @@ -287,6 +281,8 @@ ynh_psql_remove_db() { # Create a master password and set up global settings # +# [internal] +# # usage: ynh_psql_test_if_first_run # # It also make sure that postgresql is installed and running @@ -298,35 +294,5 @@ ynh_psql_test_if_first_run() { # Make sure postgresql is indeed installed dpkg --list | grep -q "ii postgresql-$PSQL_VERSION" || ynh_die --message="postgresql-$PSQL_VERSION is not installed !?" - # Check for some weird issue where postgresql could be installed but etc folder would not exist ... - [ -e "/etc/postgresql/$PSQL_VERSION" ] || ynh_die --message="It looks like postgresql was not properly configured ? /etc/postgresql/$PSQL_VERSION is missing ... Could be due to a locale issue, c.f.https://serverfault.com/questions/426989/postgresql-etc-postgresql-doesnt-exist" - - # Make sure postgresql is started and enabled - # (N.B. : to check the active state, we check the cluster state because - # postgresql could be flagged as active even though the cluster is in - # failed state because of how the service is configured..) - systemctl is-active postgresql@$PSQL_VERSION-main -q || ynh_systemd_action --service_name=postgresql --action=restart - systemctl is-enabled postgresql -q || systemctl enable postgresql --quiet - - # If this is the very first time, we define the root password - # and configure a few things - 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)" - echo "$psql_root_password" >$PSQL_ROOT_PWD_FILE - sudo --login --user=postgres psql -c"ALTER user postgres WITH PASSWORD '$psql_root_password'" postgres - - # force all user to connect to local databases using hashed passwords - # https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html#EXAMPLE-PG-HBA.CONF - # Note: we can't use peer since YunoHost create users with nologin - # See: https://github.com/YunoHost/yunohost/blob/unstable/data/helpers.d/user - ynh_replace_string --match_string="local\(\s*\)all\(\s*\)all\(\s*\)peer" --replace_string="local\1all\2all\3md5" --target_file="$pg_hba" - - # Integrate postgresql service in yunohost - yunohost service add postgresql --log "/var/log/postgresql/" - - ynh_systemd_action --service_name=postgresql --action=reload - fi + yunohost tools regen-conf postgresql } diff --git a/data/helpers.d/setting b/helpers/setting similarity index 89% rename from data/helpers.d/setting rename to helpers/setting index 66bce9717..a2cf3a93d 100644 --- a/data/helpers.d/setting +++ b/helpers/setting @@ -8,13 +8,15 @@ # # Requires YunoHost version 2.2.4 or higher. ynh_app_setting_get() { + local _globalapp=${app-:} # 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 ynh_handle_getopts_args "$@" + app="${app:-$_globalapp}" if [[ $key =~ (unprotected|protected|skipped)_ ]]; then yunohost app setting $app $key @@ -32,14 +34,16 @@ ynh_app_setting_get() { # # Requires YunoHost version 2.2.4 or higher. ynh_app_setting_set() { + local _globalapp=${app-:} # 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 # Manage arguments with getopts ynh_handle_getopts_args "$@" + app="${app:-$_globalapp}" if [[ $key =~ (unprotected|protected|skipped)_ ]]; then yunohost app setting $app $key -v $value @@ -56,13 +60,15 @@ ynh_app_setting_set() { # # Requires YunoHost version 2.2.4 or higher. ynh_app_setting_delete() { + local _globalapp=${app-:} # 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 ynh_handle_getopts_args "$@" + app="${app:-$_globalapp}" if [[ "$key" =~ (unprotected|skipped|protected)_ ]]; then yunohost app setting $app $key -d @@ -76,8 +82,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,21 +34,23 @@ 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 # Manage arguments with getopts ynh_handle_getopts_args "$@" + set +o xtrace # set +x local delimit=@ # Escape the delimiter if it's in the string. match_string=${match_string//${delimit}/"\\${delimit}"} replace_string=${replace_string//${delimit}/"\\${delimit}"} + set -o xtrace # set -x sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$target_file" } @@ -63,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 @@ -95,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 "$@" @@ -125,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/helpers/systemd similarity index 83% rename from data/helpers.d/systemd rename to helpers/systemd index d0f88b5f7..270b0144d 100644 --- a/data/helpers.d/systemd +++ b/helpers/systemd @@ -12,20 +12,16 @@ # 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=) local service local template - local others_var # Manage arguments with getopts ynh_handle_getopts_args "$@" service="${service:-$app}" template="${template:-systemd.service}" - others_var="${others_var:-}" - - [[ -z "$others_var" ]] || ynh_print_warn --message="Packagers: using --others_var is unecessary since YunoHost 4.2" ynh_add_config --template="$YNH_APP_BASEDIR/conf/$template" --destination="/etc/systemd/system/$service.service" @@ -39,18 +35,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 +67,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 +84,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 +111,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 +123,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 +141,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 +159,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/helpers/user similarity index 86% rename from data/helpers.d/user rename to helpers/user index d5ede9f73..aecbd740e 100644 --- a/data/helpers.d/user +++ b/helpers/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/helpers/utils similarity index 68% rename from data/helpers.d/utils rename to helpers/utils index 00bec89ac..8ae68fad5 100644 --- a/data/helpers.d/utils +++ b/helpers/utils @@ -1,6 +1,6 @@ #!/bin/bash -YNH_APP_BASEDIR=$(realpath $([[ "$(basename $0)" =~ ^backup|restore$ ]] && echo '../settings' || echo '..')) +YNH_APP_BASEDIR=${YNH_APP_BASEDIR:-$(realpath ..)} # Handle script crashes / failures # @@ -19,25 +19,25 @@ YNH_APP_BASEDIR=$(realpath $([[ "$(basename $0)" =~ ^backup|restore$ ]] && echo # 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 @@ -308,8 +290,18 @@ ynh_local_curl () { chown root $cookiefile chmod 700 $cookiefile + # Temporarily enable visitors if needed... + local visitors_enabled=$(ynh_permission_has_user "main" "visitors" && echo yes || echo no) + if [[ $visitors_enabled == "no" ]]; then + ynh_permission_update --permission "main" --add "visitors" + fi + # Curl the URL curl --silent --show-error --insecure --location --header "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" --cookie-jar $cookiefile --cookie $cookiefile + + if [[ $visitors_enabled == "no" ]]; then + ynh_permission_update --permission "main" --remove "visitors" + fi } # Create a dedicated config file from a template @@ -353,10 +345,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 +406,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 +439,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 @@ -473,6 +463,202 @@ ynh_replace_vars () { done } +# Get a value from heterogeneous file (yaml, json, php, python...) +# +# usage: ynh_read_var_in_file --file=PATH --key=KEY +# | arg: -f, --file= - the path to the file +# | arg: -k, --key= - the key to get +# +# This helpers match several var affectation use case in several languages +# We don't use jq or equivalent to keep comments and blank space in files +# This helpers work line by line, it is not able to work correctly +# if you have several identical keys in your files +# +# Example of line this helpers can managed correctly +# .yml +# title: YunoHost documentation +# email: 'yunohost@yunohost.org' +# .json +# "theme": "colib'ris", +# "port": 8102 +# "some_boolean": false, +# "user": null +# .ini +# some_boolean = On +# action = "Clear" +# port = 20 +# .php +# $user= +# user => 20 +# .py +# USER = 8102 +# user = 'https://donate.local' +# CUSTOM['user'] = 'YunoHost' +# +# Requires YunoHost version 4.3 or higher. +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 file + local key + local after + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + after="${after:-}" + + [[ -f $file ]] || ynh_die --message="File $file does not exists" + + set +o xtrace # set +x + + # Get the line number after which we search for the variable + local line_number=1 + if [[ -n "$after" ]]; then + line_number=$(grep -n $after $file | cut -d: -f1) + if [[ -z "$line_number" ]]; then + set -o xtrace # set -x + return 1 + fi + fi + + local filename="$(basename -- "$file")" + local ext="${filename##*.}" + local endline=',;' + local assign="=>|:|=" + local comments="#" + local string="\"'" + if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then + endline='#' + fi + if [[ "$ext" =~ ^ini|env$ ]]; then + comments="[;#]" + fi + if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then + comments="//" + fi + local list='\[\s*['$string']?\w+['$string']?\]' + local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*' + var_part+="[$string]?${key}[$string]?" + var_part+='\s*\]?\s*' + var_part+="($assign)" + var_part+='\s*' + + # Extract the part after assignation sign + local expression_with_comment="$(tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL | head -n1)" + if [[ "$expression_with_comment" == "YNH_NULL" ]]; then + set -o xtrace # set -x + echo YNH_NULL + return 0 + fi + + # Remove comments if needed + 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" + else + echo "$expression" + fi + set -o xtrace # set -x +} + +# Set a value into heterogeneous file (yaml, json, php, python...) +# +# usage: ynh_write_var_in_file --file=PATH --key=KEY --value=VALUE +# | arg: -f, --file= - the path to the file +# | arg: -k, --key= - the key to set +# | arg: -v, --value= - the value to set +# +# Requires YunoHost version 4.3 or higher. +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 file + local key + local value + local after + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + after="${after:-}" + + [[ -f $file ]] || ynh_die --message="File $file does not exists" + + set +o xtrace # set +x + + # Get the line number after which we search for the variable + local line_number=1 + if [[ -n "$after" ]]; then + line_number=$(grep -n $after $file | cut -d: -f1) + if [[ -z "$line_number" ]]; then + set -o xtrace # set -x + return 1 + fi + fi + local range="${line_number},\$ " + + local filename="$(basename -- "$file")" + local ext="${filename##*.}" + local endline=',;' + local assign="=>|:|=" + local comments="#" + local string="\"'" + if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then + endline='#' + fi + if [[ "$ext" =~ ^ini|env$ ]]; then + comments="[;#]" + fi + if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then + comments="//" + fi + local list='\[\s*['$string']?\w+['$string']?\]' + local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*' + var_part+="[$string]?${key}[$string]?" + var_part+='\s*\]?\s*' + var_part+="($assign)" + var_part+='\s*' + + # Extract the part after assignation sign + local expression_with_comment="$(tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL | head -n1)" + if [[ "$expression_with_comment" == "YNH_NULL" ]]; then + set -o xtrace # set -x + return 1 + fi + + # Remove comments if needed + 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')" + local first_char="${expression:0:1}" + delimiter=$'\001' + 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 + # \ 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 + value='\"'"$(echo "$value" | sed 's/"/\\\\"/g')"'\"' + fi + if [[ "$ext" =~ ^yaml|yml$ ]]; then + value=" $value" + fi + sed -ri "${range}s$delimiter(^${var_part}).*\$$delimiter\1${value}${endline}${delimiter}i" ${file} + fi + set -o xtrace # set -x +} + # Render templates with Jinja2 # # [internal] @@ -490,7 +676,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 @@ -499,28 +685,29 @@ 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) } -# Create a directory under /tmp -# -# [internal] -# -# Deprecated helper -# -# usage: ynh_mkdir_tmp -# | ret: the created directory path -ynh_mkdir_tmp() { - ynh_print_warn --message="The helper ynh_mkdir_tmp is deprecated." - ynh_print_warn --message="You should use 'mktemp -d' instead and manage permissions \ -properly with chmod/chown." - local TMP_DIR=$(mktemp --directory) +_acceptable_path_to_delete() { + local file=$1 - # Give rights to other users could be a security risk. - # But for retrocompatibility we need it. (This helpers is deprecated) - chmod 755 $TMP_DIR - echo $TMP_DIR + 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 @@ -529,72 +716,30 @@ 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 "$@" + 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 -} -# Extract a key from a plain command output -# -# [internal] -# -# (Deprecated, use --output-as json and jq instead) -ynh_get_plain_key() { - local prefix="#" - local founded=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 - [[ "$line" =~ ^${prefix}[^#] ]] && return - echo $line - elif [[ "$line" =~ ^${prefix}${key_}$ ]] - then - if [[ -n "${1:-}" ]] - then - prefix+="#" - key_=$1 - shift - else - founded=1 - fi - fi - done + set -o xtrace # set -x } # Read the value of a key in a ynh manifest file @@ -605,10 +750,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 @@ -635,20 +780,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*/}" @@ -665,10 +809,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 "$@" @@ -690,11 +834,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 @@ -723,7 +866,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 @@ -732,8 +875,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 @@ -768,14 +910,19 @@ _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 + + # Crons should be owned by root otherwise they probably don't run + if echo "$target" | grep -q '^/etc/cron' + then + chmod 400 $target + chown root:root $target + fi } diff --git a/data/hooks/backup/05-conf_ldap b/hooks/backup/05-conf_ldap similarity index 100% rename from data/hooks/backup/05-conf_ldap rename to hooks/backup/05-conf_ldap diff --git a/data/hooks/backup/17-data_home b/hooks/backup/17-data_home similarity index 100% rename from data/hooks/backup/17-data_home rename to hooks/backup/17-data_home diff --git a/hooks/backup/18-data_multimedia b/hooks/backup/18-data_multimedia new file mode 100644 index 000000000..f80cff0b3 --- /dev/null +++ b/hooks/backup/18-data_multimedia @@ -0,0 +1,17 @@ +#!/bin/bash + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +source /usr/share/yunohost/helpers + +# Backup destination +backup_dir="${1}/data/multimedia" + +if [ -e "/home/yunohost.multimedia/.nobackup" ]; then + exit 0 +fi + +# Backup multimedia directory +ynh_backup --src_path="/home/yunohost.multimedia" --dest_path="${backup_dir}" --is_big --not_mandatory diff --git a/data/hooks/backup/20-conf_ynh_settings b/hooks/backup/20-conf_ynh_settings similarity index 86% rename from data/hooks/backup/20-conf_ynh_settings rename to hooks/backup/20-conf_ynh_settings index 77148c4d9..76ab0aaca 100644 --- a/data/hooks/backup/20-conf_ynh_settings +++ b/hooks/backup/20-conf_ynh_settings @@ -12,6 +12,7 @@ backup_dir="${1}/conf/ynh" # Backup the configuration ynh_backup "/etc/yunohost/firewall.yml" "${backup_dir}/firewall.yml" ynh_backup "/etc/yunohost/current_host" "${backup_dir}/current_host" +[ ! -d "/etc/yunohost/domains" ] || ynh_backup "/etc/yunohost/domains" "${backup_dir}/domains" [ ! -e "/etc/yunohost/settings.json" ] || ynh_backup "/etc/yunohost/settings.json" "${backup_dir}/settings.json" [ ! -d "/etc/yunohost/dyndns" ] || ynh_backup "/etc/yunohost/dyndns" "${backup_dir}/dyndns" [ ! -d "/etc/dkim" ] || ynh_backup "/etc/dkim" "${backup_dir}/dkim" diff --git a/data/hooks/backup/21-conf_ynh_certs b/hooks/backup/21-conf_ynh_certs similarity index 100% rename from data/hooks/backup/21-conf_ynh_certs rename to hooks/backup/21-conf_ynh_certs diff --git a/data/hooks/backup/23-data_mail b/hooks/backup/23-data_mail similarity index 100% rename from data/hooks/backup/23-data_mail rename to hooks/backup/23-data_mail diff --git a/data/hooks/backup/27-data_xmpp b/hooks/backup/27-data_xmpp similarity index 100% rename from data/hooks/backup/27-data_xmpp rename to hooks/backup/27-data_xmpp diff --git a/data/hooks/backup/50-conf_manually_modified_files b/hooks/backup/50-conf_manually_modified_files similarity index 63% rename from data/hooks/backup/50-conf_manually_modified_files rename to hooks/backup/50-conf_manually_modified_files index 685fb56a8..bdea14113 100644 --- a/data/hooks/backup/50-conf_manually_modified_files +++ b/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 - ynh_backup --src_path="$file" +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/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost new file mode 100755 index 000000000..dc0bfc689 --- /dev/null +++ b/hooks/conf_regen/01-yunohost @@ -0,0 +1,248 @@ +#!/bin/bash + +set -e + +do_init_regen() { + if [[ $EUID -ne 0 ]]; then + echo "You must be root to run this script" 1>&2 + exit 1 + fi + + cd /usr/share/yunohost/conf/yunohost + + [[ -d /etc/yunohost ]] || mkdir -p /etc/yunohost + + # 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 + + # 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 + + # 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 + + # 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 service conf + touch /etc/yunohost/services.yml + + mkdir -p /var/cache/yunohost/repo + chown root:root /var/cache/yunohost + chmod 700 /var/cache/yunohost + + cp yunohost-api.service /etc/systemd/system/yunohost-api.service + cp yunohost-firewall.service /etc/systemd/system/yunohost-firewall.service + cp yunoprompt.service /etc/systemd/system/yunoprompt.service + + systemctl daemon-reload + + systemctl enable yunohost-api.service --quiet + systemctl start yunohost-api.service + # Yunohost-firewall is enabled only during postinstall, not init, not 100% sure why + + cp dpkg-origins /etc/dpkg/origins/yunohost + + # Change dpkg vendor + # see https://wiki.debian.org/Derivatives/Guidelines#Vendor + if readlink -f /etc/dpkg/origins/default | grep -q debian; + then + rm -f /etc/dpkg/origins/default + ln -s /etc/dpkg/origins/yunohost /etc/dpkg/origins/default + fi +} + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/yunohost + + 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 < /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 < /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 </dev/null; then + cat >$pending_dir/etc/cron.d/yunohost-dyndns </dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || yunohost dyndns update >> /dev/null +EOF + else + # (Delete cron if no dyndns domain found) + touch $pending_dir/etc/cron.d/yunohost-dyndns + fi + + # Skip ntp if inside a container (inspired from the conf of systemd-timesyncd) + mkdir -p ${pending_dir}/etc/systemd/system/ntp.service.d/ + cat >${pending_dir}/etc/systemd/system/ntp.service.d/ynh-override.conf <${pending_dir}/etc/systemd/system/nftables.service.d/ynh-override.conf <${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) + + # 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 + + # 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 + [[ ! "$regen_conf_files" =~ "yunohost-firewall.service" ]] || systemctl daemon-reload + [[ ! "$regen_conf_files" =~ "yunohost-api.service" ]] || 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 + if readlink -f /etc/dpkg/origins/default | grep -q debian; + then + rm -f /etc/dpkg/origins/default + ln -s /etc/dpkg/origins/yunohost /etc/dpkg/origins/default + fi +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/02-ssl b/hooks/conf_regen/02-ssl new file mode 100755 index 000000000..1aaab59d1 --- /dev/null +++ b/hooks/conf_regen/02-ssl @@ -0,0 +1,134 @@ +#!/bin/bash + +set -e + +ssl_dir="/usr/share/yunohost/ssl" +template_dir="/usr/share/yunohost/conf/ssl/" +ynh_ca="/etc/yunohost/certs/yunohost.org/ca.pem" +ynh_crt="/etc/yunohost/certs/yunohost.org/crt.pem" +ynh_key="/etc/yunohost/certs/yunohost.org/key.pem" + +regen_local_ca() { + + domain="$1" + + echo -e "\n# Creating local certification authority with domain=$domain\n" + + # create certs and SSL directories + mkdir -p "/etc/yunohost/certs/yunohost.org" + mkdir -p "${ssl_dir}/"{ca,certs,crl,newcerts} + + pushd ${ssl_dir} + + # (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 + rm -f index.txt + touch index.txt + cp ${template_dir}/openssl.cnf openssl.ca.cnf + sed -i "s/yunohost.org/${domain}/g" openssl.ca.cnf + openssl req -x509 \ + -new \ + -config openssl.ca.cnf \ + -days 3650 \ + -out ca/cacert.pem \ + -keyout ca/cakey.pem \ + -nodes \ + -batch \ + -subj /CN=${domain}/O=${domain%.*} 2>&1 + + chmod 640 ca/cacert.pem + chmod 640 ca/cakey.pem + + cp ca/cacert.pem $ynh_ca + ln -sf "$ynh_ca" /etc/ssl/certs/ca-yunohost_crt.pem + update-ca-certificates + + popd +} + +do_init_regen() { + + LOGFILE=/tmp/yunohost-ssl-init + echo "" >$LOGFILE + chown root:root $LOGFILE + chmod 640 $LOGFILE + + # Make sure this conf exists + mkdir -p ${ssl_dir}/{ca,certs,crl,newcerts} + install -D -m 644 ${template_dir}/openssl.cnf "${ssl_dir}/openssl.cnf" + + # 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 + + openssl req -new \ + -config "${ssl_dir}/openssl.cnf" \ + -out "${ssl_dir}/certs/yunohost_csr.pem" \ + -keyout "${ssl_dir}/certs/yunohost_key.pem" \ + -nodes -batch &>>$LOGFILE + + openssl ca \ + -config "${ssl_dir}/openssl.cnf" \ + -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" + + 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/ +} + +do_pre_regen() { + pending_dir=$1 + + install -D -m 644 $template_dir/openssl.cnf "${pending_dir}/${ssl_dir}/openssl.cnf" +} + +do_post_regen() { + 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) + + # Automigrate legacy folder + if [ -e /usr/share/yunohost/yunohost-config/ssl/yunoCA ] + then + mv /usr/share/yunohost/yunohost-config/ssl/yunoCA/* ${ssl_dir} + rm -rf /usr/share/yunohost/yunohost-config + # Overwrite openssl.cnf because it may still contain references to the old yunoCA dir + install -D -m 644 ${template_dir}/openssl.cnf "${ssl_dir}/openssl.cnf" + install -D -m 644 ${template_dir}/openssl.cnf "${ssl_dir}/openssl.ca.cnf" + sed -i "s/yunohost.org/${main_domain}/g" openssl.ca.cnf + fi + + mkdir -p ${ssl_dir}/{ca,certs,crl,newcerts} + chown root:root ${ssl_dir} + chmod 750 ${ssl_dir} + chmod -R o-rwx ${ssl_dir} + chmod o+x ${ssl_dir}/certs + chmod o+r ${ssl_dir}/certs/yunohost_crt.pem + + 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/$main_domain/crt.pem /etc/ssl/certs/yunohost_crt.pem + ln -sf /etc/yunohost/certs/$main_domain/key.pem /etc/ssl/private/yunohost_key.pem + fi +} + +do_$1_regen ${@:2} diff --git a/data/hooks/conf_regen/03-ssh b/hooks/conf_regen/03-ssh similarity index 68% rename from data/hooks/conf_regen/03-ssh rename to hooks/conf_regen/03-ssh index d0c4bd31c..9a7f5ce4d 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/hooks/conf_regen/03-ssh @@ -7,11 +7,7 @@ set -e do_pre_regen() { pending_dir=$1 - # If the (legacy) 'from_script' flag is here, - # we won't touch anything in the ssh config. - [[ ! -f /etc/yunohost/from_script ]] || return 0 - - cd /usr/share/yunohost/templates/ssh + cd /usr/share/yunohost/conf/ssh # do not listen to IPv6 if unavailable [[ -f /proc/net/if_inet6 ]] && ipv6_enabled=true || ipv6_enabled=false @@ -26,6 +22,7 @@ do_pre_regen() { # Support different strategy for security configurations export compatibility="$(yunohost settings get 'security.ssh.compatibility')" export port="$(yunohost settings get 'security.ssh.port')" + export password_authentication="$(yunohost settings get 'security.ssh.password_authentication')" export ssh_keys export ipv6_enabled ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config" @@ -34,10 +31,6 @@ do_pre_regen() { do_post_regen() { regen_conf_files=$1 - # If the (legacy) 'from_script' flag is here, - # we won't touch anything in the ssh config. - [[ ! -f /etc/yunohost/from_script ]] || return 0 - # If no file changed, there's nothing to do [[ -n "$regen_conf_files" ]] || return 0 @@ -48,20 +41,4 @@ do_post_regen() { systemctl restart ssh } -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/06-slapd b/hooks/conf_regen/06-slapd new file mode 100755 index 000000000..616b383ec --- /dev/null +++ b/hooks/conf_regen/06-slapd @@ -0,0 +1,193 @@ +#!/bin/bash + +set -e + +tmp_backup_dir_file="/root/slapd-backup-dir.txt" + +config="/usr/share/yunohost/conf/slapd/config.ldif" +db_init="/usr/share/yunohost/conf/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 + + do_pre_regen "" + + # Drop current existing slapd data + + rm -rf /var/backups/*.ldapdb + rm -rf /var/backups/slapd-* + + debconf-set-selections <&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 + + nscd -i group || true + nscd -i passwd || true + + systemctl restart slapd + + # We don't use mkhomedir_helper because 'admin' may not be recognized + # when this script is ran in a chroot (e.g. ISO install) + # We also refer to admin as uid 1007 for the same reason + if [ ! -d /home/admin ]; then + cp -r /etc/skel /home/admin + chown -R 1007:1007 /home/admin + fi +} + +_regenerate_slapd_conf() { + + # 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 + + chown -R openldap:openldap /etc/ldap/slapd.d/ +} + +do_pre_regen() { + pending_dir=$1 + + # 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 + + # create needed directories + ldap_dir="${pending_dir}/etc/ldap" + schema_dir="${ldap_dir}/schema" + mkdir -p "$ldap_dir" "$schema_dir" + + cd /usr/share/yunohost/conf/slapd + + # 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 + + install -D -m 644 slapd.default "${pending_dir}/etc/default/slapd" +} + +do_post_regen() { + 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/ + + # 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 +cn: admins +gidNumber: 4001 +memberUid: admin +objectClass: posixGroup +objectClass: top" + chown -R openldap: /var/lib/ldap + systemctl restart slapd + nscd -i group + fi + + [ -z "$regen_conf_files" ] && exit 0 + + # 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 + + echo "Running slapdindex" + su openldap -s "/bin/bash" -c "/usr/sbin/slapindex" + + echo "Reloading slapd" + systemctl force-reload slapd + + # on slow hardware/vm this regen conf would exit before the admin user that + # is stored in ldap is available because ldap seems to slow to restart + # so we'll wait either until we are able to log as admin or until a timeout + # is reached + # we need to do this because the next hooks executed after this one during + # postinstall requires to run as admin thus breaking postinstall on slow + # hardware which mean yunohost can't be correctly installed on those hardware + # and this sucks + # wait a maximum time of 5 minutes + # yes, force-reload behave like a restart + number_of_wait=0 + while ! su admin -c '' && ((number_of_wait < 60)); do + sleep 5 + ((number_of_wait += 1)) + done +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/09-nslcd b/hooks/conf_regen/09-nslcd new file mode 100755 index 000000000..9d5e5e538 --- /dev/null +++ b/hooks/conf_regen/09-nslcd @@ -0,0 +1,25 @@ +#!/bin/bash + +set -e + +do_init_regen() { + do_pre_regen "" + systemctl restart nslcd +} + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/nslcd + + install -D -m 644 nslcd.conf "${pending_dir}/etc/nslcd.conf" +} + +do_post_regen() { + regen_conf_files=$1 + + [[ -z "$regen_conf_files" ]] \ + || systemctl restart nslcd +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/10-apt b/hooks/conf_regen/10-apt new file mode 100755 index 000000000..bdd6d399c --- /dev/null +++ b/hooks/conf_regen/10-apt @@ -0,0 +1,74 @@ +#!/bin/bash + +set -e + +do_pre_regen() { + pending_dir=$1 + + mkdir --parents "${pending_dir}/etc/apt/preferences.d" + + # Add sury + mkdir -p ${pending_dir}/etc/apt/sources.list.d/ + echo "deb https://packages.sury.org/php/ $(lsb_release --codename --short) main" > "${pending_dir}/etc/apt/sources.list.d/extra_php_version.list" + + # Ban some packages from sury + echo " +Package: php-common +Pin: origin \"packages.sury.org\" +Pin-Priority: 500" >>"${pending_dir}/etc/apt/preferences.d/extra_php_version" + + packages_to_refuse_from_sury="php php-* openssl libssl1.1 libssl-dev" + 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" + done + + echo " + +# PLEASE READ THIS WARNING AND DON'T EDIT THIS FILE + +# You are probably reading this file because you tried to install apache2 or +# bind9. These 2 packages conflict with YunoHost. + +# Installing apache2 will break nginx and break the entire YunoHost ecosystem +# on your server, therefore don't remove those lines! + +# You have been warned. + +Package: apache2 +Pin: release * +Pin-Priority: -1 + +Package: apache2-bin +Pin: release * +Pin-Priority: -1 + +# Also bind9 will conflict with dnsmasq. +# Same story as for apache2. +# Don't install it, don't remove those lines. + +Package: bind9 +Pin: release * +Pin-Priority: -1 +" >>"${pending_dir}/etc/apt/preferences.d/ban_packages" + + +} + +do_post_regen() { + regen_conf_files=$1 + + # Add sury key + # We do this only at the post regen and if the key doesn't already exists, because we don't want the regenconf to fuck everything up if the regenconf runs while the network is down + if [[ ! -s /etc/apt/trusted.gpg.d/extra_php_version.gpg ]] + then + wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/extra_php_version.gpg" + fi + + # Make sure php7.4 is the default version when using php in cli + update-alternatives --set php /usr/bin/php7.4 +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/12-metronome b/hooks/conf_regen/12-metronome new file mode 100755 index 000000000..220d18d58 --- /dev/null +++ b/hooks/conf_regen/12-metronome @@ -0,0 +1,75 @@ +#!/bin/bash + +set -e + +if ! dpkg --list | grep -q 'ii *metronome ' +then + echo 'metronome is not installed, skipping' + exit 0 +fi + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/metronome + + # 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) + + # 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 + + # 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 + + # retrieve variables + main_domain=$(cat /etc/yunohost/current_host) + + # create metronome directories for domains + for domain in $YNH_MAIN_DOMAINS; 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 + + # fix some permissions + [ ! -e '/var/xmpp-upload' ] || chown -R metronome:www-data "/var/xmpp-upload/" + [ ! -e '/var/xmpp-upload' ] || chmod 750 "/var/xmpp-upload/" + + # 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/hooks/conf_regen/15-nginx b/hooks/conf_regen/15-nginx new file mode 100755 index 000000000..c1d943681 --- /dev/null +++ b/hooks/conf_regen/15-nginx @@ -0,0 +1,148 @@ +#!/bin/bash + +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 + + cd /usr/share/yunohost/conf/nginx + + 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" + + # 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" + + 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 + } + + exit 0 +} + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/nginx + + 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 + + # 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" + + 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" + + # 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" + + touch "${domain_conf_dir}/yunohost_local.conf" # Clean legacy conf files + + 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/ + + # 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 + + # disable default site + mkdir -p "${nginx_dir}/sites-enabled" + touch "${nginx_dir}/sites-enabled/default" +} + +do_post_regen() { + regen_conf_files=$1 + + [ -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 + + # 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/hooks/conf_regen/19-postfix b/hooks/conf_regen/19-postfix new file mode 100755 index 000000000..177ea23e9 --- /dev/null +++ b/hooks/conf_regen/19-postfix @@ -0,0 +1,84 @@ +#!/bin/bash + +set -e + +. /usr/share/yunohost/helpers + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/postfix + + postfix_dir="${pending_dir}/etc/postfix" + mkdir -p "$postfix_dir" + + default_dir="${pending_dir}/etc/default/" + mkdir -p "$default_dir" + + # install plain conf files + cp plain/* "$postfix_dir" + + # 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 + + cat <<<"[${relay_host}]:${relay_port} ${relay_user}:${relay_password}" >${postfix_dir}/sasl_passwd + fi + export main_domain + export domain_list="$YNH_DOMAINS" + ynh_render_template "main.cf" "${postfix_dir}/main.cf" + ynh_render_template "sni" "${postfix_dir}/sni" + + cat postsrsd \ + | sed "s/{{ main_domain }}/${main_domain}/g" \ + | sed "s/{{ domain_list }}/${YNH_DOMAINS}/g" \ + >"${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 + + if [ -e /etc/postfix/sasl_passwd ]; then + chmod 750 /etc/postfix/sasl_passwd* + chown postfix:root /etc/postfix/sasl_passwd* + postmap /etc/postfix/sasl_passwd + fi + + postmap -F hash:/etc/postfix/sni + + [[ -z "$regen_conf_files" ]] \ + || { systemctl restart postfix && systemctl restart postsrsd; } + +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/25-dovecot b/hooks/conf_regen/25-dovecot new file mode 100755 index 000000000..37c73b6d8 --- /dev/null +++ b/hooks/conf_regen/25-dovecot @@ -0,0 +1,67 @@ +#!/bin/bash + +set -e + +. /usr/share/yunohost/helpers + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/dovecot + + 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" + + export pop3_enabled="$(yunohost settings get 'pop3.enabled')" + export main_domain=$(cat /etc/yunohost/current_host) + export domain_list="$YNH_DOMAINS" + + 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 + + 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 + + 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 + + # 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 + chown -R vmail:mail /etc/dovecot/global_script + } + + systemctl restart dovecot +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/31-rspamd b/hooks/conf_regen/31-rspamd new file mode 100755 index 000000000..536aec7c2 --- /dev/null +++ b/hooks/conf_regen/31-rspamd @@ -0,0 +1,62 @@ +#!/bin/bash + +set -e + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/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" +} + +do_post_regen() { + + ## + ## DKIM key generation + ## + + # 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" + } + 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 +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/34-mysql b/hooks/conf_regen/34-mysql new file mode 100755 index 000000000..9ef8efe21 --- /dev/null +++ b/hooks/conf_regen/34-mysql @@ -0,0 +1,53 @@ +#!/bin/bash + +set -e +. /usr/share/yunohost/helpers + +if ! dpkg --list | grep -q 'ii *mariadb-server ' +then + echo 'mysql/mariadb is not installed, skipping' + exit 0 +fi + +do_pre_regen() { + pending_dir=$1 + + #cd /usr/share/yunohost/conf/mysql + + # Nothing to do +} + +do_post_regen() { + 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 + + systemctl -q is-active mariadb.service \ + || systemctl start mariadb + + sleep 5 + + echo "" | mysql && echo "Can't connect to mysql using unix_socket auth ... something went wrong during initial configuration of mysql !?" >&2 + 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 +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/35-postgresql b/hooks/conf_regen/35-postgresql new file mode 100755 index 000000000..0da0767cc --- /dev/null +++ b/hooks/conf_regen/35-postgresql @@ -0,0 +1,66 @@ +#!/bin/bash + +set -e +. /usr/share/yunohost/helpers + +if ! dpkg --list | grep -q "ii *postgresql-$PSQL_VERSION " +then + echo 'postgresql is not installed, skipping' + exit 0 +fi + +if [ ! -e "/etc/postgresql/$PSQL_VERSION" ] +then + ynh_die --message="It looks like postgresql was not properly configured ? /etc/postgresql/$PSQL_VERSION is missing ... Could be due to a locale issue, c.f.https://serverfault.com/questions/426989/postgresql-etc-postgresql-doesnt-exist" +fi + + +do_pre_regen() { + return 0 +} + +do_post_regen() { + regen_conf_files=$1 + + # Make sure postgresql is started and enabled + # (N.B. : to check the active state, we check the cluster state because + # postgresql could be flagged as active even though the cluster is in + # failed state because of how the service is configured..) + systemctl is-active postgresql@$PSQL_VERSION-main -q || ynh_systemd_action --service_name=postgresql --action=restart + systemctl is-enabled postgresql -q || systemctl enable postgresql --quiet + + # If this is the very first time, we define the root password + # and configure a few things + if [ ! -f "$PSQL_ROOT_PWD_FILE" ] || [ -z "$(cat $PSQL_ROOT_PWD_FILE)" ]; then + ynh_string_random >$PSQL_ROOT_PWD_FILE + fi + + sudo --login --user=postgres psql -c"ALTER user postgres WITH PASSWORD '$(cat $PSQL_ROOT_PWD_FILE)'" postgres + + # force all user to connect to local databases using hashed passwords + # https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html#EXAMPLE-PG-HBA.CONF + # Note: we can't use peer since YunoHost create users with nologin + # See: https://github.com/YunoHost/yunohost/blob/unstable/data/helpers.d/user + local pg_hba=/etc/postgresql/$PSQL_VERSION/main/pg_hba.conf + ynh_replace_string --match_string="local\(\s*\)all\(\s*\)all\(\s*\)peer" --replace_string="local\1all\2all\3md5" --target_file="$pg_hba" + + ynh_systemd_action --service_name=postgresql --action=reload +} + +FORCE=${2:-0} +DRY_RUN=${3:-0} + +case "$1" in + pre) + do_pre_regen $4 + ;; + post) + do_post_regen $4 + ;; + *) + echo "hook called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/hooks/conf_regen/36-redis b/hooks/conf_regen/36-redis new file mode 100755 index 000000000..ac486f373 --- /dev/null +++ b/hooks/conf_regen/36-redis @@ -0,0 +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 +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/37-mdns b/hooks/conf_regen/37-mdns new file mode 100755 index 000000000..3a877970b --- /dev/null +++ b/hooks/conf_regen/37-mdns @@ -0,0 +1,69 @@ +#!/bin/bash + +set -e + +_generate_config() { + echo "domains:" + # Add yunohost.local (only if yunohost.local ain't already in ynh_domains) + if ! echo "$YNH_DOMAINS" | tr ' ' '\n' | grep -q --line-regexp 'yunohost.local' + then + echo " - yunohost.local" + fi + 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 + if [[ -e /etc/yunohost/mdns.aliases ]] + then + for localalias in $(cat /etc/yunohost/mdns.aliases | grep -v "^ *$") + do + echo " - $localalias.local" + done + fi +} + +do_init_regen() { + do_pre_regen + do_post_regen /etc/systemd/system/yunomdns.service + systemctl enable yunomdns --quiet +} + +do_pre_regen() { + pending_dir="$1" + + cd /usr/share/yunohost/conf/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 + + mkdir -p ${pending_dir}/etc/yunohost + _generate_config >${pending_dir}/etc/yunohost/mdns.yml +} + +do_post_regen() { + regen_conf_files="$1" + + 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 + + systemctl disable avahi-daemon.socket --quiet --now 2>/dev/null || true + systemctl disable avahi-daemon --quiet --now 2>/dev/null || true + + # Legacy stuff to enable the new yunomdns service on legacy systems + if [[ -e /etc/avahi/avahi-daemon.conf ]] && grep -q 'yunohost' /etc/avahi/avahi-daemon.conf; then + systemctl enable yunomdns --now --quiet + sleep 2 + fi + + [[ -z "$regen_conf_files" ]] \ + || systemctl restart yunomdns +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/43-dnsmasq b/hooks/conf_regen/43-dnsmasq new file mode 100755 index 000000000..ec53d75bc --- /dev/null +++ b/hooks/conf_regen/43-dnsmasq @@ -0,0 +1,96 @@ +#!/bin/bash + +set -e +. /usr/share/yunohost/helpers + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/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" + + # add default conf files + cp plain/etcdefault ${pending_dir}/etc/default/dnsmasq + + # 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='' + interfaces="$(ip -j addr show | jq -r '[.[].ifname]|join(" ")')" + wireless_interfaces="lo" + for dev in $(ls /sys/class/net); do + if [ -d "/sys/class/net/$dev/wireless" ] && grep -q "up" "/sys/class/net/$dev/operstate"; then + wireless_interfaces+=" $dev" + fi + done + + # General configuration + export wireless_interfaces + ynh_render_template "dnsmasq.conf.tpl" "${pending_dir}/etc/dnsmasq.conf" + + # add domain conf files + export interfaces + export ipv4 + export ipv6 + for domain in $YNH_DOMAINS; do + [[ ! $domain =~ \.local$ ]] || continue + 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 + if [[ ! $YNH_DOMAINS =~ $domain ]] && [[ ! $domain =~ \.local$ ]] + then + touch "${dnsmasq_dir}/${domain}" + fi + done +} + +do_post_regen() { + regen_conf_files=$1 + + # Force permission (to cover some edge cases where root's umask is like 027 and then dnsmasq cant read this file) + chown 644 /etc/resolv.dnsmasq.conf + + # 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 + + # 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 0 + + # 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 +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/46-nsswitch b/hooks/conf_regen/46-nsswitch new file mode 100755 index 000000000..cc34d0277 --- /dev/null +++ b/hooks/conf_regen/46-nsswitch @@ -0,0 +1,25 @@ +#!/bin/bash + +set -e + +do_init_regen() { + do_pre_regen "" + systemctl restart unscd +} + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/nsswitch + + install -D -m 644 nsswitch.conf "${pending_dir}/etc/nsswitch.conf" +} + +do_post_regen() { + regen_conf_files=$1 + + [[ -z "$regen_conf_files" ]] \ + || systemctl restart unscd +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/52-fail2ban b/hooks/conf_regen/52-fail2ban new file mode 100755 index 000000000..8129e977d --- /dev/null +++ b/hooks/conf_regen/52-fail2ban @@ -0,0 +1,30 @@ +#!/bin/bash + +set -e + +. /usr/share/yunohost/helpers + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/fail2ban + + 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" + + 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 + + [[ -z "$regen_conf_files" ]] \ + || systemctl reload fail2ban +} + +do_$1_regen ${@:2} diff --git a/data/hooks/post_user_create/ynh_multimedia b/hooks/post_user_create/ynh_multimedia similarity index 99% rename from data/hooks/post_user_create/ynh_multimedia rename to hooks/post_user_create/ynh_multimedia index 26282cdc9..5b4b31b88 100644 --- a/data/hooks/post_user_create/ynh_multimedia +++ b/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/post_user_delete/ynh_multimedia b/hooks/post_user_delete/ynh_multimedia similarity index 100% rename from data/hooks/post_user_delete/ynh_multimedia rename to hooks/post_user_delete/ynh_multimedia diff --git a/data/hooks/restore/05-conf_ldap b/hooks/restore/05-conf_ldap similarity index 80% rename from data/hooks/restore/05-conf_ldap rename to hooks/restore/05-conf_ldap index c2debe018..a9eb10b1c 100644 --- a/data/hooks/restore/05-conf_ldap +++ b/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/17-data_home b/hooks/restore/17-data_home similarity index 100% rename from data/hooks/restore/17-data_home rename to hooks/restore/17-data_home diff --git a/hooks/restore/18-data_multimedia b/hooks/restore/18-data_multimedia new file mode 100644 index 000000000..c3c349e7d --- /dev/null +++ b/hooks/restore/18-data_multimedia @@ -0,0 +1,11 @@ +#!/bin/bash + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +source /usr/share/yunohost/helpers + +backup_dir="data/multimedia" + +ynh_restore_file --origin_path="${backup_dir}" --dest_path="/home/yunohost.multimedia" --not_mandatory diff --git a/data/hooks/restore/20-conf_ynh_settings b/hooks/restore/20-conf_ynh_settings similarity index 82% rename from data/hooks/restore/20-conf_ynh_settings rename to hooks/restore/20-conf_ynh_settings index 4de29a4aa..2d731bd54 100644 --- a/data/hooks/restore/20-conf_ynh_settings +++ b/hooks/restore/20-conf_ynh_settings @@ -2,6 +2,7 @@ backup_dir="$1/conf/ynh" cp -a "${backup_dir}/current_host" /etc/yunohost/current_host cp -a "${backup_dir}/firewall.yml" /etc/yunohost/firewall.yml +[ ! -d "${backup_dir}/domains" ] || cp -a "${backup_dir}/domains" /etc/yunohost/domains [ ! -e "${backup_dir}/settings.json" ] || cp -a "${backup_dir}/settings.json" "/etc/yunohost/settings.json" [ ! -d "${backup_dir}/dyndns" ] || cp -raT "${backup_dir}/dyndns" "/etc/yunohost/dyndns" [ ! -d "${backup_dir}/dkim" ] || cp -raT "${backup_dir}/dkim" "/etc/dkim" diff --git a/data/hooks/restore/21-conf_ynh_certs b/hooks/restore/21-conf_ynh_certs similarity index 100% rename from data/hooks/restore/21-conf_ynh_certs rename to hooks/restore/21-conf_ynh_certs diff --git a/data/hooks/restore/23-data_mail b/hooks/restore/23-data_mail similarity index 100% rename from data/hooks/restore/23-data_mail rename to hooks/restore/23-data_mail diff --git a/data/hooks/restore/27-data_xmpp b/hooks/restore/27-data_xmpp similarity index 100% rename from data/hooks/restore/27-data_xmpp rename to hooks/restore/27-data_xmpp diff --git a/data/hooks/restore/50-conf_manually_modified_files b/hooks/restore/50-conf_manually_modified_files similarity index 84% rename from data/hooks/restore/50-conf_manually_modified_files rename to hooks/restore/50-conf_manually_modified_files index 2d0943043..b23b95ec9 100644 --- a/data/hooks/restore/50-conf_manually_modified_files +++ b/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 diff --git a/locales/ar.json b/locales/ar.json index 3e5248917..c440e442f 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -2,11 +2,10 @@ "action_invalid": "إجراء غير صالح '{action}'", "admin_password": "كلمة السر الإدارية", "admin_password_change_failed": "لا يمكن تعديل الكلمة السرية", - "admin_password_changed": "تم تعديل الكلمة السرية الإدارية", + "admin_password_changed": "عُدلت كلمة السر الإدارية", "app_already_installed": "{app} تم تنصيبه مِن قبل", - "app_already_up_to_date": "{app} تم تحديثه مِن قَبل", + "app_already_up_to_date": "{app} حديثٌ", "app_argument_required": "المُعامِل '{name}' مطلوب", - "app_change_url_failed_nginx_reload": "فشلت عملية إعادة تشغيل NGINX. ها هي نتيجة الأمر 'nginx -t':\n{nginx_errors}", "app_extraction_failed": "تعذر فك الضغط عن ملفات التنصيب", "app_install_files_invalid": "ملفات التنصيب خاطئة", "app_not_correctly_installed": "يبدو أن التطبيق {app} لم يتم تنصيبه بشكل صحيح", @@ -39,8 +38,7 @@ "domain_created": "تم إنشاء النطاق", "domain_creation_failed": "تعذرت عملية إنشاء النطاق", "domain_deleted": "تم حذف النطاق", - "domain_exists": "اسم النطاق موجود مِن قبل", - "domain_unknown": "النطاق مجهول", + "domain_exists": "اسم النطاق موجود سلفًا", "domains_available": "النطاقات المتوفرة :", "done": "تم", "downloading": "عملية التنزيل جارية …", @@ -55,7 +53,6 @@ "pattern_domain": "يتوجب أن يكون إسم نطاق صالح (مثل my-domain.org)", "pattern_email": "يتوجب أن يكون عنوان بريد إلكتروني صالح (مثل someone@domain.org)", "pattern_password": "يتوجب أن تكون مكونة من 3 حروف على الأقل", - "pattern_positive_number": "يجب أن يكون عددا إيجابيا", "restore_extracting": "جارٍ فك الضغط عن الملفات التي نحتاجها من النسخة الاحتياطية…", "server_shutdown": "سوف ينطفئ الخادوم", "server_shutdown_confirm": "سوف ينطفئ الخادوم حالا. متأكد ؟ [{answers}]", @@ -162,4 +159,4 @@ "diagnosis_description_dnsrecords": "تسجيلات خدمة DNS", "diagnosis_description_ip": "الإتصال بالإنترنت", "diagnosis_description_basesystem": "النظام الأساسي" -} \ No newline at end of file +} diff --git a/locales/ca.json b/locales/ca.json index 5a128ebb8..b660032d2 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -6,10 +6,9 @@ "app_already_installed": "{app} ja està instal·lada", "app_already_installed_cant_change_url": "Aquesta aplicació ja està instal·lada. La URL no és pot canviar únicament amb aquesta funció. Mireu a `app changeurl` si està disponible.", "app_already_up_to_date": "{app} ja està actualitzada", - "app_argument_choice_invalid": "Utilitzeu una de les opcions «{choices}» per l'argument «{name}»", + "app_argument_choice_invalid": "Utilitzeu una de les opcions «{choices}» per l'argument «{name}» en lloc de «{value}»", "app_argument_invalid": "Escolliu un valor vàlid per l'argument «{name}»: {error}", "app_argument_required": "Es necessita l'argument '{name}'", - "app_change_url_failed_nginx_reload": "No s'ha pogut tornar a carregar NGINX. Aquí teniu el resultat de \"nginx -t\":\n{nginx_errors}", "app_change_url_identical_domains": "L'antic i el nou domini/camí són idèntics ('{domain}{path}'), no hi ha res per fer.", "app_change_url_no_script": "L'aplicació '{app_name}' encara no permet modificar la URL. Potser s'ha d'actualitzar.", "app_change_url_success": "La URL de {app} ara és {domain}{path}", @@ -18,7 +17,6 @@ "app_install_files_invalid": "Aquests fitxers no es poden instal·lar", "app_make_default_location_already_used": "No es pot fer l'aplicació '{app}' l'aplicació per defecte en el domini «{domain}», ja que ja és utilitzat per '{other_app}'", "app_location_unavailable": "Aquesta URL no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps}", - "app_manifest_invalid": "Hi ha algun error amb el manifest de l'aplicació: {error}", "app_not_correctly_installed": "{app} sembla estar mal instal·lada", "app_not_installed": "No s'ha trobat {app} en la llista d'aplicacions instal·lades: {all_apps}", "app_not_properly_removed": "{app} no s'ha pogut suprimir correctament", @@ -112,11 +110,11 @@ "certmanager_self_ca_conf_file_not_found": "No s'ha trobat el fitxer de configuració per l'autoritat del certificat auto-signat (fitxer: {file})", "certmanager_unable_to_parse_self_CA_name": "No s'ha pogut analitzar el nom de l'autoritat del certificat auto-signat (fitxer: {file})", "confirm_app_install_warning": "Atenció: Aquesta aplicació funciona, però no està ben integrada amb YunoHost. Algunes característiques com la autenticació única i la còpia de seguretat/restauració poden no estar disponibles. Voleu instal·lar-la de totes maneres? [{answers}] ", - "confirm_app_install_danger": "PERILL! Aquesta aplicació encara és experimental (si no és que no funciona directament)! No hauríeu d'instal·lar-la a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema… Si accepteu el risc, escriviu «{answers}»", + "confirm_app_install_danger": "PERILL! Aquesta aplicació encara és experimental (si no és que no funciona directament)! No hauríeu d'instal·lar-la a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema... Si accepteu el risc, escriviu «{answers}»", "confirm_app_install_thirdparty": "PERILL! Aquesta aplicació no es part del catàleg d'aplicacions de YunoHost. La instal·lació d'aplicacions de terceres parts pot comprometre la integritat i seguretat del seu sistema. No hauríeu d'instal·lar-ne a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema… Si accepteu el risc, escriviu «{answers}»", "custom_app_url_required": "Heu de especificar una URL per actualitzar la vostra aplicació personalitzada {app}", "admin_password_too_long": "Trieu una contrasenya de menys de 127 caràcters", - "dpkg_is_broken": "No es pot fer això en aquest instant perquè dpkg/APT (els gestors de paquets del sistema) sembla estar mal configurat… Podeu intentar solucionar-ho connectant-vos per SSH i executant «sudo apt install --fix-broken» i/o «sudo dpkg --configure -a».", + "dpkg_is_broken": "No es pot fer això en aquest instant perquè dpkg/APT (els gestors de paquets del sistema) sembla estar mal configurat... Podeu intentar solucionar-ho connectant-vos per SSH i executant «sudo apt install --fix-broken» i/o «sudo dpkg --configure -a».", "domain_cannot_remove_main": "No es pot eliminar «{domain}» ja que és el domini principal, primer s'ha d'establir un nou domini principal utilitzant «yunohost domain main-domain -n »; aquí hi ha una llista dels possibles dominis: {other_domains}", "domain_cert_gen_failed": "No s'ha pogut generar el certificat", "domain_created": "S'ha creat el domini", @@ -130,11 +128,9 @@ "domain_dyndns_root_unknown": "Domini DynDNS principal desconegut", "domain_hostname_failed": "No s'ha pogut establir un nou nom d'amfitrió. Això podria causar problemes més tard (podria no passar res).", "domain_uninstall_app_first": "Aquestes aplicacions encara estan instal·lades en el vostre domini:\n{apps}\n\nDesinstal·leu-les utilitzant l'ordre «yunohost app remove id_de_lapplicació» o moveu-les a un altre domini amb «yunohost app change-url id_de_lapplicació» abans d'eliminar el domini", - "domain_unknown": "Domini desconegut", "domains_available": "Dominis disponibles:", "done": "Fet", - "downloading": "Descarregant…", - "dyndns_could_not_check_provide": "No s'ha pogut verificar si {provider} pot oferir {domain}.", + "downloading": "Descarregant...", "dyndns_could_not_check_available": "No s'ha pogut verificar la disponibilitat de {domain} a {provider}.", "dyndns_ip_update_failed": "No s'ha pogut actualitzar l'adreça IP al DynDNS", "dyndns_ip_updated": "S'ha actualitzat l'adreça IP al DynDNS", @@ -177,7 +173,7 @@ "iptables_unavailable": "No podeu modificar les iptables aquí. O bé sou en un contenidor o bé el vostre nucli no és compatible amb aquesta opció", "log_corrupted_md_file": "El fitxer de metadades YAML associat amb els registres està malmès: « {md_file} »\nError: {error}", "log_link_to_log": "El registre complet d'aquesta operació: «{desc}»", - "log_help_to_get_log": "Per veure el registre de l'operació « {desc} », utilitzeu l'ordre « yunohost log show {name}{name} »", + "log_help_to_get_log": "Per veure el registre de l'operació « {desc} », utilitzeu l'ordre « yunohost log show {name} »", "log_link_to_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, proveïu el registre complete de l'operació clicant aquí", "log_help_to_get_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre « yunohost log share {name} »", "log_does_exists": "No hi ha cap registre per l'operació amb el nom« {log} », utilitzeu « yunohost log list » per veure tots els registre d'operació disponibles", @@ -219,7 +215,6 @@ "mail_unavailable": "Aquesta adreça de correu està reservada i ha de ser atribuïda automàticament el primer usuari", "main_domain_change_failed": "No s'ha pogut canviar el domini principal", "main_domain_changed": "S'ha canviat el domini principal", - "migrations_cant_reach_migration_file": "No s'ha pogut accedir als fitxers de migració al camí «%s»", "migrations_list_conflict_pending_done": "No es pot utilitzar «--previous» i «--done» al mateix temps.", "migrations_loading_migration": "Carregant la migració {id}...", "migrations_migration_has_failed": "La migració {id} ha fallat, cancel·lant. Error: {exception}", @@ -228,7 +223,6 @@ "migrations_to_be_ran_manually": "La migració {id} s'ha de fer manualment. Aneu a Eines → Migracions a la interfície admin, o executeu «yunohost tools migrations run».", "migrations_need_to_accept_disclaimer": "Per fer la migració {id}, heu d'acceptar aquesta clàusula de no responsabilitat:\n---\n{disclaimer}\n---\nSi accepteu fer la migració, torneu a executar l'ordre amb l'opció «--accept-disclaimer».", "not_enough_disk_space": "No hi ha prou espai en «{path}»", - "packages_upgrade_failed": "No s'han pogut actualitzar tots els paquets", "pattern_backup_archive_name": "Ha de ser un nom d'arxiu vàlid amb un màxim de 30 caràcters, compost per caràcters alfanumèrics i -_. exclusivament", "pattern_domain": "Ha de ser un nom de domini vàlid (ex.: el-meu-domini.cat)", "pattern_email": "Ha de ser una adreça de correu vàlida, sense el símbol «+» (ex.: algu@domini.cat)", @@ -237,7 +231,6 @@ "pattern_mailbox_quota": "Ha de ser una mida amb el sufix b/k/M/G/T o 0 per no tenir quota", "pattern_password": "Ha de tenir un mínim de 3 caràcters", "pattern_port_or_range": "Ha de ser un número de port vàlid (i.e. 0-65535) o un interval de ports (ex. 100:200)", - "pattern_positive_number": "Ha de ser un nombre positiu", "pattern_username": "Ha d'estar compost per caràcters alfanumèrics en minúscula i guió baix exclusivament", "pattern_password_app": "Les contrasenyes no poden de tenir els següents caràcters: {forbidden_chars}", "port_already_closed": "El port {port} ja està tancat per les connexions {ip_version}", @@ -254,7 +247,7 @@ "regenconf_up_to_date": "La configuració ja està al dia per la categoria «{category}»", "regenconf_updated": "S'ha actualitzat la configuració per la categoria «{category}»", "regenconf_would_be_updated": "La configuració hagués estat actualitzada per la categoria «{category}»", - "regenconf_dry_pending_applying": "Verificació de la configuració pendent que s'hauria d'haver aplicat per la categoria «{category}»…", + "regenconf_dry_pending_applying": "Verificació de la configuració pendent que s'hauria d'haver aplicat per la categoria «{category}»...", "regenconf_failed": "No s'ha pogut regenerar la configuració per la/les categoria/es : {categories}", "regenconf_pending_applying": "Aplicació de la configuració pendent per la categoria «{category}»...", "restore_already_installed_app": "Una aplicació amb la ID «{app}» ja està instal·lada", @@ -262,15 +255,15 @@ "restore_cleaning_failed": "No s'ha pogut netejar el directori temporal de restauració", "restore_complete": "Restauració completada", "restore_confirm_yunohost_installed": "Esteu segur de voler restaurar un sistema ja instal·lat? [{answers}]", - "restore_extracting": "Extracció dels fitxers necessaris de l'arxiu…", + "restore_extracting": "Extracció dels fitxers necessaris de l'arxiu...", "restore_failed": "No s'ha pogut restaurar el sistema", "restore_hook_unavailable": "El script de restauració «{part}» no està disponible en el sistema i tampoc és en l'arxiu", "restore_may_be_not_enough_disk_space": "Sembla que no hi ha prou espai disponible en el sistema (lliure: {free_space} B, espai necessari: {needed_space} B, marge de seguretat: {margin} B)", "restore_not_enough_disk_space": "No hi ha prou espai disponible (espai: {free_space} B, espai necessari: {needed_space} B, marge de seguretat: {margin} B)", "restore_nothings_done": "No s'ha restaurat res", "restore_removing_tmp_dir_failed": "No s'ha pogut eliminar un directori temporal antic", - "restore_running_app_script": "Restaurant l'aplicació «{app}»…", - "restore_running_hooks": "Execució dels hooks de restauració…", + "restore_running_app_script": "Restaurant l'aplicació «{app}»...", + "restore_running_hooks": "Execució dels hooks de restauració...", "restore_system_part_failed": "No s'ha pogut restaurar la part «{part}» del sistema", "root_password_desynchronized": "S'ha canviat la contrasenya d'administració, però YunoHost no ha pogut propagar-ho cap a la contrasenya root!", "root_password_replaced_by_admin_password": "La contrasenya root s'ha substituït per la contrasenya d'administració.", @@ -300,7 +293,6 @@ "service_disabled": "El servei «{service}» ja no començarà al arrancar el sistema.", "service_enable_failed": "No s'ha pogut fer que el servei «{service}» comenci automàticament a l'arrancada.\n\nRegistres recents: {logs}", "service_enabled": "El servei «{service}» començarà automàticament durant l'arrancada del sistema.", - "service_regen_conf_is_deprecated": "«yunohost service regen-conf» està desfasat! Utilitzeu «yunohost tools regen-conf» en el seu lloc.", "service_remove_failed": "No s'ha pogut eliminar el servei «{service}»", "service_removed": "S'ha eliminat el servei «{service}»", "service_reload_failed": "No s'ha pogut tornar a carregar el servei «{service}»\n\nRegistres recents: {logs}", @@ -315,19 +307,9 @@ "service_stopped": "S'ha aturat el servei «{service}»", "service_unknown": "Servei «{service}» desconegut", "ssowat_conf_generated": "S'ha regenerat la configuració SSOwat", - "ssowat_conf_updated": "S'ha actualitzat la configuració SSOwat", "system_upgraded": "S'ha actualitzat el sistema", "system_username_exists": "El nom d'usuari ja existeix en la llista d'usuaris de sistema", "this_action_broke_dpkg": "Aquesta acció a trencat dpkg/APT (els gestors de paquets del sistema)... Podeu intentar resoldre el problema connectant-vos amb SSH i executant «sudo apt install --fix-broken» i/o «sudo dpkg --configure -a».", - "tools_upgrade_at_least_one": "Especifiqueu «--apps», o «--system»", - "tools_upgrade_cant_both": "No es poden actualitzar tant el sistema com les aplicacions al mateix temps", - "tools_upgrade_cant_hold_critical_packages": "No es poden mantenir els paquets crítics…", - "tools_upgrade_cant_unhold_critical_packages": "No es poden deixar de mantenir els paquets crítics…", - "tools_upgrade_regular_packages": "Actualitzant els paquets «normals» (no relacionats amb YunoHost)…", - "tools_upgrade_regular_packages_failed": "No s'han pogut actualitzar els paquets següents: {packages_list}", - "tools_upgrade_special_packages": "Actualitzant els paquets «especials» (relacionats amb YunoHost)…", - "tools_upgrade_special_packages_explanation": "Aquesta actualització especial continuarà en segon pla. No comenceu cap altra acció al servidor en els pròxims ~10 minuts (depèn de la velocitat del maquinari). Després d'això, pot ser que us hagueu de tornar a connectar a la interfície d'administració. Els registres de l'actualització estaran disponibles a Eines → Registres (a la interfície d'administració) o utilitzant «yunohost log list» (des de la línia d'ordres).", - "tools_upgrade_special_packages_completed": "Actualització dels paquets YunoHost acabada.\nPremeu [Enter] per tornar a la línia d'ordres", "unbackup_app": "{app} no es guardarà", "unexpected_error": "Hi ha hagut un error inesperat: {error}", "unlimit": "Sense quota", @@ -345,7 +327,7 @@ "user_creation_failed": "No s'ha pogut crear l'usuari {user}: {error}", "user_deleted": "S'ha suprimit l'usuari", "user_deletion_failed": "No s'ha pogut suprimir l'usuari {user}: {error}", - "user_home_creation_failed": "No s'ha pogut crear la carpeta personal «home» per l'usuari", + "user_home_creation_failed": "No s'ha pogut crear la carpeta personal «{home}» per l'usuari", "user_unknown": "Usuari desconegut: {user}", "user_update_failed": "No s'ha pogut actualitzar l'usuari {user}: {error}", "user_updated": "S'ha canviat la informació de l'usuari", @@ -472,7 +454,7 @@ "diagnosis_http_unreachable": "Sembla que el domini {domain} no és accessible a través de HTTP des de fora de la xarxa local.", "diagnosis_unknown_categories": "Les següents categories són desconegudes: {categories}", "apps_catalog_init_success": "S'ha iniciat el sistema de catàleg d'aplicacions!", - "apps_catalog_updating": "S'està actualitzant el catàleg d'aplicacions…", + "apps_catalog_updating": "S'està actualitzant el catàleg d'aplicacions...", "apps_catalog_failed_to_download": "No s'ha pogut descarregar el catàleg d'aplicacions {apps_catalog}: {error}", "apps_catalog_obsolete_cache": "La memòria cau del catàleg d'aplicacions és buida o obsoleta.", "apps_catalog_update_success": "S'ha actualitzat el catàleg d'aplicacions!", @@ -485,14 +467,12 @@ "diagnosis_no_cache": "Encara no hi ha memòria cau pel diagnòstic de la categoria «{category}»", "diagnosis_http_timeout": "S'ha exhaurit el temps d'esperar intentant connectar amb el servidor des de l'exterior.
1. La causa més probable per a aquest problema és que el port 80 (i 443) no reenvien correctament cap al vostre servidor.
2. També us hauríeu d'assegurar que el servei nginx estigui funcionant
3. En configuracions més complexes: assegureu-vos que no hi ha cap tallafoc o reverse-proxy interferint.", "diagnosis_http_connection_error": "Error de connexió: no s'ha pogut connectar amb el domini demanat, segurament és inaccessible.", - "yunohost_postinstall_end_tip": "S'ha completat la post-instal·lació. Per acabar la configuració, considereu:\n - afegir un primer usuari a través de la secció «Usuaris» a la pàgina web d'administració (o emprant «yunohost user create » a la línia d'ordres);\n - diagnosticar possibles problemes a través de la secció «Diagnòstics» a la pàgina web d'administració (o emprant «yunohost diagnosis run» a la línia d'ordres);\n - llegir les seccions «Finalizing your setup» i «Getting to know Yunohost» a la documentació per administradors: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "S'ha completat la post-instal·lació. Per acabar la configuració, considereu:\n - afegir un primer usuari a través de la secció «Usuaris» a la pàgina web d'administració (o emprant «yunohost user create » a la línia d'ordres);\n - diagnosticar possibles problemes a través de la secció «Diagnòstics» a la pàgina web d'administració (o emprant «yunohost diagnosis run» a la línia d'ordres);\n - llegir les seccions «Finalizing your setup» i «Getting to know YunoHost» a la documentació per administradors: https://yunohost.org/admindoc.", "diagnosis_services_running": "El servei {service} s'està executant!", "diagnosis_services_conf_broken": "La configuració pel servei {service} està trencada!", "diagnosis_ports_needed_by": "És necessari exposar aquest port per a les funcions {category} (servei {service})", "global_settings_setting_pop3_enabled": "Activa el protocol POP3 per al servidor de correu", "log_app_action_run": "Executa l'acció de l'aplicació «{}»", - "log_app_config_show_panel": "Mostra el taulell de configuració de l'aplicació «{}»", - "log_app_config_apply": "Afegeix la configuració a l'aplicació «{}»", "diagnosis_never_ran_yet": "Sembla que el servidor s'ha configurat recentment i encara no hi cap informe de diagnòstic per mostrar. S'ha d'executar un diagnòstic complet primer, ja sigui des de la pàgina web d'administració o utilitzant la comanda «yunohost diagnosis run» al terminal.", "diagnosis_description_web": "Web", "diagnosis_basesystem_hardware": "L'arquitectura del maquinari del servidor és {virt} {arch}", @@ -552,31 +532,7 @@ "restore_already_installed_apps": "No s'han pogut restaurar les següents aplicacions perquè ja estan instal·lades: {apps}", "app_packaging_format_not_supported": "No es pot instal·lar aquesta aplicació ja que el format del paquet no és compatible amb la versió de YunoHost del sistema. Hauríeu de considerar actualitzar el sistema.", "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost dyndns update --force.", - "migration_0015_cleaning_up": "Netejant la memòria cau i els paquets que ja no són necessaris...", - "migration_0015_specific_upgrade": "Començant l'actualització dels paquets del sistema que s'han d'actualitzar de forma independent...", - "migration_0015_modified_files": "Tingueu en compte que s'han trobat els següents fitxers que es van modificar manualment i podria ser que es sobreescriguin durant l'actualització: {manually_modified_files}", - "migration_0015_problematic_apps_warning": "Tingueu en compte que s'han trobat les següents aplicacions que podrien ser problemàtiques. Sembla que aquestes aplicacions no s'han instal·lat des del catàleg d'aplicacions de YunoHost, o no estan marcades com «funcionant». En conseqüència, no es pot garantir que segueixin funcionant després de l'actualització: {problematic_apps}", - "migration_0015_general_warning": "Tingueu en compte que aquesta migració és una operació delicada. L'equip de YunoHost ha fet tots els possibles per revisar i testejar, però tot i això podria ser que la migració trenqui alguna part del sistema o algunes aplicacions.\n\nPer tant, està recomana:\n - Fer una còpia de seguretat de totes les dades o aplicacions crítiques. Més informació a https://yunohost.org/backup;\n - Ser pacient un cop comenci la migració: en funció de la connexió Internet i del maquinari, podria estar unes hores per actualitzar-ho tot.", - "migration_0015_system_not_fully_up_to_date": "El sistema no està completament al dia. Heu de fer una actualització normal abans de fer la migració a Buster.", - "migration_0015_not_enough_free_space": "Hi ha poc espai lliure a /var/! HI hauria d'haver un mínim de 1GB lliure per poder fer aquesta migració.", - "migration_0015_not_stretch": "La distribució actual de Debian no és Stretch!", - "migration_0015_yunohost_upgrade": "Començant l'actualització del nucli de YunoHost...", - "migration_0015_still_on_stretch_after_main_upgrade": "Alguna cosa ha anat malament durant la actualització principal, sembla que el sistema encara està en Debian Stretch", - "migration_0015_main_upgrade": "Començant l'actualització principal...", - "migration_0015_patching_sources_list": "Apedaçament de source.lists...", - "migration_0015_start": "Començant la migració a Buster", - "migration_description_0015_migrate_to_buster": "Actualitza els sistema a Debian Buster i YunoHost 4.x", "regenconf_need_to_explicitly_specify_ssh": "La configuració ssh ha estat modificada manualment, però heu d'especificar explícitament la categoria «ssh» amb --force per fer realment els canvis.", - "migration_0015_weak_certs": "S'han trobat els següents certificats que encara utilitzen algoritmes de signatura febles i s'han d'actualitzar per a ser compatibles amb la propera versió de nginx: {certs}", - "service_description_php7.3-fpm": "Executa aplicacions escrites en PHP amb NGINX", - "migration_0018_failed_to_reset_legacy_rules": "No s'ha pogut restaurar les regles legacy iptables: {error}", - "migration_0018_failed_to_migrate_iptables_rules": "No s'ha pogut migrar les regles legacy iptables a nftables: {error}", - "migration_0017_not_enough_space": "Feu suficient espai disponible en {path} per a realitzar la migració.", - "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 està instal·lat, però postgreSQL 11 no? Potser que hagi passat alguna cosa rara en aquest sistema :(...", - "migration_0017_postgresql_96_not_installed": "PostgreSQL no està instal·lat en aquest sistema. No s'ha de realitzar cap operació.", - "migration_description_0018_xtable_to_nftable": "Migrar les regles del trànsit de xarxa al nou sistema nftable", - "migration_description_0017_postgresql_9p6_to_11": "Migrar les bases de dades de PosrgreSQL 9.6 a 11", - "migration_description_0016_php70_to_php73_pools": "Migrar els fitxers de configuració «pool» php7.0-fpm a php7.3", "global_settings_setting_backup_compress_tar_archives": "Comprimir els arxius (.tar.gz) en lloc d'arxius no comprimits (.tar) al crear noves còpies de seguretat. N.B.: activar aquesta opció permet fer arxius de còpia de seguretat més lleugers, però el procés inicial de còpia de seguretat serà significativament més llarg i més exigent a nivell de CPU.", "global_settings_setting_smtp_relay_host": "L'amfitrió de tramesa SMTP que s'ha d'utilitzar per enviar correus electrònics en lloc d'aquesta instància de YunoHost. És útil si esteu en una de les següents situacions: el port 25 està bloquejat per el vostre proveïdor d'accés a internet o proveïdor de servidor privat virtual, si teniu una IP residencial llistada a DUHL, si no podeu configurar el DNS invers o si el servidor no està directament exposat a internet i voleu utilitzar-ne un altre per enviar correus electrònics.", "unknown_main_domain_path": "Domini o ruta desconeguda per a «{app}». Heu d'especificar un domini i una ruta per a poder especificar una URL per al permís.", @@ -587,15 +543,10 @@ "permission_protected": "El permís {permission} està protegit. No podeu afegir o eliminar el grup visitants a o d'aquest permís.", "pattern_email_forward": "Ha de ser una adreça de correu vàlida, s'accepta el símbol «+» (per exemple, algu+etiqueta@exemple.cat)", "invalid_number": "Ha de ser una xifra", - "migration_0019_slapd_config_will_be_overwritten": "Sembla que heu modificat manualment la configuració sldap. Per a aquesta migració crítica, YunoHist necessita forçar l'actualització de la configuració sldap. Es crearà una còpia de seguretat dels fitxers originals a {conf_backup_folder}.", - "migration_0019_add_new_attributes_in_ldap": "Afegir nous atributs per als permisos en la base de dades LDAP", - "migration_description_0019_extend_permissions_features": "Amplia/refés el sistema de gestió dels permisos de l'aplicació", - "migrating_legacy_permission_settings": "Migració dels paràmetres de permisos antics...", "invalid_regex": "Regex no vàlid: «{regex}»", "global_settings_setting_smtp_relay_password": "Tramesa de la contrasenya d'amfitrió SMTP", "global_settings_setting_smtp_relay_user": "Tramesa de compte d'usuari SMTP", "global_settings_setting_smtp_relay_port": "Port de tramesa SMTP", - "domain_name_unknown": "Domini «{domain}» desconegut", "diagnosis_processes_killed_by_oom_reaper": "El sistema ha matat alguns processos recentment perquè s'ha quedat sense memòria. Això acostuma a ser un símptoma de falta de memòria en el sistema o d'un procés que consumeix massa memòria. Llista dels processos que s'han matat:\n{kills_summary}", "diagnosis_package_installed_from_sury_details": "Alguns paquets s'han instal·lat per equivocació des d'un repositori de tercers anomenat Sury. L'equip de YunoHost a millorat l'estratègia per a gestionar aquests paquets, però s'espera que algunes configuracions que han instal·lat aplicacions PHP7.3 a Stretch puguin tenir algunes inconsistències. Per a resoldre aquesta situació, hauríeu d'intentar executar la següent ordre: {cmd_to_fix}", "diagnosis_package_installed_from_sury": "Alguns paquets del sistema s'han de tornar a versions anteriors", diff --git a/locales/ckb.json b/locales/ckb.json index 0967ef424..9e26dfeeb 100644 --- a/locales/ckb.json +++ b/locales/ckb.json @@ -1 +1 @@ -{} +{} \ No newline at end of file diff --git a/locales/cs.json b/locales/cs.json index cd1e9f7ae..47262064e 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -11,7 +11,6 @@ "action_invalid": "Nesprávné akce '{action}'", "aborting": "Zrušeno.", "app_change_url_identical_domains": "Stará a nová doména/url_cesta jsou totožné ('{domain}{path}'), nebudou provedeny žádné změny.", - "app_change_url_failed_nginx_reload": "Nepodařilo se znovunačís NGINX. Následuje výpis příkazu 'nginx -t':\n{nginx_errors}", "app_argument_invalid": "Vyberte správnou hodnotu pro argument '{name}': {error}", "app_argument_choice_invalid": "Vyberte jednu z možností '{choices}' pro argument'{name}'", "app_already_up_to_date": "{app} aplikace je/jsou aktuální", @@ -55,7 +54,7 @@ "global_settings_setting_smtp_relay_password": "SMTP relay heslo uživatele/hostitele", "global_settings_setting_smtp_relay_user": "SMTP relay uživatelské jméno/účet", "global_settings_setting_smtp_relay_port": "SMTP relay port", - "global_settings_setting_smtp_relay_host": "Použít SMTP relay hostitele pro odesílání emailů místo této Yunohost instance. Užitečné v různých situacích: port 25 je blokován vaším ISP nebo VPS poskytovatelem, IP adresa je na blacklistu (např. DUHL), nemůžete nastavit reverzní DNS záznam nebo tento server není přímo připojen do internetu a vy chcete použít jiný server k odesílání emailů.", + "global_settings_setting_smtp_relay_host": "Použít SMTP relay hostitele pro odesílání emailů místo této YunoHost instance. Užitečné v různých situacích: port 25 je blokován vaším ISP nebo VPS poskytovatelem, IP adresa je na blacklistu (např. DUHL), nemůžete nastavit reverzní DNS záznam nebo tento server není přímo připojen do internetu a vy chcete použít jiný server k odesílání emailů.", "global_settings_setting_smtp_allow_ipv6": "Povolit použití IPv6 pro příjem a odesílání emailů", "global_settings_setting_ssowat_panel_overlay_enabled": "Povolit SSOwat překryvný panel", "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Povolit použití (zastaralého) DSA klíče hostitele pro konfiguraci SSH služby", diff --git a/locales/da.json b/locales/da.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/locales/da.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/locales/de.json b/locales/de.json index fe4112934..686eb9251 100644 --- a/locales/de.json +++ b/locales/de.json @@ -4,13 +4,12 @@ "admin_password_change_failed": "Ändern des Passworts nicht möglich", "admin_password_changed": "Das Administrator-Kennwort wurde geändert", "app_already_installed": "{app} ist schon installiert", - "app_argument_choice_invalid": "Wähle einen der folgenden Werte '{choices}' für das Argument '{name}'", + "app_argument_choice_invalid": "Wähle einen gültigen Wert für das Argument '{name}': '{value}' ist nicht unter den verfügbaren Auswahlmöglichkeiten ({choices})", "app_argument_invalid": "Wähle einen gültigen Wert für das Argument '{name}': {error}", "app_argument_required": "Argument '{name}' wird benötigt", "app_extraction_failed": "Installationsdateien konnten nicht entpackt werden", "app_id_invalid": "Falsche App-ID", "app_install_files_invalid": "Diese Dateien können nicht installiert werden", - "app_manifest_invalid": "Mit dem App-Manifest stimmt etwas nicht: {error}", "app_not_installed": "{app} konnte nicht in der Liste installierter Apps gefunden werden: {all_apps}", "app_removed": "{app} wurde entfernt", "app_sources_fetch_failed": "Quelldateien konnten nicht abgerufen werden, ist die URL korrekt?", @@ -37,21 +36,20 @@ "backup_output_directory_not_empty": "Der gewählte Ausgabeordner sollte leer sein", "backup_output_directory_required": "Für die Datensicherung muss ein Zielverzeichnis angegeben werden", "backup_running_hooks": "Datensicherunghook wird ausgeführt...", - "custom_app_url_required": "Sie müssen eine URL angeben, um Ihre benutzerdefinierte App {app} zu aktualisieren", + "custom_app_url_required": "Du musst eine URL angeben, um deine benutzerdefinierte App {app} zu aktualisieren", "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", "domain_created": "Domäne erstellt", "domain_creation_failed": "Konnte Domäne nicht erzeugen", "domain_deleted": "Domain wurde gelöscht", "domain_deletion_failed": "Domain {domain}: {error} konnte nicht gelöscht werden", - "domain_dyndns_already_subscribed": "Sie haben sich schon für eine DynDNS-Domäne registriert", + "domain_dyndns_already_subscribed": "Du hast dich schon für eine DynDNS-Domäne registriert", "domain_dyndns_root_unknown": "Unbekannte DynDNS Hauptdomain", "domain_exists": "Die Domäne existiert bereits", - "domain_uninstall_app_first": "Diese Applikationen sind noch auf Ihrer Domäne installiert; \n{apps}\n\nBitte deinstallieren Sie sie mit dem Befehl 'yunohost app remove the_app_id' oder verschieben Sie sie mit 'yunohost app change-url the_app_id'", - "domain_unknown": "Unbekannte Domain", + "domain_uninstall_app_first": "Diese Applikationen sind noch auf deiner Domäne installiert; \n{apps}\n\nBitte deinstalliere sie mit dem Befehl 'yunohost app remove the_app_id' oder verschiebe sie mit 'yunohost app change-url the_app_id'", "done": "Erledigt", - "downloading": "Wird heruntergeladen…", + "downloading": "Wird heruntergeladen...", "dyndns_ip_update_failed": "Konnte die IP-Adresse für DynDNS nicht aktualisieren", - "dyndns_ip_updated": "Aktualisierung Ihrer IP-Adresse bei DynDNS", + "dyndns_ip_updated": "Deine IP-Adresse wurde bei DynDNS aktualisiert", "dyndns_key_generating": "Generierung des DNS-Schlüssels..., das könnte eine Weile dauern.", "dyndns_registered": "DynDNS Domain registriert", "dyndns_registration_failed": "DynDNS Domain konnte nicht registriert werden: {error}", @@ -73,7 +71,6 @@ "mail_forward_remove_failed": "Die Weiterleitungs-E-Mail '{mail}' konnte nicht gelöscht werden", "main_domain_change_failed": "Die Hauptdomain konnte nicht geändert werden", "main_domain_changed": "Die Hauptdomain wurde geändert", - "packages_upgrade_failed": "Konnte nicht alle Pakete aktualisieren", "pattern_backup_archive_name": "Muss ein gültiger Dateiname mit maximal 30 alphanumerischen sowie -_. Zeichen sein", "pattern_domain": "Muss ein gültiger Domainname sein (z.B. meine-domain.org)", "pattern_email": "Muss eine gültige E-Mail-Adresse ohne '+' Symbol sein (z.B. someone@example.com)", @@ -90,10 +87,10 @@ "restore_complete": "Vollständig wiederhergestellt", "restore_confirm_yunohost_installed": "Möchtest du die Wiederherstellung wirklich starten? [{answers}]", "restore_failed": "Das System konnte nicht wiederhergestellt werden", - "restore_hook_unavailable": "Das Wiederherstellungsskript für '{part}' steht weder in Ihrem System noch im Archiv zur Verfügung", + "restore_hook_unavailable": "Das Wiederherstellungsskript für '{part}' steht weder in deinem System noch im Archiv zur Verfügung", "restore_nothings_done": "Nichts wurde wiederhergestellt", - "restore_running_app_script": "App '{app}' wird wiederhergestellt…", - "restore_running_hooks": "Wiederherstellung wird gestartet…", + "restore_running_app_script": "App '{app}' wird wiederhergestellt...", + "restore_running_hooks": "Wiederherstellung wird gestartet...", "service_add_failed": "Der Dienst '{service}' konnte nicht hinzugefügt werden", "service_added": "Der Dienst '{service}' wurde erfolgreich hinzugefügt", "service_already_started": "Der Dienst '{service}' läuft bereits", @@ -111,9 +108,8 @@ "service_stopped": "Der Dienst '{service}' wurde erfolgreich beendet", "service_unknown": "Unbekannter Dienst '{service}'", "ssowat_conf_generated": "Konfiguration von SSOwat neu erstellt", - "ssowat_conf_updated": "Die Konfiguration von SSOwat aktualisiert", "system_upgraded": "System aktualisiert", - "system_username_exists": "Der Benutzername existiert bereits in der Liste der System-Benutzer", + "system_username_exists": "Der Anmeldename existiert bereits in der Liste der System-Konten", "unbackup_app": "'{app}' wird nicht gespeichert werden", "unexpected_error": "Etwas Unerwartetes ist passiert: {error}", "unlimit": "Kein Kontingent", @@ -125,14 +121,14 @@ "upnp_disabled": "UPnP deaktiviert", "upnp_enabled": "UPnP aktiviert", "upnp_port_open_failed": "Port konnte nicht via UPnP geöffnet werden", - "user_created": "Benutzer erstellt", - "user_creation_failed": "Benutzer konnte nicht erstellt werden {user}: {error}", - "user_deleted": "Benutzer gelöscht", - "user_deletion_failed": "Benutzer konnte nicht gelöscht werden {user}: {error}", - "user_home_creation_failed": "Persönlicher Ordner des Benutzers konnte nicht erstellt werden", - "user_unknown": "Unbekannter Benutzer: {user}", - "user_update_failed": "Benutzer konnte nicht aktualisiert werden {user}: {error}", - "user_updated": "Benutzerinformationen wurden aktualisiert", + "user_created": "Konto erstellt", + "user_creation_failed": "Konto konnte nicht erstellt werden {user}: {error}", + "user_deleted": "Konto gelöscht", + "user_deletion_failed": "Konto konnte nicht gelöscht werden {user}: {error}", + "user_home_creation_failed": "Persönlicher Ordner '{home}' für dieses Konto konnte nicht erstellt werden", + "user_unknown": "Unbekanntes Konto: {user}", + "user_update_failed": "Konto konnte nicht aktualisiert werden {user}: {error}", + "user_updated": "Kontoinformationen wurden aktualisiert", "yunohost_already_installed": "YunoHost ist bereits installiert", "yunohost_configured": "YunoHost ist nun konfiguriert", "yunohost_installing": "YunoHost wird installiert...", @@ -140,7 +136,6 @@ "app_not_properly_removed": "{app} wurde nicht ordnungsgemäß entfernt", "not_enough_disk_space": "Nicht genügend Speicherplatz auf '{path}' frei", "backup_creation_failed": "Konnte Backup-Archiv nicht erstellen", - "pattern_positive_number": "Muss eine positive Zahl sein", "app_not_correctly_installed": "{app} scheint nicht korrekt installiert zu sein", "app_requirements_checking": "Überprüfe notwendige Pakete für {app}...", "app_requirements_unmeet": "Anforderungen für {app} werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein", @@ -149,28 +144,27 @@ "domains_available": "Verfügbare Domains:", "dyndns_key_not_found": "DNS-Schlüssel für die Domain wurde nicht gefunden", "dyndns_no_domain_registered": "Keine Domain mit DynDNS registriert", - "mailbox_used_space_dovecot_down": "Der Dovecot-Mailbox-Dienst muss aktiv sein, wenn Sie den von der Mailbox belegten Speicher abrufen wollen", + "mailbox_used_space_dovecot_down": "Der Dovecot-Mailbox-Dienst muss aktiv sein, wenn du den von der Mailbox belegten Speicher abrufen willst", "certmanager_attempt_to_replace_valid_cert": "Du versuchst gerade eine richtiges und gültiges Zertifikat der Domain {domain} zu überschreiben! (Benutze --force , um diese Nachricht zu umgehen)", - "certmanager_domain_cert_not_selfsigned": "Das Zertifikat der Domain {domain} ist kein selbstsigniertes Zertifikat. Sind Sie sich sicher, dass Sie es ersetzen wollen? (Benutzen Sie dafür '--force')", + "certmanager_domain_cert_not_selfsigned": "Das Zertifikat der Domain {domain} ist kein selbstsigniertes Zertifikat. Bist du sich sicher, dass du es ersetzen willst? (Benutze dafür '--force')", "certmanager_certificate_fetching_or_enabling_failed": "Die Aktivierung des neuen Zertifikats für die {domain} ist fehlgeschlagen...", "certmanager_attempt_to_renew_nonLE_cert": "Das Zertifikat der Domain '{domain}' wurde nicht von Let's Encrypt ausgestellt. Es kann nicht automatisch erneuert werden!", "certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain} läuft nicht in Kürze ab! (Benutze --force um diese Nachricht zu umgehen)", - "certmanager_domain_http_not_working": "Es scheint, als ob die Domäne {domain} nicht über HTTP erreicht werden kann. Bitte überprüfen Sie, ob Ihre DNS- und nginx-Konfiguration in Ordnung ist. (Wenn Sie wissen was Sie tun, nutzen Sie \"--no-checks\" um die Überprüfung zu überspringen.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "Der DNS-A-Eintrag der Domain {domain} unterscheidet sich von dieser Server-IP. Für weitere Informationen überprüfen Sie bitte die 'DNS records' (basic) Kategorie in der Diagnose. Wenn Sie gerade Ihren A-Eintrag verändert haben, warten Sie bitte etwas, damit die Änderungen wirksam werden (Sie können die DNS-Propagation mittels Website überprüfen) (Wenn Sie wissen was Sie tun, können Sie --no-checks benutzen, um diese Überprüfung zu überspringen.)", + "certmanager_domain_http_not_working": "Es scheint, als ob die Domäne {domain} nicht über HTTP erreicht werden kann. Bitte überprüfe, ob deine DNS- und nginx-Konfiguration in Ordnung ist. (Wenn du weißt, was du tust, nutze '--no-checks' um die Überprüfung zu überspringen.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Der DNS-A-Eintrag der Domain {domain} unterscheidet sich von dieser Server-IP. Für weitere Informationen überprüfe bitte die 'DNS records' (basic) Kategorie in der Diagnose. Wenn du kürzlich deinen A-Eintrag verändert hast, warte bitte etwas, damit die Änderungen wirksam werden (Du kannst die DNS-Propagation mittels Website überprüfen) (Wenn du weißt, was du tust, kannst du '--no-checks' benutzen, um diese Überprüfung zu überspringen.)", "certmanager_cannot_read_cert": "Es ist ein Fehler aufgetreten, als es versucht wurde das aktuelle Zertifikat für die Domain {domain} zu öffnen (Datei: {file}), Grund: {reason}", "certmanager_cert_install_success_selfsigned": "Das selbstsignierte Zertifikat für die Domäne '{domain}' wurde erfolgreich installiert", "certmanager_cert_install_success": "Let's-Encrypt-Zertifikat für die Domäne {domain} ist jetzt installiert", "certmanager_cert_renew_success": "Das Let's Encrypt Zertifikat für die Domain {domain} wurde erfolgreich erneuert", - "certmanager_hit_rate_limit": "Es wurden innerhalb kurzer Zeit zu viele Zertifikate für dieselbe Domäne {domain} ausgestellt. Bitte versuchen Sie es später nochmal. Besuchen Sie https://letsencrypt.org/docs/rate-limits/ für mehr Informationen", + "certmanager_hit_rate_limit": "Es wurden innerhalb kurzer Zeit zu viele Zertifikate für dieselbe Domäne {domain} ausgestellt. Bitte versuche es später nochmal. Besuche https://letsencrypt.org/docs/rate-limits/ für mehr Informationen", "certmanager_cert_signing_failed": "Das neue Zertifikat konnte nicht signiert werden", "certmanager_no_cert_file": "Die Zertifikatsdatei für die Domain {domain} (Datei: {file}) konnte nicht gelesen werden", - "domain_cannot_remove_main": "Die primäre Domain konnten nicht entfernt werden. Lege zuerst einen neue primäre Domain Sie können die Domäne '{domain}' nicht entfernen, weil Sie die Hauptdomäne ist. Sie müssen zuerst eine andere Domäne als Hauptdomäne festlegen. Sie können das mit dem Befehl 'yunohost domain main-domain -n tun. Hier ist eine Liste der möglichen Domänen: {other_domains}", + "domain_cannot_remove_main": "Die Domäne '{domain}' konnten nicht entfernt werden, weil es die Haupt-Domäne ist. Du musst zuerst eine andere Domäne zur Haupt-Domäne machen. Dies ist über den Befehl 'yunohost domain main-domain -n ' möglich. Hier ist eine Liste möglicher Domänen: {other_domains}", "certmanager_self_ca_conf_file_not_found": "Die Konfigurationsdatei der Zertifizierungsstelle für selbstsignierte Zertifikate wurde nicht gefunden (Datei {file})", - "certmanager_acme_not_configured_for_domain": "Die ACME Challenge kann im Moment nicht für {domain} ausgeführt werden, weil in ihrer nginx conf das entsprechende Code-Snippet fehlt... Bitte stellen Sie sicher, dass Ihre nginx-Konfiguration mit 'yunohost tools regen-conf nginx --dry-run --with-diff' auf dem neuesten Stand ist.", + "certmanager_acme_not_configured_for_domain": "Die ACME Challenge kann im Moment nicht für {domain} ausgeführt werden, weil in deiner nginx-Konfiguration das entsprechende Code-Snippet fehlt... Bitte stelle sicher, dass deine nginx-Konfiguration mit 'yunohost tools regen-conf nginx --dry-run --with-diff' auf dem neuesten Stand ist.", "certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht aufgelöst werden (Datei: {file})", - "domain_hostname_failed": "Sie können keinen neuen Hostnamen verwenden. Das kann zukünftige Probleme verursachen (es kann auch sein, dass es funktioniert).", + "domain_hostname_failed": "Neuer Hostname wurde nicht gesetzt. Das kann zukünftige Probleme verursachen (es kann auch sein, dass es funktioniert).", "app_already_installed_cant_change_url": "Diese Applikation ist bereits installiert. Die URL kann durch diese Funktion nicht modifiziert werden. Überprüfe ob `app changeurl` verfügbar ist.", - "app_change_url_failed_nginx_reload": "NGINX konnte nicht neu gestartet werden. Hier ist der Output von 'nginx -t':\n{nginx_errors}", "app_change_url_identical_domains": "Die alte und neue domain/url_path sind identisch: ('{domain} {path}'). Es gibt nichts zu tun.", "app_already_up_to_date": "{app} ist bereits aktuell", "backup_abstract_method": "Diese Backup-Methode wird noch nicht unterstützt", @@ -183,23 +177,22 @@ "backup_archive_writing_error": "Die Dateien '{source} (im Ordner '{dest}') konnten nicht in das komprimierte Archiv-Backup '{archive}' hinzugefügt werden", "app_change_url_success": "{app} URL ist nun {domain}{path}", "global_settings_bad_type_for_setting": "Falscher Typ der Einstellung {setting}. Empfangen: {received_type}, aber erwarteter Typ: {expected_type}", - "global_settings_bad_choice_for_enum": "Wert des Einstellungsparameters {setting} ungültig. Der Wert den Sie eingegeben haben: '{choice}', die gültigen Werte für diese Einstellung: {available_choices}", + "global_settings_bad_choice_for_enum": "Wert des Einstellungsparameters {setting} ungültig. Du hast '{choice}' eingegeben. Aber nur folgende Werte sind gültig: {available_choices}", "file_does_not_exist": "Die Datei {path} existiert nicht.", - "experimental_feature": "Warnung: Der Maintainer hat diese Funktion als experimentell gekennzeichnet. Sie ist nicht stabil. Sie sollten sie nur verwenden, wenn Sie wissen, was Sie tun.", + "experimental_feature": "Warnung: Der Maintainer hat diese Funktion als experimentell gekennzeichnet. Sie ist nicht stabil. Du solltest sie nur verwenden, wenn du weißt, was du tust.", "dyndns_domain_not_provided": "Der DynDNS-Anbieter {provider} kann die Domäne(n) {domain} nicht bereitstellen.", "dyndns_could_not_check_available": "Konnte nicht überprüfen, ob {domain} auf {provider} verfügbar ist.", - "dyndns_could_not_check_provide": "Konnte nicht überprüft, ob {provider} die Domain(s) {domain} bereitstellen kann.", "domain_dns_conf_is_just_a_recommendation": "Dieser Befehl zeigt dir die *empfohlene* Konfiguration. Er konfiguriert *nicht* das DNS für dich. Es liegt in deiner Verantwortung, die DNS-Zone bei deinem DNS-Registrar nach dieser Empfehlung zu konfigurieren.", "dpkg_lock_not_available": "Dieser Befehl kann momentan nicht ausgeführt werden, da anscheinend ein anderes Programm die Sperre von dpkg (dem Systempaket-Manager) verwendet", - "confirm_app_install_thirdparty": "WARNUNG! Diese Applikation ist nicht Teil des YunoHost-Applikationskatalogs. Das Installieren von Drittanbieterapplikationen könnte die Sicherheit und Integrität Ihres Systems beeinträchtigen. Sie sollten wahrscheinlich NICHT fortfahren, es sei denn, Sie wissen, was Sie tun. Es wird KEIN SUPPORT angeboten, wenn die Applikation nicht funktionieren oder Ihr System beschädigen sollte... Wenn Sie das Risiko trotzdem eingehen möchten, tippen Sie '{answers}'", - "confirm_app_install_danger": "WARNUNG! Diese Applikation ist noch experimentell (wenn nicht ausdrücklich nicht funktionsfähig)! Sie sollten Sie wahrscheinlich NICHT installieren, es sei denn, Sie wissen, was Sie tun. Es wird keine Unterstützung angeboten, falls diese Applikation nicht funktionieren oder Ihr System beschädigen sollte... Falls Sie bereit sind, dieses Risiko einzugehen, tippen Sie '{answers}'", + "confirm_app_install_thirdparty": "WARNUNG! Diese Applikation ist nicht Teil des YunoHost-Applikationskatalogs. Das Installieren von Drittanbieterapplikationen könnte die Sicherheit und Integrität deines Systems beeinträchtigen. Du solltest wahrscheinlich NICHT fortfahren, es sei denn, du weißt, was du tust. Es wird KEINE UNTERSTÜTZUNG angeboten, wenn die Applikation nicht funktionieren oder dein System beschädigen sollte... Wenn du das Risiko trotzdem eingehen möchrst, tippe '{answers}'", + "confirm_app_install_danger": "WARNUNG! Diese Applikation ist noch experimentell (wenn nicht sogar ausdrücklich nicht funktionsfähig)! Du solltest sie wahrscheinlich NICHT installieren, es sei denn, du weißt, was du tust. Es wird keine Unterstützung angeboten, falls diese Applikation nicht funktionieren oder dein System beschädigen sollte... Falls du bereit bist, dieses Risiko einzugehen, tippe '{answers}'", "confirm_app_install_warning": "Warnung: Diese Applikation funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers}] ", "backup_with_no_restore_script_for_app": "{app} hat kein Wiederherstellungsskript. Das Backup dieser App kann nicht automatisch wiederhergestellt werden.", "backup_with_no_backup_script_for_app": "Die App {app} hat kein Sicherungsskript. Ignoriere es.", "backup_unable_to_organize_files": "Dateien im Archiv konnten nicht mit der schnellen Methode organisiert werden", "backup_system_part_failed": "Der Systemteil '{part}' konnte nicht gesichert werden", "backup_permission": "Sicherungsberechtigung für {app}", - "backup_output_symlink_dir_broken": "Ihr Archivverzeichnis '{path}' ist ein fehlerhafter Symlink. Vielleicht haben Sie vergessen, das Speichermedium, auf das er verweist, neu zu mounten oder einzustecken.", + "backup_output_symlink_dir_broken": "Dein Archivverzeichnis '{path}' ist ein fehlerhafter Symlink. Vielleicht hast du vergessen, das Speichermedium, auf das er verweist, neu zu mounten oder einzustecken.", "backup_mount_archive_for_restore": "Archiv für Wiederherstellung vorbereiten...", "backup_method_tar_finished": "Tar-Backup-Archiv erstellt", "backup_method_custom_finished": "Benutzerdefinierte Sicherungsmethode '{method}' beendet", @@ -208,7 +201,7 @@ "backup_custom_backup_error": "Bei der benutzerdefinierten Sicherungsmethode ist beim Arbeitsschritt \"Sicherung\" ein Fehler aufgetreten", "backup_csv_creation_failed": "Die zur Wiederherstellung erforderliche CSV-Datei kann nicht erstellt werden", "backup_couldnt_bind": "{src} konnte nicht an {dest} angebunden werden.", - "backup_ask_for_copying_if_needed": "Möchten Sie die Sicherung mit {size}MB temporär durchführen? (Dieser Weg wird verwendet, da einige Dateien nicht mit einer effizienteren Methode vorbereitet werden konnten.)", + "backup_ask_for_copying_if_needed": "Möchtest du die Sicherung mit {size}MB temporär durchführen? (Dieser Weg wird verwendet, da einige Dateien nicht mit einer effizienteren Methode vorbereitet werden konnten.)", "backup_actually_backuping": "Erstellt ein Backup-Archiv aus den gesammelten Dateien...", "ask_new_path": "Neuer Pfad", "ask_new_domain": "Neue Domain", @@ -222,7 +215,7 @@ "app_not_upgraded": "Die App '{failed_app}' konnte nicht aktualisiert werden. Infolgedessen wurden die folgenden App-Upgrades abgebrochen: {apps}", "app_make_default_location_already_used": "Die App \"{app}\" kann nicht als Standard für die Domain \"{domain}\" festgelegt werden. Sie wird bereits von \"{other_app}\" verwendet", "aborting": "Breche ab.", - "app_action_cannot_be_ran_because_required_services_down": "Diese erforderlichen Dienste sollten zur Durchführung dieser Aktion laufen: {services}. Versuchen Sie, sie neu zu starten, um fortzufahren (und möglicherweise zu untersuchen, warum sie nicht verfügbar sind).", + "app_action_cannot_be_ran_because_required_services_down": "Diese erforderlichen Dienste sollten zur Durchführung dieser Aktion laufen: {services}. Versuche, sie neu zu starten, um fortzufahren (und möglicherweise zu untersuchen, warum sie nicht verfügbar sind).", "already_up_to_date": "Nichts zu tun. Alles ist bereits auf dem neusten Stand.", "admin_password_too_long": "Bitte ein Passwort kürzer als 127 Zeichen wählen", "app_action_broke_system": "Diese Aktion scheint diese wichtigen Dienste unterbrochen zu haben: {services}", @@ -231,7 +224,7 @@ "global_settings_setting_security_ssh_compatibility": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den SSH-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", "group_deleted": "Gruppe '{group}' gelöscht", "group_deletion_failed": "Konnte Gruppe '{group}' nicht löschen: {error}", - "dyndns_provider_unreachable": "DynDNS-Anbieter {provider} kann nicht erreicht werden: Entweder ist Ihr YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.", + "dyndns_provider_unreachable": "DynDNS-Anbieter {provider} kann nicht erreicht werden: Entweder ist dein YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.", "group_created": "Gruppe '{group}' angelegt", "group_creation_failed": "Konnte Gruppe '{group}' nicht anlegen", "group_unknown": "Die Gruppe '{group}' ist unbekannt", @@ -241,10 +234,10 @@ "log_operation_unit_unclosed_properly": "Die Operationseinheit wurde nicht richtig geschlossen", "global_settings_setting_security_postfix_compatibility": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Postfix-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", "global_settings_unknown_type": "Unerwartete Situation, die Einstellung {setting} scheint den Typ {unknown_type} zu haben, ist aber kein vom System unterstützter Typ.", - "dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint.... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo apt install --fix-broken` sowie/oder `sudo dpkg --configure -a` ausführst.", + "dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo apt install --fix-broken` sowie/oder `sudo dpkg --configure -a` ausführst.", "global_settings_unknown_setting_from_settings_file": "Unbekannter Schlüssel in den Einstellungen: '{setting_key}', verwerfen und speichern in /etc/yunohost/settings-unknown.json", "log_link_to_log": "Vollständiges Log dieser Operation: '{desc}'", - "log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwenden Sie den Befehl 'yunohost log show {name}{name}'", + "log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwende den Befehl 'yunohost log show {name}'", "global_settings_setting_security_nginx_compatibility": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Webserver NGINX. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Erlaubt die Verwendung eines (veralteten) DSA-Hostkeys für die SSH-Daemon-Konfiguration", "log_app_remove": "Entferne die Applikation '{}'", @@ -259,7 +252,7 @@ "log_help_to_get_failed_log": "Der Vorgang'{desc}' konnte nicht abgeschlossen werden. Bitte teile das vollständige Protokoll dieser Operation mit dem Befehl 'yunohost log share {name}', um Hilfe zu erhalten", "backup_no_uncompress_archive_dir": "Dieses unkomprimierte Archivverzeichnis gibt es nicht", "log_app_change_url": "Ändere die URL der Applikation '{}'", - "global_settings_setting_security_password_user_strength": "Stärke des Benutzerpassworts", + "global_settings_setting_security_password_user_strength": "Stärke des Anmeldepassworts", "good_practices_about_user_password": "Du bist nun dabei, ein neues Nutzerpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - es ist jedoch empfehlenswert, ein längeres Passwort (z.B. eine Passphrase) und/oder verschiedene Arten von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", "log_link_to_failed_log": "Der Vorgang konnte nicht abgeschlossen werden '{desc}'. Bitte gib das vollständige Protokoll dieser Operation mit Klicken Sie hier an, um Hilfe zu erhalten", "backup_cant_mount_uncompress_archive": "Das unkomprimierte Archiv konnte nicht als schreibgeschützt gemountet werden", @@ -277,115 +270,110 @@ "diagnosis_basesystem_kernel": "Server läuft unter Linux-Kernel {kernel_version}", "diagnosis_basesystem_ynh_single_version": "{package} Version: {version} ({repo})", "diagnosis_basesystem_ynh_main_version": "Server läuft YunoHost {main_version} ({repo})", - "diagnosis_basesystem_ynh_inconsistent_versions": "Sie verwenden inkonsistente Versionen der YunoHost-Pakete... wahrscheinlich wegen eines fehlgeschlagenen oder teilweisen Upgrades.", + "diagnosis_basesystem_ynh_inconsistent_versions": "Du verwendest inkonsistente Versionen der YunoHost-Pakete... wahrscheinlich wegen eines fehlgeschlagenen oder teilweisen Upgrades.", "apps_catalog_init_success": "App-Katalogsystem initialisiert!", - "apps_catalog_updating": "Aktualisierung des Applikationskatalogs…", + "apps_catalog_updating": "Aktualisierung des Applikationskatalogs...", "apps_catalog_failed_to_download": "Der {apps_catalog} App-Katalog kann nicht heruntergeladen werden: {error}", "apps_catalog_obsolete_cache": "Der Cache des App-Katalogs ist leer oder veraltet.", "apps_catalog_update_success": "Der Apps-Katalog wurde aktualisiert!", "password_too_simple_1": "Das Passwort muss mindestens 8 Zeichen lang sein", - "diagnosis_everything_ok": "Alles schaut gut aus für {category}!", + "diagnosis_everything_ok": "Alles sieht OK aus für {category}!", "diagnosis_failed": "Kann Diagnose-Ergebnis für die Kategorie '{category}' nicht abrufen: {error}", "diagnosis_ip_connected_ipv4": "Der Server ist mit dem Internet über IPv4 verbunden!", "diagnosis_no_cache": "Kein Diagnose Cache aktuell für die Kategorie '{category}'", "diagnosis_ip_no_ipv4": "Der Server hat kein funktionierendes IPv4.", "diagnosis_ip_connected_ipv6": "Der Server ist mit dem Internet über IPv6 verbunden!", - "diagnosis_ip_no_ipv6": "Der Server hat kein funktionierendes IPv6.", + "diagnosis_ip_no_ipv6": "Der Server verfügt nicht über eine funktionierende IPv6-Adresse.", "diagnosis_ip_not_connected_at_all": "Der Server scheint überhaupt nicht mit dem Internet verbunden zu sein!?", "diagnosis_failed_for_category": "Diagnose fehlgeschlagen für die Kategorie '{category}': {error}", - "diagnosis_cache_still_valid": "(Der Cache für die Diagnose {category} ist immer noch gültig . Es wird momentan keine neue Diagnose durchgeführt!)", + "diagnosis_cache_still_valid": "(Cache noch gültig für {category} Diagnose. Es wird keine neue Diagnose durchgeführt!)", "diagnosis_cant_run_because_of_dep": "Kann Diagnose für {category} nicht ausführen während wichtige Probleme zu {dep} noch nicht behoben sind.", "diagnosis_found_errors_and_warnings": "Habe {errors} erhebliche(s) Problem(e) (und {warnings} Warnung(en)) in Verbindung mit {category} gefunden!", "diagnosis_ip_broken_dnsresolution": "Domänen-Namens-Auflösung scheint aus einem bestimmten Grund nicht zu funktionieren... Blockiert eine Firewall die DNS Anfragen?", "diagnosis_ip_broken_resolvconf": "Domänen-Namensauflösung scheint nicht zu funktionieren, was daran liegen könnte, dass in /etc/resolv.conf kein Eintrag auf 127.0.0.1 zeigt.", - "diagnosis_ip_weird_resolvconf_details": "Die Datei /etc/resolv.conf muss ein Symlink auf /etc/resolvconf/run/resolv.conf sein, welcher auf 127.0.0.1 (dnsmasq) zeigt. Falls Sie die DNS-Resolver manuell konfigurieren möchten, bearbeiten Sie bitte /etc/resolv.dnsmasq.conf.", - "diagnosis_dns_good_conf": "Die DNS-Einträge für die Domäne {domain} (Kategorie {category}) sind korrekt konfiguriert", + "diagnosis_ip_weird_resolvconf_details": "Die Datei /etc/resolv.conf muss ein Symlink auf /etc/resolvconf/run/resolv.conf sein, welcher auf 127.0.0.1 (dnsmasq) zeigt. Falls du die DNS-Resolver manuell konfigurieren möchtest, bearbeite bitte /etc/resolv.dnsmasq.conf.", + "diagnosis_dns_good_conf": "DNS Einträge korrekt konfiguriert für die Domäne {domain} (Kategorie {category})", "diagnosis_ignored_issues": "(+ {nb_ignored} ignorierte(s) Problem(e))", "diagnosis_basesystem_hardware": "Server Hardware Architektur ist {virt} {arch}", "diagnosis_found_errors": "Habe {errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!", "diagnosis_found_warnings": "Habe {warnings} Ding(e) gefunden, die verbessert werden könnten für {category}.", "diagnosis_ip_dnsresolution_working": "Domänen-Namens-Auflösung funktioniert!", - "diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber seien Sie vorsichtig wenn Sie Ihren eigenen /etc/resolv.conf verwenden.", - "diagnosis_display_tip": "Um die gefundenen Probleme zu sehen, können Sie zum Diagnose-Bereich des webadmin gehen, oder 'yunohost diagnosis show --issues --human-readable' in der Kommandozeile ausführen.", + "diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber sei vorsichtig wenn du deine eigene /etc/resolv.conf verwendest.", + "diagnosis_display_tip": "Um die gefundenen Probleme zu sehen, kannst du zum Diagnose-Bereich des webadmin gehen, oder 'yunohost diagnosis show --issues --human-readable' in der Kommandozeile ausführen.", "backup_archive_corrupted": "Das Backup-Archiv '{archive}' scheint beschädigt: {error}", "backup_archive_cant_retrieve_info_json": "Die Informationen für das Archiv '{archive}' konnten nicht geladen werden... Die Datei info.json wurde nicht gefunden (oder ist kein gültiges json).", - "app_packaging_format_not_supported": "Diese App kann nicht installiert werden da das Paketformat nicht von der YunoHost-Version unterstützt wird. Denken Sie darüber nach das System zu aktualisieren.", + "app_packaging_format_not_supported": "Diese App kann nicht installiert werden da das Paketformat nicht von der YunoHost-Version unterstützt wird. Am besten solltest du dein System aktualisieren.", "certmanager_domain_not_diagnosed_yet": "Für die Domain {domain} gibt es noch keine Diagnose-Resultate. Bitte widerhole die Diagnose für die Kategorien 'DNS records' und 'Web' im Diagnose-Bereich um zu überprüfen ob die Domain für Let's Encrypt bereit ist. (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen.)", - "migration_0015_patching_sources_list": "sources.lists wird repariert...", - "migration_0015_start": "Start der Migration auf Buster", - "migration_description_0015_migrate_to_buster": "Auf Debian Buster und YunoHost 4.x upgraden", - "mail_unavailable": "Diese E-Mail Adresse ist reserviert und wird dem ersten Benutzer automatisch zugewiesen", + "mail_unavailable": "Diese E-Mail Adresse ist reserviert und wird dem ersten Konto automatisch zugewiesen", "diagnosis_services_conf_broken": "Die Konfiguration für den Dienst {service} ist fehlerhaft!", "diagnosis_services_running": "Dienst {service} läuft!", "diagnosis_domain_expires_in": "{domain} läuft in {days} Tagen ab.", "diagnosis_domain_expiration_error": "Einige Domänen werden SEHR BALD ablaufen!", - "diagnosis_domain_expiration_success": "Ihre Domänen sind registriert und werden in nächster Zeit nicht ablaufen.", + "diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.", "diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!", "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden", - "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domain sollte automatisch von YunoHost verwaltet werden. Andernfalls können Sie mittels yunohost dyndns update --force ein Update erzwingen.", - "diagnosis_dns_point_to_doc": "Bitte schauen Sie in die Dokumentation unter https://yunohost.org/dns_config wenn Sie Hilfe bei der Konfiguration der DNS-Einträge brauchen.", - "diagnosis_dns_discrepancy": "Der folgende DNS-Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}", - "diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration sollten Sie einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}", + "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost dyndns update --force ein Update erzwingen.", + "diagnosis_dns_point_to_doc": "Bitte schaue in der Dokumentation unter https://yunohost.org/dns_config nach, wenn du Hilfe bei der Konfiguration der DNS-Einträge benötigst.", + "diagnosis_dns_discrepancy": "Der folgende DNS Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}", + "diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration solltest du einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}", "diagnosis_dns_bad_conf": "Einige DNS-Einträge für die Domäne {domain} fehlen oder sind nicht korrekt (Kategorie {category})", "diagnosis_ip_local": "Lokale IP: {local}", "diagnosis_ip_global": "Globale IP: {global}", - "diagnosis_ip_no_ipv6_tip": "Die Verwendung von IPv6 ist nicht Voraussetzung für das Funktionieren Ihres Servers, trägt aber zur Gesundheit des Internet als Ganzes bei. IPv6 sollte normalerweise automatisch von Ihrem Server oder Ihrem Provider konfiguriert werden, sofern verfügbar. Andernfalls müßen Sie einige Dinge manuell konfigurieren. Weitere Informationen finden Sie hier: https://yunohost.org/#/ipv6. Wenn Sie IPv6 nicht aktivieren können oder Ihnen das zu technisch ist, können Sie diese Warnung gefahrlos ignorieren.", - "diagnosis_services_bad_status_tip": "Sie können versuchen, den Dienst neu zu starten, und wenn das nicht funktioniert, schauen Sie sich die (Dienst-)Logs in der Verwaltung an (In der Kommandozeile können Sie dies mit yunohost service restart {service} und yunohost service log {service} tun).", + "diagnosis_ip_no_ipv6_tip": "Die Verwendung von IPv6 ist nicht Voraussetzung für das Funktionieren deines Servers, trägt aber zur Gesundheit des Internet als Ganzes bei. IPv6 sollte normalerweise automatisch von deinem Server oder deinem Provider konfiguriert werden, sofern verfügbar. Andernfalls musst du einige Dinge manuell konfigurieren. Weitere Informationen findest du hier: https://yunohost.org/#/ipv6. Wenn du IPv6 nicht aktivieren kannst oder dir das zu technisch ist, kannst du diese Warnung gefahrlos ignorieren.", + "diagnosis_services_bad_status_tip": "Du kannst versuchen, den Dienst neu zu starten, und wenn das nicht funktioniert, schaue dir die (Dienst-)Logs in der Verwaltung an (In der Kommandozeile kannst du dies mit yunohost service restart {service} und yunohost service log {service} tun).", "diagnosis_services_bad_status": "Der Dienst {service} ist {status} :(", - "diagnosis_diskusage_verylow": "Der Speicher {mountpoint} (auf Gerät {device}) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von ingesamt {total}). Sie sollten sich ernsthaft überlegen, einigen Seicherplatz frei zu machen!", + "diagnosis_diskusage_verylow": "Der Speicher {mountpoint} (auf Gerät {device}) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von ingesamt {total}). Du solltest ernsthaft in Betracht ziehen, etwas Seicherplatz frei zu machen!", "diagnosis_http_ok": "Die Domäne {domain} ist über HTTP von außerhalb des lokalen Netzwerks erreichbar.", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es Ihnen nicht gestatten, den ausgehenden Port 25 zu öffnen, da diese sich nicht um die Netzneutralität kümmern.
- Einige davon bieten als Alternative an, ein Mailserver-Relay zu verwenden, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine die Privatsphäre berücksichtigende Alternative ist die Verwendung eines VPN *mit einer dedizierten öffentlichen IP* um solche Einschränkungen zu umgehen. Schauen Sie unter https://yunohost.org/#/vpn_advantage nach.
- Sie können auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln", - "diagnosis_http_timeout": "Wartezeit wurde beim Versuch, von außen eine Verbindung zum Server aufzubauen, überschritten. Er scheint nicht erreichbar zu sein.
1. Die häufigste Ursache für dieses Problem ist daß der Port 80 (und 433) nicht richtig zu Ihrem Server weitergeleitet werden.
2. Sie sollten auch sicherstellen, daß der Dienst nginx läuft.
3. In komplexeren Umgebungen: Stellen Sie sicher, daß keine Firewall oder Reverse-Proxy stört.", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es dir nicht gestatten, den ausgehenden Port 25 zu öffnen, da diese sich nicht um die Netzneutralität kümmern.
- Einige davon bieten als Alternative an, ein Mailserver-Relay zu verwenden, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine, die Privatsphäre berücksichtigende, Alternative ist die Verwendung eines VPN *mit einer dedizierten öffentlichen IP* um solche Einschränkungen zu umgehen. Schaue unter https://yunohost.org/#/vpn_advantage nach.
- Du kannst auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln", + "diagnosis_http_timeout": "Wartezeit wurde beim Versuch, von außen eine Verbindung zum Server aufzubauen, überschritten. Er scheint nicht erreichbar zu sein.
1. Die häufigste Ursache für dieses Problem ist daß der Port 80 (und 433) nicht richtig zu deinem Server weitergeleitet werden.
2. Du solltest auch sicherstellen, daß der Dienst nginx läuft.
3. In komplexeren Umgebungen: Stelle sicher, daß keine Firewall oder Reverse-Proxy stört.", "service_reloaded_or_restarted": "Der Dienst '{service}' wurde erfolgreich neu geladen oder gestartet", "service_restarted": "Der Dienst '{service}' wurde neu gestartet", - "service_regen_conf_is_deprecated": "'yunohost service regen-conf' ist veraltet! Bitte verwenden Sie stattdessen 'yunohost tools regen-conf'.", "certmanager_warning_subdomain_dns_record": "Die Subdomäne \"{subdomain}\" löst nicht zur gleichen IP Adresse auf wie \"{domain}\". Einige Funktionen sind nicht verfügbar bis du dies behebst und die Zertifikate neu erzeugst.", "diagnosis_ports_ok": "Port {port} ist von außen erreichbar.", "diagnosis_ram_verylow": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total})", - "diagnosis_mail_outgoing_port_25_blocked_details": "Sie sollten zuerst versuchen den ausgehenden Port 25 auf Ihrer Router-Konfigurationsoberfläche oder Ihrer Hosting-Anbieter-Konfigurationsoberfläche zu öffnen. (Bei einigen Hosting-Anbieter kann es sein, daß Sie verlangen, daß man dafür ein Support-Ticket sendet).", + "diagnosis_mail_outgoing_port_25_blocked_details": "Du solltest zuerst versuchen den ausgehenden Port 25 auf deiner Router-Konfigurationsoberfläche oder deiner Hosting-Anbieter-Konfigurationsoberfläche zu öffnen. (Bei einigen Hosting-Anbietern kann es sein, daß sie verlangen, daß man dafür ein Support-Ticket sendet).", "diagnosis_mail_ehlo_ok": "Der SMTP-Server ist von von außen erreichbar und darum auch in der Lage E-Mails zu empfangen!", "diagnosis_mail_ehlo_bad_answer": "Ein nicht-SMTP-Dienst antwortete auf Port 25 per IPv{ipversion}", - "diagnosis_swap_notsomuch": "Das System hat nur {total} Swap. Sie sollten sich überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.", + "diagnosis_swap_notsomuch": "Das System hat nur {total} Swap. Du solltest dir überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.", "diagnosis_swap_ok": "Das System hat {total} Swap!", - "diagnosis_swap_tip": "Wir sind Ihnen sehr dankbar dafür, daß Sie behutsam und sich bewußt sind, dass das Betreiben einer Swap-Partition auf einer SD-Karte oder einem SSD-Speicher das Risiko einer drastischen Verkürzung der Lebenserwartung dieser Platte nach sich zieht.", + "diagnosis_swap_tip": "Bitte beachte, dass das Betreiben der Swap-Partition auf einer SD-Karte oder SSD die Lebenszeit dieser drastisch reduziert.", "diagnosis_mail_outgoing_port_25_ok": "Der SMTP-Server ist in der Lage E-Mails zu versenden (der ausgehende Port 25 ist nicht blockiert).", - "diagnosis_mail_outgoing_port_25_blocked": "Der SMTP-Server kann keine E-Mails an andere Server senden, weil der ausgehende Port 25 per IPv{ipversion} blockiert ist. Sie können versuchen diesen in der Konfigurations-Oberfläche Ihres Internet-Anbieters (oder Hosters) zu öffnen.", + "diagnosis_mail_outgoing_port_25_blocked": "Der SMTP-Server kann keine E-Mails an andere Server senden, weil der ausgehende Port 25 per IPv{ipversion} blockiert ist. Du kannst versuchen, diesen in der Konfigurations-Oberfläche deines Internet-Anbieters (oder Hosters) zu öffnen.", "diagnosis_mail_ehlo_unreachable": "Der SMTP-Server ist von außen nicht erreichbar per IPv{ipversion}. Er wird nicht in der Lage sein E-Mails zu empfangen.", - "diagnosis_diskusage_low": "Der Speicher {mountpoint} (auf Gerät {device}) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von insgesamt {total}). Seien Sie vorsichtig.", - "diagnosis_ram_low": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total}). Seien Sie vorsichtig.", + "diagnosis_diskusage_low": "Der Speicher {mountpoint} (auf Gerät {device}) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von insgesamt {total}). Sei vorsichtig.", + "diagnosis_ram_low": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total}). Sei vorsichtig.", "service_reload_or_restart_failed": "Der Dienst '{service}' konnte nicht erneut geladen oder gestartet werden.\n\nKürzlich erstellte Logs des Dienstes: {logs}", - "diagnosis_domain_expiration_not_found_details": "Die WHOIS-Informationen für die Domäne {domain} scheint keine Informationen über das Ablaufdatum zu enthalten?", + "diagnosis_domain_expiration_not_found_details": "Die WHOIS-Informationen für die Domäne {domain} scheinen keine Informationen über das Ablaufdatum zu enthalten. Stimmt das?", "diagnosis_domain_expiration_warning": "Einige Domänen werden bald ablaufen!", "diagnosis_diskusage_ok": "Der Speicher {mountpoint} (auf Gerät {device}) hat immer noch {free} ({free_percent}%) freien Speicherplatz übrig(von insgesamt {total})!", "diagnosis_ram_ok": "Das System hat immer noch {available} ({available_percent}%) RAM zu Verfügung von {total}.", - "diagnosis_swap_none": "Das System hat gar keinen Swap. Sie sollten sich überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.", - "diagnosis_mail_ehlo_unreachable_details": "Konnte keine Verbindung zu Ihrem Server auf dem Port 25 herzustellen per IPv{ipversion}. Er scheint nicht erreichbar zu sein.
1. Das häufigste Problem ist, dass der Port 25 nicht richtig zu Ihrem Server weitergeleitet ist.
2. Sie sollten auch sicherstellen, dass der Postfix-Dienst läuft.
3. In komplexeren Umgebungen: Stellen Sie sicher, daß keine Firewall oder Reverse-Proxy stört.", - "diagnosis_mail_ehlo_wrong": "Ein anderer SMTP-Server antwortet auf IPv{ipversion}. Ihr Server wird wahrscheinlich nicht in der Lage sein, E-Mails zu empfangen.", - "migration_description_0018_xtable_to_nftable": "Alte Netzwerkverkehrsregeln zum neuen nftable-System migrieren", + "diagnosis_swap_none": "Das System hat gar keinen Swap. Du solltest überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.", + "diagnosis_mail_ehlo_unreachable_details": "Konnte keine Verbindung zu deinem Server auf dem Port 25 herzustellen über IPv{ipversion}. Er scheint nicht erreichbar zu sein.
1. Das häufigste Problem ist, dass der Port 25 nicht richtig zu deinem Server weitergeleitet ist.
2. Du solltest auch sicherstellen, dass der Postfix-Dienst läuft.
3. In komplexeren Umgebungen: Stelle sicher, daß keine Firewall oder Reverse-Proxy stört.", + "diagnosis_mail_ehlo_wrong": "Ein anderer SMTP-Server antwortet auf IPv{ipversion}. Dein Server wird wahrscheinlich nicht in der Lage sein, E-Mails zu empfangen.", "service_reload_failed": "Der Dienst '{service}' konnte nicht erneut geladen werden.\n\nKürzlich erstellte Logs des Dienstes: {logs}", "service_reloaded": "Der Dienst '{service}' wurde erneut geladen", "service_restart_failed": "Der Dienst '{service}' konnte nicht erneut gestartet werden.\n\nKürzlich erstellte Logs des Dienstes: {logs}", - "app_manifest_install_ask_password": "Wählen Sie ein Verwaltungspasswort für diese Applikation", - "app_manifest_install_ask_domain": "Wählen Sie die Domäne, auf welcher die Applikation installiert werden soll", + "app_manifest_install_ask_password": "Wähle ein Verwaltungspasswort für diese Applikation", + "app_manifest_install_ask_domain": "Wähle die Domäne, auf welcher die Applikation installiert werden soll", "log_letsencrypt_cert_renew": "Erneuern des Let's Encrypt-Zeritifikates von '{}'", "log_selfsigned_cert_install": "Das selbstsignierte Zertifikat auf der Domäne '{}' installieren", "log_letsencrypt_cert_install": "Das Let’s Encrypt auf der Domäne '{}' installieren", - "diagnosis_mail_fcrdns_nok_details": "Sie sollten zuerst versuchen, in Ihrer Internet-Router-Oberfläche oder in Ihrer Hosting-Anbieter-Oberfläche den Reverse-DNS-Eintrag mit {ehlo_domain}zu konfigurieren. (Gewisse Hosting-Anbieter können dafür möglicherweise verlangen, dass Sie dafür ein Support-Ticket erstellen).", + "diagnosis_mail_fcrdns_nok_details": "Du solltest zuerst versuchen, in deiner Internet-Router-Oberfläche oder in deiner Hosting-Anbieter-Oberfläche den Reverse-DNS-Eintrag mit {ehlo_domain}zu konfigurieren. (Gewisse Hosting-Anbieter können dafür möglicherweise verlangen, dass du dafür ein Support-Ticket erstellst).", "diagnosis_mail_fcrdns_dns_missing": "Es wurde kein Reverse-DNS-Eintrag definiert für IPv{ipversion}. Einige E-Mails könnten möglicherweise zurückgewiesen oder als Spam markiert werden.", - "diagnosis_mail_fcrdns_ok": "Ihr Reverse-DNS-Eintrag ist korrekt konfiguriert!", + "diagnosis_mail_fcrdns_ok": "Dein Reverse-DNS-Eintrag ist korrekt konfiguriert!", "diagnosis_mail_ehlo_could_not_diagnose_details": "Fehler: {error}", "diagnosis_mail_ehlo_could_not_diagnose": "Konnte nicht überprüfen, ob der Postfix-Mail-Server von aussen per IPv{ipversion} erreichbar ist.", - "diagnosis_mail_ehlo_wrong_details": "Die vom Remote-Diagnose-Server per IPv{ipversion} empfangene EHLO weicht von der Domäne Ihres Servers ab.
Empfangene EHLO: {wrong_ehlo}
Erwartet: {right_ehlo}
Die geläufigste Ursache für dieses Problem ist, dass der Port 25 nicht korrekt auf Ihren Server weitergeleitet wird. Sie können sich zusätzlich auch versichern, dass keine Firewall oder Reverse-Proxy interferiert.", - "diagnosis_mail_ehlo_bad_answer_details": "Das könnte daran liegen, dass anstelle Ihres Servers eine andere Maschine antwortet.", - "ask_user_domain": "Domäne, welche für die E-Mail-Adresse und den XMPP-Account des Benutzers verwendet werden soll", - "app_manifest_install_ask_is_public": "Soll diese Applikation für anonyme Benutzer:innen sichtbar sein?", - "app_manifest_install_ask_admin": "Wählen Sie einen Administrator für diese Applikation", - "app_manifest_install_ask_path": "Wählen Sie den Pfad, in welchem die Applikation installiert werden soll", - "diagnosis_mail_blacklist_listed_by": "Ihre IP-Adresse oder Domäne {item} ist auf der Blacklist auf {blacklist_name}", + "diagnosis_mail_ehlo_wrong_details": "Die vom Remote-Diagnose-Server per IPv{ipversion} empfangene EHLO weicht von der Domäne deines Servers ab.
Empfangene EHLO: {wrong_ehlo}
Erwartet: {right_ehlo}
Die geläufigste Ursache für dieses Problem ist, dass der Port 25 nicht korrekt auf deinem Server weitergeleitet wird. Du kannst zusätzlich auch prüfen, dass keine Firewall oder Reverse-Proxy stört.", + "diagnosis_mail_ehlo_bad_answer_details": "Das könnte daran liegen, dass anstelle deines Servers eine andere Maschine antwortet.", + "ask_user_domain": "Domäne, welche für die E-Mail-Adresse und den XMPP-Account des Kontos verwendet werden soll", + "app_manifest_install_ask_is_public": "Soll diese Applikation für Gäste sichtbar sein?", + "app_manifest_install_ask_admin": "Wähle einen Administrator für diese Applikation", + "app_manifest_install_ask_path": "Wähle den URL-Pfad (nach der Domäne), unter dem die Applikation installiert werden soll", + "diagnosis_mail_blacklist_listed_by": "Deine IP-Adresse oder Domäne {item} ist auf der Blacklist auf {blacklist_name}", "diagnosis_mail_blacklist_ok": "Die IP-Adressen und die Domänen, welche von diesem Server verwendet werden, scheinen nicht auf einer Blacklist zu sein", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Aktueller Reverse-DNS-Eintrag: {rdns_domain}
Erwarteter Wert: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Der Reverse-DNS-Eintrag für IPv{ipversion} ist nicht korrekt konfiguriert. Einige E-Mails könnten abgewiesen oder als Spam markiert werden.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Einige Provider werden es Ihnen nicht erlauben, Ihren Reverse-DNS-Eintrag zu konfigurieren (oder ihre Funktionalität könnte defekt sein ...). Falls Ihr Reverse-DNS-Eintrag für IPv4 korrekt konfiguiert ist, können Sie versuchen, die Verwendung von IPv6 für das Versenden von E-Mails auszuschalten, indem Sie den Befehl yunohost settings set smtp.allow_ipv6 -v off ausführen. Bemerkung: Die Folge dieser letzten Lösung ist, dass Sie mit Servern, welche ausschliesslich über IPv6 verfügen, keine E-Mails mehr versenden oder empfangen können.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Einige Provider werden es dir nicht erlauben, deinen Reverse-DNS-Eintrag zu konfigurieren (oder ihre Funktionalität könnte defekt sein ...). Falls du deinen Reverse-DNS-Eintrag für IPv4 korrekt konfiguiert ist, kannst du versuchen, die Verwendung von IPv6 für das Versenden von E-Mails auszuschalten, indem du den Befehl yunohost settings set smtp.allow_ipv6 -v off ausführst. Bemerkung: Die Folge dieser letzten Lösung ist, dass du mit Servern, welche ausschliesslich über IPv6 verfügen, keine E-Mails mehr versenden oder empfangen kannst.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Einige Anbieter werden es dir nicht erlauben, deinen Reverse-DNS zu konfigurieren (oder deren Funktionalität ist defekt...). Falls du deswegen auf Probleme stoßen solltest, ziehe folgende Lösungen in Betracht:
- Manche ISPs stellen als Alternative die Benutzung eines Mail-Server-Relays zur Verfügung, was jedoch mit sich zieht, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine privatsphärenfreundlichere Alternative ist die Benutzung eines VPN *mit einer dedizierten öffentlichen IP* um Einschränkungen dieser Art zu umgehen. Schaue hier nach https://yunohost.org/#/vpn_advantage
- Schließlich ist es auch möglich zu einem anderen Anbieter zu wechseln", "diagnosis_mail_queue_unavailable_details": "Fehler: {error}", "diagnosis_mail_queue_unavailable": "Die Anzahl der anstehenden Nachrichten in der Warteschlange kann nicht abgefragt werden", @@ -401,7 +389,7 @@ "diagnosis_ports_partially_unreachable": "Port {port} ist von aussen per IPv{failed} nicht erreichbar.", "diagnosis_ports_unreachable": "Port {port} ist von aussen nicht erreichbar.", "diagnosis_ports_could_not_diagnose_details": "Fehler: {error}", - "diagnosis_security_vulnerable_to_meltdown_details": "Um dieses Problem zu beheben, sollten Sie Ihr System upgraden und neustarten um den neuen Linux-Kernel zu laden (oder Ihren Server-Anbieter kontaktieren, falls das nicht funktionieren sollte). Besuchen Sie https://meltdownattack.com/ für weitere Informationen.", + "diagnosis_security_vulnerable_to_meltdown_details": "Um dieses Problem zu beheben, solltest du dein System upgraden und neustarten um den neuen Linux-Kernel zu laden (oder deinen Server-Anbieter kontaktieren, falls das nicht funktionieren sollte). Besuche https://meltdownattack.com/ für weitere Informationen.", "diagnosis_ports_could_not_diagnose": "Konnte nicht diagnostizieren, ob die Ports von aussen per IPv{ipversion} erreichbar sind.", "diagnosis_description_regenconf": "Systemkonfiguration", "diagnosis_description_mail": "E-Mail", @@ -411,39 +399,38 @@ "diagnosis_description_dnsrecords": "DNS-Einträge", "diagnosis_description_ip": "Internetkonnektivität", "diagnosis_description_basesystem": "Grundsystem", - "diagnosis_security_vulnerable_to_meltdown": "Es scheint, als ob Sie durch die kritische Meltdown-Sicherheitslücke verwundbar sind", + "diagnosis_security_vulnerable_to_meltdown": "Es scheint, als ob du durch die kritische Meltdown-Sicherheitslücke verwundbar bist", "diagnosis_regenconf_manually_modified": "Die Konfigurationsdatei {file} scheint manuell verändert worden zu sein.", "diagnosis_regenconf_allgood": "Alle Konfigurationsdateien stimmen mit der empfohlenen Konfiguration überein!", "diagnosis_package_installed_from_sury": "Einige System-Pakete sollten gedowngradet werden", - "diagnosis_ports_forwarding_tip": "Um dieses Problem zu beheben, müssen Sie höchst wahrscheinlich die Port-Weiterleitung auf Ihrem Internet-Router einrichten wie in https://yunohost.org/isp_box_config beschrieben", - "diagnosis_regenconf_manually_modified_details": "Das ist wahrscheinlich OK wenn Sie wissen, was Sie tun! YunoHost wird in Zukunft diese Datei nicht mehr automatisch updaten... Aber seien Sie bitte vorsichtig, da die zukünftigen Upgrades von YunoHost wichtige empfohlene Änderungen enthalten könnten. Falls Sie möchten, können Sie die Unterschiede mit yunohost tools regen-conf {category} --dry-run --with-diff inspizieren und mit yunohost tools regen-conf {category} --force auf das Zurücksetzen die empfohlene Konfiguration erzwingen", - "diagnosis_mail_blacklist_website": "Nachdem Sie herausgefunden haben, weshalb Sie auf die Blacklist gesetzt wurden und dies behoben haben, zögern Sie nicht, nachzufragen, ob Ihre IP-Adresse oder Ihre Domäne von auf {blacklist_website} entfernt wird", + "diagnosis_ports_forwarding_tip": "Um dieses Problem zu beheben, musst du höchstwahrscheinlich die Port-Weiterleitung auf deinem Internet-Router einrichten wie in https://yunohost.org/isp_box_config beschrieben", + "diagnosis_regenconf_manually_modified_details": "Das ist wahrscheinlich OK wenn du weißt, was du tust! YunoHost wird in Zukunft diese Datei nicht mehr automatisch updaten... Aber sei bitte vorsichtig, da die zukünftigen Upgrades von YunoHost wichtige empfohlene Änderungen enthalten könnten. Wenn du möchtest, kannst du die Unterschiede mit yunohost tools regen-conf {category} --dry-run --with-diff inspizieren und mit yunohost tools regen-conf {category} --force auf das Zurücksetzen die empfohlene Konfiguration erzwingen", + "diagnosis_mail_blacklist_website": "Nachdem du herausgefunden hast, weshalb du auf die Blacklist gesetzt wurdest und dies behoben hast, zögere nicht, nachzufragen, ob deine IP-Adresse oder Ihre Domäne von auf {blacklist_website} entfernt wird", "diagnosis_unknown_categories": "Folgende Kategorien sind unbekannt: {categories}", - "diagnosis_http_hairpinning_issue": "In Ihrem lokalen Netzwerk scheint Hairpinning nicht aktiviert zu sein.", + "diagnosis_http_hairpinning_issue": "In deinem lokalen Netzwerk scheint Hairpinning nicht aktiviert zu sein.", "diagnosis_ports_needed_by": "Diesen Port zu öffnen ist nötig, um die Funktionalität des Typs {category} (service {service}) zu gewährleisten", "diagnosis_mail_queue_too_big": "Zu viele anstehende Nachrichten in der Warteschlange ({nb_pending} emails)", - "diagnosis_package_installed_from_sury_details": "Einige Pakete wurden unbeabsichtigterweise aus einem Drittanbieter-Repository, genannt Sury, installiert. Das YunoHost-Team hat die Strategie, um diese Pakete zu handhaben, verbessert, aber es wird erwartet, dass einige Setups, welche PHP7.3-Applikationen installiert haben und immer noch auf Strech laufen, ein paar Inkonsistenzen aufweisen. Um diese Situation zu beheben, sollten Sie versuchen, den folgenden Befehl auszuführen: {cmd_to_fix}", + "diagnosis_package_installed_from_sury_details": "Einige Pakete wurden unbeabsichtigterweise aus einem Drittanbieter-Repository, genannt Sury, installiert. Das YunoHost-Team hat die Strategie, um diese Pakete zu handhaben, verbessert, aber es wird erwartet, dass einige Setups, welche PHP7.3-Applikationen installiert haben und immer noch auf Strech laufen, ein paar Inkonsistenzen aufweisen. Um diese Situation zu beheben, solltest du versuchen, den folgenden Befehl auszuführen: {cmd_to_fix}", "domain_cannot_add_xmpp_upload": "Eine hinzugefügte Domain darf nicht mit 'xmpp-upload.' beginnen. Dieser Name ist für das XMPP-Upload-Feature von YunoHost reserviert.", "group_cannot_be_deleted": "Die Gruppe {group} kann nicht manuell entfernt werden.", - "group_cannot_edit_primary_group": "Die Gruppe '{group}' kann nicht manuell bearbeitet werden. Es ist die primäre Gruppe, welche dazu gedacht ist, nur einen spezifischen Benutzer zu enthalten.", + "group_cannot_edit_primary_group": "Die Gruppe '{group}' kann nicht manuell bearbeitet werden. Es ist die primäre Gruppe, welche dazu gedacht ist, nur ein spezifisches Konto zu enthalten.", "diagnosis_processes_killed_by_oom_reaper": "Das System hat einige Prozesse beendet, weil ihm der Arbeitsspeicher ausgegangen ist. Das passiert normalerweise, wenn das System ingesamt nicht genügend Arbeitsspeicher zur Verfügung hat oder wenn ein einzelner Prozess zu viel Speicher verbraucht. Zusammenfassung der beendeten Prozesse: \n{kills_summary}", - "diagnosis_description_ports": "Offene Ports", + "diagnosis_description_ports": "Geöffnete Ports", "additional_urls_already_added": "Zusätzliche URL '{url}' bereits hinzugefügt in der zusätzlichen URL für Berechtigung '{permission}'", "additional_urls_already_removed": "Zusätzliche URL '{url}' bereits entfernt in der zusätzlichen URL für Berechtigung '{permission}'", - "app_label_deprecated": "Dieser Befehl ist veraltet! Bitte nutzen Sie den neuen Befehl 'yunohost user permission update' um das Applabel zu verwalten.", - "diagnosis_http_hairpinning_issue_details": "Das ist wahrscheinlich aufgrund Ihrer ISP Box / Router. Als Konsequenz können Personen von ausserhalb Ihres Netzwerkes aber nicht von innerhalb Ihres lokalen Netzwerkes (wie wahrscheinlich Sie selber?) wie gewohnt auf Ihren Server zugreifen, wenn Sie ihre Domäne oder Ihre öffentliche IP verwenden. Sie können die Situation wahrscheinlich verbessern, indem Sie ein einen Blick in https://yunohost.org/dns_local_network werfen", - "diagnosis_http_nginx_conf_not_up_to_date": "Jemand hat anscheinend die Konfiguration von Nginx manuell geändert. Diese Änderung verhindert, dass Yunohost eine Diagnose durchführen kann, wenn er via HTTP erreichbar ist.", - "diagnosis_http_bad_status_code": "Anscheinend beantwortet ein anderes Gerät als Ihr Server die Anfrage (Vielleicht ihr Internetrouter).
1. Die häufigste Ursache ist, dass Port 80 (und 443) nicht richtig auf Ihren Server weitergeleitet wird.
2. Bei komplexeren Setups: Vergewissern Sie sich, dass keine Firewall und keine Reverse-Proxy interferieren.", - "diagnosis_never_ran_yet": "Sie haben kürzlich einen neuen Yunohost-Server installiert aber es gibt davon noch keinen Diagnosereport. Sie sollten eine Diagnose anstossen. Sie können das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwenden Sie dafür den Befehl 'yunohost diagnosis run'.", - "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein. Dieses Tool zeigt ihnen den Unterschied an. Wenn Sie damit einverstanden sind, können Sie mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", - "diagnosis_backports_in_sources_list": "Sie haben anscheinend apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie wissen was Sie tun.", + "app_label_deprecated": "Dieser Befehl ist veraltet! Bitte nutze den neuen Befehl 'yunohost user permission update' um das Applabel zu verwalten.", + "diagnosis_http_hairpinning_issue_details": "Das liegt wahrscheinlich an deinem Router. Dadurch können Personen von ausserhalb deines Netzwerkes, aber nicht von innerhalb deines lokalen Netzwerkes (wie wahrscheinlich du selbst), auf deinen Server zugreifen, wenn dazu die Domäne oder öffentliche IP verwendet wird. Du kannst das Problem eventuell beheben, indem du ein einen Blick auf https://yunohost.org/dns_local_network wirfst", + "diagnosis_http_nginx_conf_not_up_to_date": "Die Konfiguration von Nginx scheint für diese Domäne manuell geändert worden zu sein. Dies hindert YunoHost daran festzustellen, ob es über HTTP erreichbar ist.", + "diagnosis_http_bad_status_code": "Es sieht so aus als ob ein anderes Gerät (vielleicht dein Router/Modem) anstelle deines Servers antwortet.
1. Der häufigste Grund hierfür ist, dass Port 80 (und 443) nicht korrekt zu deinem Server weiterleiten.
2. Bei komplexeren Setups: prüfe ob deine Firewall oder Reverse-Proxy die Verbindung stören.", + "diagnosis_never_ran_yet": "Du hast kürzlich einen neuen YunoHost-Server installiert aber es gibt davon noch keinen Diagnosereport. Du solltest eine Diagnose anstossen. Du kannst das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwendest du dafür den Befehl 'yunohost diagnosis run'.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, gebe in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein. Dieses Tool zeigt dir den Unterschied an. Wenn du damit einverstanden bist, kannst du mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", + "diagnosis_backports_in_sources_list": "Du hast anscheinend apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, du weißt, was du tust.", "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}", - "domain_name_unknown": "Domäne '{domain}' unbekannt", - "group_user_not_in_group": "Der Benutzer {user} ist nicht in der Gruppe {group}", - "group_user_already_in_group": "Der Benutzer {user} ist bereits in der Gruppe {group}", + "group_user_not_in_group": "Konto {user} ist nicht in der Gruppe {group}", + "group_user_already_in_group": "Konto {user} ist bereits in der Gruppe {group}", "group_cannot_edit_visitors": "Die Gruppe \"Besucher\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe und repräsentiert anonyme Besucher", - "group_cannot_edit_all_users": "Die Gruppe \"all_users\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe die dafür gedacht ist alle Benutzer in Yunohost zu halten", - "group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber Yunohost wird sie entfernen...", + "group_cannot_edit_all_users": "Die Gruppe \"all_users\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe die dafür gedacht ist alle Konten in YunoHost zu halten", + "group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber YunoHost wird sie entfernen...", "group_already_exist_on_system": "Die Gruppe {group} existiert bereits in den Systemgruppen", "group_already_exist": "Die Gruppe {group} existiert bereits", "global_settings_setting_smtp_relay_password": "SMTP Relay Host Passwort", @@ -451,21 +438,18 @@ "global_settings_setting_smtp_relay_port": "SMTP Relay Port", "global_settings_setting_smtp_allow_ipv6": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden", "global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver", - "domain_cannot_remove_main_add_new_one": "Sie können '{domain}' nicht entfernen, weil es die Hauptdomäne und gleichzeitig Ihre einzige Domäne ist. Zuerst müssen Sie eine andere Domäne hinzufügen, indem Sie \"yunohost domain add another-domain.com>\" eingeben. Bestimmen Sie diese dann als Ihre Hauptdomain indem Sie 'yunohost domain main-domain -n ' eingeben. Nun können Sie die Domäne \"{domain}\" enfernen, indem Sie 'yunohost domain remove {domain}' eingeben.'", + "domain_cannot_remove_main_add_new_one": "Du kannst '{domain}' nicht entfernen, weil es die Haupt-Domäne und gleichzeitig deine einzige Domäne ist. Zuerst musst du eine andere Domäne hinzufügen, indem du 'yunohost domain add another-domain.com>' eingibst. Mache diese dann zu deiner Haupt-Domäne indem du 'yunohost domain main-domain -n ' eingibst. Nun kannst du die Domäne '{domain}' enfernen, indem du 'yunohost domain remove {domain}' eingibst.'", "diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.", "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen.", - "global_settings_setting_smtp_relay_host": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn Sie in einer der folgenden Situationen sind: Ihr ISP- oder VPS-Provider hat Ihren Port 25 geblockt, eine Ihrer residentiellen IPs ist auf DUHL gelistet, Sie können keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und Sie möchten einen anderen verwenden, um E-Mails zu versenden.", + "global_settings_setting_smtp_relay_host": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn du in einer der folgenden Situationen bist: Dein ISP- oder VPS-Provider hat deinen Port 25 geblockt, eine deinen residentiellen IPs ist auf DUHL gelistet, du kannst keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und du möchtest einen anderen verwenden, um E-Mails zu versenden.", "global_settings_setting_backup_compress_tar_archives": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen.", "log_remove_on_failed_restore": "'{}' entfernen nach einer fehlerhaften Wiederherstellung aus einem Backup-Archiv", "log_backup_restore_app": "'{}' aus einem Backup-Archiv wiederherstellen", "log_backup_restore_system": "System aus einem Backup-Archiv wiederherstellen", "log_available_on_yunopaste": "Das Protokoll ist nun via {url} verfügbar", - "log_app_config_apply": "Wende die Konfiguration auf die Applikation '{}' an", - "log_app_config_show_panel": "Zeige das Konfigurations-Panel der Applikation '{}'", "log_app_action_run": "Führe Aktion der Applikation '{}' aus", "invalid_regex": "Ungültige Regex:'{regex}'", - "migration_description_0016_php70_to_php73_pools": "Migrieren der php7.0-fpm-Konfigurationsdateien zu php7.3", - "mailbox_disabled": "E-Mail für Benutzer {user} deaktiviert", + "mailbox_disabled": "E-Mail für Konto {user} deaktiviert", "log_tools_reboot": "Server neustarten", "log_tools_shutdown": "Server ausschalten", "log_tools_upgrade": "Systempakete aktualisieren", @@ -474,12 +458,12 @@ "log_domain_main_domain": "Mache '{}' zur Hauptdomäne", "log_user_permission_reset": "Zurücksetzen der Berechtigung '{}'", "log_user_permission_update": "Aktualisiere Zugriffe für Berechtigung '{}'", - "log_user_update": "Aktualisiere Information für Benutzer '{}'", + "log_user_update": "Aktualisiere Information für Konto '{}'", "log_user_group_update": "Aktualisiere Gruppe '{}'", "log_user_group_delete": "Lösche Gruppe '{}'", "log_user_group_create": "Erstelle Gruppe '{}'", - "log_user_delete": "Lösche Benutzer '{}'", - "log_user_create": "Füge Benutzer '{}' hinzu", + "log_user_delete": "Lösche Konto '{}'", + "log_user_create": "Füge Konto '{}' hinzu", "log_permission_url": "Aktualisiere URL, die mit der Berechtigung '{}' verknüpft ist", "log_permission_delete": "Lösche Berechtigung '{}'", "log_permission_create": "Erstelle Berechtigung '{}'", @@ -488,42 +472,19 @@ "log_domain_remove": "Entfernen der Domäne '{}' aus der Systemkonfiguration", "log_domain_add": "Hinzufügen der Domäne '{}' zur Systemkonfiguration", "log_remove_on_failed_install": "Entfernen von '{}' nach einer fehlgeschlagenen Installation", - "migration_0015_still_on_stretch_after_main_upgrade": "Etwas ist schiefgelaufen während dem Haupt-Upgrade. Das System scheint immer noch auf Debian Stretch zu laufen", - "migration_0015_yunohost_upgrade": "Beginne YunoHost-Core-Upgrade...", - "migration_description_0019_extend_permissions_features": "Erweitern und überarbeiten des Applikationsberechtigungs-Managementsystems", - "migrating_legacy_permission_settings": "Migrieren der Legacy-Berechtigungseinstellungen...", - "migration_description_0017_postgresql_9p6_to_11": "Migrieren der Datenbanken von PostgreSQL 9.6 nach 11", - "migration_0015_main_upgrade": "Beginne Haupt-Upgrade...", - "migration_0015_not_stretch": "Die aktuelle Debian-Distribution ist nicht Stretch!", - "migration_0015_not_enough_free_space": "Der freie Speicher in /var/ ist sehr gering! Sie sollten minimal 1GB frei haben, um diese Migration durchzuführen.", - "domain_remove_confirm_apps_removal": "Wenn Sie diese Domäne löschen, werden folgende Applikationen entfernt:\n{apps}\n\nSind Sie sicher? [{answers}]", - "migration_0015_cleaning_up": "Bereinigung des Cache und der Pakete, welche nicht mehr benötigt werden...", - "migration_0017_postgresql_96_not_installed": "PostgreSQL wurde auf ihrem System nicht installiert. Nichts zu tun.", - "migration_0015_system_not_fully_up_to_date": "Ihr System ist nicht vollständig auf dem neuesten Stand. Bitte führen Sie ein reguläres Upgrade durch, bevor Sie die Migration auf Buster durchführen.", - "migration_0015_modified_files": "Bitte beachten Sie, dass die folgenden Dateien als manuell bearbeitet erkannt wurden und beim nächsten Upgrade überschrieben werden könnten: {manually_modified_files}", - "migration_0015_general_warning": "Bitte beachten Sie, dass diese Migration eine heikle Angelegenheit darstellt. Das YunoHost-Team hat alles unternommen, um sie zu testen und zu überarbeiten. Dennoch ist es möglich, dass diese Migration Teile des Systems oder Applikationen beschädigen könnte.\n\nDeshalb ist folgendes zu empfehlen:\n…- Führen Sie ein Backup aller kritischen Daten und Applikationen durch. Mehr unter https://yunohost.org/backup;\n…- Seien Sie geduldig nachdem Sie die Migration gestartet haben: Abhängig von Ihrer Internetverbindung und Ihrer Hardware kann es einige Stunden dauern, bis das Upgrade fertig ist.", - "migration_0015_problematic_apps_warning": "Bitte beachten Sie, dass folgende möglicherweise problematischen Applikationen auf Ihrer Installation erkannt wurden. Es scheint, als ob sie nicht aus dem YunoHost-Applikationskatalog installiert oder nicht als 'working' gekennzeichnet worden sind. Folglich kann nicht garantiert werden, dass sie nach dem Upgrade immer noch funktionieren: {problematic_apps}", - "migration_0015_specific_upgrade": "Start des Upgrades der Systempakete, deren Upgrade separat durchgeführt werden muss...", - "migration_0015_weak_certs": "Die folgenden Zertifikate verwenden immer noch schwache Signierungsalgorithmen und müssen aktualisiert werden um mit der nächsten Version von nginx kompatibel zu sein: {certs}", + "domain_remove_confirm_apps_removal": "Wenn du diese Domäne löschst, werden folgende Applikationen entfernt:\n{apps}\n\nBist du sicher? [{answers}]", "migrations_pending_cant_rerun": "Diese Migrationen sind immer noch anstehend und können deshalb nicht erneut durchgeführt werden: {ids}", - "migration_0019_add_new_attributes_in_ldap": "Hinzufügen neuer Attribute für die Berechtigungen in der LDAP-Datenbank", "migrations_not_pending_cant_skip": "Diese Migrationen sind nicht anstehend und können deshalb nicht übersprungen werden: {ids}", - "migration_0018_failed_to_reset_legacy_rules": "Zurücksetzen der veralteten iptables-Regeln fehlgeschlagen: {error}", - "migration_0019_slapd_config_will_be_overwritten": "Es schaut aus, als ob Sie die slapd-Konfigurationsdatei manuell bearbeitet haben. Für diese kritische Migration muss das Update der slapd-Konfiguration erzwungen werden. Von der Originaldatei wird ein Backup gemacht in {conf_backup_folder}.", "migrations_success_forward": "Migration {id} abgeschlossen", - "migrations_cant_reach_migration_file": "Die Migrationsdateien konnten nicht aufgerufen werden im Verzeichnis '%s'", - "migrations_dependencies_not_satisfied": "Führen Sie diese Migrationen aus: '{dependencies_id}', vor der Migration {id}.", + "migrations_dependencies_not_satisfied": "Führe diese Migrationen aus: '{dependencies_id}', bevor du {id} migrierst.", "migrations_failed_to_load_migration": "Konnte Migration nicht laden {id}: {error}", - "migrations_list_conflict_pending_done": "Sie können nicht '--previous' und '--done' gleichzeitig benützen.", + "migrations_list_conflict_pending_done": "Du kannst '--previous' und '--done' nicht gleichzeitig benützen.", "migrations_already_ran": "Diese Migrationen wurden bereits durchgeführt: {ids}", "migrations_loading_migration": "Lade Migrationen {id}...", "migrations_migration_has_failed": "Migration {id} gescheitert mit der Ausnahme {exception}: Abbruch", - "migrations_must_provide_explicit_targets": "Sie müssen konkrete Ziele angeben, wenn Sie '--skip' oder '--force-rerun' verwenden", - "migrations_need_to_accept_disclaimer": "Um die Migration {id} durchzuführen, müssen Sie den Disclaimer akzeptieren.\n---\n{disclaimer}\n---\n Wenn Sie bestätigen, dass Sie die Migration durchführen wollen, wiederholen Sie bitte den Befehl mit der Option '--accept-disclaimer'.", + "migrations_must_provide_explicit_targets": "Du musst konkrete Ziele angeben, wenn du '--skip' oder '--force-rerun' verwendest", + "migrations_need_to_accept_disclaimer": "Um die Migration {id} durchzuführen, musst du folgenden Hinweis akzeptieren:\n---\n{disclaimer}\n---\nWenn du nach dem Lesen die Migration durchführen möchtest, wiederhole bitte den Befehl mit der Option '--accept-disclaimer'.", "migrations_no_migrations_to_run": "Keine Migrationen durchzuführen", - "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 ist installiert aber nicht postgreSQL 11? Etwas komisches ist Ihrem System zugestossen :(...", - "migration_0017_not_enough_space": "Stellen Siea ausreichend Speicherplatz im Verzeichnis {path} zur Verfügung um die Migration durchzuführen.", - "migration_0018_failed_to_migrate_iptables_rules": "Migration der veralteten iptables-Regeln zu nftables fehlgeschlagen: {error}", "migrations_exclusive_options": "'--auto', '--skip' und '--force-rerun' sind Optionen, die sich gegenseitig ausschliessen.", "migrations_no_such_migration": "Es existiert keine Migration genannt '{id}'", "migrations_running_forward": "Durchführen der Migrationen {id}...", @@ -532,12 +493,12 @@ "password_listed": "Dieses Passwort zählt zu den meistgenutzten Passwörtern der Welt. Bitte wähle ein anderes, einzigartigeres Passwort.", "operation_interrupted": "Wurde die Operation manuell unterbrochen?", "invalid_number": "Muss eine Zahl sein", - "migrations_to_be_ran_manually": "Die Migration {id} muss manuell durchgeführt werden. Bitte gehen Sie zu Werkzeuge → Migrationen auf der Webadmin-Seite oder führen Sie 'yunohost tools migrations run' aus.", + "migrations_to_be_ran_manually": "Die Migration {id} muss manuell durchgeführt werden. Bitte gehe zu Werkzeuge → Migrationen auf der Webadmin-Seite oder führe 'yunohost tools migrations run' aus.", "permission_already_up_to_date": "Die Berechtigung wurde nicht aktualisiert, weil die Anfragen für Hinzufügen/Entfernen bereits mit dem aktuellen Status übereinstimmen.", "permission_already_exist": "Berechtigung '{permission}' existiert bereits", "permission_already_disallowed": "Für die Gruppe '{group}' wurde die Berechtigung '{permission}' deaktiviert", "permission_already_allowed": "Die Gruppe '{group}' hat die Berechtigung '{permission}' bereits erhalten", - "pattern_password_app": "Entschuldigen Sie bitte! Passwörter dürfen folgende Zeichen nicht enthalten: {forbidden_chars}", + "pattern_password_app": "Entschuldige Bitte! Passwörter dürfen folgende Zeichen nicht enthalten: {forbidden_chars}", "pattern_email_forward": "Es muss sich um eine gültige E-Mail-Adresse handeln. Das Symbol '+' wird akzeptiert (zum Beispiel : maxmuster@beispiel.com oder maxmuster+yunohost@beispiel.com)", "password_too_simple_4": "Das Passwort muss mindestens 12 Zeichen lang sein und Grossbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten", "password_too_simple_3": "Das Passwort muss mindestens 8 Zeichen lang sein und Grossbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten", @@ -546,38 +507,37 @@ "regenconf_file_kept_back": "Die Konfigurationsdatei '{conf}' sollte von \"regen-conf\" (Kategorie {category}) gelöscht werden, wurde aber beibehalten.", "regenconf_file_copy_failed": "Die neue Konfigurationsdatei '{new}' kann nicht nach '{conf}' kopiert werden", "regenconf_file_backed_up": "Die Konfigurationsdatei '{conf}' wurde unter '{backup}' gespeichert", - "permission_require_account": "Berechtigung {permission} ist nur für Benutzer mit einem Konto sinnvoll und kann daher nicht für Besucher aktiviert werden.", - "permission_protected": "Die Berechtigung ist geschützt. Sie können die Besuchergruppe nicht zu dieser Berechtigung hinzufügen oder daraus entfernen.", + "permission_require_account": "Berechtigung {permission} ist nur für Personen mit Konto sinnvoll und kann daher nicht für Gäste aktiviert werden.", + "permission_protected": "Die Berechtigung ist geschützt. Du kannst die Besuchergruppe nicht zu dieser Berechtigung hinzufügen oder daraus entfernen.", "permission_updated": "Berechtigung '{permission}' aktualisiert", "permission_update_failed": "Die Berechtigung '{permission}' kann nicht aktualisiert werden : {error}", "permission_not_found": "Berechtigung '{permission}' nicht gefunden", "permission_deletion_failed": "Entfernung der Berechtigung nicht möglich '{permission}': {error}", "permission_deleted": "Berechtigung '{permission}' gelöscht", - "permission_currently_allowed_for_all_users": "Diese Berechtigung wird derzeit allen Benutzern zusätzlich zu anderen Gruppen erteilt. Möglicherweise möchten Sie entweder die Berechtigung 'all_users' entfernen oder die anderen Gruppen entfernen, für die sie derzeit zulässig sind.", + "permission_currently_allowed_for_all_users": "Diese Berechtigung wird derzeit allen Konten zusätzlich zu anderen Gruppen erteilt. Möglicherweise möchtest du entweder die Berechtigung 'all_users' entfernen oder die anderen Gruppen entfernen, für die sie derzeit zulässig sind.", "permission_creation_failed": "Berechtigungserstellung nicht möglich '{permission}' : {error}", "permission_created": "Berechtigung '{permission}' erstellt", "permission_cannot_remove_main": "Entfernung einer Hauptberechtigung nicht genehmigt", "regenconf_file_updated": "Konfigurationsdatei '{conf}' aktualisiert", "regenconf_file_removed": "Konfigurationsdatei '{conf}' entfernt", "regenconf_file_remove_failed": "Konnte die Konfigurationsdatei '{conf}' nicht entfernen", - "postinstall_low_rootfsspace": "Das Root-Filesystem hat insgesamt weniger als 10GB freien Speicherplatz zur Verfügung, was ziemlich besorgniserregend ist! Sie werden sehr bald keinen freien Speicherplatz mehr haben! Für das Root-Filesystem werden mindestens 16GB empfohlen. Wenn Sie YunoHost trotz dieser Warnung installieren wollen, wiederholen Sie den Befehl mit --force-diskspace", + "postinstall_low_rootfsspace": "Das Root-Filesystem hat insgesamt weniger als 10GB freien Speicherplatz zur Verfügung, was ziemlich besorgniserregend ist! Du wirst sehr bald keinen freien Speicherplatz mehr haben! Für das Root-Filesystem werden mindestens 16GB empfohlen. Wenn du YunoHost trotz dieser Warnung installieren willst, wiederhole den Befehl mit --force-diskspace", "regenconf_up_to_date": "Die Konfiguration ist bereits aktuell für die Kategorie '{category}'", "regenconf_now_managed_by_yunohost": "Die Konfigurationsdatei '{conf}' wird jetzt von YunoHost (Kategorie {category}) verwaltet.", "regenconf_updated": "Konfiguration aktualisiert für '{category}'", "regenconf_pending_applying": "Wende die anstehende Konfiguration für die Kategorie {category} an...", "regenconf_failed": "Konnte die Konfiguration für die Kategorie(n) {categories} nicht neu erstellen", - "regenconf_dry_pending_applying": "Überprüfe die anstehende Konfiguration, welche für die Kategorie {category}' aktualisiert worden wäre…", + "regenconf_dry_pending_applying": "Überprüfe die anstehende Konfiguration, welche für die Kategorie {category}' aktualisiert worden wäre...", "regenconf_would_be_updated": "Die Konfiguration wäre für die Kategorie '{category}' aktualisiert worden", "restore_system_part_failed": "Die Systemteile '{part}' konnten nicht wiederhergestellt werden", "restore_removing_tmp_dir_failed": "Ein altes, temporäres Directory konnte nicht entfernt werden", "restore_not_enough_disk_space": "Nicht genug Speicher (Speicher: {free_space} B, benötigter Speicher: {needed_space} B, Sicherheitspuffer: {margin} B)", - "restore_may_be_not_enough_disk_space": "Ihr System scheint nicht genug Speicherplatz zu haben (frei: {free_space} B, benötigter Platz: {needed_space} B, Sicherheitspuffer: {margin} B)", - "restore_extracting": "Packe die benötigten Dateien aus dem Archiv aus…", + "restore_may_be_not_enough_disk_space": "Dein System scheint nicht genug Speicherplatz zu haben (frei: {free_space} B, benötigter Platz: {needed_space} B, Sicherheitspuffer: {margin} B)", + "restore_extracting": "Packe die benötigten Dateien aus dem Archiv aus...", "restore_already_installed_apps": "Folgende Apps können nicht wiederhergestellt werden, weil sie schon installiert sind: {apps}", "regex_with_only_domain": "Du kannst regex nicht als Domain verwenden, sondern nur als Pfad", "root_password_desynchronized": "Das Admin-Passwort wurde verändert, aber das Root-Passwort ist immer noch das alte!", - "regenconf_need_to_explicitly_specify_ssh": "Die SSH-Konfiguration wurde manuell modifiziert, aber Sie müssen explizit die Kategorie 'SSH' mit --force spezifizieren, um die Änderungen tatsächlich anzuwenden.", - "migration_update_LDAP_schema": "Aktualisiere das LDAP-Schema...", + "regenconf_need_to_explicitly_specify_ssh": "Die SSH-Konfiguration wurde manuell modifiziert, aber du musst explizit die Kategorie 'SSH' mit --force spezifizieren, um die Änderungen tatsächlich anzuwenden.", "log_backup_create": "Erstelle ein Backup-Archiv", "diagnosis_sshd_config_inconsistent": "Es sieht aus, als ob der SSH-Port manuell geändert wurde in /etc/ssh/ssh_config. Seit YunoHost 4.2 ist eine neue globale Einstellung 'security.ssh.port' verfügbar um zu verhindern, dass die Konfiguration manuell verändert wird.", "diagnosis_sshd_config_insecure": "Die SSH-Konfiguration wurde scheinbar manuell geändert und ist unsicher, weil sie keine 'AllowGroups'- oder 'AllowUsers' -Direktiven für die Beschränkung des Zugriffs durch autorisierte Benutzer enthält.", @@ -587,18 +547,17 @@ "migration_ldap_rollback_success": "System-Rollback erfolgreich.", "migration_ldap_migration_failed_trying_to_rollback": "Migrieren war nicht möglich... Versuch, ein Rollback des Systems durchzuführen.", "migration_ldap_backup_before_migration": "Vor der eigentlichen Migration ein Backup der LDAP-Datenbank und der Applikations-Einstellungen erstellen.", - "migration_description_0020_ssh_sftp_permissions": "Unterstützung für SSH- und SFTP-Berechtigungen hinzufügen", "global_settings_setting_ssowat_panel_overlay_enabled": "Das SSOwat-Overlay-Panel aktivieren", "global_settings_setting_security_ssh_port": "SSH-Port", - "diagnosis_sshd_config_inconsistent_details": "Bitte führen Sie yunohost settings set security.ssh.port -v YOUR_SSH_PORT aus, um den SSH-Port festzulegen, und prüfen Sie yunohost tools regen-conf ssh --dry-run --with-diff und yunohost tools regen-conf ssh --force um Ihre conf auf die YunoHost-Empfehlung zurückzusetzen.", - "regex_incompatible_with_tile": "/!\\ Packagers! Für Berechtigung '{permission}' ist show_tile auf 'true' gesetzt und deshalb können Sie keine regex-URL als Hauptdomäne setzen", - "permission_cant_add_to_all_users": "Die Berechtigung {permission} konnte nicht allen Benutzern gegeben werden.", + "diagnosis_sshd_config_inconsistent_details": "Bitte führe yunohost settings set security.ssh.port -v YOUR_SSH_PORT aus, um den SSH-Port festzulegen, und prüfe yunohost tools regen-conf ssh --dry-run --with-diff und yunohost tools regen-conf ssh --force um deine Konfiguration auf die YunoHost-Empfehlung zurückzusetzen.", + "regex_incompatible_with_tile": "/!\\ Packagers! Für Berechtigung '{permission}' ist show_tile auf 'true' gesetzt und deshalb kannst du keine regex-URL als Hauptdomäne setzen", + "permission_cant_add_to_all_users": "Die Berechtigung {permission} konnte nicht allen Konten gegeben werden.", "migration_ldap_can_not_backup_before_migration": "Das System-Backup konnte nicht abgeschlossen werden, bevor die Migration fehlschlug. Fehler: {error}", "service_description_fail2ban": "Schützt gegen Brute-Force-Angriffe und andere Angriffe aus dem Internet", "service_description_dovecot": "Ermöglicht es E-Mail-Clients auf Konten zuzugreifen (IMAP und POP3)", "service_description_dnsmasq": "Verarbeitet die Auflösung des Domainnamens (DNS)", "restore_backup_too_old": "Dieses Backup kann nicht wieder hergestellt werden, weil es von einer zu alten YunoHost Version stammt.", - "service_description_slapd": "Speichert Benutzer, Domains und verbundene Informationen", + "service_description_slapd": "Speichert Konten, Domänen und verbundene Informationen", "service_description_rspamd": "Spamfilter und andere E-Mail-Merkmale", "service_description_redis-server": "Eine spezialisierte Datenbank für den schnellen Datenzugriff, die Aufgabenwarteschlange und die Kommunikation zwischen Programmen", "service_description_postfix": "Wird benutzt, um E-Mails zu senden und zu empfangen", @@ -607,32 +566,123 @@ "service_description_metronome": "XMPP Sofortnachrichtenkonten verwalten", "service_description_yunohost-firewall": "Verwaltet offene und geschlossene Ports zur Verbindung mit Diensten", "service_description_yunohost-api": "Verwaltet die Interaktionen zwischen der Weboberfläche von YunoHost und dem System", - "service_description_ssh": "Ermöglicht die Verbindung zu Ihrem Server über ein Terminal (SSH-Protokoll)", - "service_description_php7.3-fpm": "Führt in PHP geschriebene Apps mit NGINX aus", - "server_reboot_confirm": "Der Server wird sofort heruntergefahren, sind Sie sicher? [{answers}]", + "service_description_ssh": "Ermöglicht die Verbindung zu deinem Server über ein Terminal (SSH-Protokoll)", + "server_reboot_confirm": "Der Server wird sofort heruntergefahren, bist du sicher? [{answers}]", "server_reboot": "Der Server wird neu gestartet", - "server_shutdown_confirm": "Der Server wird sofort heruntergefahren, sind Sie sicher? [{answers}]", + "server_shutdown_confirm": "Der Server wird sofort heruntergefahren, bist du sicher? [{answers}]", "server_shutdown": "Der Server wird heruntergefahren", - "root_password_replaced_by_admin_password": "Ihr Root Passwort wurde durch Ihr Admin Passwort ersetzt.", - "show_tile_cant_be_enabled_for_regex": "Momentan können Sie 'show_tile' nicht aktivieren, weil die URL für die Berechtigung '{permission}' ein regulärer Ausdruck ist", - "show_tile_cant_be_enabled_for_url_not_defined": "Momentan können Sie 'show_tile' nicht aktivieren, weil Sie zuerst eine URL für die Berechtigung '{permission}' definieren müssen", - "tools_upgrade_regular_packages_failed": "Konnte für die folgenden Pakete das Upgrade nicht durchführen: {packages_list}", - "tools_upgrade_regular_packages": "Momentan werden Upgrades für das System (YunoHost-unabhängige) Pakete durchgeführt…", - "tools_upgrade_cant_unhold_critical_packages": "Konnte für die kritischen Pakete das Flag 'hold' nicht aufheben…", - "tools_upgrade_cant_hold_critical_packages": "Konnte für die kritischen Pakete das Flag 'hold' nicht setzen…", - "tools_upgrade_cant_both": "Kann das Upgrade für das System und die Applikation nicht gleichzeitig durchführen", - "tools_upgrade_at_least_one": "Bitte geben Sie '--apps' oder '--system' an", - "this_action_broke_dpkg": "Diese Aktion hat unkonfigurierte Pakete verursacht, welche durch dpkg/apt (die Paketverwaltungen dieses Systems) zurückgelassen wurden... Sie können versuchen dieses Problem zu lösen, indem Sie 'sudo apt install --fix-broken' und/oder 'sudo dpkg --configure -a' ausführen.", + "root_password_replaced_by_admin_password": "Dein Root Passwort wurde durch dein Admin Passwort ersetzt.", + "show_tile_cant_be_enabled_for_regex": "Du kannst 'show_tile' momentan nicht aktivieren, weil die URL für die Berechtigung '{permission}' ein regulärer Ausdruck ist", + "show_tile_cant_be_enabled_for_url_not_defined": "Momentan kannst du 'show_tile' nicht aktivieren, weil du zuerst eine URL für die Berechtigung '{permission}' definieren musst", + "this_action_broke_dpkg": "Diese Aktion hat unkonfigurierte Pakete verursacht, welche durch dpkg/apt (die Paketverwaltungen dieses Systems) zurückgelassen wurden... Du kannst versuchen dieses Problem zu lösen, indem du 'sudo apt install --fix-broken' und/oder 'sudo dpkg --configure -a' ausführst.", "update_apt_cache_failed": "Kann den Cache von APT (Debians Paketmanager) nicht aktualisieren. Hier ist ein Auszug aus den sources.list-Zeilen, die helfen könnten, das Problem zu identifizieren:\n{sourceslist}", - "tools_upgrade_special_packages_completed": "YunoHost-Paketupdate beendet.\nDrücke [Enter], um zurück zur Kommandoziele zu kommen", - "tools_upgrade_special_packages_explanation": "Das Upgrade \"special\" wird im Hintergrund ausgeführt. Bitte starten Sie keine anderen Aktionen auf Ihrem Server für die nächsten ~10 Minuten. Die Dauer ist abhängig von der Geschwindigkeit Ihres Servers. Nach dem Upgrade müssen Sie sich eventuell erneut in das Adminportal einloggen. Upgrade-Logs sind im Adminbereich unter Tools → Log verfügbar. Alternativ können Sie in der Befehlszeile 'yunohost log list' eingeben.", - "tools_upgrade_special_packages": "\"special\" (YunoHost-bezogene) Pakete werden jetzt aktualisiert…", "unknown_main_domain_path": "Unbekannte:r Domain oder Pfad für '{app}'. Du musst eine Domain und einen Pfad setzen, um die URL für Berechtigungen zu setzen.", - "yunohost_postinstall_end_tip": "Post-install ist fertig! Um das Setup abzuschliessen, wird empfohlen:\n - einen ersten Benutzer über den Bereich 'Benutzer*in' im Adminbereich hinzuzufügen (oder mit 'yunohost user create ' in der Kommandezeile);\n - mögliche Fehler zu diagnostizieren über den Bereich 'Diagnose' im Adminbereich (oder mit 'yunohost diagnosis run' in der Kommandozeile;\n - Die Abschnitte 'Install YunoHost' und 'Geführte Tour' im Administratorenhandbuch zu lesen: https://yunohost.org/admindoc.", - "user_already_exists": "Der Benutzer '{user}' ist bereits vorhanden", + "yunohost_postinstall_end_tip": "Post-install ist fertig! Um das Setup abzuschliessen, wird empfohlen:\n - ein erstes Konto über den Bereich 'Konto' im Adminbereich hinzuzufügen (oder mit 'yunohost user create ' in der Kommandezeile);\n - mögliche Fehler zu diagnostizieren über den Bereich 'Diagnose' im Adminbereich (oder mit 'yunohost diagnosis run' in der Kommandozeile;\n - Die Abschnitte 'Install YunoHost' und 'Geführte Tour' im Administratorenhandbuch zu lesen: https://yunohost.org/admindoc.", + "user_already_exists": "Konto '{user}' ist bereits vorhanden", "update_apt_cache_warning": "Beim Versuch den Cache für APT (Debians Paketmanager) zu aktualisieren, ist etwas schief gelaufen. Hier ist ein Dump der Zeilen aus sources.list, die Ihnen vielleicht dabei helfen, das Problem zu identifizieren:\n{sourceslist}", "global_settings_setting_security_webadmin_allowlist": "IP-Adressen, die auf die Verwaltungsseite zugreifen dürfen. Kommasepariert.", "global_settings_setting_security_webadmin_allowlist_enabled": "Erlaube nur bestimmten IP-Adressen den Zugriff auf die Verwaltungsseite.", - "disk_space_not_sufficient_update": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu aktuallisieren", - "disk_space_not_sufficient_install": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu installieren" + "disk_space_not_sufficient_update": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu aktualisieren", + "disk_space_not_sufficient_install": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu installieren", + "danger": "Warnung:", + "diagnosis_apps_bad_quality": "Diese App ist im YunoHost-Applikationskatalog momentan als defekt gekennzeichnet. Es könnte sich dabei um einen vorübergehendes Problem handeln. Während der/die Betreuer:in versucht das Problem zu beheben, ist die Upgrade-Funktion für diese App gesperrt.", + "config_apply_failed": "Anwenden der neuen Konfiguration fehlgeschlagen: {error}", + "config_validate_date": "Sollte ein zulässiges Datum in folgendem Format sein: YYYY-MM-DD", + "config_validate_email": "Sollte eine zulässige eMail sein", + "config_forbidden_keyword": "Das Schlüsselwort '{keyword}' ist reserviert. Du kannst kein Konfigurationspanel mit einer Frage erstellen, die diese ID verwendet.", + "config_no_panel": "Kein Konfigurationspanel gefunden.", + "config_validate_color": "Sollte eine zulässige RGB hexadezimal Farbe sein", + "diagnosis_apps_issue": "Ein Problem für die App {app} ist aufgetreten", + "config_validate_time": "Sollte eine zulässige Zeit wie HH:MM sein", + "config_validate_url": "Sollte eine zulässige web URL sein", + "config_version_not_supported": "Konfigurationspanel Versionen '{version}' sind nicht unterstützt.", + "diagnosis_apps_allgood": "Alle installierten Apps berücksichtigen die grundlegenden Paketierungspraktiken", + "diagnosis_apps_broken": "Diese App ist im YunoHost-Applikationskatalog momentan als defekt gekennzeichnet. Es könnte sich dabei um einen vorübergehendes Problem handeln. Während der/die Betreuer:in versucht das Problem zu beheben, ist die Upgrade-Funktion für diese App gesperrt.", + "diagnosis_apps_not_in_app_catalog": "Diese App fehlt im Applikationskatalog von YunoHost oder wird in diesem nicht mehr angezeigt. Du solltest in Betracht ziehen, sie zu deinstallieren, weil sie keine Aktualisierungen mehr erhält und die Integrität und die Sicherheit deines Systems kompromittieren könnte.", + "diagnosis_apps_outdated_ynh_requirement": "Die installierte Version dieser App erfordert nur YunoHost >=2.x, was darauf hinweist, dass die App nicht nach aktuell empfohlenen Paketierungspraktiken und mit aktuellen Helpern erstellt worden ist. Du solltest wirklich in Betracht ziehen, sie zu aktualisieren.", + "diagnosis_description_apps": "Applikationen", + "config_cant_set_value_on_section": "Du kannst einen einzelnen Wert nicht auf einen gesamten Konfigurationsbereich anwenden.", + "diagnosis_apps_deprecated_practices": "Die installierte Version dieser App verwendet immer noch gewisse veraltete Paketierungspraktiken. Du solltest die App wirklich aktualisieren.", + "app_config_unable_to_apply": "Konnte die Werte des Konfigurations-Panels nicht anwenden.", + "app_config_unable_to_read": "Konnte die Werte des Konfigurations-Panels nicht auslesen.", + "config_unknown_filter_key": "Der Filterschlüssel '{filter_key}' ist inkorrekt.", + "diagnosis_dns_specialusedomain": "Die Domäne {domain} basiert auf einer Top-Level-Domain (TLD) für spezielle Zwecke wie .local oder .test und deshalb wird von ihr nicht erwartet, dass sie echte DNS-Einträge besitzt.", + "ldap_server_down": "LDAP-Server kann nicht erreicht werden", + "diagnosis_http_special_use_tld": "Die Domäne {domain} basiert auf einer Top-Level-Domäne (TLD) für besondere Zwecke wie .local oder .test und wird daher voraussichtlich nicht außerhalb des lokalen Netzwerks zugänglich sein.", + "domain_dns_push_managed_in_parent_domain": "Die automatische DNS-Konfiguration wird von der übergeordneten Domäne {parent_domain} verwaltet.", + "domain_dns_push_already_up_to_date": "Die Einträge sind auf dem neuesten Stand, es gibt nichts zu tun.", + "domain_config_auth_token": "Authentifizierungstoken", + "domain_config_auth_key": "Authentifizierungsschlüssel", + "domain_config_auth_secret": "Authentifizierungsgeheimnis", + "domain_config_api_protocol": "API-Protokoll", + "domain_unknown": "Domäne '{domain}' unbekannt", + "ldap_server_is_down_restart_it": "Der LDAP-Dienst ist nicht erreichbar, versuche ihn neu zu starten...", + "user_import_bad_file": "Deine CSV-Datei ist nicht korrekt formatiert und wird daher ignoriert, um einen möglichen Datenverlust zu vermeiden", + "global_settings_setting_security_experimental_enabled": "Aktiviere experimentelle Sicherheitsfunktionen (nur aktivieren, wenn Du weißt was Du tust!)", + "global_settings_setting_security_nginx_redirect_to_https": "HTTP-Anfragen standardmäßig auf HTTPs umleiten (NICHT AUSSCHALTEN, sofern Du nicht weißt was Du tust!)", + "user_import_missing_columns": "Die folgenden Spalten fehlen: {columns}", + "user_import_nothing_to_do": "Es muss kein Konto importiert werden", + "user_import_partial_failed": "Der Import von Konten ist teilweise fehlgeschlagen", + "user_import_bad_line": "Ungültige Zeile {line}: {details}", + "other_available_options": "… und {n} weitere verfügbare Optionen, die nicht angezeigt werden", + "domain_dns_conf_special_use_tld": "Diese Domäne basiert auf einer Top-Level-Domäne (TLD) für besondere Zwecke wie .local oder .test und wird daher vermutlich keine eigenen DNS-Einträge haben.", + "domain_dns_registrar_managed_in_parent_domain": "Diese Domäne ist eine Unterdomäne von {parent_domain_link}. Die Konfiguration des DNS-Registrars sollte auf der Konfigurationsseite von {parent_domain} verwaltet werden.", + "domain_dns_registrar_not_supported": "YunoHost konnte den Registrar, der diese Domäne verwaltet, nicht automatisch erkennen. Du solltest die DNS-Einträge, wie unter https://yunohost.org/dns beschrieben, manuell konfigurieren.", + "domain_dns_registrar_supported": "YunoHost hat automatisch erkannt, dass diese Domäne von dem Registrar **{registrar}** verwaltet wird. Wenn Du möchtest, konfiguriert YunoHost diese DNS-Zone automatisch, wenn Du die entsprechenden API-Zugangsdaten zur Verfügung stellst. Auf dieser Seite erfährst Du, wie Du deine API-Anmeldeinformationen erhältst: https://yunohost.org/registar_api_{registrar}. (Du kannst deine DNS-Einträge auch, wie unter https://yunohost.org/dns beschrieben, manuell konfigurieren)", + "service_not_reloading_because_conf_broken": "Der Dienst '{name}' wird nicht neu geladen/gestartet, da seine Konfiguration fehlerhaft ist: {errors}", + "user_import_failed": "Der Import von Konten ist komplett fehlgeschlagen", + "domain_dns_push_failed_to_list": "Auflistung der aktuellen Einträge über die API des Registrars fehlgeschlagen: {error}", + "domain_dns_pushing": "DNS-Einträge übertragen…", + "domain_dns_push_record_failed": "{action} für Eintrag {type}/{name} fehlgeschlagen: {error}", + "domain_dns_push_success": "DNS-Einträge aktualisiert!", + "domain_dns_push_failed": "Die Aktualisierung der DNS-Einträge ist leider gescheitert.", + "domain_dns_push_partial_failure": "DNS-Einträge teilweise aktualisiert: einige Warnungen/Fehler wurden gemeldet.", + "domain_config_features_disclaimer": "Bisher hat das Aktivieren/Deaktivieren von Mail- oder XMPP-Funktionen nur Auswirkungen auf die empfohlene und automatische DNS-Konfiguration, nicht auf die Systemkonfigurationen!", + "domain_config_mail_in": "Eingehende E-Mails", + "domain_config_mail_out": "Ausgehende E-Mails", + "domain_config_xmpp": "Instant Messaging (XMPP)", + "log_app_config_set": "Konfiguration auf die Applikation '{}' anwenden", + "log_user_import": "Konten importieren", + "diagnosis_high_number_auth_failures": "In letzter Zeit gab es eine verdächtig hohe Anzahl von Authentifizierungsfehlern. Stelle sicher, dass fail2ban läuft und korrekt konfiguriert ist, oder verwende einen benutzerdefinierten Port für SSH, wie unter https://yunohost.org/security beschrieben.", + "domain_dns_registrar_yunohost": "Dies ist eine nohost.me / nohost.st / ynh.fr Domäne, ihre DNS-Konfiguration wird daher automatisch von YunoHost ohne weitere Konfiguration übernommen. (siehe Befehl 'yunohost dyndns update')", + "domain_config_auth_entrypoint": "API-Einstiegspunkt", + "domain_config_auth_application_key": "Anwendungsschlüssel", + "domain_config_auth_application_secret": "Geheimer Anwendungsschlüssel", + "domain_config_auth_consumer_key": "Consumer-Schlüssel", + "invalid_number_min": "Muss größer sein als {min}", + "invalid_number_max": "Muss kleiner sein als {max}", + "invalid_password": "Ungültiges Passwort", + "ldap_attribute_already_exists": "LDAP-Attribut '{attribute}' existiert bereits mit dem Wert '{value}'", + "user_import_success": "Konten erfolgreich importiert", + "domain_registrar_is_not_configured": "Der DNS-Registrar ist noch nicht für die Domäne '{domain}' konfiguriert.", + "domain_dns_push_not_applicable": "Die automatische DNS-Konfiguration ist nicht auf die Domäne {domain} anwendbar. Konfiguriere die DNS-Einträge manuell, wie unter https://yunohost.org/dns_config beschrieben.", + "domain_dns_registrar_experimental": "Bislang wurde die Schnittstelle zur API von **{registrar}** noch nicht außreichend von der YunoHost-Community getestet und geprüft. Der Support ist **sehr experimentell** – sei vorsichtig!", + "domain_dns_push_failed_to_authenticate": "Die Authentifizierung bei der API des Registrars für die Domäne '{domain}' ist fehlgeschlagen. Wahrscheinlich sind die Anmeldedaten falsch? (Fehler: {error})", + "log_domain_config_set": "Konfiguration für die Domäne '{}' aktualisieren", + "log_domain_dns_push": "DNS-Einträge für die Domäne '{}' übertragen", + "service_description_yunomdns": "Ermöglicht es dir, deinen Server über 'yunohost.local' in deinem lokalen Netzwerk zu erreichen", + "migration_0021_start": "Beginnen von Migration zu Bullseye", + "migration_0021_patching_sources_list": "Aktualisieren der sources.lists...", + "migration_0021_main_upgrade": "Starte Hauptupdate...", + "migration_0021_still_on_buster_after_main_upgrade": "Irgendetwas ist während des Haupt-Upgrades schief gelaufen, das System scheint immer noch auf Debian Buster zu laufen", + "migration_0021_yunohost_upgrade": "Start des YunoHost Kern-Upgrades...", + "migration_0021_not_buster": "Die aktuelle Debian-Distribution ist nicht Buster!", + "migration_0021_not_enough_free_space": "Der freie Speicherplatz in /var/ ist ziemlich gering! Du solltest mindestens 1 GB frei haben, um diese Migration durchzuführen.", + "migration_0021_system_not_fully_up_to_date": "Dein System ist nicht ganz aktuell. Bitte führe ein reguläres Upgrade durch, bevor du die Migration zu Bullseye durchführst.", + "migration_0021_problematic_apps_warning": "Bitte beachte, dass die folgenden, möglicherweise problematischen installierten Anwendungen erkannt wurden. Es sieht so aus, als ob diese nicht aus dem YunoHost-Applikations-Katalog installiert wurden oder nicht als \"funktionierend\" gekennzeichnet sind. Es kann daher nicht garantiert werden, dass sie nach dem Upgrade noch funktionieren werden: {problematic_apps}", + "migration_0021_modified_files": "Bitte beachte, dass die folgenden Dateien manuell geändert wurden und nach dem Update möglicherweise überschrieben werden: {manually_modified_files}", + "migration_0021_cleaning_up": "Bereinigung von Cache und Paketen nicht mehr nötig...", + "migration_0021_patch_yunohost_conflicts": "Patch anwenden, um das Konfliktproblem zu umgehen...", + "global_settings_setting_security_ssh_password_authentication": "Passwort-Authentifizierung für SSH zulassen", + "migration_description_0021_migrate_to_bullseye": "Upgrade des Systems auf Debian Bullseye und YunoHost 11.x", + "migration_0021_general_warning": "Bitte beachte, dass diese Migration ein heikler Vorgang ist. Das YunoHost-Team hat sein Bestes getan, um sie zu überprüfen und zu testen, aber die Migration könnte immer noch Teile des Systems oder seiner Anwendungen beschädigen.\n\nEs wird daher empfohlen,:\n - Führe eine Sicherung aller kritischen Daten oder Applikationen durch. Mehr Informationen unter https://yunohost.org/backup;\n - Habe Geduld, nachdem du die Migration gestartet hast: Je nach Internetverbindung und Hardware kann es bis zu ein paar Stunden dauern, bis alles aktualisiert ist.", + "tools_upgrade": "Upgrade Systempakete", + "tools_upgrade_failed": "Pakete konnten nicht aktualisiert werden: {packages_list}", + "domain_config_default_app": "Standard-Applikation", + "migration_0023_postgresql_11_not_installed": "PostgreSQL war auf deinem System nicht installiert. Es gibt nichts zu tun.", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 ist installiert, PostgreSQL 13 allerdings nicht? Mit deinem System scheint etwas seltsam zu sein :(...", + "migration_description_0022_php73_to_php74_pools": "Migriere php7.3-fpm 'pool' conf Dateien auf php7.4", + "migration_description_0023_postgresql_11_to_13": "Migriere Datenbanken von PostgreSQL 11 auf 13", + "service_description_postgresql": "Speichert Applikations-Daten (SQL Datenbank)", + "migration_0023_not_enough_space": "Stelle sicher, dass unter {path} genug Speicherplatz zur Verfügung steht, um die Migration auszuführen." } diff --git a/locales/en.json b/locales/en.json index 8a95caaf2..2b2f10179 100644 --- a/locales/en.json +++ b/locales/en.json @@ -8,65 +8,64 @@ "admin_password_changed": "The administration password was changed", "admin_password_too_long": "Please choose a password shorter than 127 characters", "already_up_to_date": "Nothing to do. Everything is already up-to-date.", - "app_action_cannot_be_ran_because_required_services_down": "These required services should be running to run this action: {services}. Try restarting them to continue (and possibly investigate why they are down).", "app_action_broke_system": "This action seems to have broken these important services: {services}", + "app_action_cannot_be_ran_because_required_services_down": "These required services should be running to run this action: {services}. Try restarting them to continue (and possibly investigate why they are down).", "app_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}'", + "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", - "app_change_url_failed_nginx_reload": "Could not reload NGINX. Here is the output of 'nginx -t':\n{nginx_errors}", "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain}{path}'), nothing to do.", "app_change_url_no_script": "The app '{app_name}' doesn't support URL modification yet. Maybe you should upgrade it.", "app_change_url_success": "{app} URL is now {domain}{path}", + "app_config_unable_to_apply": "Failed to apply config panel values.", + "app_config_unable_to_read": "Failed to read config panel values.", "app_extraction_failed": "Could not extract the installation files", "app_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.", "app_id_invalid": "Invalid app ID", - "app_install_files_invalid": "These files cannot be installed", "app_install_failed": "Unable to install {app}: {error}", + "app_install_files_invalid": "These files cannot be installed", "app_install_script_failed": "An error occurred inside the app installation script", - "app_make_default_location_already_used": "Unable to make '{app}' the default app on the domain, '{domain}' is already in use by '{other_app}'", "app_label_deprecated": "This command is deprecated! Please use the new command 'yunohost user permission update' to manage the app label.", "app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps}", - "app_manifest_invalid": "Something is wrong with the app manifest: {error}", - "app_manifest_install_ask_domain": "Choose the domain where this app should be installed", - "app_manifest_install_ask_path": "Choose the URL path (after the domain) where this app should be installed", - "app_manifest_install_ask_password": "Choose an administration password for this app", + "app_make_default_location_already_used": "Unable to make '{app}' the default app on the domain, '{domain}' is already in use by '{other_app}'", "app_manifest_install_ask_admin": "Choose an administrator user for this app", + "app_manifest_install_ask_domain": "Choose the domain where this app should be installed", "app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?", - "app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps' upgrades have been cancelled: {apps}", + "app_manifest_install_ask_password": "Choose an administration password for this app", + "app_manifest_install_ask_path": "Choose the URL path (after the domain) where this app should be installed", "app_not_correctly_installed": "{app} seems to be incorrectly installed", "app_not_installed": "Could not find {app} in the list of installed apps: {all_apps}", "app_not_properly_removed": "{app} has not been properly removed", + "app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps' upgrades have been cancelled: {apps}", + "app_packaging_format_not_supported": "This app cannot be installed because its packaging format is not supported by your YunoHost version. You should probably consider upgrading your system.", + "app_remove_after_failed_install": "Removing the app following the installation failure...", "app_removed": "{app} uninstalled", "app_requirements_checking": "Checking required packages for {app}...", "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", - "app_remove_after_failed_install": "Removing the app following the installation failure...", "app_restore_failed": "Could not restore {app}: {error}", "app_restore_script_failed": "An error occured inside the app restore script", "app_sources_fetch_failed": "Could not fetch sources files, is the URL correct?", + "app_start_backup": "Collecting files to be backed up for {app}...", "app_start_install": "Installing {app}...", "app_start_remove": "Removing {app}...", - "app_start_backup": "Collecting files to be backed up for {app}...", "app_start_restore": "Restoring {app}...", "app_unknown": "Unknown app", "app_unsupported_remote_type": "Unsupported remote type used for the app", - "app_upgrade_several_apps": "The following apps will be upgraded: {apps}", "app_upgrade_app_name": "Now upgrading {app}...", "app_upgrade_failed": "Could not upgrade {app}: {error}", "app_upgrade_script_failed": "An error occurred inside the app upgrade script", + "app_upgrade_several_apps": "The following apps will be upgraded: {apps}", "app_upgrade_some_app_failed": "Some apps could not be upgraded", "app_upgraded": "{app} upgraded", - "app_packaging_format_not_supported": "This app cannot be installed because its packaging format is not supported by your YunoHost version. You should probably consider upgrading your system.", "apps_already_up_to_date": "All apps are already up-to-date", - "apps_catalog_init_success": "App catalog system initialized!", - "apps_catalog_updating": "Updating application catalog...", "apps_catalog_failed_to_download": "Unable to download the {apps_catalog} app catalog: {error}", + "apps_catalog_init_success": "App catalog system initialized!", "apps_catalog_obsolete_cache": "The app catalog cache is empty or obsolete.", "apps_catalog_update_success": "The application catalog has been updated!", - "ask_user_domain": "Domain to use for the user's email address and XMPP account", + "apps_catalog_updating": "Updating application catalog...", "ask_firstname": "First name", "ask_lastname": "Last name", "ask_main_domain": "Main domain", @@ -74,6 +73,7 @@ "ask_new_domain": "New domain", "ask_new_path": "New path", "ask_password": "Password", + "ask_user_domain": "Domain to use for the user's email address and XMPP account", "backup_abstract_method": "This backup method has yet to be implemented", "backup_actually_backuping": "Creating a backup archive from the collected files...", "backup_app_failed": "Could not back up {app}", @@ -82,11 +82,11 @@ "backup_applying_method_tar": "Creating the backup TAR archive...", "backup_archive_app_not_found": "Could not find {app} in the backup archive", "backup_archive_broken_link": "Could not access the backup archive (broken link to {path})", + "backup_archive_cant_retrieve_info_json": "Could not load infos for archive '{archive}'... The info.json cannot be retrieved (or is not a valid json).", + "backup_archive_corrupted": "It looks like the backup archive '{archive}' is corrupted : {error}", "backup_archive_name_exists": "A backup archive with this name already exists.", "backup_archive_name_unknown": "Unknown local backup archive named '{name}'", "backup_archive_open_failed": "Could not open the backup archive", - "backup_archive_cant_retrieve_info_json": "Could not load infos for archive '{archive}'... The info.json cannot be retrieved (or is not a valid json).", - "backup_archive_corrupted": "It looks like the backup archive '{archive}' is corrupted : {error}", "backup_archive_system_part_not_available": "System part '{part}' unavailable in this backup", "backup_archive_writing_error": "Could not add the files '{source}' (named in the archive '{dest}') to be backed up into the compressed archive '{archive}'", "backup_ask_for_copying_if_needed": "Do you want to perform the backup using {size}MB temporarily? (This way is used since some files could not be prepared using a more efficient method.)", @@ -94,8 +94,8 @@ "backup_cleaning_failed": "Could not clean up the temporary backup folder", "backup_copying_to_organize_the_archive": "Copying {size}MB to organize the archive", "backup_couldnt_bind": "Could not bind {src} to {dest}.", - "backup_created": "Backup created", "backup_create_size_estimation": "The archive will contain about {size} of data.", + "backup_created": "Backup created", "backup_creation_failed": "Could not create the backup archive", "backup_csv_addition_failed": "Could not add files to backup into the CSV file", "backup_csv_creation_failed": "Could not create the CSV file needed for restoration", @@ -130,185 +130,227 @@ "certmanager_cert_renew_success": "Let's Encrypt certificate renewed for the domain '{domain}'", "certmanager_cert_signing_failed": "Could not sign the new certificate", "certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain} did not work...", - "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off those checks.)", "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain} is not self-signed. Are you sure you want to replace it? (Use '--force' to do so.)", "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' is different from this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off those checks.)", "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off those checks.)", - "certmanager_warning_subdomain_dns_record": "Subdomain '{subdomain}' does not resolve to the same IP address as '{domain}'. Some features will not be available until you fix this and regenerate the certificate.", + "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off those checks.)", "certmanager_hit_rate_limit": "Too many certificates already issued for this exact set of domains {domain} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", "certmanager_no_cert_file": "Could not read the certificate file for the domain {domain} (file: {file})", "certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file})", "certmanager_unable_to_parse_self_CA_name": "Could not parse name of self-signing authority (file: {file})", - "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ", + "certmanager_warning_subdomain_dns_record": "Subdomain '{subdomain}' does not resolve to the same IP address as '{domain}'. Some features will not be available until you fix this and regenerate the certificate.", + "config_apply_failed": "Applying the new configuration failed: {error}", + "config_cant_set_value_on_section": "You can't set a single value on an entire config section.", + "config_forbidden_keyword": "The keyword '{keyword}' is reserved, you can't create or use a config panel with a question with this id.", + "config_no_panel": "No config panel found.", + "config_unknown_filter_key": "The filter key '{filter_key}' is incorrect.", + "config_validate_color": "Should be a valid RGB hexadecimal color", + "config_validate_date": "Should be a valid date like in the format YYYY-MM-DD", + "config_validate_email": "Should be a valid email", + "config_validate_time": "Should be a valid time like HH:MM", + "config_validate_url": "Should be a valid web URL", + "config_version_not_supported": "Config panel versions '{version}' are not supported.", "confirm_app_install_danger": "DANGER! This app is known to be still experimental (if not explicitly not working)! You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system... If you are willing to take that risk anyway, type '{answers}'", "confirm_app_install_thirdparty": "DANGER! This app is not part of YunoHost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system... If you are willing to take that risk anyway, type '{answers}'", + "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app}", + "danger": "Danger:", + "diagnosis_apps_allgood": "All installed apps respect basic packaging practices", + "diagnosis_apps_bad_quality": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", + "diagnosis_apps_broken": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", + "diagnosis_apps_deprecated_practices": "This app's installed version still uses some super-old deprecated packaging practices. You should really consider upgrading it.", + "diagnosis_apps_issue": "An issue was found for app {app}", + "diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and got removed, you should consider uninstalling this app as it won't receive upgrade, and may compromise the integrity and security of your system.", + "diagnosis_apps_outdated_ynh_requirement": "This app's installed version only requires yunohost >= 2.x, which tends to indicate that it's not up to date with recommended packaging practices and helpers. You should really consider upgrading it.", + "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage from installing packages from backports, because it's likely to create unstabilities or conflicts on your system.", "diagnosis_basesystem_hardware": "Server hardware architecture is {virt} {arch}", "diagnosis_basesystem_hardware_model": "Server model is {model}", "diagnosis_basesystem_host": "Server is running Debian {debian_version}", "diagnosis_basesystem_kernel": "Server is running Linux kernel {kernel_version}", - "diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})", - "diagnosis_basesystem_ynh_main_version": "Server is running YunoHost {main_version} ({repo})", "diagnosis_basesystem_ynh_inconsistent_versions": "You are running inconsistent versions of the YunoHost packages... most probably because of a failed or partial upgrade.", - "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage from installing packages from backports, because it's likely to create unstabilities or conflicts on your system.", - "diagnosis_package_installed_from_sury": "Some system packages should be downgraded", - "diagnosis_package_installed_from_sury_details": "Some packages were inadvertendly installed from a third-party repository called Sury. The YunoHost team improved the strategy that handle these packages, but it's expected that some setups that installed PHP7.3 apps while still on Stretch have some remaining inconsistencies. To fix this situation, you should try running the following command: {cmd_to_fix}", - "diagnosis_display_tip": "To see the issues found, you can go to the Diagnosis section of the webadmin, or run 'yunohost diagnosis show --issues --human-readable' from the command-line.", - "diagnosis_failed_for_category": "Diagnosis failed for category '{category}': {error}", + "diagnosis_basesystem_ynh_main_version": "Server is running YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})", "diagnosis_cache_still_valid": "(Cache still valid for {category} diagnosis. Won't re-diagnose it yet!)", "diagnosis_cant_run_because_of_dep": "Can't run diagnosis for {category} while there are important issues related to {dep}.", - "diagnosis_ignored_issues": "(+ {nb_ignored} ignored issue(s))", - "diagnosis_found_errors": "Found {errors} significant issue(s) related to {category}!", - "diagnosis_found_errors_and_warnings": "Found {errors} significant issue(s) (and {warnings} warning(s)) related to {category}!", - "diagnosis_found_warnings": "Found {warnings} item(s) that could be improved for {category}.", - "diagnosis_everything_ok": "Everything looks good for {category}!", - "diagnosis_failed": "Failed to fetch diagnosis result for category '{category}': {error}", - "diagnosis_no_cache": "No diagnosis cache yet for category '{category}'", - "diagnosis_ip_connected_ipv4": "The server is connected to the Internet through IPv4!", - "diagnosis_ip_no_ipv4": "The server does not have working IPv4.", - "diagnosis_ip_connected_ipv6": "The server is connected to the Internet through IPv6!", - "diagnosis_ip_no_ipv6": "The server does not have working IPv6.", - "diagnosis_ip_no_ipv6_tip": "Having a working IPv6 is not mandatory for your server to work, but it is better for the health of the Internet as a whole. IPv6 should usually be automatically configured by the system or your provider if it's available. Otherwise, you might need to configure a few things manually as explained in the documentation here: https://yunohost.org/#/ipv6. If you cannot enable IPv6 or if it seems too technical for you, you can also safely ignore this warning.", - "diagnosis_ip_global": "Global IP: {global}", - "diagnosis_ip_local": "Local IP: {local}", - "diagnosis_ip_not_connected_at_all": "The server does not seem to be connected to the Internet at all!?", - "diagnosis_ip_dnsresolution_working": "Domain name resolution is working!", - "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests ?", - "diagnosis_ip_broken_resolvconf": "Domain name resolution seems to be broken on your server, which seems related to /etc/resolv.conf not pointing to 127.0.0.1.", - "diagnosis_ip_weird_resolvconf": "DNS resolution seems to be working, but it looks like you're using a custom /etc/resolv.conf.", - "diagnosis_ip_weird_resolvconf_details": "The file /etc/resolv.conf should be a symlink to /etc/resolvconf/run/resolv.conf itself pointing to 127.0.0.1 (dnsmasq). If you want to manually configure DNS resolvers, please edit /etc/resolv.dnsmasq.conf.", - "diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})", + "diagnosis_description_apps": "Applications", + "diagnosis_description_basesystem": "Base system", + "diagnosis_description_dnsrecords": "DNS records", + "diagnosis_description_ip": "Internet connectivity", + "diagnosis_description_mail": "Email", + "diagnosis_description_ports": "Ports exposure", + "diagnosis_description_regenconf": "System configurations", + "diagnosis_description_services": "Services status check", + "diagnosis_description_systemresources": "System resources", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "Storage {mountpoint} (on device {device}) has only {free} ({free_percent}%) space remaining (out of {total}). Be careful.", + "diagnosis_diskusage_ok": "Storage {mountpoint} (on device {device}) still has {free} ({free_percent}%) space left (out of {total})!", + "diagnosis_diskusage_verylow": "Storage {mountpoint} (on device {device}) has only {free} ({free_percent}%) space remaining (out of {total}). You should really consider cleaning up some space!", + "diagnosis_display_tip": "To see the issues found, you can go to the Diagnosis section of the webadmin, or run 'yunohost diagnosis show --issues --human-readable' from the command-line.", "diagnosis_dns_bad_conf": "Some DNS records are missing or incorrect 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_discrepancy": "The following DNS record does not seem to follow the recommended configuration:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", + "diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})", + "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}", "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help about configuring DNS records.", + "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force.", - "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) and is therefore not expected to have actual DNS records.", + "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", "diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains", - "diagnosis_domain_not_found_details": "The domain {domain} doesn't exist in WHOIS database or is expired!", "diagnosis_domain_expiration_not_found_details": "The WHOIS information for domain {domain} doesn't seem to contain the information about the expiration date?", "diagnosis_domain_expiration_success": "Your domains are registered and not going to expire anytime soon.", "diagnosis_domain_expiration_warning": "Some domains will expire soon!", - "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", "diagnosis_domain_expires_in": "{domain} expires in {days} days.", - "diagnosis_services_running": "Service {service} is running!", - "diagnosis_services_conf_broken": "Configuration is broken for service {service}!", - "diagnosis_services_bad_status": "Service {service} is {status} :(", - "diagnosis_services_bad_status_tip": "You can try to restart the service, and if it doesn't work, have a look at the service logs in the webadmin (from the command line, you can do this with yunohost service restart {service} and yunohost service log {service}).", - "diagnosis_diskusage_verylow": "Storage {mountpoint} (on device {device}) has only {free} ({free_percent}%) space remaining (out of {total}). You should really consider cleaning up some space!", - "diagnosis_diskusage_low": "Storage {mountpoint} (on device {device}) has only {free} ({free_percent}%) space remaining (out of {total}). Be careful.", - "diagnosis_diskusage_ok": "Storage {mountpoint} (on device {device}) still has {free} ({free_percent}%) space left (out of {total})!", - "diagnosis_ram_verylow": "The system has only {available} ({available_percent}%) RAM available! (out of {total})", + "diagnosis_domain_not_found_details": "The domain {domain} doesn't exist in WHOIS database or is expired!", + "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}!", + "diagnosis_found_errors_and_warnings": "Found {errors} significant issue(s) (and {warnings} warning(s)) related to {category}!", + "diagnosis_found_warnings": "Found {warnings} item(s) that could be improved for {category}.", + "diagnosis_high_number_auth_failures": "There's been a suspiciously high number of authentication failures recently. You may want to make sure that fail2ban is running and is correctly configured, or use a custom port for SSH as explained in https://yunohost.org/security.", + "diagnosis_http_bad_status_code": "It looks like another machine (maybe your internet router) answered instead of your server.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", + "diagnosis_http_connection_error": "Connection error: could not connect to the requested domain, it's very likely unreachable.", + "diagnosis_http_could_not_diagnose": "Could not diagnose if domains are reachable from outside in IPv{ipversion}.", + "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_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.", + "diagnosis_http_partially_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network in IPv{failed}, though it works in IPv{passed}.", + "diagnosis_http_special_use_tld": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to be exposed outside the local network.", + "diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", + "diagnosis_http_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network.", + "diagnosis_ignored_issues": "(+ {nb_ignored} ignored issue(s))", + "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests ?", + "diagnosis_ip_broken_resolvconf": "Domain name resolution seems to be broken on your server, which seems related to /etc/resolv.conf not pointing to 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "The server is connected to the Internet through IPv4!", + "diagnosis_ip_connected_ipv6": "The server is connected to the Internet through IPv6!", + "diagnosis_ip_dnsresolution_working": "Domain name resolution is working!", + "diagnosis_ip_global": "Global IP: {global}", + "diagnosis_ip_local": "Local IP: {local}", + "diagnosis_ip_no_ipv4": "The server does not have working IPv4.", + "diagnosis_ip_no_ipv6": "The server does not have working IPv6.", + "diagnosis_ip_no_ipv6_tip": "Having a working IPv6 is not mandatory for your server to work, but it is better for the health of the Internet as a whole. IPv6 should usually be automatically configured by the system or your provider if it's available. Otherwise, you might need to configure a few things manually as explained in the documentation here: https://yunohost.org/#/ipv6. If you cannot enable IPv6 or if it seems too technical for you, you can also safely ignore this warning.", + "diagnosis_ip_not_connected_at_all": "The server does not seem to be connected to the Internet at all!?", + "diagnosis_ip_weird_resolvconf": "DNS resolution seems to be working, but it looks like you're using a custom /etc/resolv.conf.", + "diagnosis_ip_weird_resolvconf_details": "The file /etc/resolv.conf should be a symlink to /etc/resolvconf/run/resolv.conf itself pointing to 127.0.0.1 (dnsmasq). If you want to manually configure DNS resolvers, please edit /etc/resolv.dnsmasq.conf.", + "diagnosis_mail_blacklist_listed_by": "Your IP or domain {item} is blacklisted on {blacklist_name}", + "diagnosis_mail_blacklist_ok": "The IPs and domains used by this server do not appear to be blacklisted", + "diagnosis_mail_blacklist_reason": "The blacklist reason is: {reason}", + "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixed it, feel free to ask for your IP or domaine to be removed on {blacklist_website}", + "diagnosis_mail_ehlo_bad_answer": "A non-SMTP service answered on port 25 on IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an other machine answering instead of your server.", + "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from outside in IPv{ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}", + "diagnosis_mail_ehlo_ok": "The SMTP mail server is reachable from the outside and therefore is able to receive emails!", + "diagnosis_mail_ehlo_unreachable": "The SMTP mail server is unreachable from the outside on IPv{ipversion}. It won't be able to receive emails.", + "diagnosis_mail_ehlo_unreachable_details": "Could not open a connection on port 25 to your server in IPv{ipversion}. It appears to be unreachable.
1. The most common cause for this issue is that port 25 is not correctly forwarded to your server.
2. You should also make sure that service postfix is running.
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", + "diagnosis_mail_ehlo_wrong": "A different SMTP mail server answers on IPv{ipversion}. Your server will probably not be able to receive emails.", + "diagnosis_mail_ehlo_wrong_details": "The EHLO received by the remote diagnoser in IPv{ipversion} is different from your server's domain.
Received EHLO: {wrong_ehlo}
Expected: {right_ehlo}
The most common cause for this issue is that port 25 is not correctly forwarded to your server. Alternatively, make sure that no firewall or reverse-proxy is interfering.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "The reverse DNS is not correctly configured in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Current reverse DNS: {rdns_domain}
Expected value: {ehlo_domain}", + "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If you are experiencing issues because of this, consider the following solutions:
- Some ISP provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- Or it's possible to switch to a different provider", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If your reverse DNS is correctly configured for IPv4, you can try disabling the use of IPv6 when sending emails by running yunohost settings set smtp.allow_ipv6 -v off. Note: this last solution means that you won't be able to send or receive emails from the few IPv6-only servers out there.", + "diagnosis_mail_fcrdns_nok_details": "You should first try to configure the reverse DNS with {ehlo_domain} in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).", + "diagnosis_mail_fcrdns_ok": "Your reverse DNS is correctly configured!", + "diagnosis_mail_outgoing_port_25_blocked": "The SMTP mail server cannot send emails to other servers because outgoing port 25 is blocked in IPv{ipversion}.", + "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider", + "diagnosis_mail_outgoing_port_25_ok": "The SMTP mail server is able to send emails (outgoing port 25 is not blocked).", + "diagnosis_mail_queue_ok": "{nb_pending} pending emails in the mail queues", + "diagnosis_mail_queue_too_big": "Too many pending emails in mail queue ({nb_pending} emails)", + "diagnosis_mail_queue_unavailable": "Can not consult number of pending emails in queue", + "diagnosis_mail_queue_unavailable_details": "Error: {error}", + "diagnosis_never_ran_yet": "It looks like this server was setup recently and there's no diagnosis report to show yet. You should start by running a full diagnosis, either from the webadmin or using 'yunohost diagnosis run' from the command line.", + "diagnosis_no_cache": "No diagnosis cache yet for category '{category}'", + "diagnosis_package_installed_from_sury": "Some system packages should be downgraded", + "diagnosis_package_installed_from_sury_details": "Some packages were inadvertendly installed from a third-party repository called Sury. The YunoHost team improved the strategy that handle these packages, but it's expected that some setups that installed PHP7.3 apps while still on Stretch have some remaining inconsistencies. To fix this situation, you should try running the following command: {cmd_to_fix}", + "diagnosis_ports_could_not_diagnose": "Could not diagnose if ports are reachable from outside in IPv{ipversion}.", + "diagnosis_ports_could_not_diagnose_details": "Error: {error}", + "diagnosis_ports_forwarding_tip": "To fix this issue, you most probably need to configure port forwarding on your internet router as described in https://yunohost.org/isp_box_config", + "diagnosis_ports_needed_by": "Exposing this port is needed for {category} features (service {service})", + "diagnosis_ports_ok": "Port {port} is reachable from outside.", + "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from outside in IPv{failed}.", + "diagnosis_ports_unreachable": "Port {port} is not reachable from outside.", + "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process that ate up to much memory. Summary of the processes killed:\n{kills_summary}", "diagnosis_ram_low": "The system has {available} ({available_percent}%) RAM available (out of {total}). Be careful.", "diagnosis_ram_ok": "The system still has {available} ({available_percent}%) RAM available out of {total}.", + "diagnosis_ram_verylow": "The system has only {available} ({available_percent}%) RAM available! (out of {total})", + "diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!", + "diagnosis_regenconf_manually_modified": "Configuration file {file} appears to have been manually modified.", + "diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with yunohost tools regen-conf {category} --dry-run --with-diff and force the reset to the recommended configuration with yunohost tools regen-conf {category} --force", + "diagnosis_rootfstotalspace_critical": "The root filesystem only has a total of {space} which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16 GB for the root filesystem.", + "diagnosis_rootfstotalspace_warning": "The root filesystem only has a total of {space}. This may be okay, but be careful because ultimately you may run out of disk space quickly... It's recommended to have at least 16 GB for the root filesystem.", + "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown criticial security vulnerability", + "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more infos.", + "diagnosis_services_bad_status": "Service {service} is {status} :(", + "diagnosis_services_bad_status_tip": "You can try to restart the service, and if it doesn't work, have a look at the service logs in the webadmin (from the command line, you can do this with yunohost service restart {service} and yunohost service log {service}).", + "diagnosis_services_conf_broken": "Configuration is broken for service {service}!", + "diagnosis_services_running": "Service {service} is running!", + "diagnosis_sshd_config_inconsistent": "It looks like the SSH port was manually modified in /etc/ssh/sshd_config. Since YunoHost 4.2, a new global setting 'security.ssh.port' is available to avoid manually editing the configuration.", + "diagnosis_sshd_config_inconsistent_details": "Please run yunohost settings set security.ssh.port -v YOUR_SSH_PORT to define the SSH port, and check yunohost tools regen-conf ssh --dry-run --with-diff and yunohost tools regen-conf ssh --force to reset your conf to the YunoHost recommendation.", + "diagnosis_sshd_config_insecure": "The SSH configuration appears to have been manually modified, and is insecure because it contains no 'AllowGroups' or 'AllowUsers' directive to limit access to authorized users.", "diagnosis_swap_none": "The system has no swap at all. You should consider adding at least {recommended} of swap to avoid situations where the system runs out of memory.", "diagnosis_swap_notsomuch": "The system has only {total} swap. You should consider having at least {recommended} to avoid situations where the system runs out of memory.", "diagnosis_swap_ok": "The system has {total} of swap!", "diagnosis_swap_tip": "Please be careful and aware that if the server is hosting swap on an SD card or SSD storage, it may drastically reduce the life expectancy of the device`.", - "diagnosis_mail_outgoing_port_25_ok": "The SMTP mail server is able to send emails (outgoing port 25 is not blocked).", - "diagnosis_mail_outgoing_port_25_blocked": "The SMTP mail server cannot send emails to other servers because outgoing port 25 is blocked in IPv{ipversion}.", - "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider", - "diagnosis_mail_ehlo_ok": "The SMTP mail server is reachable from the outside and therefore is able to receive emails!", - "diagnosis_mail_ehlo_unreachable": "The SMTP mail server is unreachable from the outside on IPv{ipversion}. It won't be able to receive emails.", - "diagnosis_mail_ehlo_unreachable_details": "Could not open a connection on port 25 to your server in IPv{ipversion}. It appears to be unreachable.
1. The most common cause for this issue is that port 25 is not correctly forwarded to your server.
2. You should also make sure that service postfix is running.
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", - "diagnosis_mail_ehlo_bad_answer": "A non-SMTP service answered on port 25 on IPv{ipversion}", - "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an other machine answering instead of your server.", - "diagnosis_mail_ehlo_wrong": "A different SMTP mail server answers on IPv{ipversion}. Your server will probably not be able to receive emails.", - "diagnosis_mail_ehlo_wrong_details": "The EHLO received by the remote diagnoser in IPv{ipversion} is different from your server's domain.
Received EHLO: {wrong_ehlo}
Expected: {right_ehlo}
The most common cause for this issue is that port 25 is not correctly forwarded to your server. Alternatively, make sure that no firewall or reverse-proxy is interfering.", - "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from outside in IPv{ipversion}.", - "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}", - "diagnosis_mail_fcrdns_ok": "Your reverse DNS is correctly configured!", - "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.", - "diagnosis_mail_fcrdns_nok_details": "You should first try to configure the reverse DNS with {ehlo_domain} in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If you are experiencing issues because of this, consider the following solutions:
- Some ISP provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- Or it's possible to switch to a different provider", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If your reverse DNS is correctly configured for IPv4, you can try disabling the use of IPv6 when sending emails by running yunohost settings set smtp.allow_ipv6 -v off. Note: this last solution means that you won't be able to send or receive emails from the few IPv6-only servers out there.", - "diagnosis_mail_fcrdns_different_from_ehlo_domain": "The reverse DNS is not correctly configured in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.", - "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Current reverse DNS: {rdns_domain}
Expected value: {ehlo_domain}", - "diagnosis_mail_blacklist_ok": "The IPs and domains used by this server do not appear to be blacklisted", - "diagnosis_mail_blacklist_listed_by": "Your IP or domain {item} is blacklisted on {blacklist_name}", - "diagnosis_mail_blacklist_reason": "The blacklist reason is: {reason}", - "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixed it, feel free to ask for your IP or domaine to be removed on {blacklist_website}", - "diagnosis_mail_queue_ok": "{nb_pending} pending emails in the mail queues", - "diagnosis_mail_queue_unavailable": "Can not consult number of pending emails in queue", - "diagnosis_mail_queue_unavailable_details": "Error: {error}", - "diagnosis_mail_queue_too_big": "Too many pending emails in mail queue ({nb_pending} emails)", - "diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!", - "diagnosis_regenconf_manually_modified": "Configuration file {file} appears to have been manually modified.", - "diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with yunohost tools regen-conf {category} --dry-run --with-diff and force the reset to the recommended configuration with yunohost tools regen-conf {category} --force", - "diagnosis_rootfstotalspace_warning": "The root filesystem only has a total of {space}. This may be okay, but be careful because ultimately you may run out of disk space quickly... It's recommended to have at least 16 GB for the root filesystem.", - "diagnosis_rootfstotalspace_critical": "The root filesystem only has a total of {space} which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16 GB for the root filesystem.", - "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown criticial security vulnerability", - "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more infos.", - "diagnosis_description_basesystem": "Base system", - "diagnosis_description_ip": "Internet connectivity", - "diagnosis_description_dnsrecords": "DNS records", - "diagnosis_description_services": "Services status check", - "diagnosis_description_systemresources": "System resources", - "diagnosis_description_ports": "Ports exposure", - "diagnosis_description_web": "Web", - "diagnosis_description_mail": "Email", - "diagnosis_description_regenconf": "System configurations", - "diagnosis_description_apps": "Applications", - "diagnosis_apps_allgood": "All installed apps respect basic packaging practices", - "diagnosis_apps_issue": "An issue was found for app {app}", - "diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and got removed, you should consider uninstalling this app as it won't receive upgrade, and may compromise the integrity and security of your system.", - "diagnosis_apps_broken": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", - "diagnosis_apps_bad_quality": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", - "diagnosis_apps_outdated_ynh_requirement": "This app's installed version only requires yunohost >= 2.x, which tends to indicate that it's not up to date with recommended packaging practices and helpers. You should really consider upgrading it.", - "diagnosis_apps_deprecated_practices": "This app's installed version still uses some super-old deprecated packaging practices. You should really consider upgrading it.", - "diagnosis_ports_could_not_diagnose": "Could not diagnose if ports are reachable from outside in IPv{ipversion}.", - "diagnosis_ports_could_not_diagnose_details": "Error: {error}", - "diagnosis_ports_unreachable": "Port {port} is not reachable from outside.", - "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from outside in IPv{failed}.", - "diagnosis_ports_ok": "Port {port} is reachable from outside.", - "diagnosis_ports_needed_by": "Exposing this port is needed for {category} features (service {service})", - "diagnosis_ports_forwarding_tip": "To fix this issue, you most probably need to configure port forwarding on your internet router as described in https://yunohost.org/isp_box_config", - "diagnosis_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_could_not_diagnose": "Could not diagnose if domains are reachable from outside in IPv{ipversion}.", - "diagnosis_http_could_not_diagnose_details": "Error: {error}", - "diagnosis_http_localdomain": "Domain {domain}, with a .local TLD, is not expected to be exposed outside the local network.", - "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.", - "diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", - "diagnosis_http_connection_error": "Connection error: could not connect to the requested domain, it's very likely unreachable.", - "diagnosis_http_bad_status_code": "It looks like another machine (maybe your internet router) answered instead of your server.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", - "diagnosis_http_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network.", - "diagnosis_http_partially_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network in IPv{failed}, though it works in IPv{passed}.", - "diagnosis_http_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_unknown_categories": "The following categories are unknown: {categories}", - "diagnosis_never_ran_yet": "It looks like this server was setup recently and there's no diagnosis report to show yet. You should start by running a full diagnosis, either from the webadmin or using 'yunohost diagnosis run' from the command line.", - "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process that ate up to much memory. Summary of the processes killed:\n{kills_summary}", - "diagnosis_sshd_config_insecure": "The SSH configuration appears to have been manually modified, and is insecure because it contains no 'AllowGroups' or 'AllowUsers' directive to limit access to authorized users.", - "diagnosis_sshd_config_inconsistent": "It looks like the SSH port was manually modified in /etc/ssh/sshd_config. Since YunoHost 4.2, a new global setting 'security.ssh.port' is available to avoid manually editing the configuration.", - "diagnosis_sshd_config_inconsistent_details": "Please run yunohost settings set security.ssh.port -v YOUR_SSH_PORT to define the SSH port, and check yunohost tools regen-conf ssh --dry-run --with-diff and yunohost tools regen-conf ssh --force to reset your conf to the YunoHost recommendation.", "disk_space_not_sufficient_install": "There is not enough disk space left to install this application", "disk_space_not_sufficient_update": "There is not enough disk space left to update this application", - "domain_cannot_remove_main": "You cannot remove '{domain}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n '; here is the list of candidate domains: {other_domains}", "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated in YunoHost.", + "domain_cannot_remove_main": "You cannot remove '{domain}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n '; here is the list of candidate domains: {other_domains}", "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add ', then set is as the main domain using 'yunohost domain main-domain -n ' and then you can remove the domain '{domain}' using 'yunohost domain remove {domain}'.'", "domain_cert_gen_failed": "Could not generate certificate", + "domain_config_api_protocol": "API protocol", + "domain_config_auth_application_key": "Application key", + "domain_config_auth_application_secret": "Application secret key", + "domain_config_auth_consumer_key": "Consumer key", + "domain_config_auth_entrypoint": "API entry point", + "domain_config_auth_key": "Authentication key", + "domain_config_auth_secret": "Authentication secret", + "domain_config_auth_token": "Authentication token", + "domain_config_default_app": "Default app", + "domain_config_features_disclaimer": "So far, enabling/disabling mail or XMPP features only impact the recommended and automatic DNS configuration, not system configurations!", + "domain_config_mail_in": "Incoming emails", + "domain_config_mail_out": "Outgoing emails", + "domain_config_xmpp": "Instant messaging (XMPP)", "domain_created": "Domain created", "domain_creation_failed": "Unable to create domain {domain}: {error}", "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_dns_push_already_up_to_date": "Records already up to date, nothing to do.", + "domain_dns_push_failed": "Updating the DNS records failed miserably.", + "domain_dns_push_failed_to_authenticate": "Failed to authenticate on registrar's API for domain '{domain}'. Most probably the credentials are incorrect? (Error: {error})", + "domain_dns_push_failed_to_list": "Failed to list current records using the registrar's API: {error}", + "domain_dns_push_managed_in_parent_domain": "The automatic DNS configuration feature is managed in the parent domain {parent_domain}.", + "domain_dns_push_not_applicable": "The automatic DNS configuration feature is not applicable to domain {domain}. You should manually configure your DNS records following the documentation at https://yunohost.org/dns_config.", + "domain_dns_push_partial_failure": "DNS records partially updated: some warnings/errors were reported.", + "domain_dns_push_record_failed": "Failed to {action} record {type}/{name} : {error}", + "domain_dns_push_success": "DNS records updated!", + "domain_dns_pushing": "Pushing DNS records...", + "domain_dns_registrar_experimental": "So far, the interface with **{registrar}**'s API has not been properly tested and reviewed by the YunoHost community. Support is **very experimental** - be careful!", + "domain_dns_registrar_managed_in_parent_domain": "This domain is a subdomain of {parent_domain_link}. DNS registrar configuration should be managed in {parent_domain}'s configuration panel.", + "domain_dns_registrar_not_supported": "YunoHost could not automatically detect the registrar handling this domain. You should manually configure your DNS records following the documentation at https://yunohost.org/dns.", + "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", + "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", + "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", "domain_remove_confirm_apps_removal": "Removing this domain will remove those applications:\n{apps}\n\nAre you sure you want to do that? [{answers}]", "domain_uninstall_app_first": "Those applications are still installed on your domain:\n{apps}\n\nPlease uninstall them using 'yunohost app remove the_app_id' or move them to another domain using 'yunohost app change-url the_app_id' before proceeding to domain removal", - "domain_name_unknown": "Domain '{domain}' unknown", - "domain_unknown": "Unknown domain", + "domain_unknown": "Domain '{domain}' unknown", "domains_available": "Available domains:", "done": "Done", "downloading": "Downloading...", "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.", "dpkg_lock_not_available": "This command can't be run right now because another program seems to be using the lock of dpkg (the system package manager)", - "dyndns_could_not_check_provide": "Could not check if {provider} can provide {domain}.", "dyndns_could_not_check_available": "Could not check if {domain} is available on {provider}.", + "dyndns_domain_not_provided": "DynDNS provider {provider} cannot provide domain {domain}.", "dyndns_ip_update_failed": "Could not update IP address to DynDNS", "dyndns_ip_updated": "Updated your IP on DynDNS", "dyndns_key_generating": "Generating DNS key... It may take a while.", @@ -317,10 +359,9 @@ "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", "dyndns_registered": "DynDNS domain registered", "dyndns_registration_failed": "Could not register DynDNS domain: {error}", - "dyndns_domain_not_provided": "DynDNS provider {provider} cannot provide domain {domain}.", "dyndns_unavailable": "The domain '{domain}' is unavailable.", - "extracting": "Extracting...", "experimental_feature": "Warning: This feature is experimental and not considered stable, you should not use it unless you know what you are doing.", + "extracting": "Extracting...", "field_invalid": "Invalid field '{}'", "file_does_not_exist": "The file {path} does not exist.", "firewall_reload_failed": "Could not reload the firewall", @@ -333,42 +374,44 @@ "global_settings_cant_write_settings": "Could not save settings file, reason: {reason}", "global_settings_key_doesnt_exists": "The key '{settings_key}' does not exist in the global settings, you can see all the available keys by running 'yunohost settings list'", "global_settings_reset_success": "Previous settings now backed up to {path}", + "global_settings_setting_backup_compress_tar_archives": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", "global_settings_setting_pop3_enabled": "Enable the POP3 protocol for the mail server", + "global_settings_setting_security_experimental_enabled": "Enable experimental security features (don't enable this if you don't know what you're doing!)", "global_settings_setting_security_nginx_compatibility": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", + "global_settings_setting_security_nginx_redirect_to_https": "Redirect HTTP requests to HTTPs by default (DO NOT TURN OFF unless you really know what you're doing!)", "global_settings_setting_security_password_admin_strength": "Admin password strength", "global_settings_setting_security_password_user_strength": "User password strength", - "global_settings_setting_security_ssh_compatibility": "Compatibility vs. security tradeoff for the SSH server. Affects the ciphers (and other security-related aspects)", "global_settings_setting_security_postfix_compatibility": "Compatibility vs. security tradeoff for the Postfix server. Affects the ciphers (and other security-related aspects)", + "global_settings_setting_security_ssh_compatibility": "Compatibility vs. security tradeoff for the SSH server. Affects the ciphers (and other security-related aspects)", + "global_settings_setting_security_ssh_password_authentication": "Allow password authentication for SSH", "global_settings_setting_security_ssh_port": "SSH port", - "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key}', discard it and save it in /etc/yunohost/settings-unknown.json", + "global_settings_setting_security_webadmin_allowlist": "IP adresses allowed to access the webadmin. Comma-separated.", + "global_settings_setting_security_webadmin_allowlist_enabled": "Allow only some IPs to access the webadmin.", "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Allow the use of (deprecated) DSA hostkey for the SSH daemon configuration", - "global_settings_setting_ssowat_panel_overlay_enabled": "Enable SSOwat panel overlay", "global_settings_setting_smtp_allow_ipv6": "Allow the use of IPv6 to receive and send mail", "global_settings_setting_smtp_relay_host": "SMTP relay host to use in order to send mail instead of this yunohost instance. Useful if you are in one of this situation: your 25 port is blocked by your ISP or VPS provider, you have a residential IP listed on DUHL, you are not able to configure reverse DNS or this server is not directly exposed on the internet and you want use an other one to send mails.", + "global_settings_setting_smtp_relay_password": "SMTP relay host password", "global_settings_setting_smtp_relay_port": "SMTP relay port", "global_settings_setting_smtp_relay_user": "SMTP relay user account", - "global_settings_setting_smtp_relay_password": "SMTP relay host password", - "global_settings_setting_security_webadmin_allowlist_enabled": "Allow only some IPs to access the webadmin.", - "global_settings_setting_security_webadmin_allowlist": "IP adresses allowed to access the webadmin. Comma-separated.", - "global_settings_setting_security_experimental_enabled": "Enable experimental security features (don't enable this if you don't know what you're doing!)", - "global_settings_setting_backup_compress_tar_archives": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", + "global_settings_setting_ssowat_panel_overlay_enabled": "Enable SSOwat panel overlay", + "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key}', discard it and save it in /etc/yunohost/settings-unknown.json", "global_settings_unknown_type": "Unexpected situation, the setting {setting} appears to have the type {unknown_type} but it is not a type supported by the system.", "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).", "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to a variation of characters (uppercase, lowercase, digits and special characters).", "group_already_exist": "Group {group} already exists", "group_already_exist_on_system": "Group {group} already exists in the system groups", "group_already_exist_on_system_but_removing_it": "Group {group} already exists in the system groups, but YunoHost will remove it...", + "group_cannot_be_deleted": "The group {group} cannot be deleted manually.", + "group_cannot_edit_all_users": "The group 'all_users' cannot be edited manually. It is a special group meant to contain all users registered in YunoHost", + "group_cannot_edit_primary_group": "The group '{group}' cannot be edited manually. It is the primary group meant to contain only one specific user.", + "group_cannot_edit_visitors": "The group 'visitors' cannot be edited manually. It is a special group representing anonymous visitors", "group_created": "Group '{group}' created", "group_creation_failed": "Could not create the group '{group}': {error}", - "group_cannot_edit_all_users": "The group 'all_users' cannot be edited manually. It is a special group meant to contain all users registered in YunoHost", - "group_cannot_edit_visitors": "The group 'visitors' cannot be edited manually. It is a special group representing anonymous visitors", - "group_cannot_edit_primary_group": "The group '{group}' cannot be edited manually. It is the primary group meant to contain only one specific user.", - "group_cannot_be_deleted": "The group {group} cannot be deleted manually.", "group_deleted": "Group '{group}' deleted", "group_deletion_failed": "Could not delete the group '{group}': {error}", "group_unknown": "The group '{group}' is unknown", - "group_updated": "Group '{group}' updated", "group_update_failed": "Could not update the group '{group}': {error}", + "group_updated": "Group '{group}' updated", "group_user_already_in_group": "User {user} is already in group {group}", "group_user_not_in_group": "User {user} is not in group {group}", "hook_exec_failed": "Could not run script: {path}", @@ -377,104 +420,99 @@ "hook_list_by_invalid": "This property can not be used to list hooks", "hook_name_unknown": "Unknown hook name '{name}'", "installation_complete": "Installation completed", + "invalid_number": "Must be a number", + "invalid_number_max": "Must be lesser than {max}", + "invalid_number_min": "Must be greater than {min}", + "invalid_password": "Invalid password", "invalid_regex": "Invalid regex:'{regex}'", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", + "ldap_attribute_already_exists": "LDAP attribute '{attribute}' already exists with value '{value}'", "ldap_server_down": "Unable to reach LDAP server", "ldap_server_is_down_restart_it": "The LDAP service is down, attempt to restart it...", - "log_corrupted_md_file": "The YAML metadata file associated with logs is damaged: '{md_file}\nError: {error}'", - "log_link_to_log": "Full log of this operation: '{desc}'", - "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}{name}'", - "log_link_to_failed_log": "Could not complete the operation '{desc}'. Please provide the full log of this operation by clicking here to get help", - "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help", - "log_does_exists": "There is no operation log with the name '{log}', use 'yunohost log list' to see all available operation logs", - "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", + "log_app_action_run": "Run action of the '{}' app", "log_app_change_url": "Change the URL of the '{}' app", + "log_app_config_set": "Apply config to the '{}' app", "log_app_install": "Install the '{}' app", + "log_app_makedefault": "Make '{}' the default app", "log_app_remove": "Remove the '{}' app", "log_app_upgrade": "Upgrade the '{}' app", - "log_app_makedefault": "Make '{}' the default app", - "log_app_action_run": "Run action of the '{}' app", - "log_app_config_show_panel": "Show the config panel of the '{}' app", - "log_app_config_apply": "Apply config to the '{}' app", "log_available_on_yunopaste": "This log is now available via {url}", "log_backup_create": "Create a backup archive", - "log_backup_restore_system": "Restore system from a backup archive", "log_backup_restore_app": "Restore '{}' from a backup archive", - "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", - "log_remove_on_failed_install": "Remove '{}' after a failed installation", + "log_backup_restore_system": "Restore system from a backup archive", + "log_corrupted_md_file": "The YAML metadata file associated with logs is damaged: '{md_file}\nError: {error}'", + "log_does_exists": "There is no operation log with the name '{log}', use 'yunohost log list' to see all available operation logs", "log_domain_add": "Add '{}' domain into system configuration", + "log_domain_config_set": "Update configuration for domain '{}'", + "log_domain_dns_push": "Push DNS records for domain '{}'", + "log_domain_main_domain": "Make '{}' the main domain", "log_domain_remove": "Remove '{}' domain from system configuration", "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", "log_dyndns_update": "Update the IP associated with your YunoHost subdomain '{}'", + "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help", + "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}'", "log_letsencrypt_cert_install": "Install a Let's Encrypt certificate on '{}' domain", + "log_letsencrypt_cert_renew": "Renew '{}' Let's Encrypt certificate", + "log_link_to_failed_log": "Could not complete the operation '{desc}'. Please provide the full log of this operation by clicking here to get help", + "log_link_to_log": "Full log of this operation: '{desc}'", + "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", "log_permission_create": "Create permission '{}'", "log_permission_delete": "Delete permission '{}'", "log_permission_url": "Update URL related to permission '{}'", - "log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain", - "log_letsencrypt_cert_renew": "Renew '{}' Let's Encrypt certificate", "log_regen_conf": "Regenerate system configurations '{}'", + "log_remove_on_failed_install": "Remove '{}' after a failed installation", + "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", + "log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain", + "log_tools_migrations_migrate_forward": "Run migrations", + "log_tools_postinstall": "Postinstall your YunoHost server", + "log_tools_reboot": "Reboot your server", + "log_tools_shutdown": "Shutdown your server", + "log_tools_upgrade": "Upgrade system packages", "log_user_create": "Add '{}' user", "log_user_delete": "Delete '{}' user", - "log_user_import": "Import users", "log_user_group_create": "Create '{}' group", "log_user_group_delete": "Delete '{}' group", "log_user_group_update": "Update '{}' group", - "log_user_update": "Update info for user '{}'", - "log_user_permission_update": "Update accesses for permission '{}'", + "log_user_import": "Import users", "log_user_permission_reset": "Reset permission '{}'", - "log_domain_main_domain": "Make '{}' the main domain", - "log_tools_migrations_migrate_forward": "Run migrations", - "log_tools_postinstall": "Postinstall your YunoHost server", - "log_tools_upgrade": "Upgrade system packages", - "log_tools_shutdown": "Shutdown your server", - "log_tools_reboot": "Reboot your server", + "log_user_permission_update": "Update accesses for permission '{}'", + "log_user_update": "Update info for user '{}'", "mail_alias_remove_failed": "Could not remove e-mail alias '{mail}'", "mail_domain_unknown": "Invalid e-mail address for domain '{domain}'. Please, use a domain administrated by this server.", "mail_forward_remove_failed": "Could not remove e-mail forwarding '{mail}'", + "mail_unavailable": "This e-mail address is reserved and shall be automatically allocated to the very first user", "mailbox_disabled": "E-mail turned off for user {user}", "mailbox_used_space_dovecot_down": "The Dovecot mailbox service needs to be up if you want to fetch used mailbox space", - "mail_unavailable": "This e-mail address is reserved and shall be automatically allocated to the very first user", "main_domain_change_failed": "Unable to change the main domain", "main_domain_changed": "The main domain has been changed", - "migrating_legacy_permission_settings": "Migrating legacy permission settings...", - "migration_description_0015_migrate_to_buster": "Upgrade the system to Debian Buster and YunoHost 4.x", - "migration_description_0016_php70_to_php73_pools": "Migrate php7.0-fpm 'pool' conf files to php7.3", - "migration_description_0017_postgresql_9p6_to_11": "Migrate databases from PostgreSQL 9.6 to 11", - "migration_description_0018_xtable_to_nftable": "Migrate old network traffic rules to the new nftable system", - "migration_description_0019_extend_permissions_features": "Extend/rework the app permission management system", - "migration_description_0020_ssh_sftp_permissions": "Add SSH and SFTP permissions support", + "migration_0021_cleaning_up": "Cleaning up cache and packages not useful anymore...", + "migration_0021_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.", + "migration_0021_main_upgrade": "Starting main upgrade...", + "migration_0021_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}", + "migration_0021_not_buster": "The current Debian distribution is not Buster!", + "migration_0021_not_enough_free_space": "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.", + "migration_0021_patch_yunohost_conflicts": "Applying patch to workaround conflict issue...", + "migration_0021_patching_sources_list": "Patching the sources.lists...", + "migration_0021_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from the YunoHost app catalog, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}", + "migration_0021_start": "Starting migration to Bullseye", + "migration_0021_still_on_buster_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Buster", + "migration_0021_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Bullseye.", + "migration_0021_yunohost_upgrade": "Starting YunoHost core upgrade...", + "migration_0023_not_enough_space": "Make sufficient space available in {path} to run the migration.", + "migration_0023_postgresql_11_not_installed": "PostgreSQL was not installed on your system. Nothing to do.", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 is installed, but not PostgreSQL 13!? Something weird might have happened on your system :(...", + "migration_description_0021_migrate_to_bullseye": "Upgrade the system to Debian Bullseye and YunoHost 11.x", + "migration_description_0022_php73_to_php74_pools": "Migrate php7.3-fpm 'pool' conf files to php7.4", + "migration_description_0023_postgresql_11_to_13": "Migrate databases from PostgreSQL 11 to 13", "migration_ldap_backup_before_migration": "Creating a backup of LDAP database and apps settings prior to the actual migration.", "migration_ldap_can_not_backup_before_migration": "The backup of the system could not be completed before the migration failed. Error: {error}", "migration_ldap_migration_failed_trying_to_rollback": "Could not migrate... trying to roll back the system.", "migration_ldap_rollback_success": "System rolled back.", - "migration_update_LDAP_schema": "Updating LDAP schema...", - "migration_0015_start" : "Starting migration to Buster", - "migration_0015_patching_sources_list": "Patching the sources.lists...", - "migration_0015_main_upgrade": "Starting main upgrade...", - "migration_0015_still_on_stretch_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Stretch", - "migration_0015_yunohost_upgrade" : "Starting YunoHost core upgrade...", - "migration_0015_not_stretch" : "The current Debian distribution is not Stretch!", - "migration_0015_not_enough_free_space" : "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.", - "migration_0015_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Buster.", - "migration_0015_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.", - "migration_0015_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from the YunoHost app catalog, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}", - "migration_0015_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}", - "migration_0015_specific_upgrade": "Starting upgrade of system packages that needs to be upgrade independently...", - "migration_0015_cleaning_up": "Cleaning up cache and packages not useful anymore...", - "migration_0015_weak_certs": "The following certificates were found to still use weak signature algorithms and have to be upgraded to be compatible with the next version of nginx: {certs}", - "migration_0017_postgresql_96_not_installed": "PostgreSQL was not installed on your system. Nothing to do.", - "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 is installed, but not postgresql 11‽ Something weird might have happened on your system :(...", - "migration_0017_not_enough_space": "Make sufficient space available in {path} to run the migration.", - "migration_0018_failed_to_migrate_iptables_rules": "Failed to migrate legacy iptables rules to nftables: {error}", - "migration_0018_failed_to_reset_legacy_rules": "Failed to reset legacy iptables rules: {error}", - "migration_0019_add_new_attributes_in_ldap": "Add new attributes for permissions in LDAP database", - "migration_0019_slapd_config_will_be_overwritten": "It looks like you manually edited the slapd configuration. For this critical migration, YunoHost needs to force the update of the slapd configuration. The original files will be backuped in {conf_backup_folder}.", "migrations_already_ran": "Those migrations are already done: {ids}", - "migrations_cant_reach_migration_file": "Could not access migrations files at the path '%s'", "migrations_dependencies_not_satisfied": "Run these migrations: '{dependencies_id}', before migration {id}.", - "migrations_failed_to_load_migration": "Could not load migration {id}: {error}", "migrations_exclusive_options": "'--auto', '--skip', and '--force-rerun' are mutually exclusive options.", + "migrations_failed_to_load_migration": "Could not load migration {id}: {error}", "migrations_list_conflict_pending_done": "You cannot use both '--previous' and '--done' at the same time.", "migrations_loading_migration": "Loading migration {id}...", "migrations_migration_has_failed": "Migration {id} did not complete, aborting. Error: {exception}", @@ -487,12 +525,10 @@ "migrations_running_forward": "Running migration {id}...", "migrations_skip_migration": "Skipping migration {id}...", "migrations_success_forward": "Migration {id} completed", - "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`.", + "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}'", - "invalid_number": "Must be a number", - "invalid_password": "Invalid password", "operation_interrupted": "The operation was manually interrupted?", - "packages_upgrade_failed": "Could not upgrade all the packages", + "other_available_options": "... and {n} other available options not shown", "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", "password_too_simple_2": "The password needs to be at least 8 characters long and contain a digit, upper and lower characters", @@ -500,35 +536,36 @@ "password_too_simple_4": "The password needs to be at least 12 characters long and contain a digit, upper, lower and special characters", "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, alphanumeric and -_. characters only", "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", - "pattern_email_forward": "Must be a valid e-mail address, '+' symbol accepted (e.g. someone+tag@example.com)", "pattern_email": "Must be a valid e-mail address, without '+' symbol (e.g. someone@example.com)", + "pattern_email_forward": "Must be a valid e-mail address, '+' symbol accepted (e.g. someone+tag@example.com)", "pattern_firstname": "Must be a valid first name", "pattern_lastname": "Must be a valid last name", "pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to not have a quota", "pattern_password": "Must be at least 3 characters long", - "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)", - "pattern_positive_number": "Must be a positive number", - "pattern_username": "Must be lower-case alphanumeric and underscore characters only", "pattern_password_app": "Sorry, passwords can not contain the following characters: {forbidden_chars}", + "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)", + "pattern_username": "Must be lower-case alphanumeric and underscore characters only", "permission_already_allowed": "Group '{group}' already has permission '{permission}' enabled", "permission_already_disallowed": "Group '{group}' already has permission '{permission}' disabled", "permission_already_exist": "Permission '{permission}' already exists", "permission_already_up_to_date": "The permission was not updated because the addition/removal requests already match the current state.", "permission_cannot_remove_main": "Removing a main permission is not allowed", + "permission_cant_add_to_all_users": "The permission {permission} can not be added to all users.", "permission_created": "Permission '{permission}' created", "permission_creation_failed": "Could not create permission '{permission}': {error}", "permission_currently_allowed_for_all_users": "This permission is currently granted to all users in addition to other groups. You probably want to either remove the 'all_users' permission or remove the other groups it is currently granted to.", - "permission_cant_add_to_all_users": "The permission {permission} can not be added to all users.", "permission_deleted": "Permission '{permission}' deleted", "permission_deletion_failed": "Could not delete permission '{permission}': {error}", "permission_not_found": "Permission '{permission}' not found", - "permission_update_failed": "Could not update permission '{permission}': {error}", - "permission_updated": "Permission '{permission}' updated", "permission_protected": "Permission {permission} is protected. You cannot add or remove the visitors group to/from this permission.", "permission_require_account": "Permission {permission} only makes sense for users having an account, and therefore cannot be enabled for visitors.", + "permission_update_failed": "Could not update permission '{permission}': {error}", + "permission_updated": "Permission '{permission}' updated", "port_already_closed": "Port {port} is already closed for {ip_version} connections", "port_already_opened": "Port {port} is already opened for {ip_version} connections", "postinstall_low_rootfsspace": "The root filesystem has a total space less than 10 GB, which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16GB for the root filesystem. If you want to install YunoHost despite this warning, re-run the postinstall with --force-diskspace", + "regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for category '{category}'...", + "regenconf_failed": "Could not regenerate the configuration for category(s): {categories}", "regenconf_file_backed_up": "Configuration file '{conf}' backed up to '{backup}'", "regenconf_file_copy_failed": "Could not copy the new configuration file '{new}' to '{conf}'", "regenconf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by regen-conf (category {category}) but was kept back.", @@ -537,14 +574,12 @@ "regenconf_file_remove_failed": "Could not remove the configuration file '{conf}'", "regenconf_file_removed": "Configuration file '{conf}' removed", "regenconf_file_updated": "Configuration file '{conf}' updated", + "regenconf_need_to_explicitly_specify_ssh": "The ssh configuration has been manually modified, but you need to explicitly specify category 'ssh' with --force to actually apply the changes.", "regenconf_now_managed_by_yunohost": "The configuration file '{conf}' is now managed by YunoHost (category {category}).", + "regenconf_pending_applying": "Applying pending configuration for category '{category}'...", "regenconf_up_to_date": "The configuration is already up-to-date for category '{category}'", "regenconf_updated": "Configuration updated for '{category}'", "regenconf_would_be_updated": "The configuration would have been updated for category '{category}'", - "regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for category '{category}'...", - "regenconf_failed": "Could not regenerate the configuration for category(s): {categories}", - "regenconf_pending_applying": "Applying pending configuration for category '{category}'...", - "regenconf_need_to_explicitly_specify_ssh": "The ssh configuration has been manually modified, but you need to explicitly specify category 'ssh' with --force to actually apply the changes.", "regex_incompatible_with_tile": "/!\\ Packagers! Permission '{permission}' has show_tile set to 'true' and you therefore cannot define a regex URL as the main URL", "regex_with_only_domain": "You can't use a regex for domain, only for path", "restore_already_installed_app": "An app with the ID '{app}' is already installed", @@ -565,64 +600,56 @@ "restore_system_part_failed": "Could not restore the '{part}' system part", "root_password_desynchronized": "The admin password was changed, but YunoHost could not propagate this to the root password!", "root_password_replaced_by_admin_password": "Your root password have been replaced by your admin password.", - "server_shutdown": "The server will shut down", - "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers}]", "server_reboot": "The server will reboot", "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers}]", + "server_shutdown": "The server will shut down", + "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers}]", "service_add_failed": "Could not add the service '{service}'", "service_added": "The service '{service}' was added", "service_already_started": "The service '{service}' is running already", "service_already_stopped": "The service '{service}' has already been stopped", "service_cmd_exec_failed": "Could not execute the command '{command}'", - "service_description_yunomdns": "Allows you to reach your server using 'yunohost.local' in your local network", "service_description_dnsmasq": "Handles domain name resolution (DNS)", "service_description_dovecot": "Allows e-mail clients to access/fetch email (via IMAP and POP3)", "service_description_fail2ban": "Protects against brute-force and other kinds of attacks from the Internet", "service_description_metronome": "Manage XMPP instant messaging accounts", "service_description_mysql": "Stores app data (SQL database)", "service_description_nginx": "Serves or provides access to all the websites hosted on your server", - "service_description_php7.3-fpm": "Runs apps written in PHP with NGINX", "service_description_postfix": "Used to send and receive e-mails", + "service_description_postgresql": "Stores app data (SQL database)", "service_description_redis-server": "A specialized database used for rapid data access, task queue, and communication between programs", "service_description_rspamd": "Filters spam, and other e-mail related features", "service_description_slapd": "Stores users, domains and related info", "service_description_ssh": "Allows you to connect remotely to your server via a terminal (SSH protocol)", "service_description_yunohost-api": "Manages interactions between the YunoHost web interface and the system", "service_description_yunohost-firewall": "Manages open and close connection ports to services", + "service_description_yunomdns": "Allows you to reach your server using 'yunohost.local' in your local network", "service_disable_failed": "Could not make the service '{service}' not start at boot.\n\nRecent service logs:{logs}", "service_disabled": "The service '{service}' will not be started anymore when system boots.", "service_enable_failed": "Could not make the service '{service}' automatically start at boot.\n\nRecent service logs:{logs}", "service_enabled": "The service '{service}' will now be automatically started during system boots.", - "service_regen_conf_is_deprecated": "'yunohost service regen-conf' is deprecated! Please use 'yunohost tools regen-conf' instead.", + "service_not_reloading_because_conf_broken": "Not reloading/restarting service '{name}' because its configuration is broken: {errors}", + "service_reload_failed": "Could not reload the service '{service}'\n\nRecent service logs:{logs}", + "service_reload_or_restart_failed": "Could not reload or restart the service '{service}'\n\nRecent service logs:{logs}", + "service_reloaded": "Service '{service}' reloaded", + "service_reloaded_or_restarted": "The service '{service}' was reloaded or restarted", "service_remove_failed": "Could not remove the service '{service}'", "service_removed": "Service '{service}' removed", - "service_reload_failed": "Could not reload the service '{service}'\n\nRecent service logs:{logs}", - "service_reloaded": "Service '{service}' reloaded", "service_restart_failed": "Could not restart the service '{service}'\n\nRecent service logs:{logs}", "service_restarted": "Service '{service}' restarted", - "service_reload_or_restart_failed": "Could not reload or restart the service '{service}'\n\nRecent service logs:{logs}", - "service_reloaded_or_restarted": "The service '{service}' was reloaded or restarted", "service_start_failed": "Could not start the service '{service}'\n\nRecent service logs:{logs}", "service_started": "Service '{service}' started", "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 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}'", - "show_tile_cant_be_enabled_for_regex": "You cannot enable 'show_tile' right no, because the URL for the permission '{permission}' is a regex", "ssowat_conf_generated": "SSOwat configuration regenerated", - "ssowat_conf_updated": "SSOwat configuration updated", "system_upgraded": "System upgraded", "system_username_exists": "Username already exists in the list of system users", "this_action_broke_dpkg": "This action broke dpkg/APT (the system package managers)... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.", - "tools_upgrade_at_least_one": "Please specify 'apps', or 'system'", - "tools_upgrade_cant_both": "Cannot upgrade both system and apps at the same time", - "tools_upgrade_cant_hold_critical_packages": "Could not hold critical packages...", - "tools_upgrade_cant_unhold_critical_packages": "Could not unhold critical packages...", - "tools_upgrade_regular_packages": "Now upgrading 'regular' (non-yunohost-related) packages...", - "tools_upgrade_regular_packages_failed": "Could not upgrade packages: {packages_list}", - "tools_upgrade_special_packages": "Now upgrading 'special' (yunohost-related) packages...", - "tools_upgrade_special_packages_explanation": "The special upgrade will continue in the background. Please don't start any other actions on your server for the next ~10 minutes (depending on hardware speed). After this, you may have to re-log in to the webadmin. The upgrade log will be available in Tools → Log (in the webadmin) or using 'yunohost log list' (from the command-line).", - "tools_upgrade_special_packages_completed": "YunoHost package upgrade completed.\nPress [Enter] to get the command line back", + "tools_upgrade": "Upgrading system packages", + "tools_upgrade_failed": "Could not upgrade packages: {packages_list}", "unbackup_app": "{app} will not be saved", "unexpected_error": "Something unexpected went wrong: {error}", "unknown_main_domain_path": "Unknown domain or path for '{app}'. You need to specify a domain and a path to be able to specify a URL for permission.", @@ -643,19 +670,19 @@ "user_deleted": "User deleted", "user_deletion_failed": "Could not delete user {user}: {error}", "user_home_creation_failed": "Could not create home folder '{home}' for user", + "user_import_bad_file": "Your CSV file is not correctly formatted it will be ignored to avoid potential data loss", + "user_import_bad_line": "Incorrect line {line}: {details}", + "user_import_failed": "The users import operation completely failed", + "user_import_missing_columns": "The following columns are missing: {columns}", + "user_import_nothing_to_do": "No user needs to be imported", + "user_import_partial_failed": "The users import operation partially failed", + "user_import_success": "Users successfully imported", "user_unknown": "Unknown user: {user}", "user_update_failed": "Could not update user {user}: {error}", "user_updated": "User info changed", - "user_import_bad_line": "Incorrect line {line}: {details}", - "user_import_bad_file": "Your CSV file is not correctly formatted it will be ignored to avoid potential data loss", - "user_import_missing_columns": "The following columns are missing: {columns}", - "user_import_partial_failed": "The users import operation partially failed", - "user_import_failed": "The users import operation completely failed", - "user_import_nothing_to_do": "No user needs to be imported", - "user_import_success": "Users successfully imported", "yunohost_already_installed": "YunoHost is already installed", "yunohost_configured": "YunoHost is now configured", "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - adding a first user through the 'Users' section of the webadmin (or 'yunohost user create ' in command-line);\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." -} +} \ No newline at end of file diff --git a/locales/eo.json b/locales/eo.json index f40111f04..8ac32d4ce 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -28,9 +28,8 @@ "admin_password": "Pasvorto de la estro", "admin_password_too_long": "Bonvolu elekti pasvorton pli mallonga ol 127 signoj", "already_up_to_date": "Nenio por fari. Ĉio estas jam ĝisdatigita.", - "app_argument_choice_invalid": "Uzu unu el ĉi tiuj elektoj '{choices}' por la argumento '{name}'", + "app_argument_choice_invalid": "Uzu unu el ĉi tiuj elektoj '{choices}' por la argumento '{name}' anstataŭ '{value}'", "app_argument_invalid": "Elektu validan valoron por la argumento '{name}': {error}", - "app_change_url_failed_nginx_reload": "Ne eblis reŝarĝi NGINX. Jen la eligo de 'nginx -t':\n{nginx_errors}", "ask_new_admin_password": "Nova administrada pasvorto", "app_action_broke_system": "Ĉi tiu ago ŝajne rompis ĉi tiujn gravajn servojn: {services}", "app_unsupported_remote_type": "Malkontrolita fora speco uzita por la apliko", @@ -52,7 +51,6 @@ "ask_new_path": "Nova vojo", "backup_custom_mount_error": "Propra rezerva metodo ne povis preterpasi la paŝon 'monto'", "app_upgrade_app_name": "Nun ĝisdatigu {app}...", - "app_manifest_invalid": "Io misas pri la aplika manifesto: {error}", "backup_cleaning_failed": "Ne povis purigi la provizoran rezervan dosierujon", "backup_creation_failed": "Ne povis krei la rezervan ar archiveivon", "backup_hook_unknown": "La rezerva hoko '{hook}' estas nekonata", @@ -116,12 +114,11 @@ "migrations_must_provide_explicit_targets": "Vi devas provizi eksplicitajn celojn kiam vi uzas '--skip' aŭ '--force-rerun'", "permission_update_failed": "Ne povis ĝisdatigi permeson '{permission}': {error}", "permission_updated": "Ĝisdatigita \"{permission}\" rajtigita", - "tools_upgrade_cant_hold_critical_packages": "Ne povis teni kritikajn pakojn…", "upnp_dev_not_found": "Neniu UPnP-aparato trovita", "pattern_password": "Devas esti almenaŭ 3 signoj longaj", "root_password_desynchronized": "La pasvorta administranto estis ŝanĝita, sed YunoHost ne povis propagandi ĉi tion al la radika pasvorto!", "service_remove_failed": "Ne povis forigi la servon '{service}'", - "backup_permission": "Rezerva permeso por app {app}", + "backup_permission": "Rezerva permeso por {app}", "log_user_group_delete": "Forigi grupon '{}'", "log_user_group_update": "Ĝisdatigi grupon '{}'", "dyndns_provider_unreachable": "Ne povas atingi la provizanton DynDNS {provider}: ĉu via YunoHost ne estas ĝuste konektita al la interreto aŭ la dyneta servilo malŝaltiĝas.", @@ -156,8 +153,6 @@ "permission_deletion_failed": "Ne povis forigi permeson '{permission}': {error}", "permission_not_found": "Permesita \"{permission}\" ne trovita", "restore_not_enough_disk_space": "Ne sufiĉa spaco (spaco: {free_space} B, necesa spaco: {needed_space} B, sekureca marĝeno: {margin} B)", - "tools_upgrade_regular_packages": "Nun ĝisdatigi 'regulajn' (ne-yunohost-rilatajn) pakojn …", - "tools_upgrade_special_packages_explanation": "La speciala ĝisdatigo daŭros en la fono. Bonvolu ne komenci aliajn agojn en via servilo dum la sekvaj ~ 10 minutoj (depende de la aparata rapideco). Post tio, vi eble devos re-ensaluti al la retadreso. La ĝisdatiga registro estos havebla en Iloj → Ensaluto (en la retadreso) aŭ uzante 'yunohost logliston' (el la komandlinio).", "unrestore_app": "App '{app}' ne restarigos", "group_created": "Grupo '{group}' kreita", "group_creation_failed": "Ne povis krei la grupon '{group}': {error}", @@ -170,7 +165,6 @@ "ip6tables_unavailable": "Vi ne povas ludi kun ip6tabloj ĉi tie. Vi estas en ujo aŭ via kerno ne subtenas ĝin", "mail_unavailable": "Ĉi tiu retpoŝta adreso estas rezervita kaj aŭtomate estos atribuita al la unua uzanto", "certmanager_domain_dns_ip_differs_from_public_ip": "La DNS 'A' rekordo por la domajno '{domain}' diferencas de la IP de ĉi tiu servilo. Se vi lastatempe modifis vian A-registron, bonvolu atendi ĝin propagandi (iuj DNS-disvastigaj kontroliloj estas disponeblaj interrete). (Se vi scias, kion vi faras, uzu '--no-checks' por malŝalti tiujn ĉekojn.)", - "tools_upgrade_special_packages_completed": "Plenumis la ĝisdatigon de pakaĵoj de YunoHost.\nPremu [Enter] por retrovi la komandlinion", "log_remove_on_failed_install": "Forigu '{}' post malsukcesa instalado", "regenconf_file_manually_modified": "La agorddosiero '{conf}' estis modifita permane kaj ne estos ĝisdatigita", "regenconf_would_be_updated": "La agordo estus aktualigita por la kategorio '{category}'", @@ -186,10 +180,8 @@ "upgrading_packages": "Ĝisdatigi pakojn…", "custom_app_url_required": "Vi devas provizi URL por altgradigi vian kutimon app {app}", "service_reload_failed": "Ne povis reŝargi la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}", - "packages_upgrade_failed": "Ne povis ĝisdatigi ĉiujn pakojn", "hook_json_return_error": "Ne povis legi revenon de hoko {path}. Eraro: {msg}. Kruda enhavo: {raw_content}", "dyndns_key_not_found": "DNS-ŝlosilo ne trovita por la domajno", - "tools_upgrade_regular_packages_failed": "Ne povis ĝisdatigi pakojn: {packages_list}", "service_start_failed": "Ne povis komenci la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}", "service_reloaded": "Servo '{service}' reŝargita", "system_upgraded": "Sistemo ĝisdatigita", @@ -197,14 +189,12 @@ "certmanager_acme_not_configured_for_domain": "Atestilo por la domajno '{domain}' ne ŝajnas esti ĝuste instalita. Bonvolu ekzekuti 'cert-instali' por ĉi tiu regado unue.", "user_update_failed": "Ne povis ĝisdatigi uzanton {user}: {error}", "restore_confirm_yunohost_installed": "Ĉu vi vere volas restarigi jam instalitan sistemon? [{answers}]", - "pattern_positive_number": "Devas esti pozitiva nombro", "update_apt_cache_failed": "Ne eblis ĝisdatigi la kaŝmemoron de APT (paka administranto de Debian). Jen rubujo de la sources.list-linioj, kiuj povus helpi identigi problemajn liniojn:\n{sourceslist}", "migrations_no_migrations_to_run": "Neniuj migradoj por funkcii", "certmanager_attempt_to_renew_nonLE_cert": "La atestilo por la domajno '{domain}' ne estas elsendita de Let's Encrypt. Ne eblas renovigi ĝin aŭtomate!", "domain_dyndns_already_subscribed": "Vi jam abonis DynDNS-domajnon", "log_letsencrypt_cert_renew": "Renovigu '{}' Let's Encrypt atestilon", "backup_output_directory_required": "Vi devas provizi elirejan dosierujon por la sekurkopio", - "tools_upgrade_cant_unhold_critical_packages": "Ne povis malŝalti kritikajn pakojn…", "log_link_to_log": "Plena ŝtipo de ĉi tiu operacio: '{desc} '", "global_settings_cant_serialize_settings": "Ne eblis serialigi datumojn pri agordoj, motivo: {reason}", "backup_running_hooks": "Kurado de apogaj hokoj …", @@ -221,7 +211,6 @@ "pattern_mailbox_quota": "Devas esti grandeco kun la sufikso b/k/M/G/T aŭ 0 por ne havi kvoton", "user_deletion_failed": "Ne povis forigi uzanton {user}: {error}", "backup_with_no_backup_script_for_app": "La app '{app}' ne havas sekretan skripton. Ignorante.", - "service_regen_conf_is_deprecated": "'yunohost service regen-conf' malakceptas! Bonvolu uzi anstataŭe 'yunohost tools regen-conf'.", "global_settings_key_doesnt_exists": "La ŝlosilo '{settings_key}' ne ekzistas en la tutmondaj agordoj, vi povas vidi ĉiujn disponeblajn klavojn per uzado de 'yunohost settings list'", "dyndns_no_domain_registered": "Neniu domajno registrita ĉe DynDNS", "dyndns_could_not_check_available": "Ne povis kontroli ĉu {domain} haveblas sur {provider}.", @@ -237,29 +226,24 @@ "service_stop_failed": "Ne povis maldaŭrigi la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}", "unbackup_app": "App '{app}' ne konserviĝos", "updating_apt_cache": "Akirante haveblajn ĝisdatigojn por sistemaj pakoj…", - "tools_upgrade_at_least_one": "Bonvolu specifi '--apps' aŭ '--system'", "service_already_stopped": "La servo '{service}' jam ĉesis", - "tools_upgrade_cant_both": "Ne eblas ĝisdatigi ambaŭ sistemon kaj programojn samtempe", "restore_extracting": "Eltirante bezonatajn dosierojn el la ar theivo…", "upnp_port_open_failed": "Ne povis malfermi havenon per UPnP", "log_app_upgrade": "Ĝisdatigu la aplikon '{}'", "log_help_to_get_failed_log": "La operacio '{desc}' ne povis finiĝi. Bonvolu dividi la plenan ŝtipon de ĉi tiu operacio per la komando 'yunohost log share {name}' por akiri helpon", "port_already_closed": "Haveno {port} estas jam fermita por {ip_version} rilatoj", "hook_name_unknown": "Nekonata hoko-nomo '{name}'", - "dyndns_could_not_check_provide": "Ne povis kontroli ĉu {provider} povas provizi {domain}.", "restore_nothings_done": "Nenio estis restarigita", "log_tools_postinstall": "Afiŝu vian servilon YunoHost", "dyndns_unavailable": "La domajno '{domain}' ne haveblas.", "experimental_feature": "Averto: Ĉi tiu funkcio estas eksperimenta kaj ne konsiderata stabila, vi ne uzu ĝin krom se vi scias kion vi faras.", "root_password_replaced_by_admin_password": "Via radika pasvorto estis anstataŭigita per via administra pasvorto.", - "domain_unknown": "Nekonata domajno", "global_settings_setting_security_password_user_strength": "Uzanto pasvorta forto", "restore_may_be_not_enough_disk_space": "Via sistemo ne ŝajnas havi sufiĉe da spaco (libera: {free_space} B, necesa spaco: {needed_space} B, sekureca marĝeno: {margin} B)", "log_corrupted_md_file": "La YAD-metadata dosiero asociita kun protokoloj estas damaĝita: '{md_file}\nEraro: {error} '", "downloading": "Elŝutante …", "user_deleted": "Uzanto forigita", "service_enable_failed": "Ne povis fari la servon '{service}' aŭtomate komenci ĉe la ekkuro.\n\nLastatempaj servaj protokoloj: {logs}", - "tools_upgrade_special_packages": "Nun ĝisdatigi 'specialajn' (rilatajn al yunohost)…", "domains_available": "Haveblaj domajnoj:", "dyndns_registered": "Registrita domajno DynDNS", "service_description_fail2ban": "Protektas kontraŭ bruta forto kaj aliaj specoj de atakoj de la interreto", @@ -285,7 +269,6 @@ "regenconf_file_remove_failed": "Ne povis forigi la agordodosieron '{conf}'", "not_enough_disk_space": "Ne sufiĉe libera spaco sur '{path}'", "dyndns_ip_update_failed": "Ne povis ĝisdatigi IP-adreson al DynDNS", - "ssowat_conf_updated": "SSOwat-agordo ĝisdatigita", "log_link_to_failed_log": "Ne povis plenumi la operacion '{desc}'. Bonvolu provizi la plenan protokolon de ĉi tiu operacio per alklakante ĉi tie por akiri helpon", "user_home_creation_failed": "Ne povis krei dosierujon \"home\" por uzanto", "pattern_backup_archive_name": "Devas esti valida dosiernomo kun maksimume 30 signoj, alfanombraj kaj -_. signoj nur", @@ -326,7 +309,7 @@ "log_dyndns_subscribe": "Aboni al YunoHost-subdominio '{}'", "password_too_simple_4": "La pasvorto bezonas almenaŭ 12 signojn kaj enhavas ciferon, majuskle, pli malaltan kaj specialajn signojn", "regenconf_file_updated": "Agordodosiero '{conf}' ĝisdatigita", - "log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log show {name}{name}'", + "log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log show {name}'", "global_settings_setting_security_nginx_compatibility": "Kongruo vs sekureca kompromiso por la TTT-servilo NGINX. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", "restore_complete": "Restarigita", "hook_exec_failed": "Ne povis funkcii skripto: {path}", @@ -339,7 +322,6 @@ "regenconf_file_copy_failed": "Ne povis kopii la novan agordodosieron '{new}' al '{conf}'", "restore_already_installed_app": "App kun la ID '{app}' estas jam instalita", "mail_domain_unknown": "Nevalida retadreso por domajno '{domain}'. Bonvolu uzi domajnon administritan de ĉi tiu servilo.", - "migrations_cant_reach_migration_file": "Ne povis aliri migrajn dosierojn ĉe la vojo '% s'", "pattern_email": "Devas esti valida retpoŝta adreso (t.e. iu@ekzemple.com)", "mail_alias_remove_failed": "Ne povis forigi retpoŝton alias '{mail}'", "regenconf_file_manually_removed": "La dosiero de agordo '{conf}' estis forigita permane, kaj ne estos kreita", @@ -491,8 +473,6 @@ "diagnosis_description_ports": "Ekspoziciaj havenoj", "diagnosis_description_mail": "Retpoŝto", "log_app_action_run": "Funkciigu agon de la apliko '{}'", - "log_app_config_show_panel": "Montri la agordan panelon de la apliko '{}'", - "log_app_config_apply": "Apliki agordon al la apliko '{}'", "diagnosis_never_ran_yet": "Ŝajnas, ke ĉi tiu servilo estis instalita antaŭ nelonge kaj estas neniu diagnoza raporto por montri. Vi devas komenci kurante plenan diagnozon, ĉu de la retadministro aŭ uzante 'yunohost diagnosis run' el la komandlinio.", "certmanager_warning_subdomain_dns_record": "Subdominio '{subdomain}' ne solvas al la sama IP-adreso kiel '{domain}'. Iuj funkcioj ne estos haveblaj ĝis vi riparos ĉi tion kaj regeneros la atestilon.", "diagnosis_basesystem_hardware": "Arkitekturo de servila aparataro estas {virt} {arch}", diff --git a/locales/es.json b/locales/es.json index 9af875898..200057560 100644 --- a/locales/es.json +++ b/locales/es.json @@ -4,17 +4,16 @@ "admin_password_change_failed": "No se pudo cambiar la contraseña", "admin_password_changed": "La contraseña de administración fue cambiada", "app_already_installed": "{app} ya está instalada", - "app_argument_choice_invalid": "Use una de estas opciones «{choices}» para el argumento «{name}»", + "app_argument_choice_invalid": "Elija un valor válido para el argumento '{name}': '{value}' no se encuentra entre las opciones disponibles ({choices})", "app_argument_invalid": "Elija un valor válido para el argumento «{name}»: {error}", "app_argument_required": "Se requiere el argumento '{name} 7'", "app_extraction_failed": "No se pudieron extraer los archivos de instalación", "app_id_invalid": "ID de la aplicación no válida", "app_install_files_invalid": "Estos archivos no se pueden instalar", - "app_manifest_invalid": "Algo va mal con el manifiesto de la aplicación: {error}", "app_not_correctly_installed": "La aplicación {app} 8 parece estar incorrectamente instalada", "app_not_installed": "No se pudo encontrar «{app}» en la lista de aplicaciones instaladas: {all_apps}", "app_not_properly_removed": "La {app} 0 no ha sido desinstalada correctamente", - "app_removed": "Eliminado {app}", + "app_removed": "{app} Desinstalado", "app_requirements_checking": "Comprobando los paquetes necesarios para {app}…", "app_requirements_unmeet": "No se cumplen los requisitos para {app}, el paquete {pkgname} ({version}) debe ser {spec}", "app_sources_fetch_failed": "No se pudieron obtener los archivos con el código fuente, ¿es el URL correcto?", @@ -52,8 +51,7 @@ "domain_dyndns_already_subscribed": "Ya se ha suscrito a un dominio de DynDNS", "domain_dyndns_root_unknown": "Dominio raíz de DynDNS desconocido", "domain_exists": "El dominio ya existe", - "domain_uninstall_app_first": "Estas aplicaciones están todavía instaladas en tu dominio:\n{apps}\n\nPor favor desinstálalas utilizando yunohost app remove the_app_id o cambialas a otro dominio usando yunohost app change-url the_app_id antes de continuar con el borrado del dominio.", - "domain_unknown": "Dominio desconocido", + "domain_uninstall_app_first": "Estas aplicaciones siguen instaladas en tu dominio:\n{apps}\n\nPor favor desinstálalas con el comando 'yunohost app remove the_app_id' o cámbialas a otro dominio usando pulsando aquí", - "log_help_to_get_log": "Para ver el registro de la operación «{desc}», ejecute la orden «yunohost log show {name}{name}»", + "log_help_to_get_log": "Para ver el registro de la operación «{desc}», ejecute la orden «yunohost log show {name}»", "log_link_to_log": "Registro completo de esta operación: «{desc}»", "log_corrupted_md_file": "El archivo de metadatos YAML asociado con el registro está dañado: «{md_file}\nError: {error}»", "hook_json_return_error": "No se pudo leer la respuesta del gancho {path}. Error: {msg}. Contenido sin procesar: {raw_content}", @@ -336,7 +318,7 @@ "group_deleted": "Eliminado el grupo «{group}»", "group_creation_failed": "No se pudo crear el grupo «{group}»: {error}", "group_created": "Creado el grupo «{group}»", - "good_practices_about_admin_password": "Ahora está a punto de definir una nueva contraseña de administración. La contraseña debe tener al menos 8 caracteres, aunque es una buena práctica usar una contraseña más larga (es decir, una frase de contraseña) y / o usar una variación de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).", + "good_practices_about_admin_password": "Ahora está a punto de definir una nueva contraseña de usuario. La contraseña debe tener al menos 8 caracteres, aunque es una buena práctica usar una contraseña más larga (es decir, una frase de contraseña) y / o una variación de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).", "global_settings_unknown_type": "Situación imprevista, la configuración {setting} parece tener el tipo {unknown_type} pero no es un tipo compatible con el sistema.", "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permitir el uso de la llave (obsoleta) DSA para la configuración del demonio SSH", "global_settings_unknown_setting_from_settings_file": "Clave desconocida en la configuración: «{setting_key}», desechada y guardada en /etc/yunohost/settings-unknown.json", @@ -354,11 +336,11 @@ "global_settings_bad_choice_for_enum": "Opción errónea para la configuración {setting}, obtuvo «{choice}» pero las opciones disponibles son: {available_choices}", "file_does_not_exist": "El archivo {path} no existe.", "dyndns_could_not_check_available": "No se pudo comprobar si {domain} está disponible en {provider}.", - "domain_dns_conf_is_just_a_recommendation": "Esta orden muestra la configuración *recomendada*. No configura el DNS en realidad. Es su responsabilidad configurar la zona de DNS en su registrador según esta recomendación.", + "domain_dns_conf_is_just_a_recommendation": "Este comando muestra la configuración *recomendada*. No configura las entradas DNS por ti. Es tu responsabilidad configurar la zona DNS en su registrador según esta recomendación.", "dpkg_lock_not_available": "Esta orden no se puede ejecutar en este momento ,parece que programa está usando el bloqueo de dpkg (el gestor de paquetes del sistema)", "dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/APT (los gestores de paquetes del sistema) parecen estar mal configurados... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo apt install --fix-broken` y/o `sudo dpkg --configure -a`.", - "confirm_app_install_thirdparty": "¡PELIGRO! Esta aplicación no forma parte del catálogo de aplicaciones de Yunohost. La instalación de aplicaciones de terceros puede comprometer la integridad y la seguridad de su sistema. Probablemente NO debería instalarlo a menos que sepa lo que está haciendo. NO se proporcionará SOPORTE si esta aplicación no funciona o rompe su sistema ... Si de todos modos está dispuesto a correr ese riesgo, escriba '{answers}'", - "confirm_app_install_danger": "¡PELIGRO! ¡Se sabe que esta aplicación sigue siendo experimental (si no explícitamente no funciona)! Probablemente NO debería instalarlo a menos que sepa lo que está haciendo. NO se proporcionará SOPORTE si esta aplicación no funciona o rompe su sistema ... Si de todos modos está dispuesto a correr ese riesgo, escriba '{answers}'", + "confirm_app_install_thirdparty": "¡PELIGRO! Esta aplicación no forma parte del catálogo de aplicaciones de YunoHost. La instalación de aplicaciones de terceros puede comprometer la integridad y seguridad de tu sistema. Probablemente NO deberías instalarla a menos que sepas lo que estás haciendo. NO se proporcionará NINGÚN SOPORTE si esta aplicación no funciona o rompe su sistema… Si de todos modos quieres correr ese riesgo, escribe '{answers}'", + "confirm_app_install_danger": "¡PELIGRO! ¡Esta aplicación sigue siendo experimental (si no es expresamente no funcional)! Probablemente NO deberías instalarla a menos que sepas lo que estás haciendo. NO se proporcionará NINGÚN SOPORTE si esta aplicación no funciona o rompe tu sistema… Si de todos modos quieres correr ese riesgo, escribe '{answers}'", "confirm_app_install_warning": "Aviso: esta aplicación puede funcionar pero no está bien integrada en YunoHost. Algunas herramientas como la autentificación única y respaldo/restauración podrían no estar disponibles. ¿Instalar de todos modos? [{answers}] ", "backup_unable_to_organize_files": "No se pudo usar el método rápido de organización de los archivos en el archivo", "backup_permission": "Permiso de respaldo para {app}", @@ -419,7 +401,7 @@ "diagnosis_cache_still_valid": "(Caché aún válida para el diagnóstico de {category}. ¡No se volvera a comprobar de momento!)", "diagnosis_found_errors_and_warnings": "¡Encontrado(s) error(es) significativo(s) {errors} (y aviso(s) {warnings}) relacionado(s) con {category}!", "apps_catalog_init_success": "¡Sistema de catálogo de aplicaciones inicializado!", - "apps_catalog_updating": "Actualizando el catálogo de aplicaciones…", + "apps_catalog_updating": "Actualiza el catálogo de aplicaciones…", "apps_catalog_failed_to_download": "No se puede descargar el catálogo de aplicaciones {apps_catalog}: {error}", "apps_catalog_obsolete_cache": "El caché del catálogo de aplicaciones está vacío u obsoleto.", "apps_catalog_update_success": "¡El catálogo de aplicaciones ha sido actualizado!", @@ -427,15 +409,15 @@ "diagnosis_ignored_issues": "(+ {nb_ignored} problema(s) ignorado(s))", "diagnosis_found_errors": "¡Encontrado(s) error(es) significativo(s) {errors} relacionado(s) con {category}!", "diagnosis_found_warnings": "Encontrado elemento(s) {warnings} que puede(n) ser mejorado(s) para {category}.", - "diagnosis_everything_ok": "¡Todo se ve bien para {category}!", + "diagnosis_everything_ok": "¡Todo correcto en {category}!", "app_upgrade_script_failed": "Ha ocurrido un error en el script de actualización de la app", "diagnosis_no_cache": "Todavía no hay una caché de diagnóstico para la categoría '{category}'", "diagnosis_ip_no_ipv4": "El servidor no cuenta con ipv4 funcional.", "diagnosis_ip_not_connected_at_all": "¿¡Está conectado el servidor a internet!?", "diagnosis_ip_broken_resolvconf": "La resolución de nombres de dominio parece no funcionar en tu servidor, lo que parece estar relacionado con que /etc/resolv.conf no apunta a 127.0.0.1.", "diagnosis_dns_missing_record": "Según la configuración DNS recomendada, deberías añadir un registro DNS\ntipo: {type}\nnombre: {name}\nvalor: {value}", - "diagnosis_diskusage_low": "El almacenamiento {mountpoint} (en dispositivo {device}) solo tiene {free} ({free_percent}%) de espacio disponible. Ten cuidado.", - "diagnosis_services_bad_status_tip": "Puedes intentar reiniciar el servicio, y si no funciona, echar un vistazo a los logs del servicio usando 'yunohost service log {service}' o a través de la sección 'Servicios' en webadmin.", + "diagnosis_diskusage_low": "El almacenamiento {mountpoint} (en el dispositivo {device}) solo tiene {free} ({free_percent}%) de espacio disponible (de {total}). Ten cuidado.", + "diagnosis_services_bad_status_tip": "Puedes intentar reiniciar el servicio, y si no funciona, echar un vistazo a los logs del serviciode la administración web (desde la línea de comandos puedes hacerlo con yunohost service restart {service} y yunohost service log {service}).", "diagnosis_ip_connected_ipv6": "¡El servidor está conectado a internet a través de IPv6!", "diagnosis_ip_no_ipv6": "El servidor no cuenta con IPv6 funcional.", "diagnosis_ip_dnsresolution_working": "¡DNS no está funcionando!", @@ -446,8 +428,8 @@ "diagnosis_dns_bad_conf": "Algunos registros DNS faltan o están mal cofigurados para el dominio {domain} (categoría {category})", "diagnosis_dns_discrepancy": "El siguiente registro DNS parace que no sigue la configuración recomendada
Tipo: {type}
Nombre: {name}
Valor Actual: {current}
Valor esperado: {value}", "diagnosis_services_bad_status": "El servicio {service} está {status} :(", - "diagnosis_diskusage_verylow": "El almacenamiento {mountpoint} (en el dispositivo {device}) sólo tiene {free} ({free_percent}%) de espacio disponible. Deberías considerar la posibilidad de limpiar algo de espacio.", - "diagnosis_diskusage_ok": "¡El almacenamiento {mountpoint} (en el dispositivo {device}) todavía tiene {free} ({free_percent}%) de espacio libre!", + "diagnosis_diskusage_verylow": "El almacenamiento {mountpoint}(en el dispositivo {device}) sólo tiene {free} ({free_percent}%) de espacio disponible(de {total}). ¡Deberías limpiar algo de espacio!", + "diagnosis_diskusage_ok": "¡El almacenamiento {mountpoint} (en el dispositivo {device}) todavía tiene {free} ({free_percent}%) de espacio libre (de {total})!", "diagnosis_services_conf_broken": "¡Mala configuración para el servicio {service}!", "diagnosis_services_running": "¡El servicio {service} está en ejecución!", "diagnosis_failed": "Error al obtener el resultado del diagnóstico para la categoría '{category}': {error}", @@ -460,7 +442,7 @@ "diagnosis_swap_notsomuch": "Al sistema le queda solamente {total} de espacio de intercambio. Considera agregar al menos {recommended} para evitar que el sistema se quede sin memoria.", "diagnosis_mail_outgoing_port_25_blocked": "El puerto de salida 25 parece estar bloqueado. Intenta desbloquearlo con el panel de configuración de tu proveedor de servicios de Internet (o proveedor de halbergue). Mientras tanto, el servidor no podrá enviar correos electrónicos a otros servidores.", "diagnosis_regenconf_allgood": "Todos los archivos de configuración están en linea con la configuración recomendada!", - "diagnosis_regenconf_manually_modified": "El archivo de configuración {file} parece que ha sido modificado manualmente.", + "diagnosis_regenconf_manually_modified": "El archivo de configuración {file} parece que ha sido modificado manualmente.", "diagnosis_regenconf_manually_modified_details": "¡Esto probablemente esta BIEN si sabes lo que estás haciendo! YunoHost dejará de actualizar este fichero automáticamente... Pero ten en cuenta que las actualizaciones de YunoHost pueden contener importantes cambios que están recomendados. Si quieres puedes comprobar las diferencias mediante yunohost tools regen-conf {category} --dry-run --with-diff o puedes forzar el volver a las opciones recomendadas mediante el comando yunohost tools regen-conf {category} --force", "diagnosis_security_vulnerable_to_meltdown": "Pareces vulnerable a el colapso de vulnerabilidad critica de seguridad", "diagnosis_description_basesystem": "Sistema de base", @@ -480,8 +462,6 @@ "diagnosis_description_web": "Web", "diagnosis_basesystem_hardware": "La arquitectura material del servidor es {virt} {arch}", "log_domain_main_domain": "Hacer de '{}' el dominio principal", - "log_app_config_apply": "Aplica la configuración de la aplicación '{}'", - "log_app_config_show_panel": "Muestra el panel de configuración de la aplicación '{}'", "log_app_action_run": "Inicializa la acción de la aplicación '{}'", "group_already_exist_on_system_but_removing_it": "El grupo {group} ya existe en los grupos del sistema, pero YunoHost lo suprimirá …", "global_settings_setting_pop3_enabled": "Habilita el protocolo POP3 para el servidor de correo electrónico", @@ -511,11 +491,11 @@ "diagnosis_domain_expiration_not_found_details": "¿Parece que la información de WHOIS para el dominio {domain} no contiene información sobre la fecha de expiración?", "diagnosis_domain_not_found_details": "¡El dominio {domain} no existe en la base de datos WHOIS o ha expirado!", "diagnosis_domain_expiration_not_found": "No se pudo revisar la fecha de expiración para algunos dominios", - "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por Yunohost. Si no es el caso, puede intentar forzar una actualización ejecutando yunohost dyndns update --force.", + "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por YunoHost. Si no es el caso, puedes intentar forzar una actualización mediante yunohost dyndns update --force.", "diagnosis_ip_local": "IP Local: {local}", "diagnosis_ip_no_ipv6_tip": "Tener IPv6 funcionando no es obligatorio para que su servidor funcione, pero es mejor para la salud del Internet en general. IPv6 debería ser configurado automáticamente por el sistema o su proveedor si está disponible. De otra manera, es posible que tenga que configurar varias cosas manualmente, tal y como se explica en esta documentación https://yunohost.org/#/ipv6. Si no puede habilitar IPv6 o si parece demasiado técnico, puede ignorar esta advertencia con toda seguridad.", "diagnosis_display_tip": "Para ver los problemas encontrados, puede ir a la sección de diagnóstico del webadmin, o ejecutar 'yunohost diagnosis show --issues --human-readable' en la línea de comandos.", - "diagnosis_package_installed_from_sury_details": "Algunos paquetes fueron accidentalmente instalados de un repositorio de terceros llamado Sury. El equipo Yunohost ha mejorado la estrategia para manejar estos pquetes, pero es posible que algunas instalaciones con aplicaciones de PHP7.3 en Stretch puedan tener algunas inconsistencias. Para solucionar esta situación, debería intentar ejecutar el siguiente comando: {cmd_to_fix}", + "diagnosis_package_installed_from_sury_details": "Algunos paquetes fueron accidentalmente instalados de un repositorio de terceros llamado Sury. El equipo YunoHost ha mejorado la estrategia para manejar estos paquetes, pero es posible que algunas configuraciones que han instalado aplicaciones PHP7.3 al tiempo que presentes en Stretch tienen algunas inconsistencias. Para solucionar esta situación, deberías intentar ejecutar el siguiente comando: {cmd_to_fix}", "diagnosis_package_installed_from_sury": "Algunos paquetes del sistema deberían ser devueltos a una versión anterior", "certmanager_domain_not_diagnosed_yet": "Aún no hay resultado del diagnóstico para el dominio {domain}. Por favor ejecute el diagnóstico para las categorías 'Registros DNS' y 'Web' en la sección de diagnóstico para verificar si el dominio está listo para Let's Encrypt. (O si sabe lo que está haciendo, utilice '--no-checks' para deshabilitar esos chequeos.)", "backup_archive_corrupted": "Parece que el archivo de respaldo '{archive}' está corrupto : {error}", @@ -525,23 +505,10 @@ "app_manifest_install_ask_is_public": "¿Debería exponerse esta aplicación a visitantes anónimos?", "app_manifest_install_ask_admin": "Elija un usuario administrativo para esta aplicación", "app_manifest_install_ask_password": "Elija una contraseña de administración para esta aplicación", - "app_manifest_install_ask_path": "Seleccione el path donde esta aplicación debería ser instalada", + "app_manifest_install_ask_path": "Seleccione la ruta de URL (después del dominio) donde esta aplicación debería ser instalada", "app_manifest_install_ask_domain": "Seleccione el dominio donde esta app debería ser instalada", "app_label_deprecated": "Este comando está depreciado! Favor usar el nuevo comando 'yunohost user permission update' para administrar la etiqueta de app.", "app_argument_password_no_default": "Error al interpretar argumento de contraseña'{name}': El argumento de contraseña no puede tener un valor por defecto por razón de seguridad", - "migration_0015_not_enough_free_space": "¡El espacio es muy bajo en `/var/`! Deberías tener almenos 1Gb de espacio libre para ejecutar la migración.", - "migration_0015_not_stretch": "¡La distribución actual de Debian no es Stretch!", - "migration_0015_yunohost_upgrade": "Iniciando la actualización del núcleo de YunoHost...", - "migration_0015_still_on_stretch_after_main_upgrade": "Algo fue mal durante la actualización principal, el sistema parece que está todavía en Debian Stretch", - "migration_0015_main_upgrade": "Comenzando la actualización principal...", - "migration_0015_patching_sources_list": "Adaptando las sources.lists...", - "migration_0015_start": "Comenzando la migración a Buster", - "migration_description_0019_extend_permissions_features": "Extiende/rehaz el sistema de gestión de permisos de la aplicación", - "migration_description_0018_xtable_to_nftable": "Migra las viejas reglas de tráfico de red al nuevo sistema nftable", - "migration_description_0017_postgresql_9p6_to_11": "Migra las bases de datos de PostgreSQL 9.6 a 11", - "migration_description_0016_php70_to_php73_pools": "Migra el «pool» de ficheros php7.0-fpm a php7.3", - "migration_description_0015_migrate_to_buster": "Actualiza el sistema a Debian Buster y YunoHost 4.x", - "migrating_legacy_permission_settings": "Migrando los antiguos parámetros de permisos...", "invalid_regex": "Regex no valido: «{regex}»", "global_settings_setting_backup_compress_tar_archives": "Cuando se creen nuevas copias de respaldo, comprimir los archivos (.tar.gz) en lugar de descomprimir los archivos (.tar). N.B.: activar esta opción quiere decir que los archivos serán más pequeños pero que el proceso tardará más y utilizará más CPU.", "global_settings_setting_smtp_relay_password": "Clave de uso del SMTP", @@ -549,7 +516,6 @@ "global_settings_setting_smtp_relay_port": "Puerto de envio / relay SMTP", "global_settings_setting_smtp_relay_host": "El servidor relay de SMTP para enviar correo en lugar de esta instalación YunoHost. Útil si estás en una de estas situaciones: tu puerto 25 esta bloqueado por tu ISP o VPS, si estás en usado una IP marcada como residencial o DUHL, si no puedes configurar un DNS inverso o si el servidor no está directamente expuesto a internet y quieres utilizar otro servidor para enviar correos.", "global_settings_setting_smtp_allow_ipv6": "Permitir el uso de IPv6 para enviar y recibir correo", - "domain_name_unknown": "Dominio «{domain}» desconocido", "diagnosis_processes_killed_by_oom_reaper": "Algunos procesos fueron terminados por el sistema recientemente porque se quedó sin memoria. Típicamente es sintoma de falta de memoria o de un proceso que se adjudicó demasiada memoria.
Resumen de los procesos terminados:
\n{kills_summary}", "diagnosis_http_nginx_conf_not_up_to_date_details": "Para arreglar este asunto, estudia las diferencias mediante el comando yunohost tools regen-conf nginx --dry-run --with-diff y si te parecen bien aplica los cambios mediante yunohost tools regen-conf nginx --force.", "diagnosis_http_nginx_conf_not_up_to_date": "Parece que la configuración nginx de este dominio haya sido modificada manualmente, esto no deja que YunoHost pueda diagnosticar si es accesible mediante HTTP.", @@ -579,11 +545,53 @@ "diagnosis_mail_ehlo_bad_answer_details": "Podría ser debido a otra máquina en lugar de tu servidor.", "diagnosis_mail_ehlo_bad_answer": "Un servicio que no es SMTP respondió en el puerto 25 mediante IPv{ipversion}", "diagnosis_mail_ehlo_unreachable_details": "No pudo abrirse la conexión en el puerto 25 de tu servidor mediante IPv{ipversion}. Parece que no se puede contactar.
1. La causa más común en estos casos suele ser que el puerto 25 no está correctamente redireccionado a tu servidor.
2. También deberías asegurarte que el servicio postfix está en marcha.
3. En casos más complejos: asegurate que no estén interfiriendo ni el firewall ni el reverse-proxy.", - "diagnosis_mail_ehlo_unreachable": "El servidor de correo SMTP no puede contactarse desde el exterior mediante IPv{ipversion}. No puede recibir correos", + "diagnosis_mail_ehlo_unreachable": "El servidor de correo SMTP no puede contactarse desde el exterior mediante IPv{ipversion}. No puede recibir correos.", "diagnosis_mail_ehlo_ok": "¡El servidor de correo SMTP puede contactarse desde el exterior por lo que puede recibir correos!", "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algunos proveedores de internet no le permitirán desbloquear el puerto 25 porque no les importa la Neutralidad de la Red.
- Algunos proporcionan una alternativa usando un relay como servidor de correo lo que implica que el relay podrá espiar tu trafico de correo.
- Una alternativa buena para la privacidad es utilizar una VPN *con una IP pública dedicada* para evitar estas limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Otra alternativa es cambiar de proveedor de inteernet a uno más amable con la Neutralidad de la Red", "diagnosis_backports_in_sources_list": "Parece que apt (el gestor de paquetes) está configurado para usar el repositorio backports. A menos que realmente sepas lo que estás haciendo, desaconsejamos absolutamente instalar paquetes desde backports, ya que pueden provocar comportamientos intestables o conflictos en el sistema.", "diagnosis_basesystem_hardware_model": "El modelo de servidor es {model}", "additional_urls_already_removed": "La URL adicional «{url}» ya se ha eliminado para el permiso «{permission}»", - "additional_urls_already_added": "La URL adicional «{url}» ya se ha añadido para el permiso «{permission}»" + "additional_urls_already_added": "La URL adicional «{url}» ya se ha añadido para el permiso «{permission}»", + "config_apply_failed": "Falló la aplicación de la nueva configuración: {error}", + "app_restore_script_failed": "Ha ocurrido un error dentro del script de restauración de aplicaciones", + "app_config_unable_to_apply": "No se pudieron aplicar los valores del panel configuración.", + "app_config_unable_to_read": "No se pudieron leer los valores del panel configuración.", + "backup_create_size_estimation": "El archivo contendrá aproximadamente {size} de datos.", + "config_cant_set_value_on_section": "No puede establecer un único valor en una sección de configuración completa.", + "diagnosis_http_special_use_tld": "Le dominio {domain} está basado en un dominio de primer nivel (TLD) de uso especial, como un .local o .test y no debería estar expuesto fuera de la red local.", + "domain_dns_push_failed": "La actualización de las entradas DNS ha fallado.", + "domain_dns_push_partial_failure": "Entradas DNS actualizadas parcialmente: algunas advertencias/errores reportados.", + "domain_unknown": "Dominio '{domain}' desconocido", + "diagnosis_high_number_auth_failures": "Ultimamente ha habido un gran número de errores de autenticación. Asegúrate de que Fail2Ban está ejecutándose y correctamente configurado, o usa un puerto SSH personalizado como se explica en https://yunohost.org/security.", + "diagnosis_sshd_config_inconsistent": "Parece que el puerto SSH ha sido modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, hay un nuevo parámetro global 'security.ssh.port' disponible para evitar modificar manualmente la configuración.", + "diagnosis_sshd_config_inconsistent_details": "Por favor ejecuta yunohost settings set security.ssh.port -v TU_PUERTO_SSH para definir el puerto SSH, y comprueba yunohost tools regen-conf ssh --dry-run --with-diff y yunohost tools regen-conf ssh --force para resetear tu configuración a las recomendaciones de YunoHost.", + "config_forbidden_keyword": "'{keyword}' es una palabra reservada, no puedes crear ni usar un panel de configuración con una pregunta que use esta id.", + "config_no_panel": "No se ha encontrado ningún panel de configuración.", + "config_unknown_filter_key": "La clave de filtrado '{filter_key}' es incorrecta.", + "config_validate_color": "Debe ser un valor hexadecimal RGB correcto", + "danger": "Peligro:", + "diagnosis_apps_issue": "Se ha detectado un problema con la aplicación {app}", + "diagnosis_description_apps": "Aplicaciones", + "diagnosis_rootfstotalspace_critical": "¡El sistema de ficheros raíz solo tiene un total de {space}! ¡Vas a quedarte sin espacio rápidamente! Se recomienda tener al menos 16GB para ese sistema de ficheros.", + "diagnosis_rootfstotalspace_warning": "¡El sistema de ficheros raíz solo tiene un total de {space}! Podría ser suficiente, pero cuidado, puedes rellenarlo rápidamente… Se recomienda tener al menos 16GB para el sistema de ficheros raíz.", + "diagnosis_apps_allgood": "Todas las aplicaciones instaladas respetan las prácticas básicas de empaquetado", + "config_validate_date": "Debe ser una fecha válida en formato AAAA-MM-DD", + "config_validate_email": "Debe ser una dirección de correo correcta", + "config_validate_time": "Debe ser una hora valida en formato HH:MM", + "config_validate_url": "Debe ser una URL válida", + "config_version_not_supported": "Las versiones del panel de configuración '{version}' no están soportadas.", + "domain_remove_confirm_apps_removal": "La supresión de este dominio también eliminará las siguientes aplicaciones:\n{apps}\n\n¿Seguro? [{answers}]", + "domain_registrar_is_not_configured": "El registrador aún no ha configurado el dominio {domain}.", + "diagnosis_apps_not_in_app_catalog": "Esta aplicación se encuentra ausente o ya no figura en el catálogo de aplicaciones de YunoHost. Deberías considerar desinstalarla ya que no recibirá actualizaciones y podría comprometer la integridad y seguridad de tu sistema.", + "disk_space_not_sufficient_install": "No hay espacio libre suficiente para instalar esta aplicación", + "disk_space_not_sufficient_update": "No hay espacio libre suficiente para actualizar esta aplicación", + "diagnosis_dns_specialusedomain": "El dominio {domain} se basa en un dominio de primer nivel (TLD) de usos especiales como .local o .test y no debería tener entradas DNS reales.", + "diagnosis_apps_bad_quality": "Esta aplicación está etiquetada como defectuosa en el catálogo de aplicaciones YunoHost. Podría ser un problema temporal mientras las personas responsables corrigen el asunto. Mientras tanto, la actualización de esta aplicación está desactivada.", + "diagnosis_apps_broken": "Esta aplicación está etiquetada como defectuosa en el catálogo de aplicaciones YunoHost. Podría ser un problema temporal mientras las personas responsables corrigen el asunto. Mientras tanto, la actualización de esta aplicación está desactivada.", + "diagnosis_apps_deprecated_practices": "La versión instalada de esta aplicación usa aún prácticas de empaquetado obsoletas. Deberías actualizarla.", + "diagnosis_apps_outdated_ynh_requirement": "La versión instalada de esta aplicación solo necesita YunoHost >= 2.x, lo que indica que no está al día con la buena praxis de ayudas y empaquetado recomendadas. Deberías actualizarla.", + "domain_dns_conf_special_use_tld": "Este dominio se basa en un dominio de primer nivel (TLD) de usos especiales como .local o .test y no debería tener entradas DNS reales.", + "diagnosis_sshd_config_insecure": "Parece que la configuración SSH ha sido modificada manualmente, y es insegura porque no tiene ninguna instrucción 'AllowGroups' o 'AllowUsers' para limitar el acceso a los usuarios autorizados.", + "domain_dns_push_not_applicable": "La configuración automática de los registros DNS no puede realizarse en el dominio {domain}. Deberìas configurar manualmente los registros DNS siguiendo la documentación.", + "domain_dns_push_managed_in_parent_domain": "La configuración automática de los registros DNS es administrada desde el dominio superior {parent_domain}." } \ No newline at end of file diff --git a/locales/eu.json b/locales/eu.json index 1891e00a3..e0ce226d5 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -1,3 +1,688 @@ { - "password_too_simple_1": "Pasahitzak gutxienez 8 karaktere izan behar ditu" + "password_too_simple_1": "Pasahitzak gutxienez zortzi karaktere izan behar ditu", + "action_invalid": "'{action}' eragiketa baliogabea da", + "aborting": "Bertan behera uzten.", + "admin_password_changed": "Administrazio-pasahitza aldatu da", + "admin_password_change_failed": "Ezinezkoa izan da pasahitza aldatzea", + "additional_urls_already_added": "'{url}' URL gehigarria '{permission}' baimenerako gehitu da dagoeneko", + "additional_urls_already_removed": "'{url}' URL gehigarriari '{permission}' baimena kendu zaio dagoeneko", + "admin_password": "Administrazio-pasahitza", + "diagnosis_ip_global": "IP orokorra: {global}", + "app_argument_password_no_default": "Errorea egon da '{name}' pasahitzaren argumentua ikuskatzean: pasahitzak ezin du balio hori izan segurtasuna dela-eta", + "app_extraction_failed": "Ezinezkoa izan da instalazio fitxategiak ateratzea", + "app_requirements_unmeet": "{app}(e)k behar dituen baldintzak ez dira betetzen, {pkgname} ({version}) paketea {spec} izan behar da", + "backup_deleted": "Babeskopia ezabatuta", + "app_argument_required": "'{name}' argumentua ezinbestekoa da", + "certmanager_acme_not_configured_for_domain": "Ezinezkoa da ACME azterketa {domain} domeinurako burutzea une honetan nginx ezarpenek ez dutelako beharrezko kodea… Egiaztatu nginx ezarpenak egunean daudela 'yunohost tools regen-conf nginx --dry-run --with-diff' komandoa exekutatuz.", + "certmanager_domain_dns_ip_differs_from_public_ip": "'{domain}' domeinurako DNS balioak ez datoz bat zerbitzariaren IParekin. Mesedez, egiaztatu 'DNS balioak' (oinarrizkoa) kategoria diagnostikoen atalean. A balioak duela gutxi aldatu badituzu, itxaron hedatu daitezen (badaude DNSen hedapena ikusteko erramintak interneten). (Zertan ari zeren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", + "confirm_app_install_thirdparty": "KONTUZ! Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Kanpoko aplikazioek sistemaren integritate eta segurtasuna arriskuan jarri dezakete. Ziur asko EZ zenuke instalatu beharko zertan ari zaren ez badakizu. Aplikazio hau ez badabil edo sistema kaltetzen badu EZ DA LAGUNTZARIK EMANGO… aurrera jarraitu nahi duzu hala ere? Aukeratu '{answers}'", + "app_start_remove": "{app} ezabatzen…", + "diagnosis_http_hairpinning_issue_details": "Litekeena da erantzulea zure kable-modem / routerra izatea. Honen eraginez, saretik kanpo daudenek zerbitzaria arazorik gabe erabili ahal izango dute, baina sare lokalean bertan daudenek (ziur asko zure kasua) ezingo dute kanpoko IPa edo domeinu izena erabili zerbitzarira konektatzeko. Egoera hobetu edo guztiz konpontzeko, irakurri dokumentazioa", + "diagnosis_http_special_use_tld": "{domain} domeinua top-level domain (TLD) motakoa da .local edo .test bezala eta ez du sare lokaletik kanpo eskuragarri zertan egon.", + "diagnosis_ip_weird_resolvconf_details": "/etc/resolv.conf fitxategia symlink bat izan beharko litzateke 127.0.0.1ra adi dagoen /etc/resolvconf/run/resolv.conf fitxategira (dnsmasq). DNS ebazleak eskuz konfiguratu nahi badituzu, mesedez aldatu /etc/resolv.dnsmasq.conf fitxategia.", + "diagnosis_ip_connected_ipv4": "Zerbitzaria IPv4 bidez dago internetera konektatuta!", + "diagnosis_basesystem_ynh_inconsistent_versions": "YunoHost paketeen bertsioak ez datoz bat… ziur asko noizbait eguneraketa batek kale egin edo erabat amaitu ez zuelako.", + "diagnosis_high_number_auth_failures": "Azken aldian kale egin duten saio-hasiera saiakera ugari egon dira. Egiaztatu fail2ban martxan dabilela eta egoki konfiguratuta dagoela, edo erabili beste ataka bat SSHrako dokumentazioan azaldu bezala.", + "diagnosis_mail_ehlo_could_not_diagnose": "Ezinezkoa izan da postfix posta zerbitzaria IPv{ipversion}az kanpo eskuragarri dagoen egiaztatzea.", + "app_id_invalid": "Aplikazio ID okerra", + "app_install_files_invalid": "Ezin dira fitxategi hauek instalatu", + "diagnosis_description_ip": "Internet konexioa", + "diagnosis_description_dnsrecords": "DNS erregistroak", + "app_label_deprecated": "Komando hau zaharkitua dago! Mesedez, erabili 'yunohost user permission update' komando berria aplikazioaren etiketa kudeatzeko.", + "confirm_app_install_danger": "KONTUZ! Aplikazio hau esperimentala da (edo ez dabil)! Ez zenuke instalatu beharko zertan ari zaren ez badakizu. Aplikazio hau ez badabil edo sistema kaltetzen badu, EZ DA LAGUNTZARIK EMANGO… aurrera jarraitu nahi al duzu hala ere? Aukeratu '{answers}'", + "diagnosis_description_systemresources": "Sistemaren baliabideak", + "backup_csv_addition_failed": "Ezinezkoa izan da fitxategiak CSV fitxategira kopiatzea", + "backup_no_uncompress_archive_dir": "Ez dago horrelako deskonprimatutako fitxategi katalogorik", + "danger": "Arriskua:", + "diagnosis_dns_discrepancy": "Ez dirudi ondorengo DNS balioak bat datozenik proposatutako konfigurazioarekin:
Mota: {type}
Izena: {name}
Oraingo balioa: {current}
Proposatutako balioa: {value}", + "diagnosis_dns_specialusedomain": "{domain} domeinua top-level domain (TLD) erabilera berezikoa da .local edo .test bezala eta horregatik ez du DNS erregistrorik erabiltzeko beharrik.", + "diagnosis_http_bad_status_code": "Zerbitzari hau ez den beste gailu batek erantzun omen dio eskaerari (agian routerrak).
1. Honen arrazoi ohikoena 80 (eta 443) ataka zerbitzarira ondo birbidaltzen ez dela da.
2. Konfigurazio konplexua badarabilzu, egiaztatu suebakiak edo reverse-proxyk oztopatzen ez dutela.", + "diagnosis_http_timeout": "Denbora agortu da sare lokaletik kanpo zure zerbitzarira konektatzeko ahaleginean. Eskuragarri ez dagoela dirudi.
1. 80 (eta 443) ataka zerbitzarira modu egokian birzuzentzen ez direla da ohiko zergatia.
2. Badaezpada egiaztatu nginx martxan dagoela.
3. Konfigurazio konplexuetan, egiaztatu suebakiak edo reverse-proxyk konexioa oztopatzen ez dutela.", + "app_sources_fetch_failed": "Ezinezkoa izan da fitxategiak eskuratzea, zuzena al da URLa?", + "app_make_default_location_already_used": "Ezinezkoa izan da '{app}' '{domain}' domeinuan lehenestea, '{other_app}'(e)k dagoeneko '{domain}' erabiltzen duelako", + "app_already_installed_cant_change_url": "Aplikazio hau instalatuta dago dagoeneko. URLa ezin da aldatu aukera honekin. Markatu 'app changeurl' markatzeko moduan badago.", + "diagnosis_ip_not_connected_at_all": "Badirudi zerbitzaria ez dagoela internetera konektatuta!?", + "app_already_up_to_date": "{app} egunean da dagoeneko", + "app_change_url_success": "{app} aplikazioaren URLa {domain}{path} da orain", + "admin_password_too_long": "Mesedez, aukeratu 127 karaktere baino laburragoa den pasahitz bat", + "app_action_broke_system": "Eragiketa honek {services} zerbitzu garrantzitsua(k) hondatu d(it)uela dirudi", + "diagnosis_basesystem_hardware_model": "Zerbitzariaren modeloa {model} da", + "already_up_to_date": "Ez dago egiteko ezer. Guztia dago egunean.", + "backup_permission": "{app}(r)entzat babeskopia baimena", + "config_validate_date": "UUUU-HH-EE formatua duen data bat izan behar da", + "config_validate_email": "Benetazko posta elektronikoa izan behar da", + "config_validate_time": "OO:MM formatua duen ordu bat izan behar da", + "config_validate_url": "Benetazko URL bat izan behar da", + "config_version_not_supported": "Ezinezkoa da konfigurazio-panelaren '{version}' bertsioa erabiltzea.", + "app_restore_script_failed": "Errorea gertatu da aplikazioa lehengoratzeko aginduan", + "app_upgrade_some_app_failed": "Ezinezkoa izan da aplikazio batzuk eguneratzea", + "app_install_failed": "Ezinezkoa izan da {app} instalatzea: {error}", + "diagnosis_basesystem_kernel": "Zerbitzariak Linuxen {kernel_version} kernela darabil", + "app_argument_invalid": "Aukeratu balio egoki bat '{name}' argumenturako: {error}", + "app_already_installed": "{app} instalatuta dago dagoeneko", + "app_config_unable_to_apply": "Ezinezkoa izan da konfigurazio aukerak ezartzea.", + "app_config_unable_to_read": "Ezinezkoa izan da konfigurazio aukerak irakurtzea.", + "config_apply_failed": "Ezin izan da konfigurazio berria ezarri: {error}", + "config_cant_set_value_on_section": "Ezinezkoa da balio bakar bat ezartzea konfigurazio atal oso batean.", + "config_no_panel": "Ez da konfigurazio-panelik aurkitu.", + "diagnosis_found_errors_and_warnings": "{category} atalari dago(z)kion {errors} arazo (eta {warnings} abisu) aurkitu d(ir)a!", + "diagnosis_description_regenconf": "Sistemaren ezarpenak", + "app_upgrade_script_failed": "Errore bat gertatu da aplikazioaren eguneratze aginduan", + "diagnosis_basesystem_hardware": "Zerbitzariaren arkitektura {virt} {arch} da", + "diagnosis_mail_ehlo_ok": "SMTP posta zerbitzaria eskuragarri dago kanpoko saretik eta, beraz, posta elektronikoa jasotzeko gai da!", + "app_unknown": "Aplikazio ezezaguna", + "diagnosis_mail_ehlo_bad_answer": "SMTP ez den zerbitzu batek erantzun du IPv{ipversion}ko 25. atakan", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Errorea: {error}", + "diagnosis_mail_blacklist_ok": "Ez dirudi zerbitzari honek darabiltzan IPak eta domeinuak inolako zerrenda beltzean daudenik", + "diagnosis_domain_expiration_error": "Domeinu batzuk IRAUNGITZEAR daude!", + "diagnosis_domain_expiration_success": "Domeinuak erregistratuta daude eta ez dira oraingoz iraungiko.", + "app_manifest_install_ask_is_public": "Saiorik hasi gabeko bisitarientzat ikusgai egon beharko luke aplikazioak?", + "diagnosis_domain_expires_in": "{domain} {days} egun barru iraungiko da.", + "app_manifest_install_ask_domain": "Aukeratu zein domeinutan instalatu nahi duzun aplikazioa", + "custom_app_url_required": "URL bat zehaztu behar duzu {app} eguneratzeko", + "app_change_url_identical_domains": "Domeinu zahar eta berriaren bidea bera dira: ('{domain}{path}'), ez dago ezer egitekorik.", + "app_upgrade_failed": "Ezinezkoa izan da {app} eguneratzea: {error}", + "app_upgrade_app_name": "Orain {app} eguneratzen…", + "app_upgraded": "{app} eguneratu da", + "ask_firstname": "Izena", + "ask_lastname": "Abizena", + "ask_main_domain": "Domeinu nagusia", + "config_forbidden_keyword": "'{keyword}' etiketa sistemak bakarrik erabil dezake; ezin da ID hau daukan baliorik sortu edo erabili.", + "config_unknown_filter_key": "'{filter_key}' filtroaren kakoa ez da zuzena.", + "config_validate_color": "RGB hamaseitar kolore bat izan behar da", + "diagnosis_cant_run_because_of_dep": "Ezinezkoa da diagnosia abiaraztea {category} atalerako {dep}(r)i lotutako arazo garrantzitsuak / garrantzitsuek dirau(t)en artean.", + "diagnosis_dns_missing_record": "Proposatutako DNS konfigurazioaren arabera, ondorengo informazioa gehitu beharko zenuke DNS erregistroan:
Mota: {type}
Izena: {name}
Balioa: {value}", + "diagnosis_http_nginx_conf_not_up_to_date": "Domeinu honen nginx ezarpenak eskuz moldatu direla dirudi eta YunoHostek ezin du egiaztatu HTTP bidez eskuragarri dagoenik.", + "ask_new_admin_password": "Administrazio-pasahitz berria", + "ask_new_domain": "Domeinu berria", + "ask_new_path": "Bide berria", + "ask_password": "Pasahitza", + "backup_abstract_method": "Babeskopia modu hau oraindik ez da go erabilgarri", + "backup_applying_method_custom": "'{method}' neurrira egindako babeskopia sortzen…", + "backup_applying_method_copy": "Babeskopiarako fitxategi guztiak kopiatzen…", + "backup_archive_app_not_found": "Ezin izan da {app} aurkitu babeskopia fitxategian", + "backup_applying_method_tar": "Babeskopiaren TAR fitxategia sortzen…", + "backup_archive_broken_link": "Ezin izan da babeskopiaren fitxategia eskuratu ({path}ra esteka okerra)", + "backup_creation_failed": "Ezinezkoa izan da babeskopiaren fitxategia sortzea", + "backup_csv_creation_failed": "Ezinezkoa izan da lehengoratzeko beharrezkoak diren CSV fitxategiak sortzea", + "backup_custom_mount_error": "Neurrira egindako babeskopiak ezin izan du 'muntatu' urratsetik haratago egin", + "backup_delete_error": "Ezinezkoa izan da '{path}' ezabatzea", + "backup_method_copy_finished": "Babeskopiak amaitu du", + "backup_hook_unknown": "Babeskopiaren '{hook}' kakoa ezezaguna da", + "backup_method_custom_finished": "'{method}' neurrira egindako babeskopiak amaitu du", + "backup_method_tar_finished": "TAR babeskopia artxiboa sortu da", + "backup_mount_archive_for_restore": "Lehengoratzeko fitxategiak prestatzen…", + "backup_nothings_done": "Ez dago gordetzeko ezer", + "backup_output_directory_required": "Babeskopia non gorde nahi duzun zehaztu behar duzu", + "backup_system_part_failed": "Ezinezkoa izan da sistemaren '{part}' atalaren babeskopia egitea", + "apps_catalog_updating": "Aplikazioen katalogoa eguneratzen…", + "certmanager_cert_signing_failed": "Ezinezkoa izan da ziurtagiri berria sinatzea", + "certmanager_cert_renew_success": "Let's Encrypt ziurtagiria berriztu da '{domain}' domeinurako", + "app_requirements_checking": "{app}(e)k behar dituen paketeak ikuskatzen…", + "certmanager_unable_to_parse_self_CA_name": "Ezinezkoa izan da norberak sinatutako ziurtagiriaren izena prozesatzea (fitxategia: {file})", + "app_remove_after_failed_install": "Aplikazioa ezabatzen instalatzerakoan errorea dela-eta…", + "diagnosis_basesystem_ynh_single_version": "{package} bertsioa: {version} ({repo})", + "diagnosis_failed_for_category": "'{category}' ataleko diagnostikoak kale egin du: {error}", + "diagnosis_cache_still_valid": "(Cachea oraindik baliogarria da {category} ataleko diagnosirako. Ez da berrabiaraziko!)", + "diagnosis_found_errors": "{category} atalari dago(z)kion {errors} arazo aurkitu d(ir)a!", + "diagnosis_found_warnings": "{category} atalari dagokion eta hobetu daite(z)keen {warnings} abisu aurkitu d(ir)a.", + "diagnosis_ip_connected_ipv6": "Zerbitzaria IPv6 bidez dago internetera konektatuta!", + "diagnosis_everything_ok": "Badirudi guztia zuzen dagoela {category} atalean!", + "diagnosis_ip_no_ipv4": "Zerbitzariak ez du dabilen IPv4rik.", + "diagnosis_ip_no_ipv6": "Zerbitzariak ez du dabilen IPv6rik.", + "diagnosis_ip_broken_dnsresolution": "Domeinu izenaren ebazpena kaltetuta dagoela dirudi… Suebakiren bat ote dago DNS eskaerak oztopatzen?", + "diagnosis_diskusage_low": "{mountpoint} fitxategi-sistemak ({device} euskarrian) edukieraren {free} (%{free_percent}a) bakarrik ditu erabilgarri ({total} orotara). Kontuz ibili.", + "diagnosis_dns_good_conf": "DNS ezarpenak zuzen konfiguratuta daude {domain} domeinurako ({category} atala)", + "diagnosis_diskusage_verylow": "{mountpoint} fitxategi-sistemak ({device} euskarrian) edukieraren {free} (%{free_percent}a) bakarrik ditu erabilgarri ({total} orotara). Zertxobait hustu beharko zenuke!", + "diagnosis_description_basesystem": "Sistemaren oinarria", + "diagnosis_description_services": "Zerbitzuen egoeraren egiaztapena", + "diagnosis_http_could_not_diagnose": "Ezinezkoa izan da domeinuak IPv{ipversion} kanpotik eskuragarri dauden egiaztatzea.", + "diagnosis_http_ok": "{domain} domeinua HTTP bidez bisitatu daiteke sare lokaletik kanpo.", + "diagnosis_http_unreachable": "Badirudi {domain} domeinua ez dagoela eskuragarri HTTP bidez sare lokaletik kanpo.", + "apps_catalog_failed_to_download": "Ezinezkoa izan da {apps_catalog} aplikazioen zerrenda eskuratzea: {error}", + "apps_catalog_init_success": "Abiarazi da aplikazioen katalogo sistema!", + "apps_catalog_obsolete_cache": "Aplikazioen katalogoaren cachea hutsik edo zaharkituta dago.", + "diagnosis_description_mail": "Posta elektronikoa", + "diagnosis_http_connection_error": "Arazoa konexioan: ezin izan da domeinu horretara konektatu, litekeena da eskuragarri ez egotea.", + "diagnosis_description_web": "Weba", + "diagnosis_display_tip": "Aurkitu diren arazoak ikusteko joan administrazio-atariko Diagnostikoak atalera, edo exekutatu 'yunohost diagnosis show --issues --human-readable' komandoak nahiago badituzu.", + "diagnosis_dns_point_to_doc": "Mesedez, irakurri dokumentazioa DNS erregistroekin laguntza behar baduzu.", + "diagnosis_mail_ehlo_unreachable": "SMTP posta zerbitzaria ez dago eskuragarri IPv{ipversion}ko sare lokaletik kanpo eta, beraz, ez da posta elektronikoa jasotzeko gai.", + "diagnosis_mail_ehlo_bad_answer_details": "Litekeena da zure zerbitzaria ez den beste gailu batek erantzun izana.", + "diagnosis_mail_blacklist_listed_by": "Zure domeinua edo {item} IPa {blacklist_name} zerrenda beltzean ageri da", + "diagnosis_mail_blacklist_website": "Zerrenda beltzean zergatik zauden ulertu eta konpondu ondoren, {blacklist_website} webgunean zure IP edo domeinua bertatik atera dezatela eska dezakezu", + "diagnosis_http_could_not_diagnose_details": "Errorea: {error}", + "diagnosis_http_hairpinning_issue": "Dirudienez zure sareak ez du hairpinninga gaituta.", + "diagnosis_http_partially_unreachable": "Badirudi {domain} domeinua ezin dela bisitatu HTTP bidez IPv{failed} sare lokaletik kanpo, bai ordea IPv{passed} erabiliz.", + "backup_archive_cant_retrieve_info_json": "Ezinezkoa izan da '{archive}' fitxategiko informazioa eskuratzea… info.json ezin izan da eskuratu (edo ez da baliozko jsona).", + "diagnosis_domain_expiration_not_found": "Ezinezkoa izan da domeinu batzuen iraungitze data egiaztatzea", + "diagnosis_domain_expiration_not_found_details": "Badirudi {domain} domeinuari buruzko WHOIS informazioak ez duela zehazten noiz iraungiko den.", + "certmanager_domain_not_diagnosed_yet": "Oraindik ez dago {domain} domeinurako diagnostikorik. Mesedez, berrabiarazi diagnostikoak 'DNS balioak' eta 'Web' ataletarako diagnostikoen gunean Let's Encrypt ziurtagirirako prest ote dagoen egiaztatzeko. (Edo zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztatzea desgaitzeko.)", + "diagnosis_domain_expiration_warning": "Domeinu batzuk iraungitzear daude!", + "app_packaging_format_not_supported": "Aplikazio hau ezin da instalatu YunoHostek ez duelako paketea ezagutzen. Sistema eguneratzea hausnartu beharko zenuke ziur asko.", + "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost dyndns update --force erabiliz.", + "app_manifest_install_ask_path": "Aukeratu aplikazio hau instalatzeko URLaren bidea (domeinuaren atzeko aldean)", + "app_manifest_install_ask_admin": "Aukeratu administrari bat aplikazio honetarako", + "app_manifest_install_ask_password": "Aukeratu administrazio-pasahitz bat aplikazio honetarako", + "ask_user_domain": "Erabiltzailearen posta elektroniko eta XMPP konturako erabiliko den domeinua", + "app_action_cannot_be_ran_because_required_services_down": "{services} zerbitzuak martxan egon beharko lirateke eragiketa hau exekutatu ahal izateko. Saia zaitez zerbitzuok berrabiarazten (eta ikertu zergatik ez diren abiarazi).", + "apps_already_up_to_date": "Egunean daude dagoeneko aplikazio guztiak", + "app_full_domain_unavailable": "Aplikazio honek bere domeinu propioa behar du, baina beste aplikazio batzuk daude dagoeneko instalatuta '{domain}' domeinuan. Azpidomeinu bat erabil zenezake instalatu nahi duzun aplikaziorako.", + "app_install_script_failed": "Errore bat gertatu da aplikazioaren instalatzailearen aginduetan", + "diagnosis_basesystem_host": "Zerbitzariak Debian {debian_version} darabil", + "diagnosis_ignored_issues": "(kontuan hartu ez d(ir)en + {nb_ignored} arazo)", + "diagnosis_ip_dnsresolution_working": "Domeinu izenaren ebazpena badabil!", + "diagnosis_failed": "Ezinezkoa izan da '{category}' ataleko diagnostikoa lortzea: {error}", + "diagnosis_ip_weird_resolvconf": "DNS ebazpena badabilela dirudi, baina antza denez moldatutako /etc/resolv.conf fitxategia erabiltzen ari zara.", + "diagnosis_dns_bad_conf": "DNS balio batzuk falta dira edo ez dira zuzenak {domain} domeinurako ({category} atala)", + "diagnosis_diskusage_ok": "{mountpoint} fitxategi-sistemak ({device} euskarrian) edukieraren {free} (%{free_percent}a) ditu erabilgarri oraindik ({total} orotara)!", + "apps_catalog_update_success": "Aplikazioen katalogoa eguneratu da!", + "certmanager_warning_subdomain_dns_record": "'{subdomain}' azpidomeinuak ez dauka '{domain}'(e)k duen IP bera. Ezaugarri batzuk ez dira erabilgarri egongo hau zuzendu arte eta ziurtagiri bat birsortu arte.", + "app_argument_choice_invalid": "Hautatu ({choices}) aukeretako bat '{name}' argumenturako: '{value}' ez dago aukera horien artean", + "backup_create_size_estimation": "Fitxategiak {size} datu inguru izango ditu.", + "diagnosis_basesystem_ynh_main_version": "Zerbitzariak YunoHosten {main_version} ({repo}) darabil", + "backup_custom_backup_error": "Neurrira egindako babeskopiak ezin izan du 'babeskopia egin' urratsetik haratago egin", + "diagnosis_ip_broken_resolvconf": "Zure zerbitzarian domeinu izenaren ebazpena kaltetuta dagoela dirudi, antza denez /etc/resolv.conf fitxategia ez dago 127.0.0.1ra adi.", + "diagnosis_ip_no_ipv6_tip": "Dabilen IPv6 izatea ez da derrigorrezkoa zerbitzariaren funtzionamendurako, baina egokiena da interneten osasunerako. IPv6 automatikoki konfiguratu beharko luke sistemak edo operadoreak. Bestela, eskuz konfiguratu beharko zenituzke hainbat gauza dokumentazioan azaltzen den bezala. Ezin baduzu edo IPv6 gaitzea zuretzat kontu teknikoegia baldin bada, ez duzu abisu hau zertan kontuan hartu.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Egoera konpontzeko, ikuskatu desberdintasunak yunohost tools regen-conf nginx --dry-run --with-diff komandoren bidez eta, proposatutako aldaketak onartzen badituzu, ezarri itzazu yunohost tools regen-conf nginx --force erabiliz.", + "diagnosis_domain_not_found_details": "{domain} domeinua ez da WHOISen datubasean existitzen edo iraungi da!", + "app_start_backup": "{app}(r)en babeskopia egiteko fitxategiak eskuratzen…", + "app_change_url_no_script": "'{app_name}' aplikazioak oraingoz ez du URLa moldatzerik onartzen. Agian eguneratu beharko zenuke.", + "app_location_unavailable": "URL hau ez dago erabilgarri edota dagoeneko instalatutako aplikazioren batekin talka egiten du:\n{apps}", + "app_not_upgraded": "'{failed_app}' aplikazioa ezin izan da eguneratu, eta horregatik ondorengo aplikazioen eguneraketak bertan behera utzi dira: {apps}", + "app_not_correctly_installed": "Ez dirudi {app} ondo instalatuta dagoenik", + "app_not_installed": "Ezinezkoa izan da {app} aurkitzea instalatutako aplikazioen zerrendan: {all_apps}", + "app_not_properly_removed": "Ezinezkoa izan da {app} guztiz ezabatzea", + "app_start_install": "{app} instalatzen…", + "app_start_restore": "{app} lehengoratzen…", + "app_unsupported_remote_type": "Aplikazioak darabilen urruneko motak ez du babesik (Unsupported remote type)", + "app_upgrade_several_apps": "Ondorengo aplikazioak eguneratuko dira: {apps}", + "backup_app_failed": "Ezinezkoa izan da {app}(r)en babeskopia egitea", + "backup_actually_backuping": "Bildutako fitxategiekin babeskopia sortzen…", + "backup_archive_name_exists": "Dagoeneko existitzen da izen bera duen babeskopia fitxategi bat.", + "backup_archive_name_unknown": "Ez da '{name}' izeneko babeskopia ezagutzen", + "backup_archive_open_failed": "Ezinezkoa izan da babeskopien fitxategia irekitzea", + "backup_archive_system_part_not_available": "'{part}' sistemaren atala ez dago erabilgarri babeskopia honetan", + "backup_archive_writing_error": "Ezinezkoa izan da '{source}' ('{dest}' fitxategiak eskatu dituenak) fitxategia '{archive}' konprimatutako babeskopian sartzea", + "backup_ask_for_copying_if_needed": "Behin-behinean {size}MB erabili nahi dituzu babeskopia gauzatu ahal izateko? (Horrela egiten da fitxategi batzuk ezin direlako modu eraginkorragoan prestatu.)", + "backup_cant_mount_uncompress_archive": "Ezinezkoa izan da deskonprimatutako fitxategia muntatzea idazketa-babesa duelako", + "backup_created": "Babeskopia sortu da", + "backup_copying_to_organize_the_archive": "{size}MB kopiatzen fitxategia antolatzeko", + "backup_couldnt_bind": "Ezin izan da {src} {dest}-ra lotu.", + "backup_output_directory_forbidden": "Aukeratu beste katalogo bat emaitza gordetzeko. Babeskopiak ezin dira sortu /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var edo /home/yunohost.backup/archives azpi-katalogoetan", + "backup_output_directory_not_empty": "Aukeratu hutsik dagoen katalogo bat", + "backup_running_hooks": "Babeskopien kakoak exekutatzen…", + "backup_unable_to_organize_files": "Ezinezkoa izan da modu azkarra erabiltzea fitxategiko artxiboak prestatzeko", + "backup_output_symlink_dir_broken": "'{path}' fitxategi-katalogoaren symlink-a ez dabil. Agian [ber]muntatzea ahaztu zaizu edo euskarria atakara konektatzea ahaztu duzu.", + "backup_with_no_backup_script_for_app": "'{app}' aplikazioak ez du babeskopia egiteko agindurik. Ez da kontuan hartuko.", + "backup_with_no_restore_script_for_app": "{app}(e)k ez du lehengoratzeko agindurik, ezingo duzu aplikazio hau automatikoki lehengoratu.", + "certmanager_attempt_to_renew_nonLE_cert": "'{domain}' domeinurako ziurtagiria ez da Let's Encryptek jaulkitakoa. Ezin da automatikoki berriztu!", + "certmanager_attempt_to_renew_valid_cert": "'{domain}' domeinurako ziurtagiria iraungitzear dago! (Zertan ari zaren baldin badakizu, --force erabil dezakezu)", + "certmanager_cannot_read_cert": "Arazoren bat egon da {domain} (fitxategia: {file}) domeinurako oraingo ziurtagiria irekitzen saiatzerakoan, zergatia: {reason}", + "certmanager_cert_install_success": "Let's Encrypt ziurtagiria instalatu da '{domain}' domeinurako", + "certmanager_cert_install_success_selfsigned": "Norberak sinatutako ziurtagiria instalatu da '{domain}' domeinurako", + "certmanager_domain_cert_not_selfsigned": "{domain} domeinurako ziurtagiria ez da norberak sinatutakoa. Ziur al zaude ordezkatu nahi duzula? (Erabili '--force' hori egiteko.)", + "certmanager_certificate_fetching_or_enabling_failed": "{domain} domeinurako ziurtagiri berriak kale egin du…", + "certmanager_domain_http_not_working": "Ez dirudi {domain} domeinua HTTP bidez ikusgai dagoenik. Mesedez, egiaztatu 'Weba' atala diagnosien gunean informazio gehiagorako. (Zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", + "certmanager_hit_rate_limit": "{domain} domeinu-multzorako ziurtagiri gehiegi jaulki dira dagoeneko. Mesedez, saia saitez geroago. Ikus https://letsencrypt.org/docs/rate-limits/ xehetasun gehiagorako", + "certmanager_no_cert_file": "Ezinezkoa izan da {domain} domeinurako ziurtagiri fitxategia irakurrtzea (fitxategia: {file})", + "certmanager_self_ca_conf_file_not_found": "Ezinezkoa izan da konfigurazio-fitxategia aurkitzea norberak sinatutako ziurtagirirako (fitxategia: {file})", + "confirm_app_install_warning": "Adi: litekeena da aplikazio hau ibiltzea baina ez dago YunoHostera egina. Ezaugarri batzuk, SSO edo babeskopia/lehengoratzea esaterako, desgaituta egon daitezke. Instalatu hala ere? [{answers}] ", + "diagnosis_description_ports": "Ataken irisgarritasuna", + "backup_archive_corrupted": "Badirudi '{archive}' babeskopia fitxategia kaltetuta dagoela: {error}", + "diagnosis_ip_local": "IP lokala: {local}", + "diagnosis_mail_blacklist_reason": "Zerrenda beltzean egotearen zergatia zera da: {reason}", + "app_removed": "{app} desinstalatu da", + "backup_cleaning_failed": "Ezinezkoa izan da behin-behineko babeskopien karpeta hustea", + "certmanager_attempt_to_replace_valid_cert": "{domain} domeinurako egokia eta baliogarria den ziurtagiri bat ordezkatzen saiatzen ari zara! (Erabili --force mezu hau deuseztatu eta ziurtagiria ordezkatzeko)", + "diagnosis_backports_in_sources_list": "Dirudienez apt (pakete kudeatzailea) backports biltegia erabiltzeko konfiguratuta dago. Zertan ari zaren ez badakizu, ez zenuke backports biltegietako aplikaziorik instalatu beharko, ezegonkortasun eta gatazkak eragin ditzaketelako sistemarekin.", + "app_restore_failed": "Ezinezkoa izan da {app} lehengoratzea: {error}", + "diagnosis_apps_allgood": "Instalatutako aplikazioek oinarrizko pakete-jarraibideekin bat egiten dute", + "diagnosis_apps_bad_quality": "Aplikazio hau hondatuta dagoela dio YunoHosten aplikazioen katalogoak. Agian behin-behineko kontua da arduradunak arazoa konpondu bitartean. Oraingoz, ezin da aplikazioa eguneratu.", + "diagnosis_apps_broken": "Aplikazio hau YunoHosten aplikazioen katalogoan hondatuta dagoela ageri da. Agian behin-behineko kontua da arduradunak konpondu bitartean. Oraingoz, ezin da aplikazioa eguneratu.", + "diagnosis_apps_deprecated_practices": "Instalatutako aplikazio honen bertsioak oraindik darabiltza zaharkitutako pakete-jarraibideak. Eguneratzea hausnartu beharko zenuke.", + "diagnosis_apps_issue": "Arazo bat dago {app} aplikazioarekin", + "diagnosis_apps_not_in_app_catalog": "Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Iraganean egon bazen eta ezabatu izan balitz, desinstalatzea litzateke onena, ez baitu eguneraketarik jasoko eta sistemaren integritate eta segurtasuna arriskuan jarri lezakeelako.", + "diagnosis_apps_outdated_ynh_requirement": "Instalatutako aplikazio honen bertsioak yunohost >= 2.x baino ez du behar, eta horrek egungo pakete-jardunbideekin bat ez datorrela iradokitzen du. Eguneratzen saiatu beharko zinateke.", + "diagnosis_description_apps": "Aplikazioak", + "domain_dns_conf_special_use_tld": "Domeinu hau top-level domain (TLD) erabilera bereziko motakoa da .local edo .test bezala eta ez du DNS ezarpenik behar.", + "log_permission_create": "Sortu '{}' baimena", + "log_user_delete": "Ezabatu '{}' erabiltzailea", + "log_app_install": "'{}' aplikazioa instalatu", + "done": "Egina", + "group_user_already_in_group": "{user} erabiltzailea {group} taldean dago dagoeneko", + "firewall_reloaded": "Suebakia birkargatu da", + "domain_unknown": "'{domain}' domeinua ezezaguna da", + "global_settings_cant_serialize_settings": "Ezinezkoa izan da konfikurazio-datuak serializatzea, zergatia: {reason}", + "global_settings_setting_security_nginx_redirect_to_https": "Birbideratu HTTP eskaerak HTTPSra (EZ ITZALI hau ez badakizu zertan ari zaren!)", + "group_deleted": "'{group}' taldea ezabatu da", + "invalid_password": "Pasahitza ez da zuzena", + "log_domain_main_domain": "Lehenetsi '{}' domeinua", + "log_user_group_update": "Moldatu '{}' taldea", + "dyndns_could_not_check_available": "Ezinezkoa izan da {domain} {provider}(e)n eskuragarri dagoen egiaztatzea.", + "diagnosis_rootfstotalspace_critical": "'root' fitxategi-sistemak {space} baino ez ditu erabilgarri, eta hori kezkagarria da! Litekeena da oso laster memoriarik gabe geratzea! 'root' fitxategi-sistemak gutxienez 16GB erabilgarri izatea da gomendioa.", + "disk_space_not_sufficient_install": "Ez dago aplikazio hau instalatzeko nahikoa espaziorik", + "domain_dns_conf_is_just_a_recommendation": "Komando honek *iradokitako* konfigurazioa erakusten du. Ez du DNS konfigurazioa zugatik ezartzen. Zure ardura da DNS gunea zure erregistro-enpresaren gomendioen arabera ezartzea.", + "dyndns_ip_update_failed": "Ezin izan da IP helbidea DynDNSan eguneratu", + "dyndns_ip_updated": "IP helbidea DynDNS-n eguneratu da", + "dyndns_key_not_found": "Ez da domeinurako DNS gakorik aurkitu", + "dyndns_unavailable": "'{domain}' domeinua ez dago eskuragarri.", + "log_app_makedefault": "Lehenetsi '{}' aplikazioa", + "log_does_exists": "Ez dago '{log}' izena duen eragiketa-erregistrorik; erabili 'yunohost log list' eragiketa-erregistro guztiak ikusteko", + "log_user_group_delete": "Ezabatu '{}' taldea", + "log_user_import": "Inportatu erabiltzaileak", + "dyndns_key_generating": "DNS gakoa sortzen… litekeena da honek denbora behar izatea.", + "diagnosis_mail_fcrdns_ok": "Alderantzizko DNSa zuzen konfiguratuta dago!", + "diagnosis_mail_queue_unavailable_details": "Errorea: {error}", + "dyndns_provider_unreachable": "Ezinezkoa izan da DynDNS {provider} enpresarekin konektatzea: agian zure YunoHost zerbitzaria ez dago internetera konektatuta edo dynette zerbitzaria ez dago martxan.", + "dyndns_registered": "DynDNS domeinua erregistratu da", + "dyndns_registration_failed": "Ezinezkoa izan da DynDNS domeinua erregistratzea: {error}", + "extracting": "Ateratzen…", + "diagnosis_ports_unreachable": "{port}. ataka ez dago eskuragarri kanpotik.", + "diagnosis_regenconf_manually_modified_details": "Ez dago arazorik zertan ari zaren baldin badakizu! YunoHostek fitxategi hau automatikoki eguneratzeari utziko dio… Baina kontuan izan YunoHosten eguneraketek aldaketa garrantzitsuak izan ditzaketela. Nahi izatekotan, desberdintasunak aztertu ditzakezu yunohost tools regen-conf {category} --dry-run --with-diff komandoa exekutatuz, eta gomendatutako konfiguraziora bueltatu yunohost tools regen-conf {category} --force erabiliz", + "experimental_feature": "Adi: Funtzio hau esperimentala eta ezegonkorra da, ez zenuke erabili beharko ez badakizu zertan ari zaren.", + "global_settings_cant_write_settings": "Ezinezkoa izan da konfigurazio fitxategia gordetzea, zergatia: {reason}", + "dyndns_domain_not_provided": "{provider} DynDNS enpresak ezin du {domain} domeinua eskaini.", + "firewall_reload_failed": "Ezinezkoa izan da suebakia birkargatzea", + "global_settings_setting_security_password_admin_strength": "Administrazio-pasahitzaren segurtasuna", + "hook_name_unknown": "'{name}' 'hook' izen ezezaguna", + "domain_deletion_failed": "Ezinezkoa izan da {domain} ezabatzea: {error}", + "global_settings_setting_security_nginx_compatibility": "Bateragarritasun eta segurtasun arteko gatazka NGINX web zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", + "log_regen_conf": "Berregin '{}' sistemaren konfigurazioa", + "dpkg_lock_not_available": "Ezin da komando hau une honetan exekutatu beste aplikazio batek dpkg (sistemaren paketeen kudeatzailea) blokeatuta duelako, erabiltzen ari baita", + "group_created": "'{group}' taldea sortu da", + "global_settings_setting_security_password_user_strength": "Erabiltzaile-pasahitzaren segurtasuna", + "global_settings_setting_security_experimental_enabled": "Gaitu segurtasun funtzio esperimentalak (ez ezazu egin ez badakizu zertan ari zaren!)", + "good_practices_about_admin_password": "Administrazio-pasahitz berria ezartzear zaude. Pasahitzak zortzi karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", + "log_help_to_get_failed_log": "Ezin izan da '{desc}' eragiketa exekutatu. Mesedez, laguntza nahi baduzu partekatu eragiketa honen erregistro osoa 'yunohost log share {name}' komandoa erabiliz", + "global_settings_setting_security_webadmin_allowlist_enabled": "Baimendu IP zehatz batzuk bakarrik administrazio-atarian.", + "group_unknown": "'{group}' taldea ezezaguna da", + "group_updated": "'{group}' taldea eguneratu da", + "group_update_failed": "Ezinezkoa izan da '{group}' taldea eguneratzea: {error}", + "diagnosis_rootfstotalspace_warning": "'root' fitxategi-sistemak {space} baino ez ditu. Agian ez da arazorik egongo, baina kontuz ibili edo memoriarik gabe gera zaitezke laster… 'root' fitxategi-sistemak gutxienez 16GB erabilgarri izatea da gomendioa.", + "iptables_unavailable": "Ezin dituzu iptaulak hemen moldatu; edukiontzi bat erabiltzen ari zara edo kernelak ez du aukera hau onartzen", + "log_permission_delete": "Ezabatu '{}' baimena", + "group_already_exist": "{group} taldea existitzen da dagoeneko", + "group_user_not_in_group": "{user} erabiltzailea ez dago {group} taldean", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Operadore batzuek ez dute alderantzizko DNSa konfiguratzen uzten (edo funtzioa ez dabil…). IPv4rako alderantzizko DNSa zuzen konfiguratuta badago, IPv6 desgaitzen saia zaitezke posta elektronikoa bidaltzeko, yunohost settings set smtp.allow_ipv6 -v off exekutatuz. Adi: honek esan nahi du ez zarela gai izango IPv6 bakarrik darabilten zerbitzari apurren posta elektronikoa jasotzeko edo beraiei bidaltzeko.", + "diagnosis_sshd_config_inconsistent": "Dirudienez SSH ataka eskuz aldatu da /etc/ssh/sshd_config fitxategian. YunoHost 4.2tik aurrera 'security.ssh.port' izeneko ezarpen orokor bat dago konfigurazioa eskuz aldatzea ekiditeko.", + "diagnosis_sshd_config_inconsistent_details": "Mesedez, exekutatu yunohost settings set security.ssh.port -v YOUR_SSH_PORT SSH ataka zehazteko, egiaztatu yunohost tools regen-conf ssh --dry-run --with-diff erabiliz eta yunohost tools regen-conf ssh --force exekutatu gomendatutako konfiguraziora bueltatu nahi baduzu.", + "domain_dns_push_failed_to_authenticate": "Ezinezkoa izan da '{domain}' domeinurako APIa erabiliz erregistro-enpresan saioa hastea. Zuzenak al dira datuak? (Errorea: {error})", + "domain_dns_pushing": "DNS ezarpenak bidaltzen…", + "diagnosis_sshd_config_insecure": "Badirudi SSH konfigurazioa eskuz aldatu dela eta ez da segurua ez duelako 'AllowGroups' edo 'AllowUsers' baldintzarik jartzen fitxategien atzitzea oztopatzeko.", + "disk_space_not_sufficient_update": "Ez dago aplikazio hau eguneratzeko nahikoa espaziorik", + "domain_cannot_add_xmpp_upload": "Ezin dira 'xmpp-upload.' hasiera duten domeinuak gehitu. Izen mota hau YunoHosten zati den XMPP igoeretarako erabiltzen da.", + "domain_cannot_remove_main_add_new_one": "Ezin duzu '{domain}' ezabatu domeinu nagusi eta bakarra delako. Beste domeinu bat gehitu 'yunohost domain add ' exekutatuz, gero erabili 'yunohost domain main-domain -n ' domeinu nagusi bilakatzeko, eta azkenik ezabatu {domain}' domeinua 'yunohost domain remove {domain}' komandoarekin.", + "domain_dns_push_record_failed": "Ezinezkoa izan da {type}/{name} ezarpenak {action}: {error}", + "domain_dns_push_success": "DNS ezarpenak eguneratu dira!", + "domain_dns_push_failed": "DNS ezarpenen eguneratzeak kale egin du.", + "domain_dns_push_partial_failure": "DNS ezarpenak erdipurdi eguneratu dira: jakinarazpen/errore batzuk egon dira.", + "global_settings_setting_smtp_relay_host": "YunoHosten ordez posta elektronikoa bidaltzeko SMTP relay helbidea. Erabilgarri izan daiteke egoera hauetan: operadore edo VPS enpresak 25. ataka blokeatzen badu, DUHLen zure etxeko IPa ageri bada, ezin baduzu alderantzizko DNSa ezarri edo zerbitzari hau ez badago zuzenean internetera konektatuta baina posta elektronikoa bidali nahi baduzu.", + "group_deletion_failed": "Ezinezkoa izan da '{group}' taldea ezabatzea: {error}", + "invalid_number_min": "{min} baino handiagoa izan behar da", + "invalid_number_max": "{max} baino txikiagoa izan behar da", + "diagnosis_services_bad_status": "{service} zerbitzua {status} dago :(", + "diagnosis_ports_needed_by": "{category} funtzioetarako ezinbestekoa da ataka hau eskuragarri egotea ({service} zerbitzua)", + "diagnosis_package_installed_from_sury": "Sistemaren pakete batzuen lehenagoko bertsioak beharko lirateke", + "global_settings_setting_smtp_relay_password": "SMTP relay helbideko pasahitza", + "global_settings_setting_smtp_relay_port": "SMTP relay ataka", + "domain_deleted": "Domeinua ezabatu da", + "domain_dyndns_root_unknown": "Ez da ezagutzen DynDNSaren root domeinua", + "domain_exists": "Dagoeneko existitzen da domeinu hau", + "domain_registrar_is_not_configured": "Oraindik ez da {domain} domeinurako erregistro-enpresa ezarri.", + "domain_dns_push_not_applicable": "Ezin da {domain} domeinurako DNS konfigurazio automatiko funtzioa erabili. DNS erregistroak eskuz ezarri beharko zenituzke gidaorriei erreparatuz: https://yunohost.org/dns_config.", + "domain_dns_push_managed_in_parent_domain": "DNS ezarpenak automatikoki konfiguratzeko funtzioa {parent_domain} domeinu nagusian kudeatzen da.", + "domain_dns_registrar_managed_in_parent_domain": "Domeinu hau {parent_domain_link} (r)en azpidomeinua da. DNS ezarpenak {parent_domain}(r)en konfigurazio atalean kudeatu behar dira.", + "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost dyndns update' komandoa)", + "domain_dns_registrar_not_supported": "YunoHostek ezin izan du domeinu honen erregistro-enpresa automatikoki antzeman. Eskuz konfiguratu beharko dituzu DNS ezarpenak gidalerroei erreparatuz: https://yunohost.org/dns.", + "domain_dns_registrar_experimental": "Oraingoz, YunoHosten kideek ez dute **{registrar}** erregistro-enpresaren APIa nahi beste probatu eta aztertu. Funtzioa **oso esperimentala** da — kontuz!", + "domain_config_mail_in": "Jasotako mezuak", + "domain_config_auth_token": "Token autentifikazioa", + "domain_config_auth_key": "Autentifikazio gakoa", + "domain_config_auth_secret": "Autentifikazioaren \"secret\"a", + "domain_config_api_protocol": "API protokoloa", + "domain_config_auth_entrypoint": "APIaren sarrera", + "domain_config_auth_application_key": "Aplikazioaren gakoa", + "domain_config_auth_application_secret": "Aplikazioaren gako sekretua", + "domain_config_auth_consumer_key": "Erabiltzailearen gakoa", + "global_settings_setting_smtp_allow_ipv6": "Baimendu IPv6 posta elektronikoa jaso eta bidaltzeko", + "group_cannot_be_deleted": "{group} taldea ezin da eskuz ezabatu.", + "log_domain_config_set": "Aldatu '{}' domeinuko ezarpenak", + "log_domain_dns_push": "Bidali '{}' domeinuaren DNS ezarpenak", + "log_tools_migrations_migrate_forward": "Exekutatu migrazioak", + "log_tools_postinstall": "Abiarazi YunoHost zerbitzariaren instalazio ondorengo prozesua", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Operadore batzuek ez dute alderantzizko DNSa konfiguratzen uzten (edo funtzioa ez dabil…). Hau dela-eta arazoak badituzu, irtenbide batzuk eduki ditzakezu:
- Operadore batzuek relay posta zerbitzari bat eskaini dezakete, baina kasu horretan zure posta elektronikoa zelatatu dezakete.
- Pribatutasuna bermatzeko *IP publikoa* duen VPN bat erabiltzea izan daiteke irtenbidea. Ikus https://yunohost.org/#/vpn_advantage
- Edo operadore desberdin batera aldatu", + "domain_dns_registrar_supported": "YunoHostek automatikoki antzeman du domeinu hau **{registrar}** erregistro-enpresak kudeatzen duela. Nahi baduzu YunoHostek automatikoki konfiguratu ditzake DNS ezarpenak, API egiaztagiri zuzenak zehazten badituzu. API egiaztagiriak non lortzeko dokumentazioa orri honetan duzu: https://yunohost.org/registar_api_{registrar}. (Baduzu DNS erregistroak eskuz konfiguratzeko aukera ere, gidalerro hauetan ageri den bezala: https://yunohost.org/dns)", + "domain_dns_push_failed_to_list": "Ezinezkoa izan da APIa erabiliz oraingo erregistroak antzematea: {error}", + "domain_dns_push_already_up_to_date": "Ezarpenak egunean daude, ez dago zereginik.", + "domain_config_features_disclaimer": "Oraingoz, posta elektronikoa edo XMPP funtzioak gaitu/desgaitzeak DNS ezarpenei soilik eragiten die, ez sistemaren konfigurazioari!", + "domain_config_mail_out": "Bidalitako mezuak", + "domain_config_xmpp": "Bat-bateko mezularitza (XMPP)", + "global_settings_bad_choice_for_enum": "{setting} ezarpenerako aukera okerra. '{choice}' ezarri da baina hauek dira aukerak: {available_choices}", + "global_settings_setting_security_postfix_compatibility": "Bateragarritasun eta segurtasun arteko gatazka Postfix zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", + "global_settings_setting_security_ssh_compatibility": "Bateragarritasun eta segurtasun arteko gatazka SSH zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", + "good_practices_about_user_password": "Erabiltzaile-pasahitz berria ezartzear zaude. Pasahitzak zortzi karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", + "group_cannot_edit_all_users": "'all_users' taldea ezin da eskuz moldatu. YunoHosten izena emanda dauden erabiltzaile guztiak barne dituen talde berezia da", + "invalid_number": "Zenbaki bat izan behar da", + "ldap_attribute_already_exists": "'{attribute}' LDAP funtzioa existitzen da dagoeneko eta '{value}' balioa dauka", + "log_app_change_url": "'{}' aplikazioaren URLa aldatu", + "log_app_config_set": "Ezarri '{}' aplikazioko konfigurazioa", + "downloading": "Deskargatzen…", + "log_available_on_yunopaste": "Erregistroa {url} estekan ikus daiteke", + "log_dyndns_update": "Eguneratu YunoHosten '{}' domeinuari lotutako IP helbidea", + "log_letsencrypt_cert_install": "Instalatu Let's Encrypt ziurtagiria '{}' domeinurako", + "log_selfsigned_cert_install": "Instalatu '{}' domeinurako norberak sinatutako ziurtagiria", + "diagnosis_mail_ehlo_wrong": "Zurea ez den SMTP posta zerbitzari batek erantzun du IPv{ipversion}an. Litekeena da zure zerbitzariak posta elektronikorik jaso ezin izatea.", + "log_tools_upgrade": "Eguneratu sistemaren paketeak", + "log_tools_reboot": "Berrabiarazi zerbitzaria", + "diagnosis_mail_queue_unavailable": "Ezinezkoa da ilaran zenbat posta elektroniko dauden kontsultatzea", + "log_user_create": "Gehitu '{}' erabiltzailea", + "group_cannot_edit_visitors": "'bisitariak' taldea ezin da eskuz moldatu. Saiorik hasi gabeko bisitariak barne hartzen dituen talde berezia da", + "diagnosis_ram_verylow": "RAM memoriaren {available} baino ez ditu erabilgarri sistemak; memoria guztiaren ({total}) %{available_percent}a bakarrik!", + "diagnosis_ram_low": "RAM memoriaren {available} ditu erabilgarri sistemak; memoria guztiaren ({total}) %{available_percent}a. Adi ibili.", + "diagnosis_ram_ok": "RAM memoriaren {available} ditu oraindik erabilgarri sistemak; memoria guztiaren ({total}) %{available_percent}a.", + "diagnosis_swap_none": "Sistemak ez du swap-ik. Gutxienez {recommended} izaten saiatu beharko zinateke, sistema memoriarik gabe gera ez dadin.", + "diagnosis_swap_ok": "Sistemak {total} swap dauzka!", + "diagnosis_regenconf_allgood": "Konfigurazio-fitxategi guztiak bat datoz gomendatutako ezarpenekin!", + "diagnosis_regenconf_manually_modified": "Dirudienez {file} konfigurazio fitxategia eskuz aldatu da.", + "diagnosis_security_vulnerable_to_meltdown": "Badirudi Meltdown izeneko segurtasun arazo larriak eragin diezazukela", + "diagnosis_ports_could_not_diagnose": "Ezinezkoa izan da atakak IPv{ipversion} erabiliz kanpotik eskuragarri dauden egiaztatzea.", + "diagnosis_ports_ok": "{port}. ataka eskuragarri dago kanpotik.", + "diagnosis_unknown_categories": "Honako atalak ez dira ezagutzen: {categories}", + "diagnosis_services_running": "{service} zerbitzua martxan dago!", + "log_app_action_run": "'{}' aplikazioaren eragiketa exekutatu", + "diagnosis_never_ran_yet": "Badirudi zerbitzari hau duela gutxi konfiguratu dela eta oraindik ez dago erakusteko diagnostikorik. Diagnostiko osoa abiarazi beharko zenuke, administrazio-webgunetik edo 'yunohost diagnosis run' komandoa exekutatuz.", + "diagnosis_mail_outgoing_port_25_blocked": "SMTP posta zerbitzariak ezin ditu posta elektronikoak bidali 25. ataka itxita dagoelako IPv{ipversion}n.", + "diagnosis_mail_outgoing_port_25_blocked_details": "Lehenik eta behin operadorearen routerreko aukeretan saiatu beharko zinateke 25. ataka desblokeatzen. (Hosting enpresaren arabera, beraiekin harremanetan jartzea beharrezkoa izango da).", + "diagnosis_mail_ehlo_wrong_details": "Kanpo-diagnostikatzaileak IPv{ipversion}an jaso duen EHLOa eta zure zerbitzariaren domeinukoa ez datoz bat.
Jasotako EHLOa: {wrong_ehlo}
Esperotakoa: {right_ehlo}
Arazo honen zergati ohikoena 25. ataka zuzen konfiguratuta ez egotea da. Edo agian suebaki edo reverse-proxya oztopo izan daiteke.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Alderantzizko DNSa ez dago zuzen konfiguratuta IPv{ipversion}an. Litekeena da hartzaileak posta elektroniko batzuk jaso ezin izatea edo mezuok spam modura etiketatuak izatea.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Oraingo alderantzizko DNSa: {rdns_domain}
Esperotako balioa: {ehlo_domain}", + "diagnosis_mail_queue_too_big": "Mezu gehiegi posta elektronikoaren ilaran: ({nb_pending} mezu)", + "diagnosis_ports_could_not_diagnose_details": "Errorea: {error}", + "diagnosis_swap_tip": "Mesedez, kontuan hartu zerbitzari honen swap memoria SD edo SSD euskarri batean gordetzeak euskarri horren bizi-iraupena izugarri laburtu dezakeela.", + "invalid_regex": "'Regexa' ez da zuzena: '{regex}'", + "group_creation_failed": "Ezinezkoa izan da '{group}' taldea sortzea: {error}", + "log_user_permission_reset": "Berrezarri '{}' baimena", + "group_cannot_edit_primary_group": "'{group}' taldea ezin da eskuz moldatu. Erabiltzaile zehatz bakar bat duen talde nagusia da.", + "diagnosis_swap_notsomuch": "Sistemak {total} swap baino ez ditu. Gutxienez {recommended} izaten saiatu beharko zinateke sistema memoriarik gabe gera ez dadin.", + "diagnosis_security_vulnerable_to_meltdown_details": "Arazoa konpontzeko, sistema eguneratu eta berrabiarazi beharko zenuke linux-en kernel berriagoa erabiltzeko (edo zerbitzariaren arduradunarekin jarri harremanetan). Ikus https://meltdownattack.com/ argibide gehiagorako.", + "diagnosis_services_conf_broken": "{service} zerbitzuko konfigurazioa hondatuta dago!", + "diagnosis_services_bad_status_tip": "Zerbitzua berrabiarazten saia zaitezke eta nahikoa ez bada, aztertu zerbitzuaren erregistroa administrazio-atarian. (komandoak nahiago badituzu yunohost service restart {service} eta yunohost service log {service} hurrenez hurren).", + "diagnosis_mail_ehlo_unreachable_details": "Ezinezkoa izan da zure zerbitzariko 25. atakari konektatzea IPv{ipversion} erabiliz. Badirudi ez dagoela eskuragarri.
1. Arazo honen zergati ohikoena 25. ataka egoki birbideratuta ez egotea da.
2. Egiaztatu postfix zerbitzua martxan dagoela.
3. Konfigurazio konplexuagoetan: egiaztatu suebaki edo reverse-proxyak konexioa oztopatzen ez dutela.", + "group_already_exist_on_system_but_removing_it": "{group} taldea existitzen da sistemaren taldeetan, baina YunoHostek ezabatuko du…", + "diagnosis_mail_fcrdns_nok_details": "Lehenik eta behin zure routerraren konfigurazio gunean edo hostingaren enpresaren aukeretan alderantzizko DNSa konfiguratzen saiatu beharko zinateke {ehlo_domain} erabiliz. (Hosting enpresaren arabera, ezinbestekoa da beraiekin harremanetan jartzea).", + "diagnosis_mail_outgoing_port_25_ok": "SMTP posta zerbitzaria posta elektronikoa bidaltzeko gai da (25. atakaren irteera ez dago blokeatuta).", + "diagnosis_ports_partially_unreachable": "{port}. ataka ez dago eskuragarri kanpotik Pv{failed} erabiliz.", + "diagnosis_ports_forwarding_tip": "Arazoa konpontzeko, litekeena da operadorearen routerrean ataken birbideraketa konfiguratu behar izatea, https://yunohost.org/isp_box_config-n agertzen den bezala", + "domain_creation_failed": "Ezinezkoa izan da {domain} domeinua sortzea: {error}", + "domains_available": "Erabilgarri dauden domeinuak:", + "global_settings_setting_pop3_enabled": "Gaitu POP3 protokoloa posta zerbitzarirako", + "global_settings_setting_security_ssh_port": "SSH ataka", + "global_settings_unknown_type": "Gertaera ezezaguna, {setting} ezarpenak {unknown_type} mota duela dirudi baina mota hori ez da sistemarekin bateragarria.", + "group_already_exist_on_system": "{group} taldea existitzen da dagoeneko sistemaren taldeetan", + "diagnosis_processes_killed_by_oom_reaper": "Memoria agortu eta sistemak prozesu batzuk amaituarazi behar izan ditu. Honek esan nahi du sistemak ez duela memoria nahikoa edo prozesuren batek memoria gehiegi behar duela. Amaituarazi d(ir)en prozesua(k):\n{kills_summary}", + "hook_exec_not_terminated": "Aginduak ez du behar bezala amaitu: {path}", + "log_corrupted_md_file": "Erregistroei lotutako YAML metadatu fitxategia kaltetuta dago: '{md_file}\nErrorea: {error}'", + "log_letsencrypt_cert_renew": "Berriztu '{}' Let's Encrypt ziurtagiria", + "log_remove_on_failed_restore": "Ezabatu '{}' babeskopia baten lehengoratzeak huts egin eta gero", + "diagnosis_package_installed_from_sury_details": "Sury izena duen kanpoko biltegi batetik instalatu dira pakete batzuk, nahi gabe. YunoHosten taldeak hobekuntzak egin ditu pakete hauek kudeatzeko, baina litekeena da PHP7.3 aplikazioak Stretch sistema eragilean instalatu zituzten kasu batzuetan arazoak sortzea. Egoera hau konpontzeko, honako komando hau exekutatu beharko zenuke: {cmd_to_fix}", + "log_help_to_get_log": "'{desc}' eragiketaren erregistroa ikusteko, exekutatu 'yunohost log show {name}'", + "dpkg_is_broken": "Ezin duzu une honetan egin dpkg/APT (sistemaren pakateen kudeatzaileak) hondatutako itxura dutelako… Arazoa konpontzeko SSH bidez konektatzen saia zaitezke eta ondoren exekutatu 'sudo apt install --fix-broken' edota 'sudo dpkg --configure -a'.", + "domain_cannot_remove_main": "Ezin duzu '{domain}' ezabatu domeinu nagusia delako. Beste domeinu bat ezarri beharko duzu nagusi bezala 'yunohost domain main-domain -n ' erabiliz; honako hauek dituzu aukeran: {other_domains}", + "domain_created": "Sortu da domeinua", + "domain_dyndns_already_subscribed": "Dagoeneko izena eman duzu DynDNS domeinu batean", + "domain_hostname_failed": "Ezinezkoa izan da hostname berria ezartzea. Honek arazoak ekar litzake etorkizunean (litekeena da ondo egotea).", + "domain_uninstall_app_first": "Honako aplikazio hauek domeinuan instalatuta daude:\n{apps}\n\nMesedez, desinstalatu 'yunohost app remove the_app_id' exekutatuz edo alda itzazu beste domeinu batera 'yunohost app change-url the_app_id' erabiliz domeinua ezabatu baino lehen", + "file_does_not_exist": "{path} fitxategia ez da existitzen.", + "firewall_rules_cmd_failed": "Suebakiko arau batzuen exekuzioak huts egin du. Informazio gehiago erregistroetan.", + "log_app_remove": "Ezabatu '{}' aplikazioa", + "global_settings_cant_open_settings": "Ezinezkoa izan da konfigurazio fitxategia irekitzea, zergatia: {reason}", + "global_settings_reset_success": "Lehengo ezarpenak {path}-n gorde dira", + "global_settings_unknown_setting_from_settings_file": "Gako ezezaguna ezarpenetan: '{setting_key}', baztertu eta gorde ezazu hemen: /etc/yunohost/settings-unknown.json", + "domain_remove_confirm_apps_removal": "Domeinu hau ezabatzean aplikazio hauek desinstalatuko dira:\n{apps}\n\nZiur al zaude? [{answers}]", + "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Baimendu DSA gakoa (zaharkitua) SSH zerbitzuaren konfiguraziorako", + "hook_list_by_invalid": "Aukera hau ezin da 'hook'ak zerrendatzeko erabili", + "installation_complete": "Instalazioa amaitu da", + "hook_exec_failed": "Ezinezkoa izan da agindua exekutatzea: {path}", + "hook_json_return_error": "Ezin izan da {path} aginduaren erantzuna irakurri. Errorea: {msg}. Jatorrizko edukia: {raw_content}", + "ip6tables_unavailable": "Ezin dituzu ip6taulak hemen moldatu; edukiontzi bat erabiltzen ari zara edo kernelak ez du aukera hau onartzen", + "log_link_to_log": "Eragiketa honen erregistro osoa: '{desc}'", + "log_operation_unit_unclosed_properly": "Eragiketa ez da modu egokian itxi", + "log_backup_restore_app": "Lehengoratu '{}' babeskopia fitxategi bat erabiliz", + "log_remove_on_failed_install": "Ezabatu '{}' instalazioak huts egin ondoren", + "log_domain_add": "Gehitu '{}' domeinua sistemaren konfiguraziora", + "log_dyndns_subscribe": "Eman izena YunoHosten '{}' azpidomeinuan", + "diagnosis_no_cache": "Oraindik ez dago '{category}' atalerako diagnostikoaren cacherik", + "diagnosis_mail_queue_ok": "Posta elektronikoaren ilaran zain dauden mezuak: {nb_pending}", + "global_settings_setting_smtp_relay_user": "SMTP relay erabiltzailea", + "domain_cert_gen_failed": "Ezinezkoa izan da ziurtagiria sortzea", + "field_invalid": "'{}' ez da baliogarria", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Operadore batzuei bost axola zaie internetaren neutraltasuna (Net Neutrality) eta ez dute 25. ataka desblokeatzen uzten.
- Operadore batzuek relay posta zerbitzari bat eskaini dezakete, baina kasu horretan zure posta elektronikoa zelatatu dezakete.
- Pribatutasuna bermatzeko *IP publikoa* duen VPN bat erabiltzea izan daiteke irtenbidea. Ikus https://yunohost.org/#/vpn_advantage
- Edo operadore desberdin batera aldatu", + "ldap_server_down": "Ezin izan da LDAP zerbitzarira konektatu", + "ldap_server_is_down_restart_it": "LDAP zerbitzaria ez dago martxan, saia zaitez berrabiarazten…", + "log_app_upgrade": "'{}' aplikazioa eguneratu", + "log_tools_shutdown": "Itzali zerbitzaria", + "log_user_permission_update": "Eguneratu '{}' baimenerako sarbideak", + "log_user_update": "Eguneratu '{}' erabiltzailearen informazioa", + "dyndns_no_domain_registered": "Ez dago DynDNSrekin izena emandako domeinurik", + "global_settings_bad_type_for_setting": "{setting} ezarpenerako mota okerra. {received_type} ezarri da, {expected_type} espero zen", + "diagnosis_mail_fcrdns_dns_missing": "Ez da alderantzizko DNSrik ezarri IPv{ipversion}rako. Litekeena da hartzaileak posta elektroniko batzuk jaso ezin izatea edo mezuok spam modura etiketatuak izatea.", + "log_backup_create": "Sortu babeskopia fitxategia", + "global_settings_setting_backup_compress_tar_archives": "Babeskopia berriak sortzean, konprimitu fitxategiak (.tar.gz) konprimitu gabeko fitxategien (.tar) ordez. Aukera hau gaitzean babeskopiek espazio gutxiago beharko dute, baina hasierako prozesua luzeagoa izango da eta CPUari lan handiagoa eragingo dio.", + "global_settings_setting_security_webadmin_allowlist": "Administrazio-ataria bisita dezaketen IP helbideak, koma bidez bereiziak.", + "global_settings_key_doesnt_exists": "'{settings_key}' gakoa ez da existitzen konfigurazio orokorrean; erabilgarri dauden gakoak ikus ditzakezu 'yunohost settings list' exekutatuz", + "global_settings_setting_ssowat_panel_overlay_enabled": "Gaitu SSOwat paneleko \"overlay\"a", + "log_backup_restore_system": "Lehengoratu sistema babeskopia fitxategi batetik", + "log_domain_remove": "Ezabatu '{}' domeinua sistemaren ezarpenetatik", + "log_link_to_failed_log": "Ezinezkoa izan da '{desc}' eragiketa exekutatzea. Mesedez, laguntza nahi izanez gero, partekatu erakigeta honen erregistro osoa hemen sakatuz", + "log_permission_url": "Eguneratu '{}' baimenari lotutako URLa", + "log_user_group_create": "Sortu '{}' taldea", + "permission_creation_failed": "Ezinezkoa izan da '{permission}' baimena sortzea: {error}", + "permission_not_found": "Ez da '{permission}' baimena aurkitu", + "pattern_lastname": "Abizen horrek ez du balio", + "permission_deleted": "'{permission}' baimena ezabatu da", + "service_disabled": "'{service}' zerbitzua ez da etorkizunean zerbitzaria abiaraztearekin batera exekutatuko.", + "unexpected_error": "Ezusteko zerbaitek huts egin du: {error}", + "updating_apt_cache": "Sistemaren paketeen eguneraketak eskuratzen…", + "mail_forward_remove_failed": "Ezinezkoa izan da '{mail}' posta elektronikoko birbidalketa ezabatzea", + "migration_ldap_migration_failed_trying_to_rollback": "Ezinezkoa izan da migratzea… sistema lehengoratzen saiatzen.", + "migrations_exclusive_options": "'--auto', '--skip', eta '--force-rerun' aukerek batak bestea baztertzen du.", + "migrations_running_forward": "{id} migrazioa exekutatzen…", + "regenconf_dry_pending_applying": "'{category}' atalari dagokion konfigurazioa egiaztatzen…", + "regenconf_file_backed_up": "'{conf} konfigurazio fitxategia '{backup}' babeskopian kopiatu da", + "regenconf_file_manually_modified": "'{conf}' konfigurazio fitxategia eskuz moldatu da eta ez da eguneratuko", + "regenconf_file_updated": "'{conf}' konfigurazio fitxategia eguneratu da", + "regenconf_updated": "'{category}' atalerako ezarpenak eguneratu dira", + "service_started": "'{service}' zerbitzua abiarazi da", + "show_tile_cant_be_enabled_for_regex": "Ezin duzu 'show_tile' gaitu une honetan, '{permission}' baimenerako URLa regex delako", + "unknown_main_domain_path": "{app} aplikaziorako domeinu edo bide ezezaguna. Domeinua eta bidea zehaztu behar dituzu baimena emateko URLa ahalbidetzeko.", + "user_import_partial_failed": "Erabiltzaileak inportatzeko eragiketak erdizka huts egin du", + "user_import_success": "Erabiltzaileak arazorik gabe inportatu dira", + "yunohost_already_installed": "YunoHost instalatuta dago dagoeneko", + "migrations_success_forward": "{id} migrazioak amaitu du", + "migrations_to_be_ran_manually": "{id} migrazioa eskuz abiarazi behar da. Mesedez, joan Erramintak → Migrazioak atalera administrazio-atarian edo bestela exekutatu 'yunohost tools migrations run'.", + "permission_currently_allowed_for_all_users": "Baimen hau erabiltzaile guztiei esleitzen zaie eta baita beste talde batzuei ere. Litekeena da 'all users' baimena edo esleituta duten taldeei baimena kendu nahi izatea.", + "permission_require_account": "'{permission}' baimena zerbitzarian kontua duten erabiltzaileentzat da eta, beraz, ezin da gaitu bisitarientzat.", + "postinstall_low_rootfsspace": "'root' fitxategi-sistemak 10 GB edo espazio gutxiago dauka, kezkatzekoa dena! Litekeena da espaziorik gabe geratzea aurki! Gomendagarria da 'root' fitxategi-sistemak gutxienez 16 GB libre izatea. Jakinarazpen honen ondoren YunoHost instalatzen jarraitu nahi baduzu, berrabiarazi agindua '--force-diskspace' gehituz", + "this_action_broke_dpkg": "Eragiketa honek dpkg/APT (sistemaren pakete kudeatzaileak) kaltetu ditu… Arazoa konpontzeko SSH bidez konektatu eta 'sudo apt install --fix-broken' edota 'sudo dpkg --configure -a' exekutatu dezakezu.", + "user_import_bad_line": "{line} lerro okerra: {details}", + "restore_complete": "Lehengoratzea amaitu da", + "restore_extracting": "Behar diren fitxategiak ateratzen…", + "regenconf_would_be_updated": "'{category}' atalerako konfigurazioa eguneratu izango litzatekeen", + "migrations_dependencies_not_satisfied": "Exekutatu honako migrazioak: '{dependencies_id}', {id} migratu baino lehen.", + "permission_created": "'{permission}' baimena sortu da", + "regenconf_now_managed_by_yunohost": "'{conf}' konfigurazio fitxategia YunoHostek kudeatzen du orain ({category} atala).", + "service_enabled": "'{service}' zerbitzua ez da automatikoki exekutatuko sistema abiaraztean.", + "service_removed": "'{service}' zerbitzua ezabatu da", + "service_restart_failed": "Ezin izan da '{service}' zerbitzua berrabiarazi\n\nZerbitzuen azken erregistroak: {logs}", + "service_restarted": "'{service}' zerbitzua berrabiarazi da", + "service_start_failed": "Ezin izan da '{service}' zerbitzua abiarazi\n\nZerbitzuen azken erregistroak: {logs}", + "update_apt_cache_failed": "Ezin da APT Debian-en pakete kudeatzailearen cachea eguneratu. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", + "update_apt_cache_warning": "Zerbaitek huts egin du APT Debian-en pakete kudeatzailearen cachea eguneratzean. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", + "user_created": "Erabiltzailea sortu da", + "user_deletion_failed": "Ezin izan da '{user}' ezabatu: {error}", + "permission_updated": "'{permission}' baimena moldatu da", + "ssowat_conf_generated": "SSOwat ezarpenak berregin dira", + "system_upgraded": "Sistema eguneratu da", + "upnp_port_open_failed": "Ezin izan da UPnP bidez ataka zabaldu", + "user_creation_failed": "Ezin izan da '{user}' erabiltzailea sortu: {error}", + "user_deleted": "Erabiltzailea ezabatu da", + "main_domain_changed": "Domeinu nagusia aldatu da", + "migrations_already_ran": "Honako migrazio hauek amaitu dute dagoeneko: {ids}", + "yunohost_installing": "YunoHost instalatzen…", + "migrations_failed_to_load_migration": "Ezinezkoa izan da {id} migrazioa kargatzea: {error}", + "migrations_must_provide_explicit_targets": "'--skip' edo '--force-rerun' aukerak erabiltzean jomuga zehatzak zehaztu behar dituzu", + "migrations_pending_cant_rerun": "Migrazio hauek exekutatzeke daude eta, beraz, ezin dira berriro abiarazi: {ids}", + "regenconf_file_kept_back": "'{conf}' konfigurazio fitxategia regen-conf-ek ({category} atala) ezabatzekoa zen baina mantendu egin da.", + "regenconf_file_removed": "'{conf}' konfigurazio fitxategia ezabatu da", + "permission_already_allowed": "'{group} taldeak badauka dagoeneko '{permission}' baimena", + "permission_cant_add_to_all_users": "{permission} baimena ezin da erabiltzaile guztiei ezarri.", + "mailbox_disabled": "Posta elektronikoa desgaituta dago {user} erabiltzailearentzat", + "operation_interrupted": "Eragiketa eskuz geldiarazi da?", + "permission_already_exist": "'{permission}' baimena existitzen da dagoeneko", + "regenconf_pending_applying": "'{category}' atalerako konfigurazioa ezartzen…", + "user_import_nothing_to_do": "Ez dago erabiltzaileak inportatu beharrik", + "mailbox_used_space_dovecot_down": "Dovecot mailbox zerbitzua martxan egon behar da postak erabilitako espazioa ezagutzeko", + "other_available_options": "… eta erakusten ez diren beste {n} aukera daude", + "permission_cannot_remove_main": "Ezin da baimen nagusi bat kendu", + "service_not_reloading_because_conf_broken": "Ez da '{name}' zerbitzua birkargatu/berrabiarazi konfigurazioa kaltetuta dagoelako: {errors}", + "service_reloaded": "'{service}' zerbitzua birkargatu da", + "service_reloaded_or_restarted": "'{service}' zerbitzua birkargatu edo berrabiarazi da", + "user_import_bad_file": "CSV fitxategiak ez du formatu egokia eta ekidingo da balizko datuen galera saihesteko", + "user_import_failed": "Erabiltzaileak inportatzeko eragiketak huts egin du", + "user_import_missing_columns": "Ondorengo zutabeak falta dira: {columns}", + "service_disable_failed": "Ezin izan da '{service}' zerbitzua geldiarazi zerbitzaria abiaraztean.\n\nZerbitzuen erregistro berrienak: {logs}", + "migrations_skip_migration": "{id} migrazioa saihesten…", + "upnp_disabled": "UPnP itzalita dago", + "main_domain_change_failed": "Ezinezkoa izan da domeinu nagusia aldatzea", + "regenconf_failed": "Ezinezkoa izan da ondorengo atal(ar)en konfigurazioa berregitea: {categories}", + "pattern_email_forward": "Helbide elektroniko baliagarri bat izan behar da, '+' karakterea onartzen da (adibidez: izena+urtea@domeinua.eus)", + "regenconf_file_manually_removed": "'{conf}' konfigurazio fitxategia eskuz ezabatu da eta ez da berriro sortuko", + "regenconf_up_to_date": "Konfigurazioa egunean dago dagoeneko '{category}' atalerako", + "migrations_no_such_migration": "Ez dago '{id}' izeneko migraziorik", + "migrations_not_pending_cant_skip": "Migrazio hauek ez daude exekutatzeke eta, beraz, ezin dira saihestu: {ids}", + "regex_with_only_domain": "Ezin duzu regex domeinuetarako erabili; bideetarako bakarrik", + "port_already_closed": "{port}. ataka itxita dago dagoeneko {ip_version} konexioetarako", + "regenconf_file_copy_failed": "Ezinezkoa izan da '{new}' konfigurazio fitxategi berria '{conf}'-(e)n kopiatzea", + "regenconf_file_remove_failed": "Ezinezkoa izan da '{conf}' konfigurazio fitxategia ezabatzea", + "server_shutdown_confirm": "Zerbitzaria berehala itzaliko da, ziur al zaude? [{answers}]", + "restore_already_installed_app": "'{app}' IDa duen aplikazioa dagoeneko instalatuta dago", + "service_description_postfix": "Posta elektronikoa bidali eta jasotzeko erabiltzen da", + "service_enable_failed": "Ezin izan da '{service}' zerbitzua sistema abiaraztearekin batera exekutatzea lortu.\n\nZerbitzuen erregistro berrienak: {logs}", + "system_username_exists": "Erabiltzaile izena existitzen da dagoeneko sistemaren erabiltzaileen zerrendan", + "user_already_exists": "'{user}' erabiltzailea existitzen da dagoeneko", + "mail_domain_unknown": "Ezinezkoa da posta elektroniko hori '{domain}' domeinurako erabiltzea. Mesedez, erabili zerbitzari honek kudeatzen duen domeinu bat.", + "migrations_list_conflict_pending_done": "Ezin dituzu '--previous' eta '--done' aldi berean erabili.", + "migrations_loading_migration": "{id} migrazioa kargatzen…", + "migrations_no_migrations_to_run": "Ez dago exekutatzeko migraziorik", + "password_listed": "Pasahitz hau munduan erabilienetarikoa da. Mesedez, aukeratu bereziagoa den beste bat.", + "password_too_simple_2": "Pasahitzak zortzi karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat eta txikiren bat izan behar ditu", + "pattern_firstname": "Izen horrek ez du balio", + "pattern_password": "Gutxienez hiru karaktere izan behar ditu", + "restore_failed": "Ezin izan da sistema lehengoratu", + "restore_removing_tmp_dir_failed": "Ezinezkoa izan da behin-behineko direktorio zaharra ezabatzea", + "restore_running_app_script": "'{app}' aplikazioa lehengoratzen…", + "root_password_replaced_by_admin_password": "Administrazio-pasahitzak root pasahitza ordezkatu du.", + "service_description_fail2ban": "Internetetik datozen bortxaz egindako saiakerak eta bestelako erasoak ekiditen ditu", + "service_description_ssh": "Zerbitzarira sare lokaletik kanpo konektatzea ahalbidetzen du (SSH protokoloa)", + "service_description_yunohost-firewall": "Zerbitzuen konexiorako atakak ireki eta ixteko kudeatzailea da", + "service_remove_failed": "Ezin izan da '{service}' zerbitzua ezabatu", + "service_reload_failed": "Ezin izan da '{service}' zerbitzua birkargatu\n\nZerbitzuen erregistro berrienak: {logs}", + "service_reload_or_restart_failed": "Ezin izan da '{service}' zerbitzua birkargatu edo berrabiarazi\n\nZerbitzuen erregistro berrienak: {logs}", + "service_stopped": "'{service}' zerbitzua geldiarazi da", + "unbackup_app": "{app} ez da gordeko", + "unrestore_app": "{app} ez da lehengoratuko", + "upgrade_complete": "Eguneraketa amaitu da", + "upgrading_packages": "Paketeak eguneratzen…", + "upnp_dev_not_found": "Ez da UPnP gailurik aurkitu", + "user_update_failed": "Ezin izan da {user} erabiltzailea eguneratu: {error}", + "user_updated": "Erabiltzailearen informazioa aldatu da", + "yunohost_configured": "YunoHost konfiguratuta dago", + "service_description_yunomdns": "Sare lokalean zerbitzarira 'yunohost.local' erabiliz konektatzea ahalbidetzen du", + "mail_alias_remove_failed": "Ezin izan da '{mail}' e-mail ezizena ezabatu", + "mail_unavailable": "Helbide elektroniko hau lehenengo erabiltzailearentzat gorde da eta hari ezarri zaio automatikoki", + "migration_ldap_backup_before_migration": "Sortu LDAP datubase eta aplikazioen ezarpenen babeskopia migrazioa abiarazi baino lehen.", + "migration_ldap_can_not_backup_before_migration": "Sistemaren babeskopiak ez du amaitu migrazioak huts egin baino lehen. Errorea: {error}", + "migrations_migration_has_failed": "{id} migrazioak ez du amaitu, geldiarazten. Errorea: {exception}", + "migrations_need_to_accept_disclaimer": "{id} migrazioa abiarazteko, ondorengo baldintzak onartu behar dituzu:\n---\n{disclaimer}\n---\nMigrazioa onartzen baduzu, mesedez berrabiarazi prozesua komandoan '--accept-disclaimer' aukera gehituz.", + "not_enough_disk_space": "Ez dago nahikoa espazio librerik '{path}'-n", + "password_too_simple_3": "Pasahitzak zortzi karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat, txikiren bat eta karaktere bereziren bat izan behar ditu", + "pattern_backup_archive_name": "Fitxategiaren izenak 30 karaktere izan ditzake gehienez, alfanumerikoak eta ._- baino ez", + "pattern_domain": "Domeinu izen baliagarri bat izan behar da (adibidez: nire-domeinua.eus)", + "pattern_mailbox_quota": "Tamainak b/k/M/G/T zehaztu behar du edo 0 mugarik ezarri nahi ez bada", + "pattern_password_app": "Barka, baina pasahitzek ezin dituzte ondorengo karaktereak izan: {forbidden_chars}", + "pattern_port_or_range": "Ataka zenbaki (0-65535) edo errenkada (100:200) baliagarri bat izan behar da", + "permission_already_disallowed": "'{group}' taldeak desgaituta dauka dagoeneko '{permission} baimena", + "permission_already_up_to_date": "Baimena ez da eguneratu egindako eskaria egungo egoerarekin bat datorrelako.", + "permission_protected": "'{permission}' baimena babestuta dago. Ezin duzu bisitarien taldea baimen honetara gehitu / baimen honetatik kendu.", + "permission_update_failed": "Ezinezkoa izan da '{permission}' baimena aldatzea: {error}", + "port_already_opened": "{port}. ataka dagoeneko irekita dago {ip_version} konexioetarako", + "user_home_creation_failed": "Ezin izan da erabiltzailearentzat '{home}' direktorioa sortu", + "user_unknown": "Erabiltzaile ezezaguna: {user}", + "yunohost_postinstall_end_tip": "Instalazio ondorengo prozesua amaitu da! Sistemaren konfigurazioa bukatzeko:\n- gehitu erabiltzaile bat administrazio-atariko 'Erabiltzaileak' atalean (edo 'yunohost user create ' komandoa erabiliz);\n- erabili 'Diagnostikoak' atala ohiko arazoei aurre hartzeko. Administrazio-atarian abiarazi edo 'yunohost diagnosis run' exekutatu;\n- irakurri 'Finalizing your setup' eta 'Getting to know YunoHost' atalak. Dokumentazioan aurki ditzakezu: https://yunohost.org/admindoc.", + "yunohost_not_installed": "YunoHost ez da zuzen instalatu. Mesedez, exekutatu 'yunohost tools postinstall'", + "unlimit": "Mugarik ez", + "restore_already_installed_apps": "Ondorengo aplikazioak ezin dira lehengoratu dagoeneko instalatuta daudelako: {apps}", + "password_too_simple_4": "Pasahitzak 12 karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat, txikiren bat eta karaktere bereziren bat izan behar ditu", + "pattern_email": "Helbide elektroniko baliagarri bat izan behar da, '+' karaktererik gabe (adibidez: izena@domeinua.eus)", + "pattern_username": "Txikiz idatzitako karaktere alfanumerikoak eta azpiko marra soilik eduki ditzake", + "permission_deletion_failed": "Ezinezkoa izan da '{permission}' baimena ezabatzea: {error}", + "migration_ldap_rollback_success": "Sistema lehengoratu da.", + "regenconf_need_to_explicitly_specify_ssh": "SSH ezarpenak eskuz aldatu dira, baina aldaketak erabiltzeko '--force' zehaztu behar duzu 'ssh' atalean.", + "regex_incompatible_with_tile": "/!\\ Pakete-arduradunak! {permission}' baimenak show_tile aukera 'true' bezala dauka eta horregatik ezin duzue regex URLa URL nagusi bezala ezarri", + "root_password_desynchronized": "Administrariaren pasahitza aldatu da baina YunoHostek ezin izan du aldaketa root pasahitzera hedatu!", + "server_shutdown": "Zerbitzaria itzaliko da", + "service_stop_failed": "Ezin izan da '{service}' zerbitzua geldiarazi\n\nZerbitzuen azken erregistroak: {logs}", + "service_unknown": "'{service}' zerbitzu ezezaguna", + "show_tile_cant_be_enabled_for_url_not_defined": "Ezin duzu 'show_tile' gaitu une honetan, '{permission}' baimenerako URL bat zehaztu behar duzulako", + "upnp_enabled": "UPnP piztuta dago", + "restore_nothings_done": "Ez da ezer lehengoratu", + "restore_backup_too_old": "Babeskopia fitxategi hau ezin da lehengoratu YunoHosten bertsio zaharregi batetik datorrelako.", + "restore_hook_unavailable": "'{part}'-(e)rako lehengoratze agindua ez dago erabilgarri ez sisteman ezta fitxategian ere", + "restore_cleaning_failed": "Ezin izan dira lehengoratzeko behin-behineko fitxategiak ezabatu", + "restore_confirm_yunohost_installed": "Ziur al zaude dagoeneko instalatuta dagoen sistema lehengoratu nahi duzula? [{answers}]", + "restore_may_be_not_enough_disk_space": "Badirudi zure sistemak ez duela nahikoa espazio (erabilgarri: {free_space} B, beharrezkoa {needed_space} B, segurtasuneko tartea: {margin} B)", + "restore_not_enough_disk_space": "Ez dago nahikoa espazio (erabilgarri: {free_space} B, beharrezkoa {needed_space} B, segurtasuneko tartea: {margin} B)", + "restore_running_hooks": "Lehengoratzeko 'hook'ak exekutatzen…", + "restore_system_part_failed": "Ezinezkoa izan da sistemaren '{part}' atala lehengoratzea", + "server_reboot": "Zerbitzaria berrabiaraziko da", + "server_reboot_confirm": "Zerbitzaria berehala berrabiaraziko da, ziur al zaude? [{answers}]", + "service_add_failed": "Ezinezkoa izan da '{service}' zerbitzua gehitzea", + "service_added": "'{service}' zerbitzua gehitu da", + "service_already_started": "'{service}' zerbitzua matxan dago dagoeneko", + "service_already_stopped": "'{service}' zerbitzua geldiarazi da dagoeneko", + "service_cmd_exec_failed": "Ezin izan da '{command}' komandoa exekutatu", + "service_description_dnsmasq": "Domeinuen izenen ebazpena (DNSa) kudeatzen du", + "service_description_dovecot": "Posta elektronikorako programei mezuak jasotzea ahalbidetzen die (IMAP eta POP3 bidez)", + "service_description_metronome": "Bat-bateko XMPP mezularitza kontuak kudeatzen ditu", + "service_description_mysql": "Aplikazioen datuak gordetzen ditu (SQL datubasea)", + "service_description_nginx": "Zerbitzariak ostatazen dituen webguneak ikusgai egiten ditu", + "service_description_redis-server": "Datuak bizkor atzitzeko, zereginak lerratzeko eta programen arteko komunikaziorako datubase berezi bat da", + "service_description_rspamd": "Spama bahetu eta posta elektronikoarekin zerikusia duten bestelako futzioen ardura dauka", + "service_description_slapd": "Erabiltzaileak, domeinuak eta hauei lotutako informazioa gordetzen du", + "service_description_yunohost-api": "YunoHosten web-atariaren eta sistemaren arteko hartuemana kudeatzen du", + "domain_config_default_app": "Lehenetsitako aplikazioa", + "tools_upgrade": "Sistemaren paketeak eguneratzen", + "tools_upgrade_failed": "Ezin izan dira paketeak eguneratu: {packages_list}", + "service_description_postgresql": "Aplikazioen datuak gordetzen ditu (SQL datubasea)", + "migration_0021_start": "Bullseye (e)rako migrazioa abiarazten", + "migration_0021_patching_sources_list": "sources.lists petatxatzen…", + "migration_0021_main_upgrade": "Eguneraketa nagusia abiarazten…", + "migration_0021_still_on_buster_after_main_upgrade": "Zerbaitek huts egin du eguneraketa nagusian, badirudi sistemak oraindik darabilela Debian Buster", + "migration_0021_yunohost_upgrade": "YunoHosten muineko eguneraketa abiarazten…", + "migration_0021_not_buster": "Uneko Debian ez da Buster!", + "migration_0021_not_enough_free_space": "/var/-enerabilgarri dagoen espazioa oso txikia da! Guxtienez GB 1 izan beharko zenuke erabilgarri migrazioari ekiteko.", + "migration_0021_system_not_fully_up_to_date": "Sistema ez dago erabat egunean. Mesedez, egizu eguneraketa arrunt bat Bullseye-(e)rako migrazioa abiarazi baino lehen.", + "migration_0021_general_warning": "Mesedez, kontuan hartu migrazio hau konplexua dela. YunoHost taldeak ahalegin handia egin du probatzeko, baina hala ere migrazioak sistemaren zatiren bat edo aplikazioak apurt litzake.\n\nHorregatik, gomendagarria da:\n\t- Datu edo aplikazio garrantzitsuen babeskopia egitea. Informazio gehiago: https://yunohost.org/backup;\n\t- Ez izan presarik migrazioa abiaraztean: zure internet eta hardwarearen arabera ordu batzuk ere iraun lezake eguneraketa prozesuak.", + "migration_0021_modified_files": "Mesedez, kontuan hartu ondorengo fitxategiak eskuz moldatu omen direla eta eguneraketak berridatziko dituela: {manually_modified_files}", + "migration_0021_cleaning_up": "Cachea eta erabilgarriak ez diren paketeak garbitzen…", + "migration_0021_patch_yunohost_conflicts": "Arazo gatazkatsu bati adabakia jartzen…", + "migration_description_0021_migrate_to_bullseye": "Eguneratu sistema Debian Bullseye eta Yunohost 11.x-ra", + "global_settings_setting_security_ssh_password_authentication": "Baimendu pasahitz bidezko autentikazioa SSHrako", + "migration_0021_problematic_apps_warning": "Mesedez, kontuan izan ziur asko gatazkatsuak izango diren odorengo aplikazioak aurkitu direla. Badirudi ez zirela YunoHost aplikazioen katalogotik instalatu, edo ez daude 'badabiltza' bezala etiketatuak. Ondorioz, ezin da bermatu eguneratu ondoren funtzionatzen jarraituko dutenik: {problematic_apps}", + "migration_0023_not_enough_space": "{path}-en ez dago toki nahikorik migrazioa abiarazteko.", + "migration_0023_postgresql_11_not_installed": "PostgreSQL ez zegoen zure isteman instalatuta. Ez dago egitekorik.", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 dago instalatuta baina PostgreSQL 13 ez!? Zerbait arraroa gertatu omen zaio zure sistemari :( …", + "migration_description_0022_php73_to_php74_pools": "Migratu php7.3-fpm 'pool' ezarpen-fitxategiak php7.4ra", + "migration_description_0023_postgresql_11_to_13": "Migratu datubaseak PostgreSQL 11tik 13ra" } \ No newline at end of file diff --git a/locales/fa.json b/locales/fa.json index 3e78c5de0..599ab1ea7 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -1,7 +1,6 @@ { "action_invalid": "اقدام نامعتبر '{action}'", "aborting": "رها کردن.", - "app_change_url_failed_nginx_reload": "NGINX بارگیری نشد. در اینجا خروجی 'nginx -t' است:\n{nginx_errors}", "app_argument_required": "استدلال '{name}' الزامی است", "app_argument_password_no_default": "خطا هنگام تجزیه گذرواژه '{name}': به دلایل امنیتی استدلال رمز عبور نمی تواند مقدار پیش فرض داشته باشد", "app_argument_invalid": "یک مقدار معتبر انتخاب کنید برای استدلال '{name}':{error}", @@ -152,7 +151,7 @@ "apps_catalog_update_success": "کاتالوگ برنامه به روز شد!", "apps_catalog_obsolete_cache": "حافظه پنهان کاتالوگ برنامه خالی یا منسوخ شده است.", "apps_catalog_failed_to_download": "بارگیری کاتالوگ برنامه {apps_catalog} امکان پذیر نیست: {error}", - "apps_catalog_updating": "در حال به روز رسانی کاتالوگ برنامه…", + "apps_catalog_updating": "در حال به روز رسانی کاتالوگ برنامه...", "apps_catalog_init_success": "سیستم کاتالوگ برنامه راه اندازی اولیه شد!", "apps_already_up_to_date": "همه برنامه ها در حال حاضر به روز هستند", "app_packaging_format_not_supported": "این برنامه قابل نصب نیست زیرا قالب بسته بندی آن توسط نسخه YunoHost شما پشتیبانی نمی شود. احتمالاً باید ارتقاء سیستم خود را در نظر بگیرید.", @@ -184,7 +183,6 @@ "app_manifest_install_ask_password": "گذرواژه مدیریتی را برای این برنامه انتخاب کنید", "app_manifest_install_ask_path": "مسیر URL (بعد از دامنه) را انتخاب کنید که این برنامه باید در آن نصب شود", "app_manifest_install_ask_domain": "دامنه ای را انتخاب کنید که این برنامه باید در آن نصب شود", - "app_manifest_invalid": "مشکلی در مانیفست برنامه وجود دارد: {error}", "app_location_unavailable": "این نشانی وب یا در دسترس نیست یا با برنامه (هایی) که قبلاً نصب شده در تعارض است:\n{apps}", "app_label_deprecated": "این دستور منسوخ شده است! لطفاً برای مدیریت برچسب برنامه از فرمان جدید'yunohost به روز رسانی مجوز کاربر' استفاده کنید.", "app_make_default_location_already_used": "نمی توان '{app}' را برنامه پیش فرض در دامنه قرار داد ، '{domain}' قبلاً توسط '{other_app}' استفاده می شود", @@ -200,7 +198,6 @@ "diagnosis_http_connection_error": "خطای اتصال: ارتباط با دامنه درخواست شده امکان پذیر نیست، به احتمال زیاد غیرقابل دسترسی است.", "diagnosis_http_timeout": "زمان تلاش برای تماس با سرور از خارج به پایان رسید. به نظر می رسد غیرقابل دسترسی است.
1. شایع ترین علت برای این مشکل ، پورت 80 است (و 443) به درستی به سرور شما ارسال نمی شوند.
2. همچنین باید مطمئن شوید که سرویس nginx در حال اجرا است
3. در تنظیمات پیچیده تر: مطمئن شوید که هیچ فایروال یا پروکسی معکوسی تداخل نداشته باشد.", "diagnosis_http_ok": "دامنه {domain} از طریق HTTP از خارج از شبکه محلی قابل دسترسی است.", - "diagnosis_http_localdomain": "انتظار نمی رود که دامنه {domain} ، با TLD محلی. از خارج از شبکه محلی به آن دسترسی پیدا کند.", "diagnosis_http_could_not_diagnose_details": "خطا: {error}", "diagnosis_http_could_not_diagnose": "نمی توان تشخیص داد که در IPv{ipversion} دامنه ها از خارج قابل دسترسی هستند یا خیر.", "diagnosis_http_hairpinning_issue_details": "این احتمالاً به دلیل جعبه / روتر ISP شما است. در نتیجه ، افراد خارج از شبکه محلی شما می توانند به سرور شما مطابق انتظار دسترسی پیدا کنند ، اما افراد داخل شبکه محلی (احتمالاً مثل شما؟) هنگام استفاده از نام دامنه یا IP جهانی. ممکن است بتوانید وضعیت را بهبود بخشید با نگاهی به https://yunohost.org/dns_local_network", @@ -349,14 +346,11 @@ "dyndns_ip_updated": "IP خود را در DynDNS به روز کرد", "dyndns_ip_update_failed": "آدرس IP را به DynDNS به روز نکرد", "dyndns_could_not_check_available": "بررسی نشد که آیا {domain} در {provider} در دسترس است یا خیر.", - "dyndns_could_not_check_provide": "بررسی نشد که آیا {provider} می تواند {domain} را ارائه دهد یا خیر.", "dpkg_lock_not_available": "این دستور در حال حاضر قابل اجرا نیست زیرا به نظر می رسد برنامه دیگری از قفل dpkg (مدیر بسته سیستم) استفاده می کند", "dpkg_is_broken": "شما نمی توانید این کار را در حال حاضر انجام دهید زیرا dpkg/APT (اداره کنندگان سیستم بسته ها) به نظر می رسد در وضعیت خرابی است… می توانید با اتصال از طریق SSH و اجرا این فرمان `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a` مشکل را حل کنید.", - "downloading": "در حال بارگیری…", + "downloading": "در حال بارگیری...", "done": "انجام شد", "domains_available": "دامنه های موجود:", - "domain_unknown": "دامنه ناشناخته", - "domain_name_unknown": "دامنه '{domain}' ناشناخته است", "domain_uninstall_app_first": "این برنامه ها هنوز روی دامنه شما نصب هستند:\n{apps}\n\nلطفاً قبل از اقدام به حذف دامنه ، آنها را با استفاده از 'برنامه yunohost remove the_app_id' حذف کرده یا با استفاده از 'yunohost app change-url the_app_id' به دامنه دیگری منتقل کنید", "domain_remove_confirm_apps_removal": "حذف این دامنه برنامه های زیر را حذف می کند:\n{apps}\n\nآیا طمئن هستید که میخواهید انجام دهید؟ [{answers}]", "domain_hostname_failed": "نام میزبان جدید قابل تنظیم نیست. این ممکن است بعداً مشکلی ایجاد کند (ممکن هم هست خوب باشد).", @@ -378,7 +372,6 @@ "permission_already_allowed": "گروه '{group}' قبلاً مجوز '{permission}' را فعال کرده است", "pattern_password_app": "متأسفیم ، گذرواژه ها نمی توانند شامل کاراکترهای زیر باشند: {forbidden_chars}", "pattern_username": "باید فقط حروف الفبایی کوچک و خط زیر باشد", - "pattern_positive_number": "باید یک عدد مثبت باشد", "pattern_port_or_range": "باید یک شماره پورت معتبر (یعنی 0-65535) یا محدوده پورت (به عنوان مثال 100: 200) باشد", "pattern_password": "باید حداقل 3 کاراکتر داشته باشد", "pattern_mailbox_quota": "باید اندازه ای با پسوند b / k / M / G / T یا 0 داشته باشد تا سهمیه نداشته باشد", @@ -393,7 +386,6 @@ "password_too_simple_2": "گذرواژه باید حداقل 8 کاراکتر طول داشته باشد و شامل عدد ، حروف الفبائی کوچک و بزرگ باشد", "password_too_simple_1": "رمز عبور باید حداقل 8 کاراکتر باشد", "password_listed": "این رمز در بین پر استفاده ترین رمزهای عبور در جهان قرار دارد. لطفاً چیزی منحصر به فرد تر انتخاب کنید.", - "packages_upgrade_failed": "همه بسته ها را نمی توان ارتقا داد", "operation_interrupted": "عملیات به صورت دستی قطع شد؟", "invalid_password": "رمز عبور نامعتبر", "invalid_number": "باید یک عدد باشد", @@ -414,41 +406,11 @@ "migrations_exclusive_options": "'--auto', '--skip'، و '--force-rerun' گزینه های متقابل هستند.", "migrations_failed_to_load_migration": "مهاجرت بار نشد {id}: {error}", "migrations_dependencies_not_satisfied": "این مهاجرت ها را اجرا کنید: '{dependencies_id}' ، قبل از مهاجرت {id}.", - "migrations_cant_reach_migration_file": "دسترسی به پرونده های مهاجرت در مسیر '٪ s' امکان پذیر نیست", "migrations_already_ran": "این مهاجرت ها قبلاً انجام شده است: {ids}", - "migration_0019_slapd_config_will_be_overwritten": "به نظر می رسد که شما پیکربندی slapd را به صورت دستی ویرایش کرده اید. برای این مهاجرت بحرانی ، YunoHost باید به روز رسانی پیکربندی slapd را مجبور کند. فایلهای اصلی در {conf_backup_folder} پشتیبان گیری می شوند.", - "migration_0019_add_new_attributes_in_ldap": "اضافه کردن ویژگی های جدید برای مجوزها در پایگاه داده LDAP", - "migration_0018_failed_to_reset_legacy_rules": "تنظیم مجدد قوانین iptables قدیمی انجام نشد: {error}", - "migration_0018_failed_to_migrate_iptables_rules": "انتقال قوانین قدیمی iptables به nftables انجام نشد: {error}", - "migration_0017_not_enough_space": "فضای کافی در {path} برای اجرای مهاجرت در دسترس قرار دهید.", - "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 نصب شده است ، اما postgresql 11 نه؟ ممکن است اتفاق عجیبی در سیستم شما رخ داده باشد:(...", - "migration_0017_postgresql_96_not_installed": "PostgreSQL روی سیستم شما نصب نشده است. کاری برای انجام دادن نیست.", - "migration_0015_weak_certs": "گواهینامه های زیر هنوز از الگوریتم های امضای ضعیف استفاده می کنند و برای سازگاری با نسخه بعدی nginx باید ارتقاء یابند: {certs}", - "migration_0015_cleaning_up": "پاک کردن حافظه پنهان و بسته ها دیگر مفید نیست...", - "migration_0015_specific_upgrade": "شروع به روزرسانی بسته های سیستم که باید به طور مستقل ارتقا یابد...", - "migration_0015_modified_files": "لطفاً توجه داشته باشید که فایل های زیر به صورت دستی اصلاح شده اند و ممکن است پس از ارتقاء رونویسی شوند: {manually_modified_files}", - "migration_0015_problematic_apps_warning": "لطفاً توجه داشته باشید که احتمالاً برنامه های نصب شده مشکل ساز تشخیص داده شده. به نظر می رسد که آنها از فهرست برنامه YunoHost نصب نشده اند یا به عنوان 'working' علامت گذاری نشده اند. در نتیجه ، نمی توان تضمین کرد که پس از ارتقاء همچنان کار خواهند کرد: {problematic_apps}", - "migration_0015_general_warning": "لطفاً توجه داشته باشید که این مهاجرت یک عملیات ظریف است. تیم YunoHost تمام تلاش خود را برای بررسی و آزمایش آن انجام داد ، اما مهاجرت ممکن است بخشهایی از سیستم یا برنامه های آن را خراب کند.\n\nبنابراین ، توصیه می شود:\n- پشتیبان گیری از هرگونه داده یا برنامه حیاتی را انجام دهید. اطلاعات بیشتر در https://yunohost.org/backup ؛\n- پس از راه اندازی مهاجرت صبور باشید: بسته به اتصال به اینترنت و سخت افزار شما ، ممکن است چند ساعت طول بکشد تا همه چیز ارتقا یابد.", - "migration_0015_system_not_fully_up_to_date": "سیستم شما کاملاً به روز نیست. لطفاً قبل از اجرای مهاجرت به Buster ، یک ارتقاء منظم انجام دهید.", - "migration_0015_not_enough_free_space": "فضای آزاد در /var /بسیار کم است! برای اجرای این مهاجرت باید حداقل 1 گیگابایت فضای آزاد داشته باشید.", - "migration_0015_not_stretch": "توزیع دبیان فعلی استرچ نیست!", - "migration_0015_yunohost_upgrade": "شروع به روز رسانی اصلی YunoHost...", - "migration_0015_still_on_stretch_after_main_upgrade": "هنگام ارتقاء اصلی مشکلی پیش آمد ، به نظر می رسد سیستم هنوز در Debian Stretch است", - "migration_0015_main_upgrade": "شروع به روزرسانی اصلی...", - "migration_0015_patching_sources_list": "وصله منابع. لیست ها...", - "migration_0015_start": "شروع مهاجرت به باستر", - "migration_update_LDAP_schema": "در حال به روزرسانی طرح وشمای LDAP...", "migration_ldap_rollback_success": "سیستم برگردانده شد.", "migration_ldap_migration_failed_trying_to_rollback": "نمی توان مهاجرت کرد... تلاش برای بازگرداندن سیستم.", "migration_ldap_can_not_backup_before_migration": "نمی توان پشتیبان گیری سیستم را قبل از شکست مهاجرت تکمیل کرد. خطا: {error}", "migration_ldap_backup_before_migration": "ایجاد پشتیبان از پایگاه داده LDAP و تنظیمات برنامه ها قبل از مهاجرت واقعی.", - "migration_description_0020_ssh_sftp_permissions": "پشتیبانی مجوزهای SSH و SFTP را اضافه کنید", - "migration_description_0019_extend_permissions_features": "سیستم مدیریت مجوز برنامه را تمدید / دوباره کار بندازید", - "migration_description_0018_xtable_to_nftable": "مهاجرت از قوانین قدیمی ترافیک شبکه به سیستم جدید nftable", - "migration_description_0017_postgresql_9p6_to_11": "مهاجرت پایگاه های داده از PostgreSQL 9.6 به 11", - "migration_description_0016_php70_to_php73_pools": "انتقال فایلهای conf php7.0-fpm 'pool' به php7.3", - "migration_description_0015_migrate_to_buster": "سیستم را به Debian Buster و YunoHost 4.x ارتقا دهید", - "migrating_legacy_permission_settings": "در حال انتقال تنظیمات مجوز قدیمی...", "main_domain_changed": "دامنه اصلی تغییر کرده است", "main_domain_change_failed": "تغییر دامنه اصلی امکان پذیر نیست", "mail_unavailable": "این آدرس ایمیل محفوظ است و باید به طور خودکار به اولین کاربر اختصاص داده شود", @@ -488,8 +450,6 @@ "log_backup_restore_system": "بازیابی سیستم بوسیله آرشیو پشتیبان", "log_backup_create": "بایگانی پشتیبان ایجاد کنید", "log_available_on_yunopaste": "این گزارش اکنون از طریق {url} در دسترس است", - "log_app_config_apply": "پیکربندی را در برنامه '{}' اعمال کنید", - "log_app_config_show_panel": "پانل پیکربندی برنامه '{}' را نشان دهید", "log_app_action_run": "عملکرد برنامه '{}' را اجرا کنید", "log_app_makedefault": "\"{}\" را برنامه پیش فرض قرار دهید", "log_app_upgrade": "برنامه '{}' را ارتقاء دهید", @@ -500,7 +460,7 @@ "log_does_exists": "هیچ گزارش عملیاتی با نام '{log}' وجود ندارد ، برای مشاهده همه گزارش عملیّات های موجود در خط فرمان از دستور 'yunohost log list' استفاده کنید", "log_help_to_get_failed_log": "عملیات '{desc}' کامل نشد. لطفاً برای دریافت راهنمایی و کمک ، گزارش کامل این عملیات را با استفاده از دستور 'yunohost log share {name}' به اشتراک بگذارید", "log_link_to_failed_log": "عملیّات '{desc}' کامل نشد. لطفاً گزارش کامل این عملیات را ارائه دهید بواسطه اینجا را کلیک کنید برای دریافت کمک", - "log_help_to_get_log": "برای مشاهده گزارش عملیات '{desc}'، از دستور 'yunohost log show {name}{name}' استفاده کنید", + "log_help_to_get_log": "برای مشاهده گزارش عملیات '{desc}'، از دستور 'yunohost log show {name}' استفاده کنید", "log_link_to_log": "گزارش کامل این عملیات: {desc}'", "log_corrupted_md_file": "فایل فوق داده YAML مربوط به گزارش ها آسیب دیده است: '{md_file}\nخطا: {error} '", "ldap_server_is_down_restart_it": "سرویس LDAP خاموش است ، سعی کنید آن را دوباره راه اندازی کنید...", @@ -536,19 +496,9 @@ "unknown_main_domain_path": "دامنه یا مسیر ناشناخته برای '{app}'. شما باید یک دامنه و یک مسیر را مشخص کنید تا بتوانید یک آدرس اینترنتی برای مجوز تعیین کنید.", "unexpected_error": "مشکل غیر منتظره ای پیش آمده: {error}", "unbackup_app": "{app} ذخیره نمی شود", - "tools_upgrade_special_packages_completed": "ارتقاء بسته YunoHost به پایان رسید\nبرای بازگرداندن خط فرمان [Enter] را فشار دهید", - "tools_upgrade_special_packages_explanation": "ارتقاء ویژه در پس زمینه ادامه خواهد یافت. لطفاً تا 10 دقیقه دیگر (بسته به سرعت سخت افزار) هیچ اقدام دیگری را روی سرور خود شروع نکنید. پس از این کار ، ممکن است مجبور شوید دوباره وارد webadmin شوید. گزارش ارتقاء در Tools → Log (در webadmin) یا با استفاده از 'yunohost log list' (در خط فرمان) در دسترس خواهد بود.", - "tools_upgrade_special_packages": "در حال ارتقاء بسته های 'special' (مربوط به yunohost)…", - "tools_upgrade_regular_packages_failed": "بسته ها را نمی توان ارتقا داد: {packages_list}", - "tools_upgrade_regular_packages": "در حال ارتقاء بسته های 'regular' (غیر مرتبط با yunohost)…", - "tools_upgrade_cant_unhold_critical_packages": "بسته های مهم و حیاتی را نمی توان نگه نداشت…", - "tools_upgrade_cant_hold_critical_packages": "بسته های مهم و حیاتی را نمی توان نگه داشت…", - "tools_upgrade_cant_both": "نمی توان سیستم و برنامه ها را به طور همزمان ارتقا داد", - "tools_upgrade_at_least_one": "لطفاً مشخص کنید 'apps' ، یا 'system'", "this_action_broke_dpkg": "این اقدام dpkg/APT (مدیران بسته های سیستم) را خراب کرد... می توانید با اتصال از طریق SSH و اجرای فرمان `sudo apt install --fix -break` و/یا` sudo dpkg --configure -a` این مشکل را حل کنید.", "system_username_exists": "نام کاربری قبلاً در لیست کاربران سیستم وجود دارد", "system_upgraded": "سیستم ارتقا یافت", - "ssowat_conf_updated": "پیکربندی SSOwat به روزرسانی شد", "ssowat_conf_generated": "پیکربندی SSOwat بازسازی شد", "show_tile_cant_be_enabled_for_regex": "شما نمی توانید \"show_tile\" را درست فعال کنید ، چرا که آدرس اینترنتی مجوز '{permission}' یک عبارت منظم است", "show_tile_cant_be_enabled_for_url_not_defined": "شما نمی توانید \"show_tile\" را در حال حاضر فعال کنید ، زیرا ابتدا باید یک آدرس اینترنتی برای مجوز '{permission}' تعریف کنید", @@ -565,7 +515,6 @@ "service_reload_failed": "سرویس '{service}' بارگیری نشد\n\nگزارشات اخیر سرویس: {logs}", "service_removed": "سرویس '{service}' حذف شد", "service_remove_failed": "سرویس '{service}' حذف نشد", - "service_regen_conf_is_deprecated": "فرمان 'yunohost service regen-conf' منسوخ شده است! لطفاً به جای آن از 'yunohost tools regen-conf' استفاده کنید.", "service_enabled": "سرویس '{service}' اکنون بطور خودکار در هنگام بوت شدن سیستم راه اندازی می شود.", "service_enable_failed": "انجام سرویس '{service}' به طور خودکار در هنگام راه اندازی امکان پذیر نیست.\n\nگزارشات اخیر سرویس: {logs}", "service_disabled": "هنگام راه اندازی سیستم ، سرویس '{service}' دیگر راه اندازی نمی شود.", @@ -577,7 +526,6 @@ "service_description_rspamd": "هرزنامه ها و سایر ویژگی های مربوط به ایمیل را فیلتر می کند", "service_description_redis-server": "یک پایگاه داده تخصصی برای دسترسی سریع به داده ها ، صف وظیفه و ارتباط بین برنامه ها استفاده می شود", "service_description_postfix": "برای ارسال و دریافت ایمیل استفاده می شود", - "service_description_php7.3-fpm": "برنامه های نوشته شده با PHP را با NGINX اجرا می کند", "service_description_nginx": "به همه وب سایت هایی که روی سرور شما میزبانی شده اند سرویس می دهد یا دسترسی به آنها را فراهم می کند", "service_description_mysql": "ذخیره داده های برنامه (پایگاه داده SQL)", "service_description_metronome": "مدیریت حساب های پیام رسانی فوری XMPP", @@ -597,15 +545,15 @@ "root_password_replaced_by_admin_password": "گذرواژه ریشه شما با رمز مدیریت جایگزین شده است.", "root_password_desynchronized": "گذرواژه مدیریت تغییر کرد ، اما YunoHost نتوانست این را به رمز عبور ریشه منتقل کند!", "restore_system_part_failed": "بخش سیستم '{part}' بازیابی و ترمیم نشد", - "restore_running_hooks": "در حال اجرای قلاب های ترمیم و بازیابی…", - "restore_running_app_script": "ترمیم و بازیابی برنامه '{app}'…", + "restore_running_hooks": "در حال اجرای قلاب های ترمیم و بازیابی...", + "restore_running_app_script": "ترمیم و بازیابی برنامه '{app}'...", "restore_removing_tmp_dir_failed": "پوشه موقت قدیمی حذف نشد", "restore_nothings_done": "هیچ چیز ترمیم و بازسازی نشد", "restore_not_enough_disk_space": "فضای کافی موجود نیست (فضا: {free_space} B ، فضای مورد نیاز: {needed_space} B ، حاشیه امنیتی: {margin} B)", "restore_may_be_not_enough_disk_space": "به نظر می رسد سیستم شما فضای کافی ندارد (فضای آزاد: {free_space} B ، فضای مورد نیاز: {needed_space} B ، حاشیه امنیتی: {margin} B)", "restore_hook_unavailable": "اسکریپت ترمیم و بازسازی برای '{part}' در سیستم شما در دسترس نیست و همچنین در بایگانی نیز وجود ندارد", "restore_failed": "سیستم بازیابی نشد", - "restore_extracting": "استخراج فایل های مورد نیاز از بایگانی…", + "restore_extracting": "استخراج فایل های مورد نیاز از بایگانی...", "restore_confirm_yunohost_installed": "آیا واقعاً می خواهید سیستمی که هم اکنون نصب شده را بازیابی کنید؟ [{answers}]", "restore_complete": "مرمت به پایان رسید", "restore_cleaning_failed": "فهرست بازسازی موقت پاک نشد", @@ -617,7 +565,7 @@ "regenconf_need_to_explicitly_specify_ssh": "پیکربندی ssh به صورت دستی تغییر یافته است ، اما شما باید صراحتاً دسته \"ssh\" را با --force برای اعمال تغییرات در واقع مشخص کنید.", "regenconf_pending_applying": "در حال اعمال پیکربندی معلق برای دسته '{category}'...", "regenconf_failed": "پیکربندی برای دسته (ها) بازسازی نشد: {categories}", - "regenconf_dry_pending_applying": "در حال بررسی پیکربندی معلق که برای دسته '{category}' اعمال می شد…", + "regenconf_dry_pending_applying": "در حال بررسی پیکربندی معلق که برای دسته '{category}' اعمال می شد...", "regenconf_would_be_updated": "پیکربندی برای دسته '{category}' به روز می شد", "regenconf_updated": "پیکربندی برای دسته '{category}' به روز شد", "regenconf_up_to_date": "پیکربندی در حال حاضر برای دسته '{category}' به روز است", @@ -642,4 +590,4 @@ "permission_deleted": "مجوز '{permission}' حذف شد", "permission_cant_add_to_all_users": "مجوز {permission} را نمی توان به همه کاربران اضافه کرد.", "permission_currently_allowed_for_all_users": "این مجوز در حال حاضر به همه کاربران علاوه بر آن گروه های دیگر نیز اعطا شده. احتمالاً بخواهید مجوز 'all_users' را حذف کنید یا سایر گروه هایی را که در حال حاضر مجوز به آنها اعطا شده است را هم حذف کنید." -} +} \ No newline at end of file diff --git a/locales/fi.json b/locales/fi.json index 9e26dfeeb..05fe2e9a1 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -1 +1,5 @@ -{} \ No newline at end of file +{ + "aborting": "Keskeytetään.", + "password_too_simple_1": "Salasanan pitää olla ainakin 8 merkin pituinen", + "action_invalid": "Virheellinen toiminta '{action}'" +} \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index 9551fbcbd..833d9554b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -4,13 +4,12 @@ "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}', il doit être l'un de {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", "app_id_invalid": "Identifiant d'application invalide", "app_install_files_invalid": "Fichiers d'installation incorrects", - "app_manifest_invalid": "Manifeste d'application incorrect : {error}", "app_not_correctly_installed": "{app} semble être mal installé", "app_not_installed": "Nous n'avons pas trouvé {app} dans la liste des applications installées : {all_apps}", "app_not_properly_removed": "{app} n'a pas été supprimé correctement", @@ -42,7 +41,7 @@ "backup_output_directory_forbidden": "Choisissez un répertoire de destination différent. Les sauvegardes ne peuvent pas être créées dans les sous-dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", "backup_output_directory_not_empty": "Le répertoire de destination n'est pas vide", "backup_output_directory_required": "Vous devez spécifier un dossier de destination pour la sauvegarde", - "backup_running_hooks": "Exécution des scripts de sauvegarde...", + "backup_running_hooks": "Exécution des scripts de sauvegarde ...", "custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application personnalisée {app}", "disk_space_not_sufficient_install": "Il ne reste pas assez d'espace disque pour installer cette application", "disk_space_not_sufficient_update": "Il ne reste pas assez d'espace disque pour mettre à jour cette application", @@ -55,7 +54,6 @@ "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_exists": "Le domaine existe déjà", "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nVeuillez les désinstaller avec la commande 'yunohost app remove nom-de-l-application' ou les déplacer vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application' avant de procéder à la suppression du domaine", - "domain_unknown": "Domaine inconnu", "done": "Terminé", "downloading": "Téléchargement en cours...", "dyndns_ip_update_failed": "Impossible de mettre à jour l'adresse IP sur le domaine DynDNS", @@ -84,7 +82,6 @@ "main_domain_change_failed": "Impossible de modifier le domaine principal", "main_domain_changed": "Le domaine principal a été modifié", "not_enough_disk_space": "L'espace disque est insuffisant sur '{path}'", - "packages_upgrade_failed": "Impossible de mettre à jour tous les paquets", "pattern_backup_archive_name": "Doit être un nom de fichier valide avec un maximum de 30 caractères, et composé de caractères alphanumériques et -_. uniquement", "pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.fr)", "pattern_email": "Il faut une adresse électronique valide, sans le symbole '+' (par exemple johndoe@exemple.com)", @@ -93,7 +90,6 @@ "pattern_mailbox_quota": "Doit avoir une taille suffixée avec b/k/M/G/T ou 0 pour désactiver le quota", "pattern_password": "Doit être composé d'au moins 3 caractères", "pattern_port_or_range": "Doit être un numéro de port valide compris entre 0 et 65535, ou une gamme de ports (exemple : 100:200)", - "pattern_positive_number": "Doit être un nombre positif", "pattern_username": "Doit être composé uniquement de caractères alphanumériques minuscules et de tirets bas (aussi appelé tiret du 8 ou underscore)", "port_already_closed": "Le port {port} est déjà fermé pour les connexions {ip_version}", "port_already_opened": "Le port {port} est déjà ouvert pour les connexions {ip_version}", @@ -124,7 +120,6 @@ "service_stopped": "Le service '{service}' a été arrêté", "service_unknown": "Le service '{service}' est inconnu", "ssowat_conf_generated": "La configuration de SSOwat a été regénérée", - "ssowat_conf_updated": "La configuration de SSOwat a été mise à jour", "system_upgraded": "Système mis à jour", "system_username_exists": "Ce nom d'utilisateur existe déjà dans les utilisateurs système", "unbackup_app": "'{app}' ne sera pas sauvegardée", @@ -137,15 +132,15 @@ "upnp_dev_not_found": "Aucun périphérique compatible UPnP n'a été trouvé", "upnp_disabled": "L'UPnP est désactivé", "upnp_enabled": "L'UPnP est activé", - "upnp_port_open_failed": "Impossible d’ouvrir les ports UPnP", - "user_created": "L’utilisateur a été créé", - "user_creation_failed": "Impossible de créer l’utilisateur {user} : {error}", - "user_deleted": "L’utilisateur a été supprimé", - "user_deletion_failed": "Impossible de supprimer l’utilisateur {user} : {error}", - "user_home_creation_failed": "Impossible de créer le dossier personnel '{home}' de l’utilisateur", - "user_unknown": "L’utilisateur {user} est inconnu", - "user_update_failed": "Impossible de mettre à jour l’utilisateur {user} : {error}", - "user_updated": "L’utilisateur a été modifié", + "upnp_port_open_failed": "Impossible d'ouvrir les ports UPnP", + "user_created": "L'utilisateur a été créé", + "user_creation_failed": "Impossible de créer l'utilisateur {user} : {error}", + "user_deleted": "L'utilisateur a été supprimé", + "user_deletion_failed": "Impossible de supprimer l'utilisateur {user} : {error}", + "user_home_creation_failed": "Impossible de créer le dossier personnel '{home}' de l'utilisateur", + "user_unknown": "L'utilisateur {user} est inconnu", + "user_update_failed": "Impossible de mettre à jour l'utilisateur {user} : {error}", + "user_updated": "L'utilisateur a été modifié", "yunohost_already_installed": "YunoHost est déjà installé", "yunohost_configured": "YunoHost est maintenant configuré", "yunohost_installing": "L'installation de YunoHost est en cours...", @@ -169,11 +164,10 @@ "certmanager_unable_to_parse_self_CA_name": "Impossible d'analyser le nom de l'autorité du certificat auto-signé (fichier : {file})", "mailbox_used_space_dovecot_down": "Le service Dovecot doit être démarré si vous souhaitez voir l'espace disque occupé par la messagerie", "domains_available": "Domaines disponibles :", - "backup_archive_broken_link": "Impossible d’accéder à l’archive de sauvegarde (lien invalide vers {path})", + "backup_archive_broken_link": "Impossible d'accéder à l'archive de sauvegarde (lien invalide vers {path})", "certmanager_acme_not_configured_for_domain": "Le challenge ACME n'a pas pu être validé pour le domaine {domain} pour le moment car le code de la configuration NGINX est manquant... Merci de vérifier que votre configuration NGINX est à jour avec la commande : `yunohost tools regen-conf nginx --dry-run --with-diff`.", - "domain_hostname_failed": "Échec de l’utilisation d’un nouveau nom d'hôte. Cela pourrait causer des soucis plus tard (cela n'en causera peut-être pas).", - "app_already_installed_cant_change_url": "Cette application est déjà installée. L’URL ne peut pas être changé simplement par cette fonction. Vérifiez si cela est disponible avec `app changeurl`.", - "app_change_url_failed_nginx_reload": "Le redémarrage de NGINX a échoué. Voici la sortie de 'nginx -t' :\n{nginx_errors}", + "domain_hostname_failed": "Échec de l'utilisation d'un nouveau nom d'hôte. Cela pourrait causer des soucis plus tard (cela n'en causera peut-être pas).", + "app_already_installed_cant_change_url": "Cette application est déjà installée. L'URL ne peut pas être changé simplement par cette fonction. Vérifiez si cela est disponible avec `app changeurl`.", "app_change_url_identical_domains": "L'ancien et le nouveau couple domaine/chemin_de_l'URL sont identiques pour ('{domain}{path}'), rien à faire.", "app_change_url_no_script": "L'application '{app_name}' ne prend pas encore en charge le changement d'URL. Vous devriez peut-être la mettre à jour.", "app_change_url_success": "L'URL de l'application {app} a été changée en {domain}{path}", @@ -188,9 +182,9 @@ "global_settings_unknown_type": "Situation inattendue : la configuration {setting} semble avoir le type {unknown_type} mais celui-ci n'est pas pris en charge par le système.", "global_settings_unknown_setting_from_settings_file": "Clé inconnue dans les paramètres : '{setting_key}', rejet de cette clé et sauvegarde de celle-ci dans /etc/yunohost/unkown_settings.json", "backup_abstract_method": "Cette méthode de sauvegarde reste à implémenter", - "backup_applying_method_tar": "Création de l'archive TAR de la sauvegarde...", - "backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder...", - "backup_applying_method_custom": "Appel de la méthode de sauvegarde personnalisée '{method}'...", + "backup_applying_method_tar": "Création de l'archive TAR de la sauvegarde ...", + "backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder ...", + "backup_applying_method_custom": "Appel de la méthode de sauvegarde personnalisée '{method}' ...", "backup_archive_system_part_not_available": "La partie '{part}' du système n'est pas disponible dans cette sauvegarde", "backup_archive_writing_error": "Impossible d'ajouter des fichiers '{source}' (nommés dans l'archive : '{dest}') à sauvegarder dans l'archive compressée '{archive}'", "backup_ask_for_copying_if_needed": "Voulez-vous effectuer la sauvegarde en utilisant {size}Mo temporairement ? (Cette méthode est utilisée car certains fichiers n'ont pas pu être préparés avec une méthode plus efficace.)", @@ -216,7 +210,6 @@ "restore_system_part_failed": "Impossible de restaurer la partie '{part}' du système", "backup_couldnt_bind": "Impossible de lier {src} avec {dest}.", "domain_dns_conf_is_just_a_recommendation": "Cette commande vous montre la configuration *recommandée*. Elle ne configure pas le DNS pour vous. Il est de votre ressort de configurer votre zone DNS chez votre registrar/fournisseur conformément à cette recommandation.", - "migrations_cant_reach_migration_file": "Impossible d'accéder aux fichiers de migration via le chemin '%s'", "migrations_loading_migration": "Chargement de la migration {id}...", "migrations_migration_has_failed": "La migration {id} a échoué avec l'exception {exception} : annulation", "migrations_no_migrations_to_run": "Aucune migration à lancer", @@ -226,7 +219,6 @@ "server_reboot": "Le serveur va redémarrer", "server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers}]", "app_upgrade_some_app_failed": "Certaines applications n'ont pas été mises à jour", - "dyndns_could_not_check_provide": "Impossible de vérifier si {provider} peut fournir {domain}.", "dyndns_domain_not_provided": "Le fournisseur DynDNS {provider} ne peut pas fournir le domaine {domain}.", "app_make_default_location_already_used": "Impossible de configurer l'application '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par l'application '{other_app}'", "app_upgrade_app_name": "Mise à jour de {app}...", @@ -251,7 +243,7 @@ "experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l'utiliser à moins que vous ne sachiez ce que vous faites.", "log_corrupted_md_file": "Le fichier YAML de métadonnées associé aux logs est corrompu : '{md_file}'\nErreur : {error}", "log_link_to_log": "Journal complet de cette opération : ' {desc} '", - "log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log show {name}{name}'", + "log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log show {name}'", "log_link_to_failed_log": "L'opération '{desc}' a échoué ! Pour obtenir de l'aide, merci de partager le journal de l'opération en cliquant ici", "log_help_to_get_failed_log": "L'opération '{desc}' a échoué ! Pour obtenir de l'aide, merci de partager le journal de l'opération en utilisant la commande 'yunohost log share {name}'", "log_does_exists": "Il n'y a pas de journal des opérations avec le nom '{log}', utilisez 'yunohost log list' pour voir tous les journaux d'opérations disponibles", @@ -283,7 +275,7 @@ "log_tools_shutdown": "Éteindre votre serveur", "log_tools_reboot": "Redémarrer votre serveur", "mail_unavailable": "Cette adresse d'email est réservée et doit être automatiquement attribuée au tout premier utilisateur", - "good_practices_about_admin_password": "Vous êtes sur le point de définir un nouveau mot de passe d'administration. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou d'utiliser une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", + "good_practices_about_admin_password": "Vous êtes sur le point de définir un nouveau mot de passe administrateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou d'utiliser une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "good_practices_about_user_password": "Vous êtes sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "password_listed": "Ce mot de passe fait partie des mots de passe les plus utilisés dans le monde. Veuillez en choisir un autre moins commun et plus robuste.", "password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères", @@ -300,7 +292,7 @@ "app_upgrade_several_apps": "Les applications suivantes seront mises à jour : {apps}", "ask_new_domain": "Nouveau domaine", "ask_new_path": "Nouveau chemin", - "backup_actually_backuping": "Création d'une archive de sauvegarde à partir des fichiers collectés...", + "backup_actually_backuping": "Création d'une archive de sauvegarde à partir des fichiers collectés ...", "backup_mount_archive_for_restore": "Préparation de l'archive pour restauration...", "confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais n'est pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que l'authentification unique et la sauvegarde/restauration peuvent ne pas être disponibles. L'installer quand même ? [{answers}] ", "confirm_app_install_danger": "DANGER ! Cette application est connue pour être encore expérimentale (si elle ne fonctionne pas explicitement) ! Vous ne devriez probablement PAS l'installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système... Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers}'", @@ -343,17 +335,7 @@ "regenconf_dry_pending_applying": "Vérification de la configuration en attente qui aurait été appliquée pour la catégorie '{category}'...", "regenconf_failed": "Impossible de régénérer la configuration pour la ou les catégorie(s) : '{categories}'", "regenconf_pending_applying": "Applique la configuration en attente pour la catégorie '{category}'...", - "service_regen_conf_is_deprecated": "'yunohost service regen-conf' est obsolète ! Veuillez plutôt utiliser 'yunohost tools regen-conf' à la place.", - "tools_upgrade_at_least_one": "Veuillez spécifier '--apps' ou '--system'", - "tools_upgrade_cant_both": "Impossible de mettre à niveau le système et les applications en même temps", - "tools_upgrade_cant_hold_critical_packages": "Impossibilité d'ajouter le drapeau 'hold' pour les paquets critiques...", - "tools_upgrade_regular_packages": "Mise à jour des paquets du système (non liés a YunoHost)...", - "tools_upgrade_regular_packages_failed": "Impossible de mettre à jour les paquets suivants : {packages_list}", - "tools_upgrade_special_packages": "Mise à jour des paquets 'spécifiques' (liés a YunoHost)...", - "tools_upgrade_special_packages_completed": "La mise à jour des paquets de YunoHost est finie !\nPressez [Entrée] pour revenir à la ligne de commande", "dpkg_lock_not_available": "Cette commande ne peut pas être exécutée pour le moment car un autre programme semble utiliser le verrou de dpkg (le gestionnaire de package système)", - "tools_upgrade_cant_unhold_critical_packages": "Impossible d'enlever le drapeau 'hold' pour les paquets critiques...", - "tools_upgrade_special_packages_explanation": "La mise à niveau spécifique à YunoHost se poursuivra en arrière-plan. Veuillez ne pas lancer d'autres actions sur votre serveur pendant les 10 prochaines minutes (selon la vitesse du matériel). Après cela, vous devrez peut-être vous reconnecter à la webadmin. Le journal de mise à niveau sera disponible dans Outils → Journal (dans le webadmin) ou en utilisant 'yunohost log list' (à partir de la ligne de commande).", "update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", "update_apt_cache_warning": "Des erreurs se sont produites lors de la mise à jour du cache APT (gestionnaire de paquets Debian). Voici un extrait des lignes du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", "backup_permission": "Permission de sauvegarde pour {app}", @@ -421,7 +403,7 @@ "diagnosis_ip_weird_resolvconf": "La résolution DNS semble fonctionner, mais il semble que vous utilisez un /etc/resolv.conf personnalisé.", "diagnosis_ip_weird_resolvconf_details": "Le fichier /etc/resolv.conf doit être un lien symbolique vers /etc/resolvconf/run/resolv.conf lui-même pointant vers 127.0.0.1 (dnsmasq). Si vous souhaitez configurer manuellement les résolveurs DNS, veuillez modifier /etc/resolv.dnsmasq.conf.", "diagnosis_dns_missing_record": "Selon la configuration DNS recommandée, vous devez ajouter un enregistrement DNS
Type : {type}
Nom : {name}
Valeur : {value}", - "diagnosis_diskusage_ok": "L'espace de stockage {mountpoint} (sur le périphérique {device}) a encore {free} ({free_percent}%) espace restant (sur {total}) !", + "diagnosis_diskusage_ok": "L'espace de stockage {mountpoint} (sur le périphérique {device}) a encore {free} ({free_percent}%) d'espace restant (sur {total}) !", "diagnosis_ram_ok": "Le système dispose encore de {available} ({available_percent}%) de RAM sur {total}.", "diagnosis_regenconf_allgood": "Tous les fichiers de configuration sont conformes à la configuration recommandée !", "diagnosis_security_vulnerable_to_meltdown": "Vous semblez vulnérable à la vulnérabilité de sécurité critique de Meltdown", @@ -434,7 +416,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.", @@ -447,9 +429,9 @@ "diagnosis_dns_bad_conf": "Certains enregistrements DNS sont manquants ou incorrects pour le domaine {domain} (catégorie {category})", "diagnosis_dns_discrepancy": "Cet enregistrement DNS ne semble pas correspondre à la configuration recommandée :
Type : {type}
Nom : {name}
La valeur actuelle est : {current}
La valeur attendue est : {value}", "diagnosis_services_bad_status": "Le service {service} est {status} :-(", - "diagnosis_diskusage_verylow": "L'espace de stockage {mountpoint} (sur l'appareil {device}) ne dispose que de {free} ({free_percent}%) espace restant (sur {total}). Vous devriez vraiment envisager de nettoyer de l'espace !", - "diagnosis_diskusage_low": "L'espace de stockage {mountpoint} (sur l'appareil {device}) ne dispose que de {free} ({free_percent}%) espace restant (sur {total}). Faites attention.", - "diagnosis_ram_verylow": "Le système ne dispose plus que de {available} ({available_percent}%)! (sur {total})", + "diagnosis_diskusage_verylow": "L'espace de stockage {mountpoint} (sur l'appareil {device}) ne dispose que de {free} ({free_percent}%) d'espace restant (sur {total}). Vous devriez vraiment envisager de nettoyer de l'espace !", + "diagnosis_diskusage_low": "L'espace de stockage {mountpoint} (sur l'appareil {device}) ne dispose que de {free} ({free_percent}%) d'espace restant (sur {total}). Faites attention.", + "diagnosis_ram_verylow": "Le système ne dispose plus que de {available} ({available_percent}%) de RAM ! (sur {total})", "diagnosis_ram_low": "Le système n'a plus de {available} ({available_percent}%) RAM sur {total}. Faites attention.", "diagnosis_swap_none": "Le système n'a aucun espace de swap. Vous devriez envisager d'ajouter au moins {recommended} de swap pour éviter les situations où le système manque de mémoire.", "diagnosis_swap_notsomuch": "Le système ne dispose que de {total} de swap. Vous devez envisager d'avoir au moins {recommended} pour éviter les situations où le système manque de mémoire.", @@ -488,14 +470,12 @@ "diagnosis_ports_forwarding_tip": "Pour résoudre ce problème, vous devez probablement configurer la redirection de port sur votre routeur Internet comme décrit dans https://yunohost.org/isp_box_config", "diagnosis_http_connection_error": "Erreur de connexion : impossible de se connecter au domaine demandé, il est probablement injoignable.", "diagnosis_no_cache": "Pas encore de cache de diagnostique pour la catégorie '{category}'", - "yunohost_postinstall_end_tip": "La post-installation terminée ! Pour finaliser votre configuration, il est recommandé de :\n- ajouter un premier utilisateur depuis la section \"Utilisateurs\" de l'interface web (ou 'yunohost user create ' en ligne de commande) ;\n- diagnostiquer les potentiels problèmes dans la section \"Diagnostic\" de l'interface web (ou 'yunohost diagnosis run' en ligne de commande) ;\n- lire les parties 'Finalisation de votre configuration' et 'Découverte de YunoHost' dans le guide de l'administrateur : https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "La post-installation est terminée ! Pour finaliser votre configuration, il est recommandé de :\n- ajouter un premier utilisateur depuis la section \"Utilisateurs\" de l'interface web (ou 'yunohost user create ' en ligne de commande) ;\n- diagnostiquer les potentiels problèmes dans la section \"Diagnostic\" de l'interface web (ou 'yunohost diagnosis run' en ligne de commande) ;\n- lire les parties 'Finalisation de votre configuration' et 'Découverte de YunoHost' dans le guide de l'administrateur : https://yunohost.org/admindoc.", "diagnosis_services_bad_status_tip": "Vous pouvez essayer de redémarrer le service, et si cela ne fonctionne pas, consultez les journaux de service dans le webadmin (à partir de la ligne de commande, vous pouvez le faire avec yunohost service restart {service} et yunohost service log {service} ).", "diagnosis_http_bad_status_code": "Le système de diagnostique n'a pas réussi à contacter votre serveur. Il se peut qu'une autre machine réponde à la place de votre serveur. Vérifiez que le port 80 est correctement redirigé, que votre configuration Nginx est à jour et qu'un reverse-proxy n'interfère pas.", "diagnosis_http_timeout": "Expiration du délai en essayant de contacter votre serveur de l'extérieur. Il semble être inaccessible. Vérifiez que vous transférez correctement le port 80, que Nginx est en cours d'exécution et qu'un pare-feu n'interfère pas.", "global_settings_setting_pop3_enabled": "Activer le protocole POP3 pour le serveur de messagerie", "log_app_action_run": "Lancer l'action de l'application '{}'", - "log_app_config_show_panel": "Montrer le panneau de configuration de l'application '{}'", - "log_app_config_apply": "Appliquer la configuration à l'application '{}'", "diagnosis_never_ran_yet": "Il apparaît que le serveur a été installé récemment et qu'il n'y a pas encore eu de diagnostic. Vous devriez en lancer un depuis la webadmin ou en utilisant 'yunohost diagnosis run' depuis la ligne de commande.", "diagnosis_description_web": "Web", "diagnosis_basesystem_hardware": "L'architecture du serveur est {virt} {arch}", @@ -528,7 +508,7 @@ "diagnosis_mail_ehlo_ok": "Le serveur de messagerie SMTP est accessible de l'extérieur et peut donc recevoir des emails !", "diagnosis_mail_ehlo_unreachable": "Le serveur de messagerie SMTP est inaccessible de l'extérieur en IPv{ipversion}. Il ne pourra pas recevoir des emails.", "diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible.
1. La cause la plus courante de ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur.
2. Vous devez également vous assurer que le service postfix est en cours d'exécution.
3. Sur les configurations plus complexes: assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.", - "diagnosis_mail_ehlo_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur.
EHLO reçu: {wrong_ehlo}
Attendu : {right_ehlo}
La cause la plus courante ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur . Vous pouvez également vous assurer qu'aucun pare-feu ou proxy inversé n'interfère.", + "diagnosis_mail_ehlo_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur.
EHLO reçu : {wrong_ehlo}
Attendu : {right_ehlo}
La cause la plus courante à ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur. Vous pouvez également vous assurer qu'aucun pare-feu ou reverse-proxy n'interfère.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes :
- Certains FAI fournissent l'alternative de à l'aide d'un relais de serveur de messagerie bien que cela implique que le relais pourra espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Enfin, il est également possible de changer de fournisseur", "diagnosis_mail_fcrdns_nok_alternatives_6": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si votre DNS inversé est correctement configuré en IPv4, vous pouvez essayer de désactiver l'utilisation d'IPv6 lors de l'envoi d'emails en exécutant yunohost settings set smtp.allow_ipv6 -v off. Remarque : cette dernière solution signifie que vous ne pourrez pas envoyer ou recevoir de emails avec les quelques serveurs qui ont uniquement de l'IPv6.", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS inverse actuel : {rdns_domain}
Valeur attendue : {ehlo_domain}", @@ -554,33 +534,9 @@ "diagnosis_swap_tip": "Merci d'être prudent et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire drastiquement l'espérance de vie du périphérique.", "restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}", "regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.", - "migration_0015_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus nécessaires ...", - "migration_0015_specific_upgrade": "Démarrage de la mise à jour des paquets du système qui doivent être mis à jour séparément...", - "migration_0015_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés à la suite de la mise à niveau : {manually_modified_files}", - "migration_0015_problematic_apps_warning": "Veuillez noter que des applications qui peuvent poser problèmes ont été détectées. Il semble qu'elles n'aient pas été installées à partir du catalogue d'applications YunoHost, ou bien qu'elles ne soient pas signalées comme \"fonctionnelles\". Par conséquent, il n'est pas possible de garantir que les applications suivantes fonctionneront encore après la mise à niveau : {problematic_apps}", - "migration_0015_general_warning": "Veuillez noter que cette migration est une opération délicate. L'équipe YunoHost a fait de son mieux pour la revérifier et la tester, mais la migration pourrait quand même casser des éléments du système ou de ses applications.\n\nIl est donc recommandé :\n...- de faire une sauvegarde de toute donnée ou application critique. Plus d'informations ici https://yunohost.org/backup ;\n...- d'être patient après le lancement de la migration. Selon votre connexion internet et votre matériel, la mise à niveau peut prendre jusqu'à quelques heures.", - "migration_0015_system_not_fully_up_to_date": "Votre système n'est pas entièrement à jour. Veuillez effectuer une mise à jour normale avant de lancer la migration vers Buster.", - "migration_0015_not_enough_free_space": "L'espace libre est très faible dans /var/ ! Vous devriez avoir au moins 1 Go de libre pour effectuer cette migration.", - "migration_0015_not_stretch": "La distribution Debian actuelle n'est pas Stretch !", - "migration_0015_yunohost_upgrade": "Démarrage de la mise à jour de YunoHost ...", - "migration_0015_still_on_stretch_after_main_upgrade": "Quelque chose s'est mal passé lors de la mise à niveau, le système semble toujours être sous Debian Stretch", - "migration_0015_main_upgrade": "Démarrage de la mise à niveau générale...", - "migration_0015_patching_sources_list": "Mise à jour du fichier sources.lists...", - "migration_0015_start": "Démarrage de la migration vers Buster", - "migration_description_0015_migrate_to_buster": "Mise à niveau du système vers Debian Buster et YunoHost 4.x", "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost dyndns update --force.", "app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de YunoHost. Vous devriez probablement envisager de mettre à jour votre système.", - "migration_0015_weak_certs": "Il a été constaté que les certificats suivants utilisent encore des algorithmes de signature peu robustes et doivent être mis à jour pour être compatibles avec la prochaine version de NGINX : {certs}", "global_settings_setting_backup_compress_tar_archives": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar). N.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.", - "migration_description_0018_xtable_to_nftable": "Migrer les anciennes règles de trafic réseau vers le nouveau système basé sur nftables", - "service_description_php7.3-fpm": "Exécute les applications écrites en PHP avec NGINX", - "migration_0018_failed_to_reset_legacy_rules": "La réinitialisation des règles iptable par défaut a échoué : {error}", - "migration_0018_failed_to_migrate_iptables_rules": "Échec de la migration des anciennes règles iptables vers nftables : {error}", - "migration_0017_not_enough_space": "Laissez suffisamment d'espace disponible dans {path} avant de lancer la migration.", - "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 est installé mais pas posgreSQL 11 ? Il s'est sans doute passé quelque chose d'étrange sur votre système :(...", - "migration_0017_postgresql_96_not_installed": "PostgreSQL n'a pas été installé sur votre système. Aucune opération à effectuer.", - "migration_description_0017_postgresql_9p6_to_11": "Migrer les bases de données de PostgreSQL 9.6 vers 11", - "migration_description_0016_php70_to_php73_pools": "Migrer les configurations php7.0 vers php7.3", "diagnosis_processes_killed_by_oom_reaper": "Certains processus ont été arrêtés récemment par le système car il manquait de mémoire. Cela apparaît généralement quand le système manque de mémoire ou qu'un processus consomme trop de mémoire. Liste des processus tués :\n{kills_summary}", "ask_user_domain": "Domaine à utiliser pour l'adresse email de l'utilisateur et le compte XMPP", "app_manifest_install_ask_is_public": "Cette application devrait-elle être visible par les visiteurs anonymes ?", @@ -598,20 +554,15 @@ "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", "permission_protected": "L'autorisation {permission} est protégée. Vous ne pouvez pas ajouter ou supprimer le groupe visiteurs à/de cette autorisation.", - "migration_0019_slapd_config_will_be_overwritten": "Il semble que vous ayez modifié manuellement la configuration de slapd. Pour cette migration critique, YunoHost doit forcer la mise à jour de la configuration slapd. Les fichiers originaux seront sauvegardés dans {conf_backup_folder}.", - "migration_0019_add_new_attributes_in_ldap": "Ajouter de nouveaux attributs pour les autorisations dans la base de données LDAP", - "migrating_legacy_permission_settings": "Migration des anciens paramètres d'autorisation...", "invalid_regex": "Regex non valide : '{regex}'", - "domain_name_unknown": "Domaine '{domain}' inconnu", "app_label_deprecated": "Cette commande est obsolète ! Veuillez utiliser la nouvelle commande 'yunohost user permission update' pour gérer l'étiquette de l'application.", "additional_urls_already_removed": "URL supplémentaire '{url}' déjà supprimées pour la permission '{permission}'", "invalid_number": "Doit être un nombre", - "migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives", "diagnosis_basesystem_hardware_model": "Le modèle/architecture du serveur est {model}", "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système.", "postinstall_low_rootfsspace": "Le système de fichiers a une taille totale inférieure à 10 Go, ce qui est préoccupant et devrait attirer votre attention ! Vous allez certainement arriver à court d'espace disque (très) rapidement ! Il est recommandé d'avoir au moins 16 Go à la racine pour ce système de fichiers. Si vous voulez installer YunoHost malgré cet avertissement, relancez la post-installation avec --force-diskspace", @@ -619,8 +570,7 @@ "diagnosis_rootfstotalspace_critical": "Le système de fichiers racine ne fait que {space} ! Vous allez certainement le remplir très rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.", "diagnosis_rootfstotalspace_warning": "Le système de fichiers racine n'est que de {space}. Cela peut suffire, mais faites attention car vous risquez de les remplir rapidement... Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.", "app_restore_script_failed": "Une erreur s'est produite dans le script de restauration de l'application", - "restore_backup_too_old": "Cette sauvegarde ne peut pas être restaurée car elle provient d'une version trop ancienne de YunoHost.", - "migration_update_LDAP_schema": "Mise à jour du schéma LDAP...", + "restore_backup_too_old": "Cette sauvegarde ne peut pas être restaurée car elle provient d'une version de YunoHost trop ancienne.", "log_backup_create": "Créer une archive de sauvegarde", "global_settings_setting_ssowat_panel_overlay_enabled": "Activer la superposition de la vignette SSOwat", "migration_ldap_rollback_success": "Système rétabli dans son état initial.", @@ -628,7 +578,6 @@ "migration_ldap_migration_failed_trying_to_rollback": "Impossible de migrer... tentative de restauration du système.", "migration_ldap_can_not_backup_before_migration": "La sauvegarde du système n'a pas pu être terminée avant l'échec de la migration. Erreur : {error }", "migration_ldap_backup_before_migration": "Création d'une sauvegarde de la base de données LDAP et des paramètres des applications avant la migration proprement dite.", - "migration_description_0020_ssh_sftp_permissions": "Ajouter la prise en charge des autorisations SSH et SFTP", "global_settings_setting_security_ssh_port": "Port SSH", "diagnosis_sshd_config_inconsistent_details": "Veuillez exécuter yunohost settings set security.ssh.port -v VOTRE_PORT_SSH pour définir le port SSH, et vérifiez yunohost tools regen-conf ssh --dry-run --with-diff et yunohost tools regen-conf ssh --force pour réinitialiser votre configuration aux recommandations YunoHost.", "diagnosis_sshd_config_inconsistent": "Il semble que le port SSH a été modifié manuellement dans /etc/ssh/sshd_config. Depuis YunoHost 4.2, un nouveau paramètre global 'security.ssh.port' est disponible pour éviter de modifier manuellement la configuration.", @@ -636,8 +585,7 @@ "backup_create_size_estimation": "L'archive contiendra environ {size} de données.", "global_settings_setting_security_webadmin_allowlist": "Adresses IP autorisées à accéder à la webadmin. Elles doivent être séparées par une virgule.", "global_settings_setting_security_webadmin_allowlist_enabled": "Autoriser seulement certaines IP à accéder à la webadmin.", - "diagnosis_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", @@ -657,5 +605,84 @@ "user_import_missing_columns": "Les colonnes suivantes sont manquantes : {columns}", "user_import_bad_file": "Votre fichier CSV n'est pas correctement formaté, il sera ignoré afin d'éviter une potentielle perte de données", "user_import_bad_line": "Ligne incorrecte {line} : {details}", - "log_user_import": "Importer des utilisateurs" -} + "log_user_import": "Importer des utilisateurs", + "diagnosis_high_number_auth_failures": "Il y a eu récemment un grand nombre d'échecs d'authentification. Assurez-vous que Fail2Ban est en cours d'exécution et est correctement configuré, ou utilisez un port personnalisé pour SSH comme expliqué dans https://yunohost.org/security.", + "global_settings_setting_security_nginx_redirect_to_https": "Rediriger les requêtes HTTP vers HTTPS par défaut (NE PAS DÉSACTIVER à moins de savoir vraiment ce que vous faites !)", + "config_validate_color": "Doit être une couleur hexadécimale RVB valide", + "app_config_unable_to_apply": "Échec de l'application des valeurs du panneau de configuration.", + "app_config_unable_to_read": "Échec de la lecture des valeurs du panneau de configuration.", + "config_apply_failed": "Échec de l'application de la nouvelle configuration : {error}", + "config_cant_set_value_on_section": "Vous ne pouvez pas définir une seule valeur sur une section de configuration entière.", + "config_forbidden_keyword": "Le mot-clé '{keyword}' est réservé, vous ne pouvez pas créer ou utiliser un panneau de configuration avec une question avec cet identifiant.", + "config_no_panel": "Aucun panneau de configuration trouvé.", + "config_unknown_filter_key": "La clé de filtre '{filter_key}' est incorrecte.", + "config_validate_date": "Doit être une date valide comme dans le format AAAA-MM-JJ", + "config_validate_email": "Doit être un email valide", + "config_validate_time": "Doit être une heure valide comme HH:MM", + "config_validate_url": "Doit être une URL Web valide", + "config_version_not_supported": "Les versions du panneau de configuration '{version}' ne sont pas prises en charge.", + "danger": "Danger :", + "invalid_number_min": "Doit être supérieur à {min}", + "invalid_number_max": "Doit être inférieur à {max}", + "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}", + "domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.", + "domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.", + "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost dyndns update')", + "domain_dns_registrar_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 registrar qui gère votre nom de domaine internet pour '{domain}'. Il est très probable que les informations d'identification soient 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...", + "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 '{}'", + "diagnosis_http_special_use_tld": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et n'est donc pas censé être exposé en dehors du réseau local.", + "domain_dns_conf_special_use_tld": "Ce domaine est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.", + "other_available_options": "... et {n} autres options disponibles non affichées", + "domain_config_auth_consumer_key": "Consumer key", + "domain_unknown": "Domaine '{domain}' inconnu", + "migration_0021_start": "Démarrage de la migration vers Bullseye", + "migration_0021_patching_sources_list": "Mise à jour du fichier sources.lists...", + "migration_0021_main_upgrade": "Démarrage de la mise à niveau générale...", + "migration_0021_still_on_buster_after_main_upgrade": "Quelque chose s'est mal passé lors de la mise à niveau, le système semble toujours être sous Debian Buster", + "migration_0021_yunohost_upgrade": "Démarrage de la mise à jour du noyau YunoHost...", + "migration_0021_not_enough_free_space": "L'espace libre est très faible dans /var/ ! Vous devriez avoir au moins 1 Go de libre pour effectuer cette migration.", + "migration_0021_system_not_fully_up_to_date": "Votre système n'est pas entièrement à jour. Veuillez effectuer une mise à jour normale avant de lancer la migration vers Bullseye.", + "migration_0021_general_warning": "Veuillez noter que cette migration est une opération délicate. L'équipe YunoHost a fait de son mieux pour la revérifier et la tester, mais la migration pourrait quand même casser des éléments du système ou de ses applications.\n\nIl est donc recommandé :\n - de faire une sauvegarde de toute donnée ou application critique. Plus d'informations ici https://yunohost.org/backup ;\n - d'être patient après le lancement de la migration. Selon votre connexion internet et votre matériel, la mise à niveau peut prendre jusqu'à quelques heures.", + "migration_0021_problematic_apps_warning": "Veuillez noter que des applications qui peuvent poser problèmes ont été détectées. Il semble qu'elles n'aient pas été installées à partir du catalogue d'applications YunoHost, ou bien qu'elles ne soient pas signalées comme \\\"fonctionnelles\\\". Par conséquent, il n'est pas possible de garantir que les applications suivantes fonctionneront encore après la mise à niveau : {problematic_apps}", + "migration_0021_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés à la suite de la mise à niveau : {manually_modified_files}", + "migration_0021_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus nécessaires...", + "migration_0021_patch_yunohost_conflicts": "Application du correctif pour contourner le problème de conflit...", + "migration_0021_not_buster": "La distribution Debian actuelle n'est pas Buster !", + "migration_description_0021_migrate_to_bullseye": "Mise à niveau du système vers Debian Bullseye et YunoHost 11.x", + "global_settings_setting_security_ssh_password_authentication": "Autoriser l'authentification par mot de passe pour SSH", + "domain_config_default_app": "Application par défaut", + "migration_description_0022_php73_to_php74_pools": "Migration des fichiers de configuration php7.3-fpm 'pool' vers php7.4", + "migration_description_0023_postgresql_11_to_13": "Migration des bases de données de PostgreSQL 11 vers 13", + "service_description_postgresql": "Stocke les données d'application (base de données SQL)", + "tools_upgrade": "Mise à niveau des packages système", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 est installé, mais pas PostgreSQL 13 ! ? Quelque chose d'anormal s'est peut-être produit sur votre système :(...", + "tools_upgrade_failed": "Impossible de mettre à jour les paquets : {packages_list}", + "migration_0023_not_enough_space": "Prévoyez suffisamment d'espace disponible dans {path} pour exécuter la migration.", + "migration_0023_postgresql_11_not_installed": "PostgreSQL n'a pas été installé sur votre système. Il n'y a rien à faire." +} \ No newline at end of file diff --git a/locales/gl.json b/locales/gl.json index 0c06bcab8..4a77645d6 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -14,11 +14,10 @@ "additional_urls_already_removed": "URL adicional '{url}' xa foi eliminada das URL adicionais para o permiso '{permission}'", "additional_urls_already_added": "URL adicional '{url}' xa fora engadida ás URL adicionais para o permiso '{permission}'", "action_invalid": "Acción non válida '{action}'", - "app_change_url_failed_nginx_reload": "Non se recargou NGINX. Aquí tes a saída de 'nginx -t':\n{nginx_errors}", "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}'", + "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}", @@ -45,7 +44,7 @@ "apps_catalog_update_success": "O catálogo de aplicacións foi actualizado!", "apps_catalog_obsolete_cache": "A caché do catálogo de apps está baleiro ou obsoleto.", "apps_catalog_failed_to_download": "Non se puido descargar o catálogo de apps {apps_catalog}: {error}", - "apps_catalog_updating": "Actualizando o catálogo de aplicacións…", + "apps_catalog_updating": "Actualizando o catálogo de aplicacións...", "apps_catalog_init_success": "Sistema do catálogo de apps iniciado!", "apps_already_up_to_date": "Xa tes tódalas apps ao día", "app_packaging_format_not_supported": "Esta app non se pode instalar porque o formato de empaquetado non está soportado pola túa versión de YunoHost. Deberías considerar actualizar o teu sistema.", @@ -77,7 +76,6 @@ "app_manifest_install_ask_password": "Elixe un contrasinal de administración para esta app", "app_manifest_install_ask_path": "Elixe a ruta URL (após o dominio) onde será instalada esta app", "app_manifest_install_ask_domain": "Elixe o dominio onde queres instalar esta app", - "app_manifest_invalid": "Hai algún erro no manifesto da app: {error}", "app_location_unavailable": "Este URL ou ben non está dispoñible ou entra en conflito cunha app(s) xa instalada:\n{apps}", "app_label_deprecated": "Este comando está anticuado! Utiliza o novo comando 'yunohost user permission update' para xestionar a etiqueta da app.", "app_make_default_location_already_used": "Non se puido establecer a '{app}' como app por defecto no dominio, '{domain}' xa está utilizado por '{other_app}'", @@ -103,7 +101,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.", @@ -289,14 +287,11 @@ "dyndns_ip_updated": "Actualizouse o IP en DynDNS", "dyndns_ip_update_failed": "Non se actualizou o enderezo IP en DynDNS", "dyndns_could_not_check_available": "Non se comprobou se {domain} está dispoñible en {provider}.", - "dyndns_could_not_check_provide": "Non se comprobou se {provider} pode proporcionar {domain}.", "dpkg_lock_not_available": "Non se pode executar agora mesmo este comando porque semella que outro programa está a utilizar dpkg (o xestos de paquetes do sistema)", "dpkg_is_broken": "Non podes facer isto agora mesmo porque dpkg/APT (o xestor de paquetes do sistema) semella que non está a funcionar... Podes intentar solucionalo conectándote a través de SSH e executando `sudo apt install --fix-broken`e/ou `sudo dpkg --configure -a`.", - "downloading": "Descargando…", + "downloading": "Descargando...", "done": "Feito", "domains_available": "Dominios dispoñibles:", - "domain_unknown": "Dominio descoñecido", - "domain_name_unknown": "Dominio '{domain}' descoñecido", "domain_uninstall_app_first": "Aínda están instaladas estas aplicacións no teu dominio:\n{apps}\n\nPrimeiro desinstalaas utilizando 'yunohost app remove id_da_app' ou móveas a outro dominio con 'yunohost app change-url id_da_app' antes de eliminar o dominio", "domain_remove_confirm_apps_removal": "Ao eliminar o dominio tamén vas eliminar estas aplicacións:\n{apps}\n\nTes a certeza de querer facelo? [{answers}]", "domain_hostname_failed": "Non se puido establecer o novo nome de servidor. Esto pode causar problemas máis tarde (tamén podería ser correcto).", @@ -358,7 +353,7 @@ "global_settings_setting_security_webadmin_allowlist_enabled": "Permitir que só algúns IPs accedan á webadmin.", "disk_space_not_sufficient_update": "Non hai espazo suficiente no disco para actualizar esta aplicación", "disk_space_not_sufficient_install": "Non queda espazo suficiente no disco para instalar esta aplicación", - "log_help_to_get_log": "Para ver o rexistro completo da operación '{desc}', usa o comando 'yunohost log show {name}{name}'", + "log_help_to_get_log": "Para ver o rexistro completo da operación '{desc}', usa o comando 'yunohost log show {name}'", "log_link_to_log": "Rexistro completo desta operación: '{desc}'", "log_corrupted_md_file": "O ficheiro YAML con metadatos asociado aos rexistros está danado: '{md_file}\nErro: {error}'", "iptables_unavailable": "Non podes andar remexendo en iptables aquí. Ou ben estás nun contedor ou o teu kernel non ten soporte para isto", @@ -386,8 +381,6 @@ "log_backup_restore_system": "Restablecer o sistema desde unha copia de apoio", "log_backup_create": "Crear copia de apoio", "log_available_on_yunopaste": "Este rexistro está dispoñible en {url}", - "log_app_config_apply": "Aplicar a configuración da app '{}'", - "log_app_config_show_panel": "Mostrar o panel de configuración da app '{}'", "log_app_action_run": "Executar acción da app '{}'", "log_app_makedefault": "Converter '{}' na app por defecto", "log_app_upgrade": "Actualizar a app '{}'", @@ -398,19 +391,10 @@ "log_does_exists": "Non hai rexistro de operación co nome '{log}', usa 'yunohost log list' para ver tódolos rexistros de operacións dispoñibles", "log_help_to_get_failed_log": "A operación '{desc}' non se completou. Comparte o rexistro completo da operación utilizando o comando 'yunohost log share {name}' para obter axuda", "log_link_to_failed_log": "Non se completou a operación '{desc}'. Por favor envía o rexistro completo desta operación premendo aquí para obter axuda", - "migration_0015_start": "Comezando a migración a Buster", - "migration_update_LDAP_schema": "Actualizando esquema LDAP...", "migration_ldap_rollback_success": "Sistema restablecido.", "migration_ldap_migration_failed_trying_to_rollback": "Non se puido migrar... intentando volver á versión anterior do sistema.", "migration_ldap_can_not_backup_before_migration": "O sistema de copia de apoio do sistema non se completou antes de que fallase a migración. Erro: {error}", "migration_ldap_backup_before_migration": "Crear copia de apoio da base de datos LDAP e axustes de apps antes de realizar a migración.", - "migration_description_0020_ssh_sftp_permissions": "Engadir soporte para permisos SSH e SFTP", - "migration_description_0019_extend_permissions_features": "Extender/recrear o sistema de xestión de permisos de apps", - "migration_description_0018_xtable_to_nftable": "Migrar as regras de tráfico de rede antigas ao novo sistema nftable", - "migration_description_0017_postgresql_9p6_to_11": "Migrar bases de datos desde PostgreSQL 9.6 a 11", - "migration_description_0016_php70_to_php73_pools": "Migrar o ficheiros de configuración 'pool' de php7.0-fpm a php7.3", - "migration_description_0015_migrate_to_buster": "Actualizar o sistema a Debian Buster e YunoHost 4.x", - "migrating_legacy_permission_settings": "Migrando os axustes dos permisos anteriores...", "main_domain_changed": "Foi cambiado o dominio principal", "main_domain_change_failed": "Non se pode cambiar o dominio principal", "mail_unavailable": "Este enderezo de email está reservado e debería adxudicarse automáticamente á primeira usuaria", @@ -437,29 +421,8 @@ "log_letsencrypt_cert_renew": "Anovar certificado Let's Encrypt para '{}'", "log_selfsigned_cert_install": "Instalar certificado auto-asinado para o dominio '{}'", "log_permission_url": "Actualizar URL relativo ao permiso '{}'", - "migration_0015_general_warning": "Ten en conta que a migración é unha operación delicada. O equipo YunoHost esforzouse revisando e comprobandoa, aínda así algo podería fallar en partes do teu sistema ou as súas apps.\n\nPor tanto, é recomendable:\n- realiza unha copia de apoio de tódolos datos ou apps importantes. Máis info en https://yunohost.org/backup;\n - ten paciencia tras iniciar o proceso: dependendo da túa conexión de internet e hardware podería demorar varias horas a actualización de tódolos compoñentes.", - "migration_0015_system_not_fully_up_to_date": "O teu sistema non está ao día. Realiza unha actualización común antes de realizar a migración a Buster.", - "migration_0015_not_enough_free_space": "Queda moi pouco espazo en /var/! Deberías ter polo menos 1GB libre para realizar a migración.", - "migration_0015_not_stretch": "A distribución Debian actual non é Stretch!", - "migration_0015_yunohost_upgrade": "Iniciando a actualización do núcleo YunoHost...", - "migration_0015_still_on_stretch_after_main_upgrade": "Algo foi mal durante a actualiza ión principal, o sistema semella que aínda está en Debian Stretch", - "migration_0015_main_upgrade": "Iniciando a actualización principal...", - "migration_0015_patching_sources_list": "Correxindo os sources.lists...", "migrations_already_ran": "Xa se realizaron estas migracións: {ids}", - "migration_0019_slapd_config_will_be_overwritten": "Semella que editaches manualmente a configuración slapd. Para esta migración crítica YunoHost precisa forzar a actualización da configuración slapd. Os ficheiros orixinais van ser copiados en {conf_backup_folder}.", - "migration_0019_add_new_attributes_in_ldap": "Engadir novos atributos para os permisos na base de datos LDAP", - "migration_0018_failed_to_reset_legacy_rules": "Fallou o restablecemento das regras antigas de iptables: {error}", - "migration_0018_failed_to_migrate_iptables_rules": "Fallou a migración das regras antigas de iptables a nftables: {error}", - "migration_0017_not_enough_space": "Crea espazo suficiente en {path} para executar a migración.", - "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 está instado, pero non postgresql 11? Algo raro debeu acontecer no teu sistema :(...", - "migration_0017_postgresql_96_not_installed": "PostgreSQL non está instalado no teu sistema. Nada que facer.", - "migration_0015_weak_certs": "Os seguintes certificados están a utilizar algoritmos de sinatura débiles e teñen que ser actualizados para ser compatibles coa seguinte versión de nginx: {certs}", - "migration_0015_cleaning_up": "Limpando a caché e paquetes que xa non son útiles...", - "migration_0015_specific_upgrade": "Iniciando a actualización dos paquetes do sistema que precisan ser actualizados de xeito independente...", - "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 sexa accesible desde o 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}", @@ -471,7 +434,6 @@ "permission_already_allowed": "O grupo '{group}' xa ten o permiso '{permission}' activado", "pattern_password_app": "Lamentámolo, os contrasinais non poden conter os seguintes caracteres: {forbidden_chars}", "pattern_username": "Só admite caracteres alfanuméricos en minúscula e trazo baixo", - "pattern_positive_number": "Ten que ser un número positivo", "pattern_port_or_range": "Debe ser un número válido de porto (entre 0-65535) ou rango de portos (ex. 100:200)", "pattern_password": "Ten que ter polo menos 3 caracteres", "pattern_mailbox_quota": "Ten que ser un tamaño co sufixo b/k/M/G/T ou 0 para non ter unha cota", @@ -485,7 +447,6 @@ "password_too_simple_3": "O contrasinal ten que ter 8 caracteres como mínimo e conter un díxito, maiúsculas, minúsculas e caracteres especiais", "password_too_simple_2": "O contrasinal ten que ter 8 caracteres como mínimo e conter un díxito, maiúsculas e minúsculas", "password_listed": "Este contrasinal está entre os máis utilizados no mundo. Por favor elixe outro que sexa máis orixinal.", - "packages_upgrade_failed": "Non se puideron actualizar tódolos paquetes", "operation_interrupted": "Foi interrumpida manualmente a operación?", "invalid_number": "Ten que ser un número", "not_enough_disk_space": "Non hai espazo libre abondo en '{path}'", @@ -505,7 +466,6 @@ "migrations_exclusive_options": "'--auto', '--skip', e '--force-rerun' son opcións que se exclúen unhas a outras.", "migrations_failed_to_load_migration": "Non se cargou a migración {id}: {error}", "migrations_dependencies_not_satisfied": "Executar estas migracións: '{dependencies_id}', antes da migración {id}.", - "migrations_cant_reach_migration_file": "Non se pode acceder aos ficheiros de migración na ruta '%s'", "regenconf_file_manually_removed": "O ficheiro de configuración '{conf}' foi eliminado manualmente e non será creado", "regenconf_file_manually_modified": "O ficheiro de configuración '{conf}' foi modificado manualmente e non vai ser actualizado", "regenconf_file_kept_back": "Era de agardar que o ficheiro de configuración '{conf}' fose eliminado por regen-conf (categoría {category}) mais foi mantido.", @@ -524,7 +484,7 @@ "permission_cant_add_to_all_users": "O permiso {permission} non pode ser concecido a tódalas usuarias.", "permission_currently_allowed_for_all_users": "Este permiso está concedido actualmente a tódalas usuarias ademáis de a outros grupos. Probablemente queiras ben eliminar o permiso 'all_users' ou ben eliminar os outros grupos que teñen permiso.", "restore_failed": "Non se puido restablecer o sistema", - "restore_extracting": "Extraendo os ficheiros necesarios desde o arquivo…", + "restore_extracting": "Extraendo os ficheiros necesarios desde o arquivo...", "restore_confirm_yunohost_installed": "Tes a certeza de querer restablecer un sistema xa instalado? [{answers}]", "restore_complete": "Restablecemento completado", "restore_cleaning_failed": "Non se puido despexar o directorio temporal de restablecemento", @@ -536,7 +496,7 @@ "regenconf_need_to_explicitly_specify_ssh": "A configuración ssh foi modificada manualmente, pero tes que indicar explícitamente a categoría 'ssh' con --force para realmente aplicar os cambios.", "regenconf_pending_applying": "Aplicando a configuración pendente para categoría '{category}'...", "regenconf_failed": "Non se rexenerou a configuración para a categoría(s): {categories}", - "regenconf_dry_pending_applying": "Comprobando as configuracións pendentes que deberían aplicarse á categoría '{category}'…", + "regenconf_dry_pending_applying": "Comprobando as configuracións pendentes que deberían aplicarse á categoría '{category}'...", "regenconf_would_be_updated": "A configuración debería ser actualizada para a categoría '{category}'", "regenconf_updated": "Configuración actualizada para '{category}'", "regenconf_up_to_date": "A configuración xa está ao día para a categoría '{category}'", @@ -549,12 +509,11 @@ "service_disable_failed": "Non se puido iniciar o servizo '{service}' ao inicio.\n\nRexistro recente do servizo: {logs}", "service_description_yunohost-firewall": "Xestiona, abre e pecha a conexións dos portos aos servizos", "service_description_yunohost-api": "Xestiona as interaccións entre a interface web de YunoHost e o sistema", - "service_description_ssh": "Permíteche conectar de xeito remoto co teu servidor a través dun terminal (protocolo SSH)", + "service_description_ssh": "Permíteche acceder de xeito remoto ao teu servidor a través dun terminal (protocolo SSH)", "service_description_slapd": "Almacena usuarias, dominios e info relacionada", "service_description_rspamd": "Filtra spam e outras características relacionadas co email", "service_description_redis-server": "Unha base de datos especial utilizada para o acceso rápido a datos, cola de tarefas e comunicación entre programas", "service_description_postfix": "Utilizado para enviar e recibir emails", - "service_description_php7.3-fpm": "Executa aplicacións escritas en PHP con NGINX", "service_description_nginx": "Serve ou proporciona acceso a tódolos sitios web hospedados no teu servidor", "service_description_mysql": "Almacena datos da app (base de datos SQL)", "service_description_metronome": "Xestiona as contas de mensaxería instantánea XMPP", @@ -574,8 +533,8 @@ "root_password_replaced_by_admin_password": "O contrasinal root foi substituído polo teu contrasinal de administración.", "root_password_desynchronized": "Mudou o contrasinal de administración, pero YunoHost non puido transferir este cambio ao contrasinal root!", "restore_system_part_failed": "Non se restableceu a parte do sistema '{part}'", - "restore_running_hooks": "Executando os ganchos do restablecemento…", - "restore_running_app_script": "Restablecendo a app '{app}'…", + "restore_running_hooks": "Executando os ganchos do restablecemento...", + "restore_running_app_script": "Restablecendo a app '{app}'...", "restore_removing_tmp_dir_failed": "Non se puido eliminar o directorio temporal antigo", "restore_nothings_done": "Nada foi restablecido", "restore_not_enough_disk_space": "Non hai espazo abondo (espazo: {free_space.d} B, espazo necesario: {needed_space} B, marxe de seguridade: {margin} B)", @@ -593,7 +552,7 @@ "user_updated": "Cambiada a info da usuaria", "user_update_failed": "Non se actualizou usuaria {user}: {error}", "user_unknown": "Usuaria descoñecida: {user}", - "user_home_creation_failed": "Non se puido crear cartafol 'home' para a usuaria", + "user_home_creation_failed": "Non se puido crear cartafol home '{home}' para a usuaria", "user_deletion_failed": "Non se puido eliminar a usuaria {user}: {error}", "user_deleted": "Usuaria eliminada", "user_creation_failed": "Non se puido crear a usuaria {user}: {error}", @@ -611,19 +570,9 @@ "unknown_main_domain_path": "Dominio ou ruta descoñecida '{app}'. Tes que indicar un dominio e ruta para poder especificar un URL para o permiso.", "unexpected_error": "Aconteceu un fallo non agardado: {error}", "unbackup_app": "{app} non vai ser gardada", - "tools_upgrade_special_packages_completed": "Completada a actualización dos paquetes YunoHost.\nPreme [Enter] para recuperar a liña de comandos", - "tools_upgrade_special_packages_explanation": "A actualización especial continuará en segundo plano. Non inicies outras tarefas no servidor nos seguintes ~10 minutos (depende do hardware). Após isto, podes volver a conectar na webadmin. O rexistro da actualización estará dispoñible en Ferramentas → Rexistro (na webadmin) ou con 'yunohost log list' (na liña de comandos).", - "tools_upgrade_special_packages": "Actualizando paquetes 'special' (yunohost-related)…", - "tools_upgrade_regular_packages_failed": "Non se actualizaron os paquetes: {packages_list}", - "tools_upgrade_regular_packages": "Actualizando os paquetes 'regular' (non-yunohost-related)…", - "tools_upgrade_cant_unhold_critical_packages": "Non se desbloquearon os paquetes críticos…", - "tools_upgrade_cant_hold_critical_packages": "Non se puideron bloquear os paquetes críticos…", - "tools_upgrade_cant_both": "Non se pode actualizar o sistema e as apps ao mesmo tempo", - "tools_upgrade_at_least_one": "Por favor indica 'apps', ou 'system'", "this_action_broke_dpkg": "Esta acción rachou dpkg/APT (xestores de paquetes do sistema)... Podes intentar resolver o problema conectando a través de SSH e executando `sudo apt install --fix-broken`e/ou `sudo dpkg --configure -a`.", "system_username_exists": "Xa existe este nome de usuaria na lista de usuarias do sistema", "system_upgraded": "Sistema actualizado", - "ssowat_conf_updated": "Actualizada a configuración SSOwat", "ssowat_conf_generated": "Rexenerada a configuración para SSOwat", "show_tile_cant_be_enabled_for_regex": "Non podes activar 'show_tile' neste intre, porque o URL para o permiso '{permission}' é un regex", "show_tile_cant_be_enabled_for_url_not_defined": "Non podes activar 'show_tile' neste intre, primeiro tes que definir un URL para o permiso '{permission}'", @@ -640,6 +589,100 @@ "service_reload_failed": "Non se recargou o servizo '{service}'\n\nRexistros recentes do servizo: {logs}", "service_removed": "Eliminado o servizo '{service}'", "service_remove_failed": "Non se eliminou o servizo '{service}'", - "service_regen_conf_is_deprecated": "'yunohost service regen-conf' xa non se utiliza! Executa 'yunohost tools regen-conf' no seu lugar.", - "service_enabled": "O servizo '{service}' vai ser iniciado automáticamente no inicio do sistema." -} + "service_enabled": "O servizo '{service}' vai ser iniciado automáticamente no inicio do sistema.", + "diagnosis_apps_allgood": "Tódalas apps instaladas respectan as prácticas básicas de empaquetado", + "diagnosis_apps_bad_quality": "Esta aplicación está actualmente marcada como estragada no catálogo de aplicacións de YunoHost. Podería ser un problema temporal mentras as mantedoras intentan arranxar o problema. Ata ese momento a actualización desta app está desactivada.", + "global_settings_setting_security_nginx_redirect_to_https": "Redirixir peticións HTTP a HTTPs por defecto (NON DESACTIVAR ISTO a non ser que realmente saibas o que fas!)", + "log_user_import": "Importar usuarias", + "user_import_failed": "A operación de importación de usuarias fracasou", + "user_import_missing_columns": "Faltan as seguintes columnas: {columns}", + "user_import_nothing_to_do": "Ningunha usuaria precisa ser importada", + "user_import_partial_failed": "A operación de importación de usuarias fallou parcialmente", + "diagnosis_apps_deprecated_practices": "A versión instalada desta app aínda utiliza algunha das antigas prácticas de empaquetado xa abandonadas. Deberías considerar actualizala.", + "diagnosis_apps_outdated_ynh_requirement": "A versión instalada desta app só require yunohost >= 2.x, que normalmente indica que non está ao día coas prácticas recomendadas de empaquetado e asistentes. Deberías considerar actualizala.", + "user_import_success": "Usuarias importadas correctamente", + "diagnosis_high_number_auth_failures": "Hai un alto número sospeitoso de intentos fallidos de autenticación. Deberías comprobar que fail2ban está a executarse e que está correctamente configurado, ou utiliza un porto personalizado para SSH tal como se explica en https://yunohost.org/security.", + "user_import_bad_file": "O ficheiro CSV non ten o formato correcto e será ignorado para evitar unha potencial perda de datos", + "user_import_bad_line": "Liña incorrecta {line}: {details}", + "diagnosis_description_apps": "Aplicacións", + "diagnosis_apps_broken": "Actualmente esta aplicación está marcada como estragada no catálogo de aplicacións de YunoHost. Podería tratarse dun problema temporal mentras as mantedoras intentan arraxala. Entanto así a actualización da app está desactivada.", + "diagnosis_apps_issue": "Atopouse un problema na app {app}", + "diagnosis_apps_not_in_app_catalog": "Esta aplicación non está no catálgo de aplicacións de YunoHost. Se estivo no pasado e foi eliminada, deberías considerar desinstalala porque non recibirá actualizacións, e podería comprometer a integridade e seguridade do teu sistema.", + "config_validate_date": "Debe ser unha data válida co formato YYYY-MM-DD", + "config_validate_email": "Debe ser un email válido", + "config_validate_time": "Debe ser unha hora válida tal que HH:MM", + "config_validate_url": "Debe ser un URL válido", + "danger": "Perigo:", + "app_config_unable_to_read": "Fallou a lectura dos valores de configuración.", + "config_apply_failed": "Fallou a aplicación da nova configuración: {error}", + "config_forbidden_keyword": "O palabra chave '{keyword}' está reservada, non podes crear ou usar un panel de configuración cunha pregunta con este id.", + "config_no_panel": "Non se atopa panel configurado.", + "config_unknown_filter_key": "A chave do filtro '{filter_key}' non é correcta.", + "config_validate_color": "Debe ser un valor RGB hexadecimal válido", + "invalid_number_min": "Ten que ser maior que {min}", + "log_app_config_set": "Aplicar a configuración á app '{}'", + "app_config_unable_to_apply": "Fallou a aplicación dos valores de configuración.", + "config_cant_set_value_on_section": "Non podes establecer un valor único na sección completa de configuración.", + "config_version_not_supported": "A versión do panel de configuración '{version}' non está soportada.", + "invalid_number_max": "Ten que ser menor de {max}", + "service_not_reloading_because_conf_broken": "Non se recargou/reiniciou o servizo '{name}' porque a súa configuración está estragada: {errors}", + "diagnosis_http_special_use_tld": "O dominio {domain} baséase nun dominio de alto-nivel (TLD) especial como .local ou .test e por isto non é de agardar que esté exposto fóra da rede local.", + "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 **{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...", + "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 **{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", + "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 '{}'", + "domain_unknown": "Dominio '{domain}' descoñecido", + "migration_0021_start": "Comezando a migración a Bullseye", + "migration_0021_patching_sources_list": "Actualizando sources.list...", + "migration_0021_main_upgrade": "Iniciando a actualización principal...", + "migration_0021_still_on_buster_after_main_upgrade": "Algo fallou durante a actualización principal, o sistema semlla que aínda está en Debian Buster", + "migration_0021_yunohost_upgrade": "Iniciando actualización compoñente core de YunoHost...", + "migration_0021_not_buster": "A distribución Debian actual non é Buster!", + "migration_0021_not_enough_free_space": "Queda pouco espazo en /var/! Deberías ter polo menos 1GB libre para facer a migración.", + "migration_0021_problematic_apps_warning": "Detectamos que están instaladas estas app que poderían ser problemáticas. Semella que non foron instaladas desde o catálogo YunoHost, ou non están marcadas como que 'funcionan'. Así, non podemos garantir que seguiran funcionando ben tras a migración: {problematic_apps}", + "migration_0021_modified_files": "Ten en conta que os seguintes ficheiros semella que foron editados manualmente e poderían ser sobrescritos durante a migración: {manually_modified_files}", + "migration_0021_cleaning_up": "Limpando a caché e os paquetes que xa non son precisos...", + "migration_0021_patch_yunohost_conflicts": "Solucionando os problemas e conflitos...", + "migration_description_0021_migrate_to_bullseye": "Actualizar o sistema a Debian Bullseye e YunoHost 11.x", + "migration_0021_system_not_fully_up_to_date": "O teu sistema non está completamente actualizado. Fai unha actualización normal antes de executar a migración a Bullseye.", + "migration_0021_general_warning": "Ten en conta que a migración é unha operación delicada. O equipo de YunoHost fixo todo o que puido para revisalo e probalo, pero aínda así poderían acontecer fallos no sistema ou apps.\n\nAsí as cousas, é recomendable:\n - Facer unha copia de apoio dos datos e apps importantes. Máis info en https://yunohost.org/backup;\n - Ter paciencia unha vez inicias a migración: dependendo da túa conexión a internet e hardware, podería levarlle varias horas completar o proceso.", + "global_settings_setting_security_ssh_password_authentication": "Permitir autenticación con contrasinal para SSH", + "tools_upgrade_failed": "Non se actualizaron os paquetes: {packages_list}", + "migration_0023_not_enough_space": "Crear espazo suficiente en {path} para realizar a migración.", + "migration_0023_postgresql_11_not_installed": "PostgreSQL non estaba instalado no sistema. Nada que facer.", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 está instalado, pero PostgreSQL 13 non!? Algo raro debeu pasarlle ao teu sistema :(...", + "migration_description_0022_php73_to_php74_pools": "Migrar ficheiros de configuración de php7.3-fpm 'pool' a php7.4", + "migration_description_0023_postgresql_11_to_13": "Migrar bases de datos de PostgreSQL 11 a 13", + "service_description_postgresql": "Almacena datos da app (Base datos SQL)", + "tools_upgrade": "Actualizando paquetes do sistema", + "domain_config_default_app": "App por defecto" +} \ No newline at end of file diff --git a/locales/hi.json b/locales/hi.json index 5f521b1dc..1eed9faa4 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -10,7 +10,6 @@ "app_extraction_failed": "इन्सटाल्ड फ़ाइलों को निकालने में असमर्थ", "app_id_invalid": "अवैध एप्लिकेशन id", "app_install_files_invalid": "फाइलों की अमान्य स्थापना", - "app_manifest_invalid": "एप्लीकेशन का मैनिफेस्ट अमान्य", "app_not_correctly_installed": "{app} ठीक ढंग से इनस्टॉल नहीं हुई", "app_not_installed": "{app} इनस्टॉल नहीं हुई", "app_not_properly_removed": "{app} ठीक ढंग से नहीं अनइन्सटॉल की गई", diff --git a/locales/id.json b/locales/id.json new file mode 100644 index 000000000..47aff8b2e --- /dev/null +++ b/locales/id.json @@ -0,0 +1,58 @@ +{ + "admin_password": "Kata sandi administrasi", + "admin_password_change_failed": "Tidak dapat mengubah kata sandi", + "admin_password_changed": "Kata sandi administrasi diubah", + "admin_password_too_long": "Harap pilih kata sandi yang lebih pendek dari 127 karakter", + "already_up_to_date": "Tak ada yang harus dilakukan. Semuanya sudah mutakhir.", + "app_action_broke_system": "Tindakan ini sepertinya telah merusak layanan-layanan penting ini: {services}", + "app_already_installed": "{app} sudah terpasang", + "app_already_up_to_date": "{app} sudah dalam versi mutakhir", + "app_argument_required": "Argumen '{name}' dibutuhkan", + "app_change_url_identical_domains": "Domain)url_path yang lama dan baru identik ('{domain}{path}'), tak ada yang perlu dilakukan.", + "app_change_url_no_script": "Aplikasi '{app_name}' belum mendukung pengubahan URL. Mungkin Anda harus memperbaruinya.", + "app_change_url_success": "URL {app} sekarang adalah {domain}{path}", + "app_id_invalid": "ID aplikasi tidak valid", + "app_install_failed": "Tidak dapat memasang {app}: {error}", + "app_install_files_invalid": "Berkas-berkas ini tidak dapat dipasang", + "app_install_script_failed": "Sebuah kesalahan terjadi pada script pemasangan aplikasi", + "app_manifest_install_ask_admin": "Pilih seorang administrator untuk aplikasi ini", + "app_manifest_install_ask_domain": "Pilih di domain mana aplikasi ini harus dipasang", + "app_not_installed": "Tidak dapat menemukan {app} di daftar aplikasi yang terpasang: {all_apps}", + "app_not_properly_removed": "{app} belum dihapus dengan benar", + "app_remove_after_failed_install": "Menghapus aplikasi mengikuti kegagalan pemasangan...", + "app_removed": "{app} dihapus", + "app_restore_failed": "Tidak dapat memulihkan {app}: {error}", + "app_upgrade_some_app_failed": "Beberapa aplikasi tidak dapat diperbarui", + "app_upgraded": "{app} diperbarui", + "apps_already_up_to_date": "Semua aplikasi sudah pada versi mutakhir", + "apps_catalog_update_success": "Katalog aplikasi telah diperbarui!", + "apps_catalog_updating": "Memperbarui katalog aplikasi...", + "ask_firstname": "Nama depan", + "ask_lastname": "Nama belakang", + "ask_main_domain": "Domain utama", + "ask_new_domain": "Domain baru", + "ask_user_domain": "Domain yang digunakan untuk alamat surel dan akun XMPP pengguna", + "app_not_correctly_installed": "{app} kelihatannya terpasang dengan salah", + "app_start_restore": "Memulihkan {app}...", + "app_unknown": "Aplikasi tak dikenal", + "ask_new_admin_password": "Kata sandi administrasi baru", + "ask_password": "Kata sandi", + "app_upgrade_app_name": "Memperbarui {app}...", + "app_upgrade_failed": "Tidak dapat memperbarui {app}: {error}", + "app_start_install": "Memasang {app}...", + "app_start_remove": "Menghapus {app}...", + "app_manifest_install_ask_password": "Pilih kata sandi administrasi untuk aplikasi ini", + "app_upgrade_several_apps": "Aplikasi-aplikasi berikut akan diperbarui: {apps}", + "backup_app_failed": "Tidak dapat mencadangkan {app}", + "backup_archive_name_exists": "Arsip cadangan dengan nama ini sudah ada.", + "backup_created": "Cadangan dibuat", + "backup_creation_failed": "Tidak dapat membuat arsip cadangan", + "backup_delete_error": "Tidak dapat menghapus '{path}'", + "backup_deleted": "Cadangan dihapus", + "diagnosis_apps_issue": "Sebuah masalah ditemukan pada aplikasi {app}", + "backup_applying_method_tar": "Membuat arsip TAR cadangan...", + "backup_method_tar_finished": "Arsip TAR cadanagan dibuat", + "backup_nothings_done": "Tak ada yang harus disimpan", + "certmanager_cert_install_success": "Sertifikat Let's Encrypt sekarang sudah terpasang pada domain '{domain}'", + "backup_mount_archive_for_restore": "Menyiapkan arsip untuk pemulihan..." +} \ No newline at end of file diff --git a/locales/it.json b/locales/it.json index dc998d8d4..844b756ea 100644 --- a/locales/it.json +++ b/locales/it.json @@ -26,7 +26,6 @@ "admin_password_change_failed": "Impossibile cambiare la password", "admin_password_changed": "La password d'amministrazione è stata cambiata", "app_install_files_invalid": "Questi file non possono essere installati", - "app_manifest_invalid": "C'è qualcosa di scorretto nel manifesto dell'applicazione: {error}", "app_not_correctly_installed": "{app} sembra di non essere installata correttamente", "app_not_properly_removed": "{app} non è stata correttamente rimossa", "action_invalid": "L'azione '{action}' non è valida", @@ -42,7 +41,7 @@ "ask_new_admin_password": "Nuova password dell'amministrazione", "backup_app_failed": "Non è possibile fare il backup {app}", "backup_archive_app_not_found": "{app} non è stata trovata nel archivio di backup", - "app_argument_choice_invalid": "Usa una delle seguenti scelte '{choices}' per il parametro '{name}'", + "app_argument_choice_invalid": "Scegli un opzione valida per il parametro '{name}': '{value}' non è fra le opzioni disponibili ('{choices}')", "app_argument_invalid": "Scegli un valore valido per il parametro '{name}': {error}", "app_argument_required": "L'argomento '{name}' è requisito", "app_id_invalid": "Identificativo dell'applicazione non valido", @@ -67,7 +66,6 @@ "domain_dyndns_root_unknown": "Dominio radice DynDNS sconosciuto", "domain_hostname_failed": "Impossibile impostare il nuovo hostname. Potrebbe causare problemi in futuro (o anche no).", "domain_uninstall_app_first": "Queste applicazioni sono già installate su questo dominio:\n{apps}\n\nDisinstallale eseguendo 'yunohost app remove app_id' o spostale in un altro dominio eseguendo 'yunohost app change-url app_id' prima di procedere alla cancellazione del dominio", - "domain_unknown": "Dominio sconosciuto", "done": "Terminato", "domains_available": "Domini disponibili:", "downloading": "Scaricamento…", @@ -97,14 +95,12 @@ "main_domain_change_failed": "Impossibile cambiare il dominio principale", "main_domain_changed": "Il dominio principale è stato cambiato", "not_enough_disk_space": "Non c'è abbastanza spazio libero in '{path}'", - "packages_upgrade_failed": "Impossibile aggiornare tutti i pacchetti", "pattern_backup_archive_name": "Deve essere un nome di file valido di massimo 30 caratteri di lunghezza, con caratteri alfanumerici e \"-_.\" come unica punteggiatura", "pattern_domain": "Deve essere un nome di dominio valido (es. il-mio-dominio.org)", "pattern_firstname": "Deve essere un nome valido", "pattern_lastname": "Deve essere un cognome valido", "pattern_password": "Deve contenere almeno 3 caratteri", "pattern_port_or_range": "Deve essere un numero di porta valido (es. 0-65535) o una fascia di porte valida (es. 100:200)", - "pattern_positive_number": "Deve essere un numero positivo", "pattern_username": "Caratteri minuscoli alfanumerici o trattini bassi soli", "port_already_closed": "La porta {port} è già chiusa per le connessioni {ip_version}", "restore_already_installed_app": "Un'applicazione con l'ID '{app}' è già installata", @@ -116,8 +112,8 @@ "user_update_failed": "Impossibile aggiornare l'utente {user}: {error}", "restore_hook_unavailable": "Lo script di ripristino per '{part}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio", "restore_nothings_done": "Nulla è stato ripristinato", - "restore_running_app_script": "Ripristino dell'app '{app}'…", - "restore_running_hooks": "Esecuzione degli hook di ripristino…", + "restore_running_app_script": "Ripristino dell'app '{app}'...", + "restore_running_hooks": "Esecuzione degli hook di ripristino...", "service_added": "Il servizio '{service}' è stato aggiunto", "service_already_started": "Il servizio '{service}' è già avviato", "service_already_stopped": "Il servizio '{service}' è già stato fermato", @@ -129,7 +125,6 @@ "service_stopped": "Servizio '{service}' fermato", "service_unknown": "Servizio '{service}' sconosciuto", "ssowat_conf_generated": "La configurazione SSOwat rigenerata", - "ssowat_conf_updated": "Configurazione SSOwat aggiornata", "system_upgraded": "Sistema aggiornato", "unbackup_app": "{app} non verrà salvata", "unexpected_error": "È successo qualcosa di inatteso: {error}", @@ -143,7 +138,7 @@ "user_created": "Utente creato", "user_creation_failed": "Impossibile creare l'utente {user}: {error}", "user_deletion_failed": "Impossibile cancellare l'utente {user}: {error}", - "user_home_creation_failed": "Impossibile creare la 'home' directory del utente", + "user_home_creation_failed": "Impossibile creare la home directory '{home}' del utente", "user_unknown": "Utente sconosciuto: {user}", "user_updated": "Info dell'utente cambiate", "yunohost_already_installed": "YunoHost è già installato", @@ -159,7 +154,6 @@ "certmanager_domain_http_not_working": "Il dominio {domain} non sembra accessibile attraverso HTTP. Verifica nella sezione 'Web' nella diagnosi per maggiori informazioni. (Se sai cosa stai facendo, usa '--no-checks' per disattivare i controlli.)", "app_already_installed_cant_change_url": "Questa applicazione è già installata. L'URL non può essere cambiato solo da questa funzione. Controlla se `app changeurl` è disponibile.", "app_already_up_to_date": "{app} è già aggiornata", - "app_change_url_failed_nginx_reload": "Non riesco a riavviare NGINX. Questo è il risultato di 'nginx -t':\n{nginx_errors}", "app_change_url_identical_domains": "Il vecchio ed il nuovo dominio/percorso_url sono identici ('{domain}{path}'), nessuna operazione necessaria.", "app_change_url_no_script": "L'applicazione '{app_name}' non supporta ancora la modifica dell'URL. Forse dovresti aggiornarla.", "app_change_url_success": "L'URL dell'applicazione {app} è stato cambiato in {domain}{path}", @@ -226,7 +220,6 @@ "dpkg_is_broken": "Non puoi eseguire questo ora perchè dpkg/APT (i gestori di pacchetti del sistema) sembrano essere in stato danneggiato... Puoi provare a risolvere il problema connettendoti via SSH ed eseguire `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.", "domain_cannot_remove_main": "Non puoi rimuovere '{domain}' essendo il dominio principale, prima devi impostare un nuovo dominio principale con il comando 'yunohost domain main-domain -n '; ecco la lista dei domini candidati: {other_domains}", "domain_dns_conf_is_just_a_recommendation": "Questo comando ti mostra la configurazione *raccomandata*. Non ti imposta la configurazione DNS al tuo posto. È tua responsabilità configurare la tua zona DNS nel tuo registrar in accordo con queste raccomandazioni.", - "dyndns_could_not_check_provide": "Impossibile controllare se {provider} possano fornire {domain}.", "dyndns_could_not_check_available": "Impossibile controllare se {domain} è disponibile su {provider}.", "dyndns_domain_not_provided": "Il fornitore DynDNS {provider} non può fornire il dominio {domain}.", "experimental_feature": "Attenzione: Questa funzionalità è sperimentale e non è considerata stabile, non dovresti utilizzarla a meno che tu non sappia cosa stai facendo.", @@ -249,7 +242,7 @@ "good_practices_about_admin_password": "Stai per impostare una nuova password di amministratore. La password deve essere almeno di 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una frase, una serie di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).", "log_corrupted_md_file": "Il file dei metadati YAML associato con i registri è danneggiato: '{md_file}'\nErrore: {error}", "log_link_to_log": "Registro completo di questa operazione: '{desc}'", - "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log show {name}{name}'", + "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log show {name}'", "global_settings_setting_security_postfix_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", "log_link_to_failed_log": "Impossibile completare l'operazione '{desc}'! Per ricevere aiuto, per favore fornisci il registro completo dell'operazione cliccando qui", "log_help_to_get_failed_log": "L'operazione '{desc}' non può essere completata. Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log share {name}'", @@ -304,7 +297,7 @@ "app_manifest_install_ask_is_public": "Quest'applicazione dovrà essere visibile ai visitatori anonimi?", "app_manifest_install_ask_admin": "Scegli un utente amministratore per quest'applicazione", "app_manifest_install_ask_password": "Scegli una password di amministrazione per quest'applicazione", - "app_manifest_install_ask_path": "Scegli il percorso dove installare quest'applicazione", + "app_manifest_install_ask_path": "Scegli il percorso URL (dopo il dominio) dove installare quest'applicazione", "app_manifest_install_ask_domain": "Scegli il dominio dove installare quest'app", "app_argument_password_no_default": "Errore durante il parsing dell'argomento '{name}': l'argomento password non può avere un valore di default per ragioni di sicurezza", "additional_urls_already_added": "L'URL aggiuntivo '{url}' è già utilizzato come URL aggiuntivo per il permesso '{permission}'", @@ -396,15 +389,6 @@ "update_apt_cache_warning": "Qualcosa è andato storto mentre eseguivo l'aggiornamento della cache APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}", "update_apt_cache_failed": "Impossibile aggiornare la cache di APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}", "unknown_main_domain_path": "Percorso o dominio sconosciuto per '{app}'. Devi specificare un dominio e un percorso per poter specificare un URL per il permesso.", - "tools_upgrade_special_packages_completed": "Aggiornamento pacchetti YunoHost completato.\nPremi [Invio] per tornare al terminale", - "tools_upgrade_special_packages_explanation": "L'aggiornamento speciale continuerà in background. Per favore non iniziare nessun'altra azione sul tuo server per i prossimi ~10 minuti (dipende dalla velocità hardware). Dopo questo, dovrai ri-loggarti nel webadmin. Il registro di aggiornamento sarà disponibile in Strumenti → Log/Registri (nel webadmin) o dalla linea di comando eseguendo 'yunohost log list'.", - "tools_upgrade_special_packages": "Adesso aggiorno i pacchetti 'speciali' (correlati a yunohost)…", - "tools_upgrade_regular_packages_failed": "Impossibile aggiornare i pacchetti: {packages_list}", - "tools_upgrade_regular_packages": "Adesso aggiorno i pacchetti 'normali' (non correlati a yunohost)…", - "tools_upgrade_cant_unhold_critical_packages": "Impossibile annullare il blocco dei pacchetti critici/importanti…", - "tools_upgrade_cant_hold_critical_packages": "Impossibile bloccare i pacchetti critici/importanti…", - "tools_upgrade_cant_both": "Impossibile aggiornare sia il sistema e le app nello stesso momento", - "tools_upgrade_at_least_one": "Specifica 'apps', o 'system'", "show_tile_cant_be_enabled_for_regex": "Non puoi abilitare 'show_tile' in questo momento, perché l'URL del permesso '{permission}' è una regex", "show_tile_cant_be_enabled_for_url_not_defined": "Non puoi abilitare 'show_tile' in questo momento, devi prima definire un URL per il permesso '{permission}'", "service_reloaded_or_restarted": "Il servizio '{service}' è stato ricaricato o riavviato", @@ -413,7 +397,6 @@ "service_restart_failed": "Impossibile riavviare il servizio '{service}'\n\nUltimi registri del servizio: {logs}", "service_reloaded": "Servizio '{service}' ricaricato", "service_reload_failed": "Impossibile ricaricare il servizio '{service}'\n\nUltimi registri del servizio: {logs}", - "service_regen_conf_is_deprecated": "'yunohost service regen-conf' è obsoleto! Per favore usa 'yunohost tools regen-conf' al suo posto.", "service_description_yunohost-firewall": "Gestisce l'apertura e la chiusura delle porte ai servizi", "service_description_yunohost-api": "Gestisce l'interazione tra l'interfaccia web YunoHost ed il sistema", "service_description_ssh": "Ti consente di accedere da remoto al tuo server attraverso il terminale (protocollo SSH)", @@ -421,7 +404,6 @@ "service_description_rspamd": "Filtra SPAM, e altre funzionalità legate alle mail", "service_description_redis-server": "Un database specializzato usato per un veloce accesso ai dati, task queue, e comunicazioni tra programmi", "service_description_postfix": "Usato per inviare e ricevere email", - "service_description_php7.3-fpm": "Esegue app scritte in PHP con NGINX", "service_description_nginx": "Serve o permette l'accesso a tutti i siti pubblicati sul tuo server", "service_description_mysql": "Memorizza i dati delle app (database SQL)", "service_description_metronome": "Gestisce gli account di messaggistica instantanea XMPP", @@ -438,14 +420,14 @@ "restore_removing_tmp_dir_failed": "Impossibile rimuovere una vecchia directory temporanea", "restore_not_enough_disk_space": "Spazio libero insufficiente (spazio: {free_space}B, necessario: {needed_space}B, margine di sicurezza: {margin}B)", "restore_may_be_not_enough_disk_space": "Il tuo sistema non sembra avere abbastanza spazio (libero: {free_space}B, necessario: {needed_space}B, margine di sicurezza: {margin}B)", - "restore_extracting": "Sto estraendo i file necessari dall'archivio…", + "restore_extracting": "Sto estraendo i file necessari dall'archivio...", "restore_already_installed_apps": "Le seguenti app non possono essere ripristinate perché sono già installate: {apps}", "regex_with_only_domain": "Non puoi usare una regex per il dominio, solo per i percorsi", "regex_incompatible_with_tile": "/!\\ Packagers! Il permesso '{permission}' ha show_tile impostato su 'true' e perciò non è possibile definire un URL regex per l'URL principale", "regenconf_need_to_explicitly_specify_ssh": "La configurazione ssh è stata modificata manualmente, ma devi specificare la categoria 'ssh' con --force per applicare le modifiche.", "regenconf_pending_applying": "Applico le configurazioni in attesa per la categoria '{category}'...", "regenconf_failed": "Impossibile rigenerare la configurazione per le categorie: {categories}", - "regenconf_dry_pending_applying": "Controllo configurazioni in attesa che potrebbero essere applicate alla categoria '{category}'…", + "regenconf_dry_pending_applying": "Controllo configurazioni in attesa che potrebbero essere applicate alla categoria '{category}'...", "regenconf_would_be_updated": "La configurazione sarebbe stata aggiornata per la categoria '{category}'", "regenconf_updated": "Configurazione aggiornata per '{category}'", "regenconf_up_to_date": "Il file di configurazione è già aggiornato per la categoria '{category}'", @@ -493,35 +475,7 @@ "migrations_exclusive_options": "'--auto', '--skip', e '--force-rerun' sono opzioni che si escludono a vicenda.", "migrations_failed_to_load_migration": "Impossibile caricare la migrazione {id}: {error}", "migrations_dependencies_not_satisfied": "Esegui queste migrazioni: '{dependencies_id}', prima di {id}.", - "migrations_cant_reach_migration_file": "Impossibile accedere ai file di migrazione nel path '%s'", "migrations_already_ran": "Migrazioni già effettuate: {ids}", - "migration_0019_slapd_config_will_be_overwritten": "Sembra che tu abbia modificato manualmente la configurazione slapd. Per questa importante migrazione, YunoHost deve forzare l'aggiornamento della configurazione slapd. I file originali verranno back-uppati in {conf_backup_folder}.", - "migration_0019_add_new_attributes_in_ldap": "Aggiungi nuovi attributi ai permessi nel database LDAP", - "migration_0018_failed_to_reset_legacy_rules": "Impossibile resettare le regole iptables legacy: {error}", - "migration_0018_failed_to_migrate_iptables_rules": "Migrazione fallita delle iptables legacy a nftables: {error}", - "migration_0017_not_enough_space": "Libera abbastanza spazio in {path} per eseguire la migrazione.", - "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 è installato, ma non PostgreSQL 11 ?! Qualcosa di strano potrebbe esser successo al tuo sistema :'( ...", - "migration_0017_postgresql_96_not_installed": "PostgreSQL non è stato installato sul tuo sistema. Nulla da fare.", - "migration_0015_weak_certs": "I seguenti certificati utilizzano ancora un algoritmo di firma debole e dovrebbero essere aggiornati per essere compatibili con la prossima versione di nginx: {certs}", - "migration_0015_cleaning_up": "Sto pulendo la cache e i pacchetti non più utili...", - "migration_0015_specific_upgrade": "Inizio l'aggiornamento dei pacchetti di sistema che necessitano di essere aggiornati da soli...", - "migration_0015_modified_files": "Attenzioni, i seguenti file sembrano esser stati modificati manualmente, e potrebbero essere sovrascritti dopo l'aggiornamento: {manually_modified_files}", - "migration_0015_problematic_apps_warning": "Alcune applicazioni potenzialmente problematiche sono state rilevate nel sistema. Sembra che non siano state installate attraverso il catalogo app YunoHost, o non erano flaggate come 'working'/'funzionanti'. Di conseguenza, non è possibile garantire che funzioneranno ancora dopo l'aggiornamento: {problematic_apps}", - "migration_0015_general_warning": "Attenzione, sappi che questa migrazione è un'operazione delicata. Il team YunoHost ha fatto del suo meglio nel controllarla e testarla, ma le probabilità che il sistema e/o qualche app si danneggi non sono nulle.\n\nPerciò, ti raccomandiamo di:\n\t- Effettuare un backup di tutti i dati e app importanti. Maggiori informazioni su https://yunohost.org/backup;\n\t- Sii paziente dopo aver lanciato l'operazione: in base alla tua connessione internet e al tuo hardware, potrebbero volerci alcune ore per aggiornare tutto.", - "migration_0015_system_not_fully_up_to_date": "Il tuo sistema non è completamente aggiornato. Esegui un aggiornamento classico prima di lanciare la migrazione a Buster.", - "migration_0015_not_enough_free_space": "Poco spazio libero disponibile in /var/! Dovresti avere almeno 1GB libero per effettuare questa migrazione.", - "migration_0015_not_stretch": "La distribuzione Debian corrente non è Stretch!", - "migration_0015_yunohost_upgrade": "Inizio l'aggiornamento del core di YunoHost...", - "migration_0015_still_on_stretch_after_main_upgrade": "Qualcosa è andato storto durante l'aggiornamento principale, il sistema sembra essere ancora su Debian Stretch", - "migration_0015_main_upgrade": "Inizio l'aggiornamento principale...", - "migration_0015_patching_sources_list": "Applico le patch a sources.lists...", - "migration_0015_start": "Inizio migrazione a Buster", - "migration_description_0019_extend_permissions_features": "Estendi il sistema di gestione dei permessi app", - "migration_description_0018_xtable_to_nftable": "Migra le vecchie regole di traffico network sul nuovo sistema nftable", - "migration_description_0017_postgresql_9p6_to_11": "Migra i database da PostgreSQL 9.6 a 11", - "migration_description_0016_php70_to_php73_pools": "MIgra i file di configurazione 'pool' di php7.0-fpm su php7.3", - "migration_description_0015_migrate_to_buster": "Aggiorna il sistema a Debian Buster e YunoHost 4.X", - "migrating_legacy_permission_settings": "Impostando le impostazioni legacy dei permessi..", "mailbox_disabled": "E-mail disabilitate per l'utente {user}", "log_user_permission_reset": "Resetta il permesso '{}'", "log_user_permission_update": "Aggiorna gli accessi del permesso '{}'", @@ -531,8 +485,6 @@ "log_permission_url": "Aggiorna l'URL collegato al permesso '{}'", "log_permission_delete": "Cancella permesso '{}'", "log_permission_create": "Crea permesso '{}'", - "log_app_config_apply": "Applica la configurazione all'app '{}'", - "log_app_config_show_panel": "Mostra il pannello di configurazione dell'app '{}'", "log_app_action_run": "Esegui l'azione dell'app '{}'", "log_operation_unit_unclosed_properly": "Operazion unit non è stata chiusa correttamente", "invalid_regex": "Regex invalida:'{regex}'", @@ -563,7 +515,6 @@ "global_settings_setting_pop3_enabled": "Abilita il protocollo POP3 per il server mail", "dyndns_provider_unreachable": "Incapace di raggiungere il provider DynDNS {provider}: o il tuo YunoHost non è connesso ad internet o il server dynette è down.", "dpkg_lock_not_available": "Impossibile eseguire il comando in questo momento perché un altro programma sta bloccando dpkg (il package manager di sistema)", - "domain_name_unknown": "Dominio '{domain}' sconosciuto", "domain_cannot_remove_main_add_new_one": "Non puoi rimuovere '{domain}' visto che è il dominio principale nonché il tuo unico dominio, devi prima aggiungere un altro dominio eseguendo 'yunohost domain add ', impostarlo come dominio principale con 'yunohost domain main-domain n ', e solo allora potrai rimuovere il dominio '{domain}' eseguendo 'yunohost domain remove {domain}'.'", "domain_cannot_add_xmpp_upload": "Non puoi aggiungere domini che iniziano per 'xmpp-upload.'. Questo tipo di nome è riservato per la funzionalità di upload XMPP integrata in YunoHost.", "diagnosis_processes_killed_by_oom_reaper": "Alcuni processi sono stati terminati dal sistema che era a corto di memoria. Questo è un sintomo di insufficienza di memoria nel sistema o di un processo che richiede troppa memoria. Lista dei processi terminati:\n{kills_summary}", @@ -617,12 +568,10 @@ "diagnosis_rootfstotalspace_warning": "La radice del filesystem ha un totale di solo {space}. Potrebbe non essere un problema, ma stai attento perché potresti consumare tutta la memoria velocemente... Raccomandiamo di avere almeno 16 GB per la radice del filesystem.", "restore_backup_too_old": "Questo archivio backup non può essere ripristinato perché è stato generato da una versione troppo vecchia di YunoHost.", "permission_cant_add_to_all_users": "Il permesso {permission} non può essere aggiunto a tutto gli utenti.", - "migration_update_LDAP_schema": "Aggiorno lo schema LDAP...", "migration_ldap_rollback_success": "Sistema ripristinato allo stato precedente.", "migration_ldap_migration_failed_trying_to_rollback": "Impossibile migrare... provo a ripristinare il sistema.", "migration_ldap_can_not_backup_before_migration": "Il backup del sistema non è stato completato prima che la migrazione fallisse. Errore: {error}", "migration_ldap_backup_before_migration": "Sto generando il backup del database LDAP e delle impostazioni delle app prima di effettuare la migrazione.", - "migration_description_0020_ssh_sftp_permissions": "Aggiungi il supporto ai permessi SSH e SFTP", "log_backup_create": "Crea un archivio backup", "global_settings_setting_ssowat_panel_overlay_enabled": "Abilita il pannello sovrapposto SSOwat", "global_settings_setting_security_ssh_port": "Porta SSH", @@ -634,5 +583,81 @@ "global_settings_setting_security_webadmin_allowlist": "Indirizzi IP con il permesso di accedere al webadmin, separati da virgola.", "global_settings_setting_security_webadmin_allowlist_enabled": "Permetti solo ad alcuni IP di accedere al webadmin.", "disk_space_not_sufficient_update": "Non c'è abbastanza spazio libero per aggiornare questa applicazione", - "disk_space_not_sufficient_install": "Non c'è abbastanza spazio libero per installare questa applicazione" + "disk_space_not_sufficient_install": "Non c'è abbastanza spazio libero per installare questa applicazione", + "app_config_unable_to_apply": "Applicazione dei valori nel pannello di configurazione non riuscita.", + "app_config_unable_to_read": "Lettura dei valori nel pannello di configurazione non riuscita.", + "diagnosis_apps_issue": "È stato rilevato un errore per l’app {app}", + "global_settings_setting_security_nginx_redirect_to_https": "Reindirizza richieste HTTP a HTTPs di default (NON DISABILITARE a meno che tu non sappia veramente bene cosa stai facendo!)", + "diagnosis_http_special_use_tld": "Il dominio {domain} è basato su un dominio di primo livello (TLD) dall’uso speciale, come .local o .test, perciò non è previsto che sia esposto al di fuori della rete locale.", + "domain_dns_conf_special_use_tld": "Questo dominio è basato su un dominio di primo livello (TLD) dall’uso speciale, come .local o .test, perciò non è previsto abbia reali record DNS.", + "domain_dns_push_not_applicable": "La configurazione automatica del DNS non è applicabile al dominio {domain}. Dovresti configurare i tuoi record DNS manualmente, seguendo la documentazione su https://yunohost.org/dns_config.", + "domain_dns_registrar_not_supported": "YunoHost non è riuscito a riconoscere quale registrar sta gestendo questo dominio. Dovresti configurare i tuoi record DNS manualmente, seguendo la documentazione.", + "domain_dns_registrar_experimental": "Per ora, il collegamento con le API di **{registrar}** non è stata opportunamente testata e revisionata dalla comunità di YunoHost. Questa funzionalità è **altamente sperimentale**, fai attenzione!", + "domain_dns_push_failed_to_authenticate": "L’autenticazione sulle API del registrar per il dominio '{domain}' è fallita. Probabilmente le credenziali non sono corrette. (Error: {error})", + "domain_dns_push_failed_to_list": "Il reperimento dei record attuali usando le API del registrar è fallito: {error}", + "domain_dns_push_already_up_to_date": "I record sono aggiornati, nulla da fare.", + "domain_dns_pushing": "Sincronizzando i record DNS…", + "domain_config_mail_out": "Email in uscita", + "domain_config_xmpp": "Messaggistica (XMPP)", + "domain_config_auth_token": "Token di autenticazione", + "domain_config_auth_key": "Chiave di autenticazione", + "domain_config_auth_secret": "Autenticazione segreta", + "domain_config_api_protocol": "Protocollo API", + "domain_config_auth_entrypoint": "API entry point", + "other_available_options": "… e {n} altre opzioni di variabili non mostrate", + "service_description_yunomdns": "Ti permette di raggiungere il tuo server usando 'yunohost.local' all’interno della tua rete locale", + "user_import_nothing_to_do": "Nessun utente deve essere importato", + "user_import_partial_failed": "L’importazione degli utenti è parzialmente fallita", + "domain_unknown": "Il dominio '{domain}' è sconosciuto", + "log_user_import": "Importa utenti", + "invalid_password": "Password non valida", + "diagnosis_high_number_auth_failures": "Recentemente c’è stato un numero insolitamente alto di autenticazioni fallite. Potresti assicurarti che fail2ban stia funzionando e che sia configurato correttamente, oppure usare una differente porta SSH, come spiegato in https://yunohost.org/security.", + "diagnosis_apps_allgood": "Tutte le applicazioni installate rispettano le pratiche di packaging di base", + "config_apply_failed": "L’applicazione della nuova configurazione è fallita: {error}", + "diagnosis_apps_outdated_ynh_requirement": "La versione installata di quest’app richiede esclusivamente YunoHost >= 2.x, che tendenzialmente significa che non è aggiornata secondo le pratiche di packaging raccomandate. Dovresti proprio considerare di aggiornarla.", + "global_settings_setting_security_experimental_enabled": "Abilita funzionalità di sicurezza sperimentali (non abilitare se non sai cosa stai facendo!)", + "invalid_number_min": "Deve essere più grande di {min}", + "invalid_number_max": "Deve essere meno di {max}", + "log_app_config_set": "Applica la configurazione all’app '{}'", + "log_domain_dns_push": "Sincronizza i record DNS per il dominio '{}'", + "user_import_bad_file": "Il tuo file CSV non è formattato correttamente e sarà ignorato per evitare potenziali perdite di dati", + "user_import_failed": "L’operazione di importazione è completamente fallita", + "user_import_missing_columns": "Mancano le seguenti colonne: {columns}", + "user_import_success": "Utenti importati con successo", + "diagnosis_apps_bad_quality": "Sul catalogo delle applicazioni di YunoHost, questa applicazione è momentaneamente segnalata come non funzionante. Potrebbe trattarsi di un problema temporaneo, mentre i manutentori provano a risolverlo. Nel frattempo, l’aggiornamento di quest’app è disabilitato.", + "diagnosis_apps_broken": "Sul catalogo delle applicazioni di YunoHost, questa applicazione è momentaneamente segnalata come non funzionante. Potrebbe trattarsi di un problema temporaneo, mentre i manutentori provano a risolverlo. Nel frattempo, l’aggiornamento di quest’app è disabilitato.", + "diagnosis_apps_deprecated_practices": "La versione installata di questa app usa ancora delle pratiche di packaging super-vecchie oppure deprecate. Dovresti proprio considerare di aggiornarla.", + "diagnosis_apps_not_in_app_catalog": "Questa applicazione non è nel catalogo delle applicazioni di YunoHost. Se precedentemente lo era ed è stata rimossa, dovresti considerare di disinstallare l’app, dato che non riceverà aggiornamenti e potrebbe compromettere l’integrità e la sicurezza del tuo sistema.", + "diagnosis_dns_specialusedomain": "Il dominio {domain} è basato su un dominio di primo livello (TLD) dall’uso speciale, come .local o .test, perciò non è previsto abbia reali record DNS.", + "domain_dns_registrar_supported": "YunoHost ha automaticamente riconosciuto che questo dominio è gestito dal registrar **{registrar}**. Se vuoi e se fornirai le credenziali API appropriate, YunoHost può configurare automaticamente questa zona DNS. Puoi trovare la documentazione su come ottenere le tue credenziali API su questa pagina. (Puoi anche configurare i tuoi record DNS manualmente, seguendo la documentazione)", + "service_not_reloading_because_conf_broken": "Non sto ricaricando/riavviando il servizio '{name}' perché la sua configurazione è rotta: {errors}", + "config_cant_set_value_on_section": "Non puoi impostare un unico parametro in un’intera sezione della configurazione.", + "config_forbidden_keyword": "La parola chiave '{keyword}' è riservata, non puoi creare o utilizzare un pannello di configurazione con una domanda con questo id.", + "config_no_panel": "Nessun panello di configurazione trovato.", + "config_unknown_filter_key": "Il valore del filtro '{filter_key}' non è corretto.", + "config_validate_color": "È necessario inserire un codice colore in RGB esadecimale", + "config_validate_date": "È necessario inserire una data valida nel formato AAAA-MM-GG", + "config_validate_email": "È necessario inserire un’email valida", + "diagnosis_description_apps": "Applicazioni", + "domain_registrar_is_not_configured": "Il registrar non è ancora configurato per il dominio {domain}.", + "domain_dns_registrar_managed_in_parent_domain": "Questo dominio è un sotto-dominio di {parent_domain_link}. La configurazione del registrar DNS dovrebbe essere gestita dal pannello di configurazione di {parent_domain}.", + "domain_dns_registrar_yunohost": "Questo dominio è un nohost.me / nohost.st / ynh.fr, perciò la sua configurazione DNS è gestita automaticamente da YunoHost, senza alcuna ulteriore configurazione. (vedi il comando yunohost dyndns update)", + "domain_dns_push_success": "Record DNS aggiornati!", + "domain_dns_push_failed": "L’aggiornamento dei record DNS è miseramente fallito.", + "domain_dns_push_partial_failure": "Record DNS parzialmente aggiornati: alcuni segnali/errori sono stati riportati.", + "domain_config_features_disclaimer": "Per ora, abilitare/disabilitare le impostazioni di posta o XMPP impatta unicamente sulle configurazioni DNS raccomandate o ottimizzate, non cambia quelle di sistema!", + "domain_config_mail_in": "Email in arrivo", + "domain_config_auth_application_key": "Chiave applicazione", + "domain_config_auth_application_secret": "Chiave segreta applicazione", + "domain_config_auth_consumer_key": "Chiave consumatore", + "ldap_attribute_already_exists": "L’attributo LDAP '{attribute}' esiste già con il valore '{value}'", + "config_validate_time": "È necessario inserire un orario valido, come HH:MM", + "config_version_not_supported": "Le versioni '{version}' del pannello di configurazione non sono supportate.", + "danger": "Attenzione:", + "log_domain_config_set": "Aggiorna la configurazione per il dominio '{}'", + "domain_dns_push_managed_in_parent_domain": "La configurazione automatica del DNS è gestita nel dominio genitore {parent_domain}.", + "user_import_bad_line": "Linea errata {line}: {details}", + "config_validate_url": "È necessario inserire un URL web valido", + "ldap_server_down": "Impossibile raggiungere il server LDAP", + "ldap_server_is_down_restart_it": "Il servizio LDAP è down, prova a riavviarlo…" } \ No newline at end of file diff --git a/locales/kab.json b/locales/kab.json new file mode 100644 index 000000000..5daa7cef0 --- /dev/null +++ b/locales/kab.json @@ -0,0 +1,14 @@ +{ + "ask_firstname": "Isem", + "ask_lastname": "Isem n tmagit", + "ask_password": "Awal n uɛeddi", + "diagnosis_description_apps": "Isnasen", + "diagnosis_description_mail": "Imayl", + "domain_deleted": "Taɣult tettwakkes", + "done": "Immed", + "invalid_password": "Yir awal uffir", + "user_created": "Aseqdac yettwarna", + "diagnosis_description_dnsrecords": "Ikalasen DNS", + "diagnosis_description_web": "Réseau", + "domain_created": "Taɣult tettwarna" +} diff --git a/locales/mk.json b/locales/mk.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/locales/mk.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/locales/nb_NO.json b/locales/nb_NO.json index 037e09cb6..e81d3af05 100644 --- a/locales/nb_NO.json +++ b/locales/nb_NO.json @@ -27,11 +27,9 @@ "app_action_cannot_be_ran_because_required_services_down": "Dette programmet krever noen tjenester som ikke kjører. Før du fortsetter, du bør prøve å starte følgende tjenester på ny (og antagelig undersøke hvorfor de er nede): {services}", "app_already_installed_cant_change_url": "Dette programmet er allerede installert. Nettadressen kan ikke endres kun med denne funksjonen. Ta en titt på `app changeurl` hvis den er tilgjengelig.", "domain_exists": "Domenet finnes allerede", - "app_change_url_failed_nginx_reload": "Kunne ikke gjeninnlaste NGINX. Her har du utdataen for 'nginx -t'\n{nginx_errors}", "domains_available": "Tilgjengelige domener:", "done": "Ferdig", "downloading": "Laster ned…", - "dyndns_could_not_check_provide": "Kunne ikke sjekke om {provider} kan tilby {domain}.", "dyndns_could_not_check_available": "Kunne ikke sjekke om {domain} er tilgjengelig på {provider}.", "mail_domain_unknown": "Ukjent e-postadresse for domenet '{domain}'", "log_remove_on_failed_restore": "Fjern '{}' etter mislykket gjenoppretting fra sikkerhetskopiarkiv", @@ -83,7 +81,6 @@ "domain_created": "Domene opprettet", "domain_creation_failed": "Kunne ikke opprette domene", "domain_dyndns_root_unknown": "Ukjent DynDNS-rotdomene", - "domain_unknown": "Ukjent domene", "dyndns_ip_update_failed": "Kunne ikke oppdatere IP-adresse til DynDNS", "dyndns_ip_updated": "Oppdaterte din IP på DynDNS", "dyndns_key_generating": "Oppretter DNS-nøkkel… Dette kan ta en stund.", @@ -115,7 +112,7 @@ "domain_deletion_failed": "Kunne ikke slette domene", "domain_dyndns_already_subscribed": "Du har allerede abonnement på et DynDNS-domene", "log_link_to_log": "Full logg for denne operasjonen: '{desc}'", - "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log show {name}{name}'", + "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log show {name}'", "log_user_create": "Legg til '{}' bruker", "app_change_url_success": "{app} nettadressen er nå {domain}{path}", "app_install_failed": "Kunne ikke installere {app}: {error}" diff --git a/locales/nl.json b/locales/nl.json index 1995cbf62..f8b6df327 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -1,20 +1,19 @@ { "action_invalid": "Ongeldige actie '{action}'", "admin_password": "Administrator wachtwoord", - "admin_password_changed": "Het administratie wachtwoord werd gewijzigd", + "admin_password_changed": "Het administratie wachtwoord is gewijzigd", "app_already_installed": "{app} is al geïnstalleerd", "app_argument_invalid": "Kies een geldige waarde voor '{name}': {error}", "app_argument_required": "Het '{name}' moet ingevuld worden", - "app_extraction_failed": "Kan installatiebestanden niet uitpakken", + "app_extraction_failed": "Het lukt niet om de installatiebestanden uit te pakken", "app_id_invalid": "Ongeldige app-id", "app_install_files_invalid": "Deze bestanden kunnen niet worden geïnstalleerd", - "app_manifest_invalid": "Ongeldig app-manifest", - "app_not_installed": "{app} is niet geïnstalleerd", - "app_removed": "{app} succesvol verwijderd", - "app_sources_fetch_failed": "Kan bronbestanden niet ophalen, klopt de URL?", + "app_not_installed": "Het lukte niet om {app} te vinden in de lijst met geïnstalleerde apps: {all_apps}", + "app_removed": "{app} is verwijderd", + "app_sources_fetch_failed": "Het is niet gelukt bronbestanden op te halen, klopt de URL?", "app_unknown": "Onbekende app", - "app_upgrade_failed": "Kan app {app} niet updaten", - "app_upgraded": "{app} succesvol geüpgraded", + "app_upgrade_failed": "Het is niet gelukt app {app} bij te werken: {error}", + "app_upgraded": "{app} is bijgewerkt", "ask_firstname": "Voornaam", "ask_lastname": "Achternaam", "ask_new_admin_password": "Nieuw administratorwachtwoord", @@ -32,7 +31,6 @@ "domain_dyndns_root_unknown": "Onbekend DynDNS root domein", "domain_exists": "Domein bestaat al", "domain_uninstall_app_first": "Een of meerdere apps zijn geïnstalleerd op dit domein, verwijder deze voordat u het domein verwijdert", - "domain_unknown": "Onbekend domein", "done": "Voltooid", "downloading": "Downloaden...", "dyndns_ip_update_failed": "Kan het IP adres niet updaten bij DynDNS", @@ -42,7 +40,7 @@ "extracting": "Uitpakken...", "installation_complete": "Installatie voltooid", "mail_alias_remove_failed": "Kan mail-alias '{mail}' niet verwijderen", - "pattern_email": "Moet een geldig emailadres bevatten (bv. abc@example.org)", + "pattern_email": "Moet een geldig e-mailadres bevatten, zonder '+' symbool er in (bv. abc@example.org)", "pattern_mailbox_quota": "Mailbox quota moet een waarde bevatten met b/k/M/G/T erachter of 0 om geen quota in te stellen", "pattern_password": "Wachtwoord moet tenminste 3 karakters lang zijn", "port_already_closed": "Poort {port} is al gesloten voor {ip_version} verbindingen", @@ -71,8 +69,8 @@ "user_unknown": "Gebruikersnaam {user} is onbekend", "user_update_failed": "Kan gebruiker niet bijwerken", "yunohost_configured": "YunoHost configuratie is OK", - "admin_password_change_failed": "Wachtwoord kan niet veranderd worden", - "app_argument_choice_invalid": "Ongeldige keuze voor argument '{name}'. Het moet een van de volgende keuzes zijn {choices}", + "admin_password_change_failed": "Wachtwoord wijzigen is niet gelukt", + "app_argument_choice_invalid": "Kiel een geldige waarde voor argument '{name}'; {value}' komt niet voor in de keuzelijst {choices}", "app_not_correctly_installed": "{app} schijnt niet juist geïnstalleerd te zijn", "app_not_properly_removed": "{app} werd niet volledig verwijderd", "app_requirements_checking": "Noodzakelijke pakketten voor {app} aan het controleren...", @@ -100,19 +98,47 @@ "app_install_failed": "Kan {app} niet installeren: {error}", "app_remove_after_failed_install": "Bezig de app te verwijderen na gefaalde installatie...", "app_manifest_install_ask_domain": "Kies het domein waar deze app op geïnstalleerd moet worden", - "app_manifest_install_ask_path": "Kies het pad waar deze app geïnstalleerd moet worden", + "app_manifest_install_ask_path": "Kies het URL-pad (achter het domein) waar deze app geïnstalleerd moet worden", "app_manifest_install_ask_admin": "Kies een administrator voor deze app", - "app_change_url_failed_nginx_reload": "Kon NGINX niet opnieuw laden. Hier is de output van 'nginx -t':\n{nginx_errors}", "app_change_url_success": "{app} URL is nu {domain}{path}", - "app_full_domain_unavailable": "Sorry, deze app moet op haar eigen domein geïnstalleerd worden, maar andere apps zijn al geïnstalleerd op het domein '{domain}'. U kunt wel een subdomein aan deze app toewijden.", + "app_full_domain_unavailable": "Sorry, deze app moet op haar eigen domein geïnstalleerd worden, maar andere apps zijn al geïnstalleerd op het domein '{domain}'. Een mogelijke oplossing is om een nieuw subdomein toe te voegen, speciaal voor deze app.", "app_install_script_failed": "Er is een fout opgetreden in het installatiescript van de app", "app_location_unavailable": "Deze URL is niet beschikbaar of is in conflict met de al geïnstalleerde app(s):\n{apps}", "app_manifest_install_ask_password": "Kies een administratiewachtwoord voor deze app", "app_manifest_install_ask_is_public": "Moet deze app zichtbaar zijn voor anomieme bezoekers?", "app_not_upgraded": "De app '{failed_app}' kon niet upgraden en daardoor zijn de upgrades van de volgende apps geannuleerd: {apps}", - "app_start_install": "{app} installeren...", - "app_start_remove": "{app} verwijderen...", + "app_start_install": "Bezig met installeren van {app}...", + "app_start_remove": "Bezig met verwijderen van {app}...", "app_start_backup": "Bestanden aan het verzamelen voor de backup van {app}...", "app_start_restore": "{app} herstellen...", - "app_upgrade_several_apps": "De volgende apps zullen worden geüpgraded: {apps}" + "app_upgrade_several_apps": "De volgende apps zullen worden geüpgraded: {apps}", + "app_upgrade_script_failed": "Er is een fout opgetreden in het upgradescript van de app", + "apps_already_up_to_date": "Alle apps zijn al bijgewerkt met de nieuwste versie", + "app_restore_script_failed": "Er ging iets mis in het helstelscript van de app", + "app_change_url_identical_domains": "De oude en nieuwe domeinnaam/url_path zijn identiek ('{domain}{path}'), er is niets te doen.", + "app_already_up_to_date": "{app} is al de meest actuele versie", + "app_action_broke_system": "Deze actie lijkt de volgende belangrijke services te hebben kapotgemaakt: {services}", + "app_config_unable_to_apply": "De waarden in het configuratiescherm konden niet toegepast worden.", + "app_config_unable_to_read": "Het is niet gelukt de waarden van het configuratiescherm te lezen.", + "app_argument_password_no_default": "Foutmelding tijdens het lezen van wachtwoordargument '{name}': het wachtwoordargument mag om veiligheidsredenen geen standaardwaarde hebben.", + "app_already_installed_cant_change_url": "Deze app is al geïnstalleerd. De URL kan niet veranderd worden met deze functie. Probeer of dat lukt via `app changeurl`.", + "apps_catalog_init_success": "De app-catalogus is succesvol geinitieerd!", + "apps_catalog_failed_to_download": "Het is niet gelukt de {apps_catalog} app-catalogus te downloaden: {error}", + "app_packaging_format_not_supported": "Deze app kon niet geinstalleerd worden, omdat het pakketformaat niet ondersteund wordt door je Yunohost. Probeer of je Yunohost bijgewerkt kan worden.", + "additional_urls_already_added": "Extra URL '{url:s}' is al toegevoegd in de extra URL voor privilege '{permission:s}'", + "additional_urls_already_removed": "Extra URL '{url}' is al verwijderd in de extra URL voor privilege '{permission}'", + "app_label_deprecated": "Dit commando is vervallen. Gebruik alsjeblieft het nieuwe commando 'yunohost user permission update' om het label van de app te beheren.", + "app_change_url_no_script": "De app '{app_name}' ondersteunt nog geen URL-aanpassingen. Misschien wel na een upgrade.", + "app_upgrade_some_app_failed": "Sommige apps konden niet worden bijgewerkt", + "other_available_options": "... en {n} andere beschikbare opties die niet getoond worden", + "password_listed": "Dit wachtwoord is een van de meest gebruikte wachtwoorden ter wereld. Kies alstublieft iets wat minder voor de hand ligt.", + "password_too_simple_4": "Het wachtwoord moet minimaal 12 tekens lang zijn en moet cijfers, hoofdletters, kleine letters en speciale tekens bevatten", + "pattern_email_forward": "Het moet een geldig e-mailadres zijn, '+' symbool is toegestaan (ikzelf@mijndomein.nl bijvoorbeeld, of ikzelf+yunohost@mijndomein.nl)", + "password_too_simple_2": "Het wachtwoord moet minimaal 8 tekens lang zijn en moet cijfers, hoofdletters en kleine letters bevatten", + "operation_interrupted": "Werd de bewerking handmatig onderbroken?", + "pattern_backup_archive_name": "Moet een geldige bestandsnaam zijn van maximaal 30 tekens; alleen alfanumerieke tekens en -_. zijn toegestaan", + "pattern_domain": "Moet een geldige domeinnaam zijn (mijneigendomein.nl, bijvoorbeeld)", + "pattern_firstname": "Het moet een geldige voornaam zijn", + "pattern_lastname": "Het moet een geldige achternaam zijn", + "password_too_simple_3": "Het wachtwoord moet minimaal 8 tekens lang zijn en moet cijfers, hoofdletters, kleine letters en speciale tekens bevatten" } \ No newline at end of file diff --git a/locales/oc.json b/locales/oc.json index 995c61b16..a6afa32e6 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -30,11 +30,9 @@ "app_argument_choice_invalid": "Utilizatz una de las opcions « {choices} » per l’argument « {name} »", "app_argument_invalid": "Causissètz una valor invalida pel paramètre « {name} » : {error}", "app_argument_required": "Lo paramètre « {name} » es requesit", - "app_change_url_failed_nginx_reload": "Reaviada de NGINX impossibla. Vaquí la sortida de « nginx -t » :\n{nginx_errors}", "app_change_url_identical_domains": "L’ancian e lo novèl coble domeni/camin son identics per {domain}{path}, pas res a far.", "app_change_url_success": "L’URL de l’aplicacion {app} es ara {domain}{path}", "app_extraction_failed": "Extraccion dels fichièrs d’installacion impossibla", - "app_manifest_invalid": "I a quicòm que truca amb lo manifest de l’aplicacion : {error}", "app_requirements_checking": "Verificacion dels paquets requesits per {app}...", "app_sources_fetch_failed": "Recuperacion dels fichièrs fonts impossibla, l’URL es corrècta ?", "app_unsupported_remote_type": "Lo tipe alonhat utilizat per l’aplicacion es pas suportat", @@ -79,8 +77,8 @@ "upnp_enabled": "UPnP es activat", "upnp_port_open_failed": "Impossible de dobrir los pòrts amb UPnP", "yunohost_already_installed": "YunoHost es ja installat", - "yunohost_configured": "YunoHost es estat configurat", - "yunohost_installing": "Installacion de YunoHost…", + "yunohost_configured": "YunoHost es ara configurat", + "yunohost_installing": "Installacion de YunoHost...", "backup_csv_creation_failed": "Creacion impossibla del fichièr CSV necessari a las operacions futuras de restauracion", "backup_output_symlink_dir_broken": "Vòstre repertòri d’archiu « {path} » es un ligam simbolic copat. Saique oblidèretz de re/montar o de connectar supòrt.", "backup_with_no_backup_script_for_app": "L’aplicacion {app} a pas cap de script de salvagarda. I fasèm pas cas.", @@ -108,11 +106,9 @@ "domain_dyndns_root_unknown": "Domeni DynDNS màger desconegut", "domain_exists": "Lo domeni existís ja", "domain_hostname_failed": "Fracàs de la creacion d’un nòu nom d’òst. Aquò poirà provocar de problèmas mai tard (mas es pas segur… benlèu que coparà pas res).", - "domain_unknown": "Domeni desconegut", "domains_available": "Domenis disponibles :", "done": "Acabat", "downloading": "Telecargament…", - "dyndns_could_not_check_provide": "Impossible de verificar se {provider} pòt provesir {domain}.", "dyndns_ip_update_failed": "Impossible d’actualizar l’adreça IP sul domeni DynDNS", "dyndns_ip_updated": "Vòstra adreça IP actualizada pel domeni DynDNS", "dyndns_key_generating": "La clau DNS es a se generar… pòt trigar una estona.", @@ -130,24 +126,21 @@ "global_settings_unknown_setting_from_settings_file": "Clau desconeguda dins los paramètres : {setting_key}, apartada e salvagardada dins /etc/yunohost/settings-unknown.json", "main_domain_change_failed": "Modificacion impossibla del domeni màger", "main_domain_changed": "Lo domeni màger es estat modificat", - "migrations_cant_reach_migration_file": "Impossible d’accedir als fichièrs de migracion amb lo camin %s", "migrations_list_conflict_pending_done": "Podètz pas utilizar --previous e --done a l’encòp.", "migrations_loading_migration": "Cargament de la migracion {id}…", "migrations_no_migrations_to_run": "Cap de migracion de lançar", - "packages_upgrade_failed": "Actualizacion de totes los paquets impossibla", "pattern_domain": "Deu èsser un nom de domeni valid (ex : mon-domeni.org)", "pattern_email": "Deu èsser una adreça electronica valida (ex : escais@domeni.org)", "pattern_firstname": "Deu èsser un pichon nom valid", "pattern_lastname": "Deu èsser un nom valid", "pattern_password": "Deu conténer almens 3 caractèrs", "pattern_port_or_range": "Deu èsser un numèro de pòrt valid (ex : 0-65535) o un interval de pòrt (ex : 100:200)", - "pattern_positive_number": "Deu èsser un nombre positiu", "port_already_closed": "Lo pòrt {port} es ja tampat per las connexions {ip_version}", "port_already_opened": "Lo pòrt {port} es ja dubèrt per las connexions {ip_version}", "restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app} »", "app_restore_failed": "Impossible de restaurar l’aplicacion « {app} »: {error}", "backup_ask_for_copying_if_needed": "Volètz far una salvagarda en utilizant {size} Mo temporàriament ? (Aqueste biais de far es emplegat perque unes fichièrs an pas pogut èsser preparats amb un metòde mai eficaç.)", - "yunohost_not_installed": "YunoHost es pas installat o corrèctament installat. Mercés d’executar « yunohost tools postinstall »", + "yunohost_not_installed": "YunoHost es pas corrèctament installat. Mercés d’executar « yunohost tools postinstall »", "backup_output_directory_forbidden": "Causissètz un repertòri de destinacion deferent. Las salvagardas pòdon pas se realizar dins los repertòris bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", "certmanager_attempt_to_replace_valid_cert": "Sètz a remplaçar un certificat corrècte e valid pel domeni {domain} ! (Utilizatz --force per cortcircuitar)", "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let’s Encrypt pel domeni « {domain} »", @@ -179,7 +172,6 @@ "service_started": "Lo servici « {service} » es aviat", "service_stop_failed": "Impossible d’arrestar lo servici « {service} »↵\n\nJornals recents : {logs}", "ssowat_conf_generated": "La configuracion SSowat es generada", - "ssowat_conf_updated": "La configuracion SSOwat es estada actualizada", "system_upgraded": "Lo sistèma es estat actualizat", "system_username_exists": "Lo nom d’utilizaire existís ja dins los utilizaires sistèma", "unexpected_error": "Una error inesperada s’es producha", @@ -248,7 +240,7 @@ "experimental_feature": "Atencion : aquesta foncionalitat es experimentala e deu pas èsser considerada coma establa, deuriatz pas l’utilizar levat que sapiatz çò que fasètz.", "log_corrupted_md_file": "Lo fichièr YAML de metadonadas ligat als jornals d’audit es damatjat : « {md_file} »\nError : {error}", "log_link_to_log": "Jornal complèt d’aquesta operacion : {desc}", - "log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log show {name}{name} »", + "log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log show {name} »", "log_link_to_failed_log": "L’operacion « {desc} » a pas capitat ! Per obténer d’ajuda, mercés de fornir lo jornal complèt de l’operacion", "log_help_to_get_failed_log": "L’operacion « {desc} » a pas reüssit ! Per obténer d’ajuda, mercés de partejar lo jornal d’audit complèt d’aquesta operacion en utilizant la comanda « yunohost log share {name} »", "log_does_exists": "I a pas cap de jornal d’audit per l’operacion amb lo nom « {log} », utilizatz « yunohost log list » per veire totes los jornals d’operacion disponibles", @@ -315,8 +307,6 @@ "dpkg_lock_not_available": "Aquesta comanda pòt pas s’executar pel moment perque un autre programa sembla utilizar lo varrolh de dpkg (lo gestionari de paquets del sistèma)", "log_regen_conf": "Regenerar las configuracions del sistèma « {} »", "service_reloaded_or_restarted": "Lo servici « {service} » es estat recargat o reaviat", - "tools_upgrade_regular_packages_failed": "Actualizacion impossibla dels paquets seguents : {packages_list}", - "tools_upgrade_special_packages_completed": "L’actualizacion dels paquets de YunoHost es acabada !\nQuichatz [Entrada] per tornar a la linha de comanda", "dpkg_is_broken": "Podètz pas far aquò pel moment perque dpkg/APT (los gestionaris de paquets del sistèma) sembla èsser mal configurat… Podètz ensajar de solucionar aquò en vos connectar via SSH e en executar « sudo dpkg --configure -a ».", "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Autorizar l’utilizacion de la clau òst DSA (obsolèta) per la configuracion del servici SSH", "hook_json_return_error": "Fracàs de la lectura del retorn de l’script {path}. Error : {msg}. Contengut brut : {raw_content}", @@ -335,22 +325,14 @@ "regenconf_dry_pending_applying": "Verificacion de la configuracion que seriá estada aplicada a la categoria « {category} »…", "regenconf_failed": "Regeneracion impossibla de la configuracion per la(s) categoria(s) : {categories}", "regenconf_pending_applying": "Aplicacion de la configuracion en espèra per la categoria « {category} »…", - "tools_upgrade_cant_both": "Actualizacion impossibla del sistèma e de las aplicacions a l’encòp", - "tools_upgrade_cant_hold_critical_packages": "Manteniment impossible dels paquets critiques…", "global_settings_setting_security_nginx_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor web NGINX Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", "global_settings_setting_security_ssh_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor SSH. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", "global_settings_setting_security_postfix_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor Postfix. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", - "service_regen_conf_is_deprecated": "« yunohost service regen-conf » es despreciat ! Utilizatz « yunohost tools regen-conf » allòc.", "service_reload_failed": "Impossible de recargar lo servici « {service} »\n\nJornal d’audit recent : {logs}", "service_restart_failed": "Impossible de reaviar lo servici « {service} »\n\nJornal d’audit recent : {logs}", "service_reload_or_restart_failed": "Impossible de recargar o reaviar lo servici « {service} »\n\nJornal d’audit recent : {logs}", "regenconf_file_kept_back": "S’espèra que lo fichièr de configuracion « {conf} » siá suprimit per regen-conf (categoria {category} mas es estat mantengut.", "this_action_broke_dpkg": "Aquesta accion a copat dpkg/apt (los gestionaris de paquets del sistèma)… Podètz ensajar de resòlver aqueste problèma en vos connectant amb SSH e executant « sudo dpkg --configure -a ».", - "tools_upgrade_at_least_one": "Especificatz --apps O --system", - "tools_upgrade_cant_unhold_critical_packages": "Se pòt pas quitar de manténer los paquets critics…", - "tools_upgrade_regular_packages": "Actualizacion dels paquets « normals » (pas ligats a YunoHost)…", - "tools_upgrade_special_packages": "Actualizacion dels paquets « especials » (ligats a YunoHost)…", - "tools_upgrade_special_packages_explanation": "Aquesta accion s’acabarà mas l’actualizacion especiala actuala contunharà en rèire-plan. Comencetz pas cap d’autra accion sul servidor dins las ~ 10 minutas que venon (depend de la velocitat de la maquina). Un còp acabat, benlèu que vos calrà vos tornar connectar a l’interfàcia d’administracion. Los jornals d’audit de l’actualizacion seràn disponibles a Aisinas > Jornals d’audit (dins l’interfàcia d’administracion) o amb « yunohost log list » (en linha de comanda).", "update_apt_cache_failed": "I a agut d’errors en actualizar la memòria cache d’APT (lo gestionari de paquets de Debian). Aquí avètz las linhas de sources.list que pòdon vos ajudar a identificar las linhas problematicas : \n{sourceslist}", "update_apt_cache_warning": "I a agut d’errors en actualizar la memòria cache d’APT (lo gestionari de paquets de Debian). Aquí avètz las linhas de sources.list que pòdon vos ajudar a identificar las linhas problematicas : \n{sourceslist}", "backup_permission": "Autorizacion de salvagarda per l’aplicacion {app}", @@ -448,7 +430,7 @@ "diagnosis_ports_could_not_diagnose_details": "Error : {error}", "diagnosis_http_could_not_diagnose": "Impossible de diagnosticar se lo domeni es accessible de l’exterior.", "diagnosis_http_could_not_diagnose_details": "Error : {error}", - "apps_catalog_updating": "Actualizacion del catalòg d’aplicacion…", + "apps_catalog_updating": "Actualizacion del catalòg d’aplicacion...", "apps_catalog_failed_to_download": "Telecargament impossible del catalòg d’aplicacions {apps_catalog} : {error}", "apps_catalog_obsolete_cache": "La memòria cache del catalòg d’aplicacion es voida o obsolèta.", "apps_catalog_update_success": "Lo catalòg d’aplicacions es a jorn !", @@ -483,7 +465,6 @@ "diagnosis_basesystem_hardware": "L’arquitectura del servidor es {virt} {arch}", "backup_archive_corrupted": "Sembla que l’archiu de la salvagarda « {archive} » es corromput : {error}", "diagnosis_domain_expires_in": "{domain} expiraà d’aquí {days} jorns.", - "migration_0015_cleaning_up": "Netejatge de la memòria cache e dels paquets pas mai necessaris…", "restore_already_installed_apps": "Restauracion impossibla de las aplicacions seguentas que son ja installadas : {apps}", "diagnosis_package_installed_from_sury": "D’unes paquets sistèma devon èsser meses a nivèl", "ask_user_domain": "Domeni d’utilizar per l’adreça de corrièl de l’utilizaire e lo compte XMPP", @@ -496,16 +477,6 @@ "app_label_deprecated": "Aquesta comanda es estada renduda obsolèta. Mercés d'utilizar lo nòva \"yunohost user permission update\" per gerir letiquetada de l'aplication", "additional_urls_already_removed": "URL addicionala {url} es ja estada elimida per la permission «#permission:s»", "additional_urls_already_added": "URL addicionadal «{url}'» es ja estada aponduda per la permission «{permission}»", - "migration_0015_yunohost_upgrade": "Aviada de la mesa a jorn de YunoHost...", - "migration_0015_main_upgrade": "Aviada de la mesa a nivèl generala...", - "migration_0015_patching_sources_list": "Mesa a jorn del fichièr sources.lists...", - "migration_0015_start": "Aviar la migracion cap a Buster", - "migration_description_0017_postgresql_9p6_to_11": "Migrar las basas de donadas de PostgreSQL 9.6 cap a 11", - "migration_description_0016_php70_to_php73_pools": "Migrar los fichièrs de configuracion php7.0 cap a php7.3", - "migration_description_0015_migrate_to_buster": "Mesa a nivèl dels sistèmas Debian Buster e YunoHost 4.x", - "migrating_legacy_permission_settings": "Migracion dels paramètres de permission ancians...", - "log_app_config_apply": "Aplicar la configuracion a l’aplicacion « {} »", - "log_app_config_show_panel": "Mostrar lo panèl de configuracion de l’aplicacion « {} »", "log_app_action_run": "Executar l’accion de l’aplicacion « {} »", "diagnosis_basesystem_hardware_model": "Lo modèl del servidor es {model}", "backup_archive_cant_retrieve_info_json": "Obtencion impossibla de las informacions de l’archiu « {archive} »... Se pòt pas recuperar lo fichièr info.json (o es pas un fichièr json valid).", diff --git a/locales/pl.json b/locales/pl.json index caf108367..01cd71471 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -7,6 +7,6 @@ "admin_password_changed": "Hasło administratora zostało zmienione", "admin_password_change_failed": "Nie można zmienić hasła", "admin_password": "Hasło administratora", - "action_invalid": "Nieprawidłowa operacja '{action}'", + "action_invalid": "Nieprawidłowe działanie '{action:s}'", "aborting": "Przerywanie." } \ No newline at end of file diff --git a/locales/pt.json b/locales/pt.json index 4b4248f09..6b462bb6f 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -7,7 +7,6 @@ "app_extraction_failed": "Não foi possível extrair os arquivos para instalação", "app_id_invalid": "App ID invaĺido", "app_install_files_invalid": "Esses arquivos não podem ser instalados", - "app_manifest_invalid": "Manifesto da aplicação inválido: {error}", "app_not_installed": "Não foi possível encontrar {app} na lista de aplicações instaladas: {all_apps}", "app_removed": "{app} desinstalada", "app_sources_fetch_failed": "Não foi possível carregar os arquivos de código fonte, a URL está correta?", @@ -20,26 +19,25 @@ "ask_new_admin_password": "Nova senha de administração", "ask_password": "Senha", "backup_created": "Backup completo", - "backup_output_directory_not_empty": "A pasta de destino não se encontra vazia", + "backup_output_directory_not_empty": "Você deve escolher um diretório de saída que esteja vazio", "custom_app_url_required": "Deve fornecer um link para atualizar a sua aplicação personalizada {app}", "domain_cert_gen_failed": "Não foi possível gerar o certificado", "domain_created": "Domínio criado com êxito", - "domain_creation_failed": "Não foi possível criar o domínio", + "domain_creation_failed": "Não foi possível criar o domínio {domain}: {error}", "domain_deleted": "Domínio removido com êxito", - "domain_deletion_failed": "Não foi possível eliminar o domínio", + "domain_deletion_failed": "Não foi possível eliminar o domínio {domain}: {error}", "domain_dyndns_already_subscribed": "Já subscreveu um domínio DynDNS", "domain_dyndns_root_unknown": "Domínio root (administrador) DynDNS desconhecido", "domain_exists": "O domínio já existe", "domain_uninstall_app_first": "Existem uma ou mais aplicações instaladas neste domínio. Por favor desinstale-as antes de proceder com a remoção do domínio.", - "domain_unknown": "Domínio desconhecido", "done": "Concluído.", "downloading": "Transferência em curso...", - "dyndns_ip_update_failed": "Não foi possível atualizar o endereço IP a partir de DynDNS", - "dyndns_ip_updated": "Endereço IP atualizado com êxito a partir de DynDNS", + "dyndns_ip_update_failed": "Não foi possível atualizar o endereço IP para DynDNS", + "dyndns_ip_updated": "Endereço IP atualizado com êxito para DynDNS", "dyndns_key_generating": "A chave DNS está a ser gerada, isto pode demorar um pouco...", "dyndns_registered": "Dom+inio DynDNS registado com êxito", "dyndns_registration_failed": "Não foi possível registar o domínio DynDNS: {error}", - "dyndns_unavailable": "Subdomínio DynDNS indisponível", + "dyndns_unavailable": "O domínio '{domain}' não está disponível.", "extracting": "Extração em curso...", "field_invalid": "Campo inválido '{}'", "firewall_reloaded": "Firewall recarregada com êxito", @@ -50,7 +48,6 @@ "mail_forward_remove_failed": "Não foi possível remover o reencaminhamento de correio '{mail}'", "main_domain_change_failed": "Incapaz alterar o domínio raiz", "main_domain_changed": "Domínio raiz alterado com êxito", - "packages_upgrade_failed": "Não foi possível atualizar todos os pacotes", "pattern_domain": "Deve ser um nome de domínio válido (p.e. meu-dominio.org)", "pattern_email": "Deve ser um endereço de correio válido (p.e. alguem@dominio.org)", "pattern_firstname": "Deve ser um primeiro nome válido", @@ -75,7 +72,6 @@ "service_stopped": "O serviço '{service}' foi parado com êxito", "service_unknown": "Serviço desconhecido '{service}'", "ssowat_conf_generated": "Configuração SSOwat gerada com êxito", - "ssowat_conf_updated": "Configuração persistente SSOwat atualizada com êxito", "system_upgraded": "Sistema atualizado com êxito", "system_username_exists": "O utilizador já existe no registo do sistema", "unexpected_error": "Ocorreu um erro inesperado", @@ -97,34 +93,33 @@ "app_not_properly_removed": "{app} não foi corretamente removido", "app_requirements_checking": "Verificando os pacotes necessários para {app}...", "app_unsupported_remote_type": "A aplicação não possui suporte ao tipo remoto utilizado", - "backup_archive_app_not_found": "A aplicação '{app}' não foi encontrada no arquivo de backup", - "backup_archive_broken_link": "Impossível acessar o arquivo de backup (link quebrado ao {path})", - "backup_archive_name_exists": "O nome do arquivo de backup já existe", - "backup_archive_open_failed": "Não é possível abrir o arquivo de backup", - "backup_cleaning_failed": "Não é possível limpar a pasta temporária de backups", - "backup_creation_failed": "A criação do backup falhou", - "backup_delete_error": "Impossível apagar '{path}'", - "backup_deleted": "O backup foi suprimido", - "backup_hook_unknown": "Gancho de backup '{hook}' desconhecido", - "backup_nothings_done": "Não há nada para guardar", - "backup_output_directory_forbidden": "Diretório de saída proibido. Os backups não podem ser criados em /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives subpastas", + "backup_archive_app_not_found": "Não foi possível encontrar {app} no arquivo de backup", + "backup_archive_broken_link": "Não foi possível acessar o arquivo de backup (link quebrado ao {path})", + "backup_archive_name_exists": "Já existe um arquivo de backup com esse nome.", + "backup_archive_open_failed": "Não foi possível abrir o arquivo de backup", + "backup_cleaning_failed": "Não foi possível limpar o diretório temporário de backup", + "backup_creation_failed": "Não foi possível criar o arquivo de backup", + "backup_delete_error": "Não foi possível remover '{path}'", + "backup_deleted": "Backup removido", + "backup_hook_unknown": "O gancho de backup '{hook}' é desconhecido", + "backup_nothings_done": "Nada há se salvar", + "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}'", + "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_change_url_failed_nginx_reload": "Não foi possível reiniciar o nginx. Aqui está o retorno de 'nginx -t':\n{nginx_errors}", "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}", "app_upgrade_app_name": "Atualizando {app}…", "app_upgrade_some_app_failed": "Não foi possível atualizar algumas aplicações", - "backup_abstract_method": "Este metodo de backup ainda não foi implementado", - "backup_app_failed": "Não foi possível fazer o backup dos aplicativos '{app}'", - "backup_applying_method_custom": "Chamando o metodo personalizado de backup '{method}'…", - "backup_applying_method_tar": "Criando o arquivo tar de backup…", + "backup_abstract_method": "Este método de backup ainda não foi implementado", + "backup_app_failed": "Não foi possível fazer o backup de '{app}'", + "backup_applying_method_custom": "Chamando o método personalizado de backup '{method}'…", + "backup_applying_method_tar": "Criando o arquivo TAR de backup…", "backup_archive_name_unknown": "Desconhece-se o arquivo local de backup de nome '{name}'", - "backup_archive_system_part_not_available": "A seção do sistema '{part}' está indisponivel neste backup", - "backup_ask_for_copying_if_needed": "Alguns arquivos não consiguiram ser preparados para backup utilizando o metodo que não gasta espaço de disco temporariamente. Para realizar o backup {size}MB precisam ser usados temporariamente. Você concorda?", - "backup_cant_mount_uncompress_archive": "Não foi possível montar em modo leitura o diretorio de arquivos não comprimido", + "backup_archive_system_part_not_available": "A seção do sistema '{part}' está indisponível neste backup", + "backup_ask_for_copying_if_needed": "Você quer efetuar o backup usando {size}MB temporariamente? (E necessário fazer dessa forma porque alguns arquivos não puderam ser preparados usando um método mais eficiente)", + "backup_cant_mount_uncompress_archive": "Não foi possível montar o arquivo descomprimido como protegido contra escrita", "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo", "app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain}{path}'), nada para fazer.", "password_too_simple_1": "A senha precisa ter pelo menos 8 caracteres", @@ -143,7 +138,7 @@ "app_change_url_success": "A URL agora é {domain}{path}", "apps_catalog_obsolete_cache": "O cache do catálogo de aplicações está vazio ou obsoleto.", "apps_catalog_failed_to_download": "Não foi possível fazer o download do catálogo de aplicações {apps_catalog}: {error}", - "apps_catalog_updating": "Atualizado o catálogo de aplicações…", + "apps_catalog_updating": "Atualizando o catálogo de aplicações...", "apps_catalog_init_success": "Catálogo de aplicações do sistema inicializado!", "apps_already_up_to_date": "Todas as aplicações já estão atualizadas", "app_packaging_format_not_supported": "Essa aplicação não pode ser instalada porque o formato dela não é suportado pela sua versão do YunoHost. Considere atualizar seu sistema.", @@ -164,5 +159,97 @@ "app_manifest_install_ask_path": "Escolha o caminho da url (depois do domínio) em que essa aplicação deve ser instalada", "app_manifest_install_ask_domain": "Escolha o domínio em que esta aplicação deve ser instalada", "app_label_deprecated": "Este comando está deprecado! Por favor use o novo comando 'yunohost user permission update' para gerenciar a etiqueta da aplicação.", - "app_make_default_location_already_used": "Não foi passível fazer a aplicação '{app}' ser a padrão no domínio, '{domain}' já está sendo usado por '{other_app}'" -} + "app_make_default_location_already_used": "Não foi passível fazer a aplicação '{app}' ser a padrão no domínio, '{domain}' já está sendo usado por '{other_app}'", + "backup_archive_writing_error": "Não foi possível adicionar os arquivos '{source}' (nomeados dentro do arquivo '{dest}') ao backup no arquivo comprimido '{archive}'", + "backup_archive_corrupted": "Parece que o arquivo de backup '{archive}' está corrompido: {error}", + "backup_archive_cant_retrieve_info_json": "Não foi possível carregar informações para o arquivo '{archive}'... Não foi possível carregar info.json (ou não é um JSON válido).", + "backup_applying_method_copy": "Copiando todos os arquivos para o backup...", + "backup_actually_backuping": "Criando cópia de backup dos arquivos obtidos...", + "ask_user_domain": "Domínio para usar para o endereço de email e conta XMPP do usuário", + "ask_new_path": "Novo caminho", + "ask_new_domain": "Novo domínio", + "apps_catalog_update_success": "O catálogo de aplicações foi atualizado!", + "backup_no_uncompress_archive_dir": "Não existe tal diretório de arquivo descomprimido", + "backup_mount_archive_for_restore": "Preparando o arquivo para restauração...", + "backup_method_tar_finished": "Arquivo de backup TAR criado", + "backup_method_custom_finished": "Método de backup personalizado '{method}' finalizado", + "backup_method_copy_finished": "Cópia de backup finalizada", + "backup_custom_mount_error": "O método personalizado de backup não pôde passar do passo de 'mount'", + "backup_custom_backup_error": "O método personalizado de backup não pôde passar do passo de 'backup'", + "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}.", + "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.", + "backup_unable_to_organize_files": "Não foi possível usar o método rápido de organizar os arquivos no arquivo de backup", + "backup_system_part_failed": "Não foi possível fazer o backup da parte do sistema '{part}'", + "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", + "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." +} \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index 5a74524bf..9c857f7a6 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -5,29 +5,188 @@ "admin_password_changed": "Пароль администратора был изменен", "app_already_installed": "{app} уже установлено", "app_already_installed_cant_change_url": "Это приложение уже установлено. URL не может быть изменен только с помощью этой функции. Изучите `app changeurl`, если это доступно.", - "app_argument_choice_invalid": "Неверный выбор для аргумента '{name}', Это должно быть '{choices}'", - "app_argument_invalid": "Недопустимое значение аргумента '{name}': {error}'", + "app_argument_choice_invalid": "Выберите корректное значение аргумента '{name}'; '{value}' не входит в число возможных вариантов: '{choices}'", + "app_argument_invalid": "Недопустимое значение аргумента '{name}': {error}", "app_already_up_to_date": "{app} уже обновлено", "app_argument_required": "Аргумент '{name}' необходим", "app_change_url_identical_domains": "Старый и новый domain/url_path идентичны ('{domain}{path}'), ничего делать не надо.", - "app_change_url_no_script": "Приложение '{app_name}' не поддерживает изменение url. Наверное, вам нужно обновить приложение.", - "app_change_url_success": "Успешно изменён {app} url на {domain}{path}", - "app_extraction_failed": "Невозможно извлечь файлы для инсталляции", - "app_id_invalid": "Неправильный id приложения", - "app_install_files_invalid": "Неправильные файлы инсталляции", - "app_location_unavailable": "Этот url отсутствует или конфликтует с уже установленным приложением или приложениями: {apps}", - "app_manifest_invalid": "Недопустимый манифест приложения: {error}", + "app_change_url_no_script": "Приложение '{app_name}' не поддерживает изменение URL. Возможно, вам нужно обновить приложение.", + "app_change_url_success": "Успешно изменён URL {app} на {domain}{path}", + "app_extraction_failed": "Невозможно извлечь файлы для установки", + "app_id_invalid": "Неправильный ID приложения", + "app_install_files_invalid": "Эти файлы не могут быть установлены", + "app_location_unavailable": "Этот URL отсутствует или конфликтует с уже установленным приложением или приложениями:\n{apps}", "app_not_correctly_installed": "{app} , кажется, установлены неправильно", - "app_not_installed": "{app} не установлены", + "app_not_installed": "{app} не найдено в списке установленных приложений: {all_apps}", "app_not_properly_removed": "{app} удалены неправильно", "app_removed": "{app} удалено", - "app_requirements_checking": "Проверяю необходимые пакеты для {app}...", - "app_sources_fetch_failed": "Невозможно получить исходные файлы", + "app_requirements_checking": "Проверка необходимых пакетов для {app}...", + "app_sources_fetch_failed": "Невозможно получить исходные файлы, проверьте правильность URL", "app_unknown": "Неизвестное приложение", - "app_upgrade_app_name": "Обновление приложения {app}...", - "app_upgrade_failed": "Невозможно обновить {app}", - "app_upgrade_some_app_failed": "Невозможно обновить некоторые приложения", + "app_upgrade_app_name": "Обновление {app}...", + "app_upgrade_failed": "Невозможно обновить {app}: {error}", + "app_upgrade_some_app_failed": "Некоторые приложения не удалось обновить", "app_upgraded": "{app} обновлено", "installation_complete": "Установка завершена", - "password_too_simple_1": "Пароль должен быть не менее 8 символов" + "password_too_simple_1": "Пароль должен быть не менее 8 символов", + "admin_password_too_long": "Пожалуйста, выберите пароль короче 127 символов", + "password_listed": "Этот пароль является одним из наиболее часто используемых паролей в мире. Пожалуйста, выберите что-то более уникальное.", + "backup_applying_method_copy": "Копирование всех файлов в резервную копию...", + "domain_dns_conf_is_just_a_recommendation": "Эта страница показывает вам *рекомендуемую* конфигурацию. Она *не* создаёт для вас конфигурацию DNS. Вы должны сами конфигурировать зону вашего DNS у вашего регистратора в соответствии с этой рекомендацией.", + "good_practices_about_user_password": "Выберите пароль пользователя длиной не менее 8 символов, хотя рекомендуется использовать более длинные (например, парольную фразу) и / или использовать символы различного типа (прописные, строчные буквы, цифры и специальные символы).", + "password_too_simple_3": "Пароль должен содержать не менее 8 символов и содержать цифры, заглавные и строчные буквы и специальные символы", + "upnp_enabled": "UPnP включен", + "user_deleted": "Пользователь удалён", + "ask_lastname": "Фамилия", + "app_action_broke_system": "Это действие, по-видимому, нарушило эти важные службы: {services}", + "already_up_to_date": "Ничего делать не требуется. Всё уже обновлено.", + "operation_interrupted": "Действие было прервано вручную?", + "user_created": "Пользователь создан", + "aborting": "Прерывание.", + "ask_firstname": "Имя", + "ask_main_domain": "Основной домен", + "ask_new_admin_password": "Новый пароль администратора", + "ask_new_domain": "Новый домен", + "ask_new_path": "Новый путь", + "ask_password": "Пароль", + "app_remove_after_failed_install": "Удаление приложения после сбоя установки...", + "app_upgrade_script_failed": "Внутри скрипта обновления приложения произошла ошибка", + "upnp_disabled": "UPnP отключен", + "app_manifest_install_ask_domain": "Выберите домен, в котором должно быть установлено это приложение", + "app_manifest_install_ask_path": "Выберите URL путь (часть после домена), по которому должно быть установлено это приложение", + "app_manifest_install_ask_admin": "Выберите пользователя администратора для этого приложения", + "app_manifest_install_ask_password": "Выберите пароль администратора для этого приложения", + "app_manifest_install_ask_is_public": "Должно ли это приложение быть открыто для анонимных посетителей?", + "apps_already_up_to_date": "Все приложения уже обновлены", + "app_full_domain_unavailable": "Извините, это приложение должно быть установлено в собственном домене, но другие приложения уже установлены в домене '{domain}'. Вместо этого вы можете использовать отдельный поддомен для этого приложения.", + "app_install_script_failed": "Произошла ошибка в скрипте установки приложения", + "apps_catalog_update_success": "Каталог приложений был обновлён!", + "apps_catalog_updating": "Обновление каталога приложений...", + "yunohost_installing": "Установка YunoHost...", + "app_start_remove": "Удаление {app}...", + "app_label_deprecated": "Эта команда устарела! Пожалуйста, используйте новую команду 'yunohost user permission update', чтобы управлять ярлыком приложения.", + "app_start_restore": "Восстановление {app}...", + "app_upgrade_several_apps": "Будут обновлены следующие приложения: {apps}", + "password_too_simple_2": "Пароль должен содержать не менее 8 символов и включать цифры, заглавные и строчные буквы", + "password_too_simple_4": "Пароль должен содержать не менее 12 символов и включать цифры, заглавные и строчные буквы и специальные символы", + "upgrade_complete": "Обновление завершено", + "user_unknown": "Неизвестный пользователь: {user}", + "yunohost_already_installed": "YunoHost уже установлен", + "yunohost_configured": "Теперь YunoHost настроен", + "upgrading_packages": "Обновление пакетов...", + "app_requirements_unmeet": "Необходимые требования для {app} не выполнены, пакет {pkgname} ({version}) должен быть {spec}", + "app_make_default_location_already_used": "Невозможно сделать '{app}' приложением по умолчанию на домене, '{domain}' уже используется '{other_app}'", + "app_config_unable_to_apply": "Не удалось применить значения панели конфигурации.", + "app_config_unable_to_read": "Не удалось прочитать значения панели конфигурации.", + "app_install_failed": "Невозможно установить {app}: {error}", + "apps_catalog_init_success": "Система каталога приложений инициализирована!", + "backup_abstract_method": "Этот метод резервного копирования еще не реализован", + "backup_actually_backuping": "Создание резервного архива из собранных файлов...", + "backup_applying_method_custom": "Вызов пользовательского метода резервного копирования {method}'...", + "backup_archive_app_not_found": "Не удалось найти {app} в резервной копии", + "backup_applying_method_tar": "Создание резервной копии в TAR-архиве...", + "backup_archive_broken_link": "Не удалось получить доступ к резервной копии (неправильная ссылка {path})", + "apps_catalog_failed_to_download": "Невозможно загрузить каталог приложений {apps_catalog}: {error}", + "apps_catalog_obsolete_cache": "Кэш каталога приложений пуст или устарел.", + "backup_archive_cant_retrieve_info_json": "Не удалось загрузить информацию об архиве '{archive}'... info.json не может быть получен (или не является корректным json).", + "app_packaging_format_not_supported": "Это приложение не может быть установлено, поскольку его формат не поддерживается вашей версией YunoHost. Возможно, вам следует обновить систему.", + "app_restore_failed": "Не удалось восстановить {app}: {error}", + "app_restore_script_failed": "Произошла ошибка внутри сценария восстановления приложения", + "ask_user_domain": "Домен, используемый для адреса электронной почты пользователя и учетной записи XMPP", + "app_not_upgraded": "Не удалось обновить приложение '{failed_app}', и, как следствие, обновление следующих приложений было отменено: {apps}", + "app_start_backup": "Сбор файлов для резервного копирования {app}...", + "app_start_install": "Устанавливается {app}...", + "backup_app_failed": "Не удалось создать резервную копию {app}", + "backup_archive_name_exists": "Резервная копия с таким именем уже существует.", + "backup_archive_name_unknown": "Неизвестный локальный архив резервного копирования с именем '{name}'", + "backup_archive_open_failed": "Не удалось открыть архив резервной копии", + "backup_archive_corrupted": "Похоже, что архив резервной копии '{archive}' поврежден : {error}", + "certmanager_cert_install_success_selfsigned": "Самоподписанный сертификат для домена '{domain}' установлен", + "backup_created": "Создана резервная копия", + "config_unknown_filter_key": "Ключ фильтра '{filter_key}' неверен.", + "config_validate_date": "Должна быть правильная дата в формате YYYY-MM-DD", + "config_validate_email": "Должен быть правильный email", + "config_validate_time": "Должно быть правильное время формата HH:MM", + "backup_ask_for_copying_if_needed": "Хотите ли вы временно выполнить резервное копирование с использованием {size}MB? (Этот способ используется, поскольку некоторые файлы не могут быть подготовлены более эффективным методом.)", + "backup_permission": "Разрешить резервное копирование для {app}", + "certmanager_domain_dns_ip_differs_from_public_ip": "DNS-записи для домена '{domain}' отличаются от IP этого сервера. Пожалуйста, проверьте категорию 'DNS-записи' (основные) в диагностике для получения дополнительной информации. Если вы недавно изменили свою A-запись, пожалуйста, подождите, пока она распространится (некоторые программы проверки распространения DNS доступны в интернете). (Если вы знаете, что делаете, используйте '--no-checks', чтобы отключить эти проверки.)", + "certmanager_domain_not_diagnosed_yet": "Для домена {domain} еще нет результатов диагностики. Пожалуйста, перезапустите диагностику для категорий 'DNS-записи' и 'Web', чтобы проверить, готов ли домен к Let's Encrypt. (Или, если вы знаете, что делаете, используйте '--no-checks', чтобы отключить эти проверки.)", + "config_validate_url": "Должна быть правильная ссылка", + "config_version_not_supported": "Версии конфигурационной панели '{version}' не поддерживаются.", + "confirm_app_install_danger": "ОПАСНО! Это приложение все еще является экспериментальным (если не сказать, что оно явно не работает)! Вам не следует устанавливать его, если вы не знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы не будем оказывать техническую поддержку... Если вы все равно готовы рискнуть, введите '{answers}'", + "confirm_app_install_thirdparty": "ВАЖНО! Это приложение не входит в каталог приложений YunoHost. Установка сторонних приложений может нарушить целостность и безопасность вашей системы. Вам не следует устанавливать его, если вы не знаете, что делаете. ТЕХНИЧЕСКОЙ ПОДДЕРЖКИ НЕ БУДЕТ, если это приложение не будет работать или сломает вашу систему... Если вы все равно готовы рискнуть, введите '{answers}'", + "config_apply_failed": "Не удалось применить новую конфигурацию: {error}", + "config_cant_set_value_on_section": "Вы не можете установить одно значение на весь раздел конфигурации.", + "config_forbidden_keyword": "Ключевое слово '{keyword}' зарезервировано, вы не можете создать или использовать панель конфигурации с вопросом с таким id.", + "config_no_panel": "Панель конфигурации не найдена.", + "danger": "Опасно:", + "certmanager_warning_subdomain_dns_record": "Субдомен '{subdomain}' не соответствует IP-адресу основного домена '{domain}'. Некоторые функции будут недоступны, пока вы не исправите это и не перегенерируете сертификат.", + "app_argument_password_no_default": "Ошибка при парсинге аргумента пароля '{name}': аргумент пароля не может иметь значение по умолчанию по причинам безопасности", + "custom_app_url_required": "Вы должны указать URL для обновления вашего пользовательского приложения {app}", + "backup_creation_failed": "Не удалось создать резервную копию", + "backup_csv_addition_failed": "Не удалось добавить файлы для резервного копирования в CSV-файл", + "backup_csv_creation_failed": "Не удалось создать CSV-файл, необходимый для восстановления", + "backup_deleted": "Резервная копия удалена", + "backup_delete_error": "Не удалось удалить '{path}'", + "backup_method_copy_finished": "Создание копии бэкапа завершено", + "backup_method_tar_finished": "Создан резервный TAR-архив", + "backup_mount_archive_for_restore": "Подготовка архива для восстановления...", + "backup_method_custom_finished": "Пользовательский метод резервного копирования '{method}' завершен", + "backup_nothings_done": "Нечего сохранять", + "backup_output_directory_required": "Вы должны выбрать каталог для сохранения резервной копии", + "backup_system_part_failed": "Не удалось создать резервную копию системной части '{part}'", + "certmanager_cert_renew_success": "Обновлен сертификат Let's Encrypt для домена '{domain}'", + "certmanager_cert_signing_failed": "Не удалось подписать новый сертификат", + "diagnosis_apps_bad_quality": "В настоящее время это приложение отмечено как неработающее в каталоге приложений YunoHost. Это может быть временной проблемой, пока мэинтейнеры пытаются исправить проблему. Пока что обновление этого приложения отключено.", + "diagnosis_apps_broken": "В настоящее время это приложение отмечено как неработающее в каталоге приложений YunoHost. Это может быть временной проблемой, пока мэинтейнеры пытаются исправить проблему. Пока что обновления для этого приложения отключены.", + "diagnosis_apps_allgood": "Все установленные приложения соблюдают основные правила упаковки", + "diagnosis_apps_issue": "Обнаружена проблема для приложения {app}", + "diagnosis_apps_not_in_app_catalog": "Этого приложения нет в каталоге приложений YunoHost. Если оно было там раньше, а теперь удалено, вам стоит подумать об удалении этого приложения, так как оно больше не получит обновлений и может нарушить целостность и безопасность вашей системы.", + "diagnosis_apps_deprecated_practices": "Установленная версия этого приложения все еще использует некоторые устаревшие пакеты. Вам стоит подумать об обновлении.", + "additional_urls_already_added": "Этот URL '{url}' уже добавлен в дополнительный URL для разрешения '{permission}'", + "additional_urls_already_removed": "Этот URL '{url}' уже удален из дополнительных URL для разрешения '{permission}'", + "app_action_cannot_be_ran_because_required_services_down": "Для выполнения этого действия должны быть запущены следующие службы: {services}. Попробуйте перезапустить их, чтобы продолжить (и, возможно, выяснить, почему они не работают).", + "app_unsupported_remote_type": "Неподдерживаемый удаленный тип, используемый для приложения", + "backup_archive_system_part_not_available": "Системная часть '{part}' недоступна в этой резервной копии", + "backup_output_directory_not_empty": "Вы должны выбрать пустой каталог для сохранения", + "backup_archive_writing_error": "Не удалось добавить файлы '{source}' (названные в архиве '{dest}') для резервного копирования в архив '{archive}'", + "backup_cant_mount_uncompress_archive": "Не удалось смонтировать несжатый архив как защищенный от записи", + "backup_copying_to_organize_the_archive": "Копирование {size}MB для создания архива", + "backup_couldnt_bind": "Не удалось связать {src} с {dest}.", + "backup_output_directory_forbidden": "Выберите другой каталог для сохранения. Резервные копии не могут быть созданы в подкаталогах /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var или /home/yunohost.backup/archives", + "backup_with_no_backup_script_for_app": "Приложение '{app}' не имеет сценария резервного копирования. Оно будет проигнорировано.", + "certmanager_attempt_to_renew_nonLE_cert": "Сертификат для домена '{domain}' не выпущен Let's Encrypt. Невозможно продлить его автоматически!", + "certmanager_attempt_to_renew_valid_cert": "Срок действия сертификата для домена '{domain}' НЕ истекает! (Вы можете использовать --force, если знаете, что делаете)", + "certmanager_cannot_read_cert": "При попытке открыть текущий сертификат для домена {domain} произошло что-то неправильное (файл: {file}), причина: {reason}", + "certmanager_cert_install_success": "Сертификат Let's Encrypt для домена '{domain}' установлен", + "certmanager_domain_cert_not_selfsigned": "Сертификат для домена {domain} не самоподписанный. Вы уверены, что хотите заменить его? (Для этого используйте '--force'.)", + "certmanager_certificate_fetching_or_enabling_failed": "Попытка использовать новый сертификат для {domain} не сработала...", + "certmanager_domain_http_not_working": "Похоже, домен {domain} не доступен через HTTP. Пожалуйста, проверьте категорию 'Web' в диагностике для получения дополнительной информации. (Если вы знаете, что делаете, используйте '--no-checks', чтобы отключить эти проверки.)", + "certmanager_hit_rate_limit": "Для этого набора доменов {domain} в последнее время было выпущено слишком много сертификатов. Пожалуйста, повторите попытку позже. См. https://letsencrypt.org/docs/rate-limits/ для получения более подробной информации", + "certmanager_no_cert_file": "Не удалось прочитать файл сертификата для домена {domain} (файл: {file})", + "confirm_app_install_warning": "Предупреждение: Это приложение может работать, но пока еще недостаточно интегрировано в YunoHost. Некоторые функции, такие как единая регистрация и резервное копирование/восстановление, могут быть недоступны. Все равно устанавливать? [{answers}] ", + "yunohost_not_installed": "YunoHost установлен неправильно. Пожалуйста, запустите 'yunohost tools postinstall'", + "backup_cleaning_failed": "Не удалось очистить временную папку резервного копирования", + "certmanager_attempt_to_replace_valid_cert": "Вы пытаетесь перезаписать хороший и действительный сертификат для домена {domain}! (Используйте --force для обхода)", + "backup_create_size_estimation": "Архив будет содержать около {size} данных.", + "diagnosis_description_regenconf": "Конфигурации системы", + "diagnosis_description_services": "Проверка статусов сервисов", + "config_validate_color": "Должен быть правильный hex цвета RGB", + "diagnosis_basesystem_hardware": "Аппаратная архитектура сервера – {virt} {arch}", + "certmanager_acme_not_configured_for_domain": "Задача ACME не может быть запущена для {domain} прямо сейчас, потому что в его nginx conf отсутствует соответствующий фрагмент кода... Пожалуйста, убедитесь, что конфигурация вашего nginx обновлена, используя `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "diagnosis_basesystem_ynh_single_version": "{package} версия: {version} ({repo})", + "diagnosis_description_mail": "Email", + "diagnosis_basesystem_kernel": "Версия ядра Linux на сервере {kernel_version}", + "diagnosis_description_apps": "Приложения", + "diagnosis_diskusage_low": "В хранилище {mountpoint} (на устройстве {device}) осталось {free} ({free_percent}%) места (из {total}). Будьте осторожны.", + "diagnosis_description_dnsrecords": "DNS записи", + "diagnosis_description_ip": "Интернет-соединение", + "diagnosis_description_basesystem": "Основная система", + "diagnosis_description_web": "Web", + "diagnosis_basesystem_host": "На сервере запущен Debian {debian_version}", + "diagnosis_dns_bad_conf": "Некоторые записи DNS для домена {domain} (категория {category}) отсутствуют или неверны", + "diagnosis_description_systemresources": "Системные ресурсы", + "backup_with_no_restore_script_for_app": "{app} не имеет сценария восстановления, вы не сможете автоматически восстановить это приложение из резервной копии.", + "diagnosis_description_ports": "Открытые порты", + "diagnosis_basesystem_hardware_model": "Модель сервера {model}" } \ No newline at end of file diff --git a/locales/sl.json b/locales/sl.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/locales/sl.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/locales/uk.json b/locales/uk.json index 47c5991b0..9a32a597b 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -1,6 +1,5 @@ { "app_manifest_install_ask_domain": "Оберіть домен, в якому треба встановити цей застосунок", - "app_manifest_invalid": "Щось не так з маніфестом застосунку: {error}", "app_location_unavailable": "Ця URL-адреса або недоступна, або конфліктує з уже встановленим застосунком (застосунками):\n{apps}", "app_label_deprecated": "Ця команда застаріла! Будь ласка, використовуйте нову команду 'yunohost user permission update' для управління заголовком застосунку.", "app_make_default_location_already_used": "Неможливо зробити '{app}' типовим застосунком на домені, '{domain}' вже використовується '{other_app}'", @@ -13,11 +12,10 @@ "app_change_url_success": "URL-адреса {app} тепер {domain}{path}", "app_change_url_no_script": "Застосунок '{app_name}' поки не підтримує зміну URL-адрес. Можливо, вам слід оновити його.", "app_change_url_identical_domains": "Старий і новий domain/url_path збігаються ('{domain}{path}'), нічого робити не треба.", - "app_change_url_failed_nginx_reload": "Не вдалося перезавантажити NGINX. Ось результат 'nginx -t':\n{nginx_errors}", "app_argument_required": "Аргумент '{name}' необхідний", "app_argument_password_no_default": "Помилка під час розбору аргументу пароля '{name}': аргумент пароля не може мати типове значення з причин безпеки", "app_argument_invalid": "Виберіть правильне значення для аргументу '{name}': {error}", - "app_argument_choice_invalid": "Використовуйте один з цих варіантів '{choices}' для аргументу '{name}'", + "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} уже встановлено", @@ -25,71 +23,69 @@ "app_action_cannot_be_ran_because_required_services_down": "Для виконання цієї дії повинні бути запущені наступні необхідні служби: {services}. Спробуйте перезапустити їх, щоб продовжити (і, можливо, з'ясувати, чому вони не працюють).", "already_up_to_date": "Нічого не потрібно робити. Все вже актуально.", "admin_password_too_long": "Будь ласка, виберіть пароль коротше 127 символів", - "admin_password_changed": "Пароль адміністратора було змінено", + "admin_password_changed": "Пароль адміністрації було змінено", "admin_password_change_failed": "Неможливо змінити пароль", - "admin_password": "Пароль адміністратора", + "admin_password": "Пароль адміністрації", "additional_urls_already_removed": "Додаткова URL-адреса '{url}' вже видалена в додатковій URL-адресі для дозволу '{permission}'", "additional_urls_already_added": "Додаткова URL-адреса '{url}' вже додана в додаткову URL-адресу для дозволу '{permission}'", "action_invalid": "Неприпустима дія '{action}'", "aborting": "Переривання.", "diagnosis_description_web": "Мережа", - "service_reloaded_or_restarted": "Служба '{service}' була перезавантажена або перезапущено", - "service_reload_or_restart_failed": "Не вдалося перезавантажити або перезапустити службу '{service}' Recent service logs: {logs}", - "service_restarted": "Служба '{service}' перезапущено", - "service_restart_failed": "Не вдалося запустити службу '{service}' Недавні журнали служб: {logs}", + "service_reloaded_or_restarted": "Службу '{service}' була перезавантажено або перезапущено", + "service_reload_or_restart_failed": "Не вдалося перезавантажити або перезапустити службу '{service}' \n\nНедавні журнали служби: {logs}", + "service_restarted": "Службу '{service}' перезапущено", + "service_restart_failed": "Не вдалося запустити службу '{service}' \n\nНедавні журнали служб: {logs}", "service_reloaded": "Служба '{service}' перезавантажена", - "service_reload_failed": "Не вдалося перезавантажити службу '{service}' Останні журнали служби: {logs}", + "service_reload_failed": "Не вдалося перезавантажити службу '{service}'\n\nОстанні журнали служби: {logs}", "service_removed": "Служба '{service}' вилучена", "service_remove_failed": "Не вдалося видалити службу '{service}'", - "service_regen_conf_is_deprecated": "'Yunohost service regen-conf' застарів! Будь ласка, використовуйте 'yunohost tools regen-conf' замість цього.", - "service_enabled": "Служба '{service}' тепер буде автоматично запускатися при завантаженні системи.", - "service_enable_failed": "Неможливо змусити службу '{service}' автоматично запускатися при завантаженні. Недавні журнали служб: {logs}", - "service_disabled": "Служба '{service}' більше не буде запускатися при завантаженні системи.", - "service_disable_failed": "Неможливо змусити службу '{service} \"не запускатися при завантаженні. Останні журнали служби: {logs}", - "service_description_yunohost-firewall": "Управляє відкритими і закритими портами підключення до сервісів", - "service_description_yunohost-api": "Управляє взаємодією між веб-інтерфейсом YunoHost і системою", - "service_description_ssh": "Дозволяє віддалено підключатися до сервера через термінал (протокол SSH)", - "service_description_slapd": "Зберігає користувачів, домени і пов'язану з ними інформацію", - "service_description_rspamd": "Фільтрує спам і інші функції, пов'язані з електронною поштою", + "service_enabled": "Служба '{service}' тепер буде автоматично запускатися під час завантаження системи.", + "service_enable_failed": "Неможливо змусити службу '{service}' автоматично запускатися під час завантаження.\n\nНедавні журнали служби: {logs}", + "service_disabled": "Служба '{service}' більше не буде запускатися під час завантаження системи.", + "service_disable_failed": "Неможливо змусити службу '{service}' не запускатися під час завантаження.\n\nОстанні журнали служби: {logs}", + "service_description_yunohost-firewall": "Управляє відкритими і закритими портами з'єднання зі службами", + "service_description_yunohost-api": "Управляє взаємодією між вебінтерфейсом YunoHost і системою", + "service_description_ssh": "Дозволяє віддалено під'єднуватися до сервера через термінал (протокол SSH)", + "service_description_slapd": "Зберігає користувачів, домени і пов'язані з ними дані", + "service_description_rspamd": "Фільтри спаму і інші функції, пов'язані з е-поштою", "service_description_redis-server": "Спеціалізована база даних, яка використовується для швидкого доступу до даних, черги завдань і зв'язку між програмами", - "service_description_postfix": "Використовується для відправки та отримання електронної пошти", - "service_description_php7.3-fpm": "Запускає програми, написані на мові програмування PHP, за допомогою NGINX", - "service_description_nginx": "Обслуговує або надає доступ до всіх веб-сайтів, розміщених на вашому сервері", + "service_description_postfix": "Використовується для надсилання та отримання е-пошти", + "service_description_nginx": "Обслуговує або надає доступ до всіх вебсайтів, розміщених на вашому сервері", "service_description_mysql": "Зберігає дані застосунків (база даних SQL)", - "service_description_metronome": "Служба захисту миттєвого обміну повідомленнями XMPP", - "service_description_fail2ban": "Захист від перебору та інших видів атак з Інтернету", + "service_description_metronome": "Управління обліковими записами миттєвих повідомлень XMPP", + "service_description_fail2ban": "Захист від перебирання (брутфорсу) та інших видів атак з Інтернету", "service_description_dovecot": "Дозволяє поштовим клієнтам отримувати доступ до електронної пошти (через IMAP і POP3)", - "service_description_dnsmasq": "Обробляє дозвіл доменних імен (DNS)", + "service_description_dnsmasq": "Обробляє роздільність доменних імен (DNS)", "service_description_yunomdns": "Дозволяє вам отримати доступ до вашого сервера, використовуючи 'yunohost.local' у вашій локальній мережі", "service_cmd_exec_failed": "Не вдалося виконати команду '{command}'", - "service_already_stopped": "Служба '{service}' вже зупинена", - "service_already_started": "Служба '{service}' вже запущена", - "service_added": "Служба '{service}' була додана", + "service_already_stopped": "Службу '{service}' вже зупинено", + "service_already_started": "Службу '{service}' вже запущено", + "service_added": "Службу '{service}' було додано", "service_add_failed": "Не вдалося додати службу '{service}'", - "server_reboot_confirm": "Сервер негайно перезавантажиться, ви впевнені? [{answers}]", - "server_reboot": "сервер перезавантажиться", - "server_shutdown_confirm": "Сервер буде негайно виключений, ви впевнені? [{answers}].", - "server_shutdown": "сервер вимкнеться", - "root_password_replaced_by_admin_password": "Ваш кореневої пароль був замінений на пароль адміністратора.", - "root_password_desynchronized": "Пароль адміністратора був змінений, але YunoHost не зміг поширити це на пароль root!", - "restore_system_part_failed": "Не вдалося відновити системну частину '{part}'.", - "restore_running_hooks": "Запуск хуков відновлення…", - "restore_running_app_script": "Відновлення програми \"{app} '…", + "server_reboot_confirm": "Сервер буде негайно перезавантажено, ви впевнені? [{answers}]", + "server_reboot": "Сервер буде перезавантажено", + "server_shutdown_confirm": "Сервер буде негайно вимкнено, ви впевнені? [{answers}]", + "server_shutdown": "Сервер буде вимкнено", + "root_password_replaced_by_admin_password": "Ваш кореневий (root) пароль було замінено на пароль адміністратора.", + "root_password_desynchronized": "Пароль адміністратора було змінено, але YunoHost не зміг поширити це на кореневий (root) пароль!", + "restore_system_part_failed": "Не вдалося відновити системний розділ '{part}'", + "restore_running_hooks": "Запуск хуків відновлення…", + "restore_running_app_script": "Відновлення застосунку '{app}'…", "restore_removing_tmp_dir_failed": "Неможливо видалити старий тимчасовий каталог", "restore_nothings_done": "Нічого не було відновлено", - "restore_not_enough_disk_space": "Недостатньо місця (простір: {free_space: d} B, необхідний простір: {needed_space: d} B, маржа безпеки: {margin: d} B)", - "restore_may_be_not_enough_disk_space": "Схоже, у вашій системі недостатньо місця (вільного: {free_space: d} B, необхідний простір: {needed_space: d} B, запас міцності: {margin: d} B)", - "restore_hook_unavailable": "Сценарій відновлення для '{part}' недоступним у вашій системі і в архіві його теж немає", + "restore_not_enough_disk_space": "Недостатньо місця (простір: {free_space} Б, необхідний простір: {needed_space} Б, межа безпеки: {margin: d} Б)", + "restore_may_be_not_enough_disk_space": "Схоже, у вашій системі недостатньо місця (вільно: {free_space} Б, необхідний простір: {needed_space} Б, межа безпеки: {margin: d} Б)", + "restore_hook_unavailable": "Скрипт відновлення для '{part}' недоступний у вашій системі і в архіві його теж немає", "restore_failed": "Не вдалося відновити систему", - "restore_extracting": "Витяг необхідних файлів з архіву…", - "restore_confirm_yunohost_installed": "Ви дійсно хочете відновити вже встановлену систему? [{answers}].", - "restore_complete": "відновлення завершено", + "restore_extracting": "Витягнення необхідних файлів з архіву…", + "restore_confirm_yunohost_installed": "Ви дійсно хочете відновити вже встановлену систему? [{answers}]", + "restore_complete": "Відновлення завершено", "restore_cleaning_failed": "Не вдалося очистити тимчасовий каталог відновлення", - "restore_backup_too_old": "Цей архів резервних копій не може бути відновлений, бо він отриманий з дуже старою версією YunoHost.", + "restore_backup_too_old": "Цей архів резервних копій не може бути відновлений, бо він отриманий з дуже старої версії YunoHost.", "restore_already_installed_apps": "Наступні програми не можуть бути відновлені, тому що вони вже встановлені: {apps}", - "restore_already_installed_app": "Застосунок з ідентифікатором \"{app} 'вже встановлено", - "regex_with_only_domain": "Ви не можете використовувати regex для домену, тільки для шляху.", - "regex_incompatible_with_tile": "/! \\ Packagers! Дозвіл '{permission}' має значення show_tile 'true', тому ви не можете визначити regex URL в якості основного URL.", + "restore_already_installed_app": "Застосунок з ID «{app}» вже встановлено", + "regex_with_only_domain": "Ви не можете використовувати regex для домену, тільки для шляху", + "regex_incompatible_with_tile": "/! \\ Packagers! Дозвіл '{permission}' має значення show_tile 'true', тому ви не можете визначити regex URL в якості основної URL", "regenconf_need_to_explicitly_specify_ssh": "Конфігурація ssh була змінена вручну, але вам потрібно явно вказати категорію 'ssh' з --force, щоб застосувати зміни.", "regenconf_pending_applying": "Застосування очікує конфігурації для категорії '{category}'...", "regenconf_failed": "Не вдалося відновити конфігурацію для категорії (категорій): {categories}", @@ -98,400 +94,352 @@ "regenconf_updated": "Конфігурація оновлена для категорії '{category}'", "regenconf_up_to_date": "Конфігурація вже оновлена для категорії '{category}'", "regenconf_now_managed_by_yunohost": "Конфігураційний файл '{conf}' тепер управляється YunoHost (категорія {category}).", - "regenconf_file_updated": "Конфігураційний файл '{conf}' оновлений", - "regenconf_file_removed": "Конфігураційний файл '{conf}' видалений", + "regenconf_file_updated": "Конфігураційний файл '{conf}' оновлено", + "regenconf_file_removed": "Конфігураційний файл '{conf}' видалено", "regenconf_file_remove_failed": "Неможливо видалити файл конфігурації '{conf}'", - "regenconf_file_manually_removed": "Конфігураційний файл '{conf}' був видалений вручну і не буде створено", - "regenconf_file_manually_modified": "Конфігураційний файл '{conf}' був змінений вручну і не буде оновлено", - "regenconf_file_kept_back": "Конфігураційний файл '{conf}' повинен був бути вилучений regen-conf (категорія {category}), але був збережений.", + "regenconf_file_manually_removed": "Конфігураційний файл '{conf}' було видалено вручну і не буде створено", + "regenconf_file_manually_modified": "Конфігураційний файл '{conf}' було змінено вручну і не буде оновлено", + "regenconf_file_kept_back": "Очікувалося видалення конфігураційного файлу '{conf}' за допомогою regen-conf (категорія {category}), але його було збережено.", "regenconf_file_copy_failed": "Не вдалося скопіювати новий файл конфігурації '{new}' в '{conf}'", - "regenconf_file_backed_up": "Конфігураційний файл '{conf}' збережений в '{backup}'", - "postinstall_low_rootfsspace": "Загальна площа кореневої файлової системи становить менше 10 ГБ, що викликає занепокоєння! Швидше за все, дисковий простір закінчиться дуже швидко! Рекомендується мати не менше 16 ГБ для кореневої файлової системи. Якщо ви хочете встановити YunoHost, незважаючи на це попередження, повторно запустіть постінсталляцію з параметром --force-diskspace", - "port_already_opened": "Порт {port: d} вже відкритий для з'єднань {ip_version}.", - "port_already_closed": "Порт {port: d} вже закритий для з'єднань {ip_version}.", - "permission_require_account": "Дозвіл {permission} має сенс тільки для користувачів, що мають обліковий запис, і тому не може бути включено для відвідувачів.", - "permission_protected": "Дозвіл {permission} захищено. Ви не можете додавати або видаляти групу відвідувачів в/з цього дозволу.", + "regenconf_file_backed_up": "Конфігураційний файл '{conf}' збережено в '{backup}'", + "postinstall_low_rootfsspace": "Загальне місце кореневої файлової системи становить менше 10 ГБ, що викликає занепокоєння! Швидше за все, дисковий простір закінчиться дуже скоро! Рекомендовано мати не менше 16 ГБ для кореневої файлової системи. Якщо ви хочете встановити YunoHost попри це попередження, повторно запустіть післявстановлення з параметром --force-diskspace", + "port_already_opened": "Порт {port} вже відкрито для з'єднань {ip_version}", + "port_already_closed": "Порт {port} вже закрито для з'єднань {ip_version}", + "permission_require_account": "Дозвіл {permission} має зміст тільки для користувачів, що мають обліковий запис, і тому не може бути увімкненим для відвідувачів.", + "permission_protected": "Дозвіл {permission} захищено. Ви не можете додавати або вилучати групу відвідувачів до/з цього дозволу.", "permission_updated": "Дозвіл '{permission}' оновлено", "permission_update_failed": "Не вдалося оновити дозвіл '{permission}': {error}", - "permission_not_found": "Дозвіл '{permission}', не знайдено", + "permission_not_found": "Дозвіл '{permission}' не знайдено", "permission_deletion_failed": "Не вдалося видалити дозвіл '{permission}': {error}", "permission_deleted": "Дозвіл '{permission}' видалено", "permission_cant_add_to_all_users": "Дозвіл {permission} не може бути додано всім користувачам.", "permission_currently_allowed_for_all_users": "Наразі цей дозвіл надається всім користувачам на додачу до інших груп. Імовірно, вам потрібно або видалити дозвіл 'all_users', або видалити інші групи, яким його зараз надано.", "permission_creation_failed": "Не вдалося створити дозвіл '{permission}': {error}", "permission_created": "Дозвіл '{permission}' створено", - "permission_cannot_remove_main": "Видалення основного дозволу заборонено", - "permission_already_up_to_date": "Дозвіл не було оновлено, тому що запити на додавання/видалення вже відповідають поточному стану.", + "permission_cannot_remove_main": "Вилучення основного дозволу заборонене", + "permission_already_up_to_date": "Дозвіл не було оновлено, тому що запити на додавання/вилучення вже відповідають поточному стану.", "permission_already_exist": "Дозвіл '{permission}' вже існує", - "permission_already_disallowed": "Група '{group}' вже має дозвіл \"{permission} 'відключено", - "permission_already_allowed": "Для гурту \"{group} 'вже включено дозвіл' {permission} '", + "permission_already_disallowed": "Група '{group}' вже має вимкнений дозвіл '{permission}'", + "permission_already_allowed": "Група '{group}' вже має увімкнений дозвіл '{permission}'", "pattern_password_app": "На жаль, паролі не можуть містити такі символи: {forbidden_chars}", - "pattern_username": "Повинен складатися тільки з букв і цифр в нижньому регістрі і символів підкреслення.", - "pattern_positive_number": "Повинно бути позитивним числом", - "pattern_port_or_range": "Повинен бути дійсний номер порту (наприклад, 0-65535) або діапазон портів (наприклад, 100: 200).", - "pattern_password": "Повинен бути довжиною не менше 3 символів", - "pattern_mailbox_quota": "Повинен бути розмір з суфіксом b/k/M/G/T або 0, щоб не мати квоти.", - "pattern_lastname": "Повинна бути дійсне прізвище", - "pattern_firstname": "Повинно бути дійсне ім'я", - "pattern_email": "Повинен бути дійсною адресою електронної пошти, без символу '+' (наприклад, someone@example.com ).", - "pattern_email_forward": "Повинен бути дійсну адресу електронної пошти, символ '+' приймається (наприклад, someone+tag@example.com )", - "pattern_domain": "Повинно бути дійсне доменне ім'я (наприклад, my-domain.org)", - "pattern_backup_archive_name": "Повинно бути правильне ім'я файлу, що містить не більше 30 символів, тільки букви і цифри символи і символ -_.", - "password_too_simple_4": "Пароль повинен бути довжиною не менше 12 символів і містити цифру, верхні, нижні і спеціальні символи.", - "password_too_simple_3": "Пароль повинен бути довжиною не менше 8 символів і містити цифру, верхні, нижні і спеціальні символи", - "password_too_simple_2": "Пароль повинен складатися не менше ніж з 8 символів і містити цифру, верхній і нижній символи", - "password_too_simple_1": "Пароль повинен складатися не менше ніж з 8 символів", - "password_listed": "Цей пароль входить в число найбільш часто використовуваних паролів в світі. Будь ласка, виберіть щось більш унікальне.", - "packages_upgrade_failed": "Не вдалося оновити всі пакети", - "operation_interrupted": "Операція була перервана вручну?", - "invalid_number": "Повинно бути число", - "not_enough_disk_space": "Недостатньо вільного місця на \"{path} '.", - "migrations_to_be_ran_manually": "Міграція {id} повинна бути запущена вручну. Будь ласка, перейдіть в розділ Інструменти → Міграції на сторінці веб-адміністратора або виконайте команду `yunohost tools migrations run`.", - "migrations_success_forward": "Міграція {id} завершена", - "migrations_skip_migration": "Пропуск міграції {id}...", + "pattern_username": "Має складатися тільки з букв і цифр в нижньому регістрі і символів підкреслення", + "pattern_port_or_range": "Має бути припустимий номер порту (наприклад, 0-65535) або діапазон портів (наприклад, 100:200)", + "pattern_password": "Має бути довжиною не менше 3 символів", + "pattern_mailbox_quota": "Має бути розмір з суфіксом b/k/M/G/T або 0, щоб не мати квоти", + "pattern_lastname": "Має бути припустиме прізвище", + "pattern_firstname": "Має бути припустиме ім'я", + "pattern_email": "Має бути припустима адреса е-пошти, без символу '+' (наприклад, someone@example.com)", + "pattern_email_forward": "Має бути припустима адреса е-пошти, символ '+' приймається (наприклад, someone+tag@example.com)", + "pattern_domain": "Має бути припустиме доменне ім'я (наприклад, my-domain.org)", + "pattern_backup_archive_name": "Має бути правильна назва файлу, що містить не більше 30 символів, тільки букви і цифри і символи -_", + "password_too_simple_4": "Пароль має складатися не менше ніж з 12 символів і містити цифри, великі та малі символи і спеціальні символи", + "password_too_simple_3": "Пароль має складатися не менше ніж з 8 символів і містити цифри, великі та малі символи і спеціальні символи", + "password_too_simple_2": "Пароль має складатися не менше ніж з 8 символів і містити цифри, великі та малі символи", + "password_too_simple_1": "Пароль має складатися не менше ніж з 8 символів", + "password_listed": "Цей пароль входить в число найбільш часто використовуваних паролів у світі. Будь ласка, виберіть щось неповторюваніше.", + "operation_interrupted": "Операція була вручну перервана?", + "invalid_number": "Має бути числом", + "not_enough_disk_space": "Недостатньо вільного місця на '{path}'", + "migrations_to_be_ran_manually": "Міграція {id} повинна бути запущена вручну. Будь ласка, перейдіть в розділ Засоби → Міграції на сторінці вебадміністрації або виконайте команду `yunohost tools migrations run`.", + "migrations_success_forward": "Міграцію {id} завершено", + "migrations_skip_migration": "Пропускання міграції {id}...", "migrations_running_forward": "Виконання міграції {id}...", - "migrations_pending_cant_rerun": "Ці міграції ще не завершені, тому не можуть бути запущені знову: {ids}", - "migrations_not_pending_cant_skip": "Ці міграції не очікують виконання, тому не можуть бути пропущені: {ids}", - "migrations_no_such_migration": "Не існує міграції під назвою '{id}'.", + "migrations_pending_cant_rerun": "Наступні міграції ще не завершені, тому не можуть бути запущені знову: {ids}", + "migrations_not_pending_cant_skip": "Наступні міграції не очікують виконання, тому не можуть бути пропущені: {ids}", + "migrations_no_such_migration": "Не існує міграції під назвою '{id}'", "migrations_no_migrations_to_run": "Немає міграцій для запуску", - "migrations_need_to_accept_disclaimer": "Щоб запустити міграцію {id}, ви повинні прийняти наступний відмова від відповідальності: --- {disclaimer} --- Якщо ви згодні запустити міграцію, будь ласка, повторіть команду з опцією '--accept-disclaimer'.", - "migrations_must_provide_explicit_targets": "Ви повинні вказати явні цілі при використанні '--skip' або '--force-rerun'.", - "migrations_migration_has_failed": "Міграція {id} не завершена, переривається. Помилка: {exception}.", + "migrations_need_to_accept_disclaimer": "Щоб запустити міграцію {id}, ви повинні прийняти наступну відмову від відповідальності:\n---\n{disclaimer}\n---\nЯкщо ви згодні запустити міграцію, будь ласка, повторіть команду з опцією '--accept-disclaimer'.", + "migrations_must_provide_explicit_targets": "Ви повинні вказати явні цілі при використанні '--skip' або '--force-rerun'", + "migrations_migration_has_failed": "Міграція {id} не завершена, перериваємо. Помилка: {exception}", "migrations_loading_migration": "Завантаження міграції {id}...", "migrations_list_conflict_pending_done": "Ви не можете одночасно використовувати '--previous' і '--done'.", - "migrations_exclusive_options": "'--Auto', '--skip' і '--force-rerun' є взаємовиключними опціями.", + "migrations_exclusive_options": "'--auto', '--skip', і '--force-rerun' є взаємовиключними опціями.", "migrations_failed_to_load_migration": "Не вдалося завантажити міграцію {id}: {error}", "migrations_dependencies_not_satisfied": "Запустіть ці міграції: '{dependencies_id}', перед міграцією {id}.", - "migrations_cant_reach_migration_file": "Не вдалося отримати доступ до файлів міграцій по шляху '% s'.", - "migrations_already_ran": "Ці міграції вже виконані: {ids}", - "migration_0019_slapd_config_will_be_overwritten": "Схоже, що ви вручну відредагували конфігурацію slapd. Для цього критичного переходу YunoHost повинен примусово оновити конфігурацію slapd. Оригінальні файли будуть збережені в {conf_backup_folder}.", - "migration_0019_add_new_attributes_in_ldap": "Додавання нових атрибутів для дозволів в базі даних LDAP", - "migration_0018_failed_to_reset_legacy_rules": "Не вдалося скинути застарілі правила iptables: {error}", - "migration_0018_failed_to_migrate_iptables_rules": "Не вдалося перенести застарілі правила iptables в nftables: {error}", - "migration_0017_not_enough_space": "Звільніть достатньо місця в {path} для запуску міграції.", - "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 встановлений, але не postgresql 11‽ Можливо, у вашій системі відбулося щось дивне: (...", - "migration_0017_postgresql_96_not_installed": "PostgreSQL ні встановлено у вашій системі. Нічого не потрібно робити.", - "migration_0015_weak_certs": "Було виявлено, що такі сертифікати все ще використовують слабкі алгоритми підпису і повинні бути оновлені для сумісності з наступною версією nginx: {certs}", - "migration_0015_cleaning_up": "Очищення кеш-пам'яті і пакетів, які більше не потрібні...", - "migration_0015_specific_upgrade": "Початок поновлення системних пакетів, які повинні бути оновлені незалежно...", - "migration_0015_modified_files": "Зверніть увагу, що такі файли були змінені вручну і можуть бути перезаписані після поновлення: {manually_modified_files}.", - "migration_0015_problematic_apps_warning": "Зверніть увагу, що були виявлені наступні, можливо, проблемні встановлені додатки. Схоже, що вони не були встановлені з каталогу застосунків YunoHost або не зазначені як \"робочі\". Отже, не можна гарантувати, що вони будуть працювати після оновлення: {problematic_apps}.", - "migration_0015_general_warning": "Будь ласка, зверніть увагу, що ця міграція є делікатною операцією. Команда YunoHost зробила все можливе, щоб перевірити і протестувати її, але міграція все ще може порушити частина системи або її застосунків. Тому рекомендується: - Виконати резервне копіювання всіх важливих даних або застосунків. Більш детальна інформація на сайті https://yunohost.org/backup; - Наберіться терпіння після запуску міграції: В залежності від вашого підключення до Інтернету і апаратного забезпечення, оновлення може зайняти до декількох годин.", - "migration_0015_system_not_fully_up_to_date": "Ваша система не повністю оновлена. Будь ласка, виконайте регулярне оновлення перед запуском міграції на Buster.", - "migration_0015_not_enough_free_space": "Вільного місця в/var/досить мало! У вас повинно бути не менше 1 ГБ вільного місця, щоб запустити цю міграцію.", - "migration_0015_not_stretch": "Поточний дистрибутив Debian не є Stretch!", - "migration_0015_yunohost_upgrade": "Починаємо оновлення ядра YunoHost...", - "migration_0015_still_on_stretch_after_main_upgrade": "Щось пішло не так під час основного поновлення, система, схоже, все ще знаходиться на Debian Stretch", - "migration_0015_main_upgrade": "Початок основного поновлення...", - "migration_0015_patching_sources_list": "Виправлення sources.lists...", - "migration_0015_start": "Початок міграції на Buster", - "migration_update_LDAP_schema": "Оновлення схеми LDAP...", + "migrations_already_ran": "Наступні міграції вже виконано: {ids}", "migration_ldap_rollback_success": "Система відкотилася.", - "migration_ldap_migration_failed_trying_to_rollback": "Не вдалося виконати міграцію... спроба відкату системи.", - "migration_ldap_can_not_backup_before_migration": "Не вдалося завершити резервне копіювання системи перед невдалої міграцією. Помилка: {error}", - "migration_ldap_backup_before_migration": "Створення резервної копії бази даних LDAP і установки застосунків перед фактичної міграцією.", - "migration_description_0020_ssh_sftp_permissions": "Додайте підтримку дозволів SSH і SFTP", - "migration_description_0019_extend_permissions_features": "Розширення/переробка системи управління дозволами застосунків", - "migration_description_0018_xtable_to_nftable": "Перенесення старих правил мережевого трафіку в нову систему nftable", - "migration_description_0017_postgresql_9p6_to_11": "Перенесення баз даних з PostgreSQL 9.6 на 11", - "migration_description_0016_php70_to_php73_pools": "Перенесіть php7.0-fpm 'pool' conf файли на php7.3", - "migration_description_0015_migrate_to_buster": "Оновлення системи до Debian Buster і YunoHost 4.x", - "migrating_legacy_permission_settings": "Перенесення застарілих налаштувань дозволів...", - "main_domain_changed": "Основний домен був змінений", + "migration_ldap_migration_failed_trying_to_rollback": "Не вдалося виконати міграцію... Пробуємо відкотити систему.", + "migration_ldap_can_not_backup_before_migration": "Не вдалося завершити резервне копіювання системи перед невдалою міграцією. Помилка: {error}", + "migration_ldap_backup_before_migration": "Створення резервної копії бази даних LDAP і налаштування застосунків перед фактичною міграцією.", + "main_domain_changed": "Основний домен було змінено", "main_domain_change_failed": "Неможливо змінити основний домен", - "mail_unavailable": "Ця електронна адреса зарезервований і буде автоматично виділено найпершого користувачеві", - "mailbox_used_space_dovecot_down": "Поштова служба Dovecot повинна бути запущена, якщо ви хочете отримати використане місце в поштовій скриньці.", - "mailbox_disabled": "Електронна пошта відключена для користувача {user}", + "mail_unavailable": "Ця е-пошта зарезервована і буде автоматично виділена найпершому користувачеві", + "mailbox_used_space_dovecot_down": "Поштова служба Dovecot повинна бути запущена, якщо ви хочете отримати використане місце в поштовій скриньці", + "mailbox_disabled": "Е-пошта вимкнена для користувача {user}", "mail_forward_remove_failed": "Не вдалося видалити переадресацію електронної пошти '{mail}'", - "mail_domain_unknown": "Неправильну адресу електронної пошти для домену '{domain}'. Будь ласка, використовуйте домен, адмініструється цим сервером.", - "mail_alias_remove_failed": "Не вдалося видалити псевдонім електронної пошти '{mail}'", - "log_tools_reboot": "перезавантажити сервер", - "log_tools_shutdown": "Вимкнути ваш сервер", + "mail_domain_unknown": "Неправильна адреса е-пошти для домену '{domain}'. Будь ласка, використовуйте домен, що адмініструється цим сервером.", + "mail_alias_remove_failed": "Не вдалося видалити аліас електронної пошти '{mail}'", + "log_tools_reboot": "Перезавантаження сервера", + "log_tools_shutdown": "Вимикання сервера", "log_tools_upgrade": "Оновлення системних пакетів", - "log_tools_postinstall": "Постінсталляція вашого сервера YunoHost", - "log_tools_migrations_migrate_forward": "запустіть міграції", - "log_domain_main_domain": "Зробити '{}' основним доменом", - "log_user_permission_reset": "Скинути дозвіл \"{} '", - "log_user_permission_update": "Оновити доступи для дозволу '{}'", - "log_user_update": "Оновити інформацію для користувача '{}'", - "log_user_group_update": "Оновити групу '{}'", - "log_user_group_delete": "Видалити групу \"{} '", - "log_user_group_create": "Створити групу '{}'", - "log_user_delete": "Видалити користувача '{}'", - "log_user_create": "Додати користувача '{}'", - "log_regen_conf": "Регенерувати системні конфігурації '{}'", - "log_letsencrypt_cert_renew": "Оновити сертифікат Let's Encrypt на домені '{}'", - "log_selfsigned_cert_install": "Встановити самоподпісанний сертифікат на домені '{}'", - "log_permission_url": "Оновити URL, пов'язаний з дозволом '{}'", - "log_permission_delete": "Видалити дозвіл \"{} '", - "log_permission_create": "Створити дозвіл \"{} '", - "log_letsencrypt_cert_install": "Встановіть сертифікат Let's Encrypt на домен '{}'", - "log_dyndns_update": "Оновити IP, пов'язаний з вашим піддоменом YunoHost '{}'", + "log_tools_postinstall": "Післявстановлення сервера YunoHost", + "log_tools_migrations_migrate_forward": "Запущено міграції", + "log_domain_main_domain": "Зроблено '{}' основним доменом", + "log_user_permission_reset": "Скинуто дозвіл «{}»", + "log_user_permission_update": "Оновлено доступи для дозволу '{}'", + "log_user_update": "Оновлено відомості для користувача '{}'", + "log_user_group_update": "Оновлено групу '{}'", + "log_user_group_delete": "Видалено групу «{}»", + "log_user_group_create": "Створено групу '{}'", + "log_user_delete": "Видалення користувача '{}'", + "log_user_create": "Додавання користувача '{}'", + "log_regen_conf": "Перестворення системних конфігурацій '{}'", + "log_letsencrypt_cert_renew": "Оновлення сертифікату Let's Encrypt на домені '{}'", + "log_selfsigned_cert_install": "Установлення самопідписаного сертифікату на домені '{}'", + "log_permission_url": "Оновлення URL, пов'язаногл з дозволом '{}'", + "log_permission_delete": "Видалення дозволу '{}'", + "log_permission_create": "Створення дозволу '{}'", + "log_letsencrypt_cert_install": "Установлення сертифікату Let's Encrypt на домен '{}'", + "log_dyndns_update": "Оновлення IP, пов'язаного з вашим піддоменом YunoHost '{}'", "log_dyndns_subscribe": "Підписка на піддомен YunoHost '{}'", - "log_domain_remove": "Видалити домен '{}' з конфігурації системи", - "log_domain_add": "Додати домен '{}' в конфігурацію системи", - "log_remove_on_failed_install": "Видалити '{}' після невдалої установки", - "log_remove_on_failed_restore": "Видалити '{}' після невдалого відновлення з резервного архіву", + "log_domain_remove": "Вилучення домену '{}' з конфігурації системи", + "log_domain_add": "Додавання домену '{}' в конфігурацію системи", + "log_remove_on_failed_install": "Вилучення '{}' після невдалого встановлення", + "log_remove_on_failed_restore": "Вилучення '{}' після невдалого відновлення з резервного архіву", "log_backup_restore_app": "Відновлення '{}' з архіву резервних копій", "log_backup_restore_system": "Відновлення системи з резервного архіву", "log_backup_create": "Створення резервного архіву", "log_available_on_yunopaste": "Цей журнал тепер доступний за посиланням {url}", - "log_app_config_apply": "Застосувати конфігурацію до додатка \"{} '", - "log_app_config_show_panel": "Показати панель конфігурації програми \"{} '", - "log_app_action_run": "Активації дії додатка \"{} '", - "log_app_makedefault": "Зробити '{}' додатком за замовчуванням", - "log_app_upgrade": "Оновити застосунок '{}'", - "log_app_remove": "Для видалення програми '{}'", - "log_app_install": "Встановіть застосунок '{}'", - "log_app_change_url": "Змініть URL-адресу додатка \"{} '", + "log_app_action_run": "Запуск дії застосунку «{}»", + "log_app_makedefault": "Застосунок '{}' зроблено типовим", + "log_app_upgrade": "Оновлення застосунку '{}'", + "log_app_remove": "Вилучення застосунку '{}'", + "log_app_install": "Установлення застосунку '{}'", + "log_app_change_url": "Змінення URL-адреси застосунку «{}»", "log_operation_unit_unclosed_properly": "Блок операцій не був закритий належним чином", - "log_does_exists": "Немає журналу операцій з ім'ям '{log}', використовуйте 'yunohost log list', щоб подивитися всі публічні журнали операцій", - "log_help_to_get_failed_log": "Операція '{desc}' не може бути завершена. Будь ласка, поділіться повним журналом цієї операції, використовуючи команду 'yunohost log share {name}', щоб отримати допомогу.", - "log_link_to_failed_log": "Не вдалося завершити операцію '{desc}'. Будь ласка, надайте повний журнал цієї операції, натиснувши тут , щоб отримати допомогу.", - "log_help_to_get_log": "Щоб переглянути журнал операції '{desc}', використовуйте команду 'yunohost log show {name} {name}'.", - "log_link_to_log": "Повний журнал цієї операції: ' {desc} '", - "log_corrupted_md_file": "Файл метаданих YAML, пов'язаний з журналами, пошкоджений: '{md_file} Помилка: {error}'", - "iptables_unavailable": "Ви не можете грати з iptables тут. Ви перебуваєте або в контейнері, або ваше ядро не підтримує його.", - "ip6tables_unavailable": "Ви не можете грати з ip6tables тут. Ви перебуваєте або в контейнері, або ваше ядро не підтримує його.", - "invalid_regex": "Невірний regex: '{regex}'", - "installation_complete": "установка завершена", - "hook_name_unknown": "Невідоме ім'я хука '{name}'", - "hook_list_by_invalid": "Це властивість не може бути використано для перерахування хуков", + "log_does_exists": "Немає журналу операцій з назвою '{log}', використовуйте 'yunohost log list', щоб подивитися всі доступні журнали операцій", + "log_help_to_get_failed_log": "Операція '{desc}' не може бути завершена. Будь ласка, поділіться повним журналом цієї операції, використовуючи команду 'yunohost log share {name}', щоб отримати допомогу", + "log_link_to_failed_log": "Не вдалося завершити операцію '{desc}'. Будь ласка, надайте повний журнал цієї операції, натиснувши тут, щоб отримати допомогу", + "log_help_to_get_log": "Щоб переглянути журнал операції '{desc}', використовуйте команду 'yunohost log show {name}'", + "log_link_to_log": "Повний журнал цієї операції: '{desc}'", + "log_corrupted_md_file": "Файл метаданих YAML, пов'язаний з журналами, пошкоджено: '{md_file}\nПомилка: {error}'", + "iptables_unavailable": "Ви не можете відтворювати з iptables тут. Ви перебуваєте або в контейнері, або ваше ядро не підтримує його", + "ip6tables_unavailable": "Ви не можете відтворювати з ip6tables тут. Ви перебуваєте або в контейнері, або ваше ядро не підтримує його", + "invalid_regex": "Неприпустимий regex: '{regex}'", + "installation_complete": "Установлення завершено", + "hook_name_unknown": "Невідома назва хука '{name}'", + "hook_list_by_invalid": "Цю властивість не може бути використано для перерахування хуків (гачків)", "hook_json_return_error": "Не вдалося розпізнати повернення з хука {path}. Помилка: {msg}. Необроблений контент: {raw_content}", "hook_exec_not_terminated": "Скрипт не завершився належним чином: {path}", "hook_exec_failed": "Не вдалося запустити скрипт: {path}", "group_user_not_in_group": "Користувач {user} не входить в групу {group}", "group_user_already_in_group": "Користувач {user} вже в групі {group}", "group_update_failed": "Не вдалося оновити групу '{group}': {error}", - "group_updated": "Група '{group}' оновлена", - "group_unknown": "Група '{group}' невідома.", + "group_updated": "Групу '{group}' оновлено", + "group_unknown": "Група '{group}' невідома", "group_deletion_failed": "Не вдалося видалити групу '{group}': {error}", - "group_deleted": "Група '{group}' вилучена", + "group_deleted": "Групу '{group}' видалено", "group_cannot_be_deleted": "Група {group} не може бути видалена вручну.", - "group_cannot_edit_primary_group": "Група '{group}' не може бути відредаговано вручну. Це основна група, призначена тільки для одного конкретного користувача.", - "group_cannot_edit_visitors": "Група 'visitors' не може бути відредаговано вручну. Це спеціальна група, що представляє анонімних відвідувачів.", - "group_cannot_edit_all_users": "Група 'all_users' не може бути відредаговано вручну. Це спеціальна група, призначена для всіх користувачів, зареєстрованих в YunoHost.", - "group_creation_failed": "Не вдалося створити групу \"{group} ': {error}", - "group_created": "Група '{group}' створена", - "group_already_exist_on_system_but_removing_it": "Група {group} вже існує в групах системи, але YunoHost видалить її...", + "group_cannot_edit_primary_group": "Група '{group}' не може бути відредагована вручну. Це основна група, призначена тільки для одного конкретного користувача.", + "group_cannot_edit_visitors": "Група 'visitors' не може бути відредагована вручну. Це спеціальна група, що представляє анонімних відвідувачів", + "group_cannot_edit_all_users": "Група 'all_users' не може бути відредагована вручну. Це спеціальна група, призначена для всіх користувачів, зареєстрованих в YunoHost", + "group_creation_failed": "Не вдалося створити групу '{group}': {error}", + "group_created": "Групу '{group}' створено", + "group_already_exist_on_system_but_removing_it": "Група {group} вже існує в групах системи, але YunoHost вилучить її...", "group_already_exist_on_system": "Група {group} вже існує в групах системи", "group_already_exist": "Група {group} вже існує", - "good_practices_about_user_password": "Зараз ви маєте визначити новий пароль користувача. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольної фрази) і/або використання різних символів (заголовних, малих, цифр і спеціальних символів).", - "good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адміністратора. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольної фрази) і/або використання різних символів (прописних, малих, цифр і спеціальних символів).", - "global_settings_unknown_type": "Несподівана ситуація, параметр {setting} має тип {unknown_type}, але це не тип, підтримуваний системою.", - "global_settings_setting_backup_compress_tar_archives": "При створенні нових резервних копій стискати архіви (.tar.gz) замість незжатих архівів (.tar). NB: включення цієї опції означає створення більш легких архівів резервних копій, але початкова процедура резервного копіювання буде значно довше і важче для CPU.", - "global_settings_setting_security_webadmin_allowlist": "IP-адреси, яким дозволений доступ до веб-адміну. Через кому.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Дозволити доступ до веб-адміну тільки деяким IP-адресами.", + "good_practices_about_user_password": "Зараз ви збираєтеся поставити новий пароль користувача. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", + "good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адміністрації. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольного гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", + "global_settings_unknown_type": "Несподівана ситуація, налаштування {setting} має тип {unknown_type}, але це не тип, підтримуваний системою.", + "global_settings_setting_backup_compress_tar_archives": "При створенні нових резервних копій стискати архіви (.tar.gz) замість нестислих архівів (.tar). NB: вмикання цієї опції означає створення легших архівів резервних копій, але початкова процедура резервного копіювання буде значно довшою і важчою для CPU.", + "global_settings_setting_security_webadmin_allowlist": "IP-адреси, яким дозволений доступ до вебадміністрації. Через кому.", + "global_settings_setting_security_webadmin_allowlist_enabled": "Дозволити доступ до вебадміністрації тільки деяким IP-адресам.", "global_settings_setting_smtp_relay_password": "Пароль хоста SMTP-ретрансляції", - "global_settings_setting_smtp_relay_user": "Обліковий запис користувача SMTP-реле", - "global_settings_setting_smtp_relay_port": "Порт SMTP-реле", - "global_settings_setting_smtp_relay_host": "SMTP релейний хост, який буде використовуватися для відправки пошти замість цього примірника yunohost. Корисно, якщо ви знаходитеся в одній із цих ситуацій: ваш 25 порт заблокований вашим провайдером або VPS провайдером, у вас є житловий IP в списку DUHL, ви не можете налаштувати зворотний DNS або цей сервер не доступний безпосередньо в інтернеті і ви хочете використовувати інший сервер для відправки пошти.", - "global_settings_setting_smtp_allow_ipv6": "Дозволити використання IPv6 для отримання і відправки пошти", - "global_settings_setting_ssowat_panel_overlay_enabled": "Включити накладення панелі SSOwat", + "global_settings_setting_smtp_relay_user": "Обліковий запис користувача SMTP-ретрансляції", + "global_settings_setting_smtp_relay_port": "Порт SMTP-ретрансляції", + "global_settings_setting_smtp_relay_host": "Хост SMTP-ретрансляції, який буде використовуватися для надсилання е-пошти замість цього зразка Yunohost. Корисно, якщо ви знаходитеся в одній із цих ситуацій: ваш 25 порт заблокований вашим провайдером або VPS провайдером, у вас є житловий IP в списку DUHL, ви не можете налаштувати зворотний DNS або цей сервер не доступний безпосередньо в Інтернеті і ви хочете використовувати інший сервер для відправки електронних листів.", + "global_settings_setting_smtp_allow_ipv6": "Дозволити використання IPv6 для отримання і надсилання листів е-пошти", + "global_settings_setting_ssowat_panel_overlay_enabled": "Увімкнути накладення панелі SSOwat", "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Дозволити використання (застарілого) ключа DSA для конфігурації демона SSH", - "global_settings_unknown_setting_from_settings_file": "Невідомий ключ в настройках: '{setting_key}', відкиньте його і збережіть в /etc/yunohost/settings-unknown.json", + "global_settings_unknown_setting_from_settings_file": "Невідомий ключ в налаштуваннях: '{setting_key}', відхиліть його і збережіть у /etc/yunohost/settings-unknown.json", "global_settings_setting_security_ssh_port": "SSH-порт", "global_settings_setting_security_postfix_compatibility": "Компроміс між сумісністю і безпекою для сервера Postfix. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", - "global_settings_setting_security_ssh_compatibility": "Сумісність і співвідношення безпеки для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", + "global_settings_setting_security_ssh_compatibility": "Компроміс між сумісністю і безпекою для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", "global_settings_setting_security_password_user_strength": "Надійність пароля користувача", "global_settings_setting_security_password_admin_strength": "Надійність пароля адміністратора", - "global_settings_setting_security_nginx_compatibility": "Компроміс між сумісністю і безпекою для веб-сервера NGINX. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", - "global_settings_setting_pop3_enabled": "Включити протокол POP3 для поштового сервера.", - "global_settings_reset_success": "Попередні настройки тепер збережені в {path}.", - "global_settings_key_doesnt_exists": "Ключ '{settings_key}' не існує в глобальних налаштуваннях, ви можете побачити всі доступні ключі, виконавши команду 'yunohost settings list'.", - "global_settings_cant_write_settings": "Неможливо зберегти файл настройок, причина: {reason}", - "global_settings_cant_serialize_settings": "Не вдалося серіалізовать дані налаштувань, причина: {reason}", - "global_settings_cant_open_settings": "Не вдалося відкрити файл настройок, причина: {reason}", - "global_settings_bad_type_for_setting": "Поганий тип для настройки {setting}, отриманий {received_type}, очікується {expected_type}", - "global_settings_bad_choice_for_enum": "Поганий вибір для настройки {setting}, отримано '{choice}', але доступні наступні варіанти: {available_choices}.", - "firewall_rules_cmd_failed": "Деякі команди правил брандмауера не спрацювали. Більш детальна інформація в журналі.", - "firewall_reloaded": "брандмауер перезавантажений", - "firewall_reload_failed": "Не вдалося перезавантажити брандмауер", + "global_settings_setting_security_nginx_compatibility": "Компроміс між сумісністю і безпекою для вебсервера NGINX. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", + "global_settings_setting_pop3_enabled": "Увімкніть протокол POP3 для поштового сервера", + "global_settings_reset_success": "Попередні налаштування тепер збережені в {path}", + "global_settings_key_doesnt_exists": "Ключ '{settings_key}' не існує в глобальних налаштуваннях, ви можете побачити всі доступні ключі, виконавши команду 'yunohost settings list'", + "global_settings_cant_write_settings": "Неможливо зберегти файл налаштувань, причина: {reason}", + "global_settings_cant_serialize_settings": "Не вдалося серіалізувати дані налаштувань, причина: {reason}", + "global_settings_cant_open_settings": "Не вдалося відкрити файл налаштувань, причина: {reason}", + "global_settings_bad_type_for_setting": "Поганий тип для налаштування {setting}, отримано {received_type}, а очікується {expected_type}", + "global_settings_bad_choice_for_enum": "Поганий вибір для налаштування {setting}, отримано '{choice}', але доступні наступні варіанти: {available_choices}", + "firewall_rules_cmd_failed": "Деякі команди правил фаєрвола не спрацювали. Подробиці в журналі.", + "firewall_reloaded": "Фаєрвол перезавантажено", + "firewall_reload_failed": "Не вдалося перезавантажити фаєрвол", "file_does_not_exist": "Файл {path} не існує.", "field_invalid": "Неприпустиме поле '{}'", "experimental_feature": "Попередження: Ця функція є експериментальною і не вважається стабільною, ви не повинні використовувати її, якщо не знаєте, що робите.", - "extracting": "Витяг...", + "extracting": "Витягнення...", "dyndns_unavailable": "Домен '{domain}' недоступний.", "dyndns_domain_not_provided": "DynDNS провайдер {provider} не може надати домен {domain}.", - "dyndns_registration_failed": "Не вдалося зареєструвати домен DynDNS: {error}.", - "dyndns_registered": "Домен DynDNS зареєстрований", - "dyndns_provider_unreachable": "Неможливо зв'язатися з провайдером DynDNS {provider}: або ваш YunoHost неправильно підключений до інтернету, або сервер dynette не працює.", - "dyndns_no_domain_registered": "Домен не зареєстрований в DynDNS", - "dyndns_key_not_found": "DNS-ключ не знайдений для домену", - "dyndns_key_generating": "Генерація DNS-ключа... Це може зайняти деякий час.", - "dyndns_ip_updated": "Оновлення свій IP-адресу в DynDNS", - "dyndns_ip_update_failed": "Не вдалося оновити IP-адреса в DynDNS", - "dyndns_could_not_check_available": "Не вдалося перевірити наявність певної {domain} на {provider}.", - "dyndns_could_not_check_provide": "Не вдалося перевірити, чи може {provider} надати {domain}.", - "dpkg_lock_not_available": "Ця команда не може бути виконана прямо зараз, тому що інша програма, схоже, використовує блокування dpkg (системного менеджера пакетів).", - "dpkg_is_broken": "Ви не можете зробити це прямо зараз, тому що dpkg/APT (системні менеджери пакетів), схоже, знаходяться в зламаному стані... Ви можете спробувати вирішити цю проблему, підключившись через SSH і виконавши `sudo apt install --fix-broken` і/або `sudo dpkg --configure -a`.", + "dyndns_registration_failed": "Не вдалося зареєструвати домен DynDNS: {error}", + "dyndns_registered": "Домен DynDNS зареєстровано", + "dyndns_provider_unreachable": "Неможливо зв'язатися з провайдером DynDNS {provider}: або ваш YunoHost неправильно під'єднано до Інтернету, або сервер dynette не працює.", + "dyndns_no_domain_registered": "Домен не зареєстровано в DynDNS", + "dyndns_key_not_found": "DNS-ключ для домену не знайдено", + "dyndns_key_generating": "Утворення DNS-ключа... Це може зайняти деякий час.", + "dyndns_ip_updated": "Вашу IP-адресу в DynDNS оновлено", + "dyndns_ip_update_failed": "Не вдалося оновити IP-адресу в DynDNS", + "dyndns_could_not_check_available": "Не вдалося перевірити, чи {domain} доступний у {provider}.", + "dpkg_lock_not_available": "Ця команда не може бути виконана прямо зараз, тому що інша програма, схоже, використовує блокування dpkg (системного менеджера пакетів)", + "dpkg_is_broken": "Ви не можете зробити це прямо зараз, тому що dpkg/APT (системні менеджери пакетів), схоже, знаходяться в зламаному стані... Ви можете спробувати вирішити цю проблему, під'єднавшись через SSH і виконавши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`.", "downloading": "Завантаження…", "done": "Готово", "domains_available": "Доступні домени:", - "domain_unknown": "невідомий домен", - "domain_name_unknown": "Домен '{domain}' невідомий", - "domain_uninstall_app_first": "Ці додатки все ще встановлені на вашому домені: {apps} ласка, видаліть їх за допомогою 'yunohost app remove the_app_id' або перемістити їх на інший домен за допомогою 'yunohost app change-url the_app_id', перш ніж приступити до видалення домену.", - "domain_remove_confirm_apps_removal": "Видалення цього домену призведе до видалення цих застосунків: {apps} Ви впевнені, що хочете це зробити? [{answers}].", - "domain_hostname_failed": "Неможливо встановити нове ім'я хоста. Це може викликати проблеми в подальшому (можливо, все буде в порядку).", - "domain_exists": "Домен вже існує", - "domain_dyndns_root_unknown": "Невідомий кореневої домен DynDNS", + "domain_uninstall_app_first": "Ці застосунки все ще встановлені на вашому домені:\n{apps}\n\nВидаліть їх за допомогою 'yunohost app remove the_app_id' або перемістіть їх на інший домен за допомогою 'yunohost app change-url the_app_id', перш ніж приступити до вилучення домену", + "domain_remove_confirm_apps_removal": "Вилучення цього домену призведе до вилучення таких застосунків:\n{apps}\n\nВи впевнені, що хочете це зробити? [{answers}]", + "domain_hostname_failed": "Неможливо встановити нову назву хоста. Це може викликати проблеми в подальшому (можливо, все буде в порядку).", + "domain_exists": "Цей домен уже існує", + "domain_dyndns_root_unknown": "Невідомий кореневий домен DynDNS", "domain_dyndns_already_subscribed": "Ви вже підписалися на домен DynDNS", - "domain_dns_conf_is_just_a_recommendation": "Ця команда показує * рекомендовану * конфігурацію. Насправді вона не встановлює конфігурацію DNS для вас. Ви самі повинні налаштувати свою зону DNS у реєстратора відповідно до цих рекомендацій.", + "domain_dns_conf_is_just_a_recommendation": "Ця команда показує *рекомендовану* конфігурацію. Насправді вона не встановлює конфігурацію DNS для вас. Ви самі повинні налаштувати свою зону DNS у реєстратора відповідно до цих рекомендацій.", "domain_deletion_failed": "Неможливо видалити домен {domain}: {error}", - "domain_deleted": "домен видалений", + "domain_deleted": "Домен видалено", "domain_creation_failed": "Неможливо створити домен {domain}: {error}", - "domain_created": "домен створений", - "domain_cert_gen_failed": "Не вдалося згенерувати сертифікат", - "domain_cannot_remove_main_add_new_one": "Ви не можете видалити '{domain}', так як це основний домен і ваш єдиний домен, вам потрібно спочатку додати інший домен за допомогою 'yunohost domain add ', потім встановити його як основний домен за допомогою ' yunohost domain main-domain -n 'і потім ви можете видалити домен' {domain} 'за допомогою' yunohost domain remove {domain} ''.", - "domain_cannot_add_xmpp_upload": "Ви не можете додавати домени, що починаються з 'xmpp-upload.'. Таке ім'я зарезервовано для функції XMPP upload, вбудованої в YunoHost.", - "domain_cannot_remove_main": "Ви не можете видалити '{domain}', так як це основний домен, спочатку вам потрібно встановити інший домен в якості основного за допомогою 'yunohost domain main-domain -n '; ось список доменів-кандидатів: {other_domains}", - "disk_space_not_sufficient_update": "Недостатньо місця на диску для поновлення цього додатка", - "disk_space_not_sufficient_install": "Бракує місця на диску для установки цього додатка", - "diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте yunohost settings set security.ssh.port -v YOUR_SSH_PORT , щоб визначити порт SSH, і перевірте yunohost tools regen-conf ssh --dry-run --with-diff yunohost tools regen-conf ssh --force , щоб скинути ваш conf на рекомендований YunoHost.", - "diagnosis_sshd_config_inconsistent": "Схоже, що порт SSH був вручну змінений в/etc/ssh/sshd_config. Починаючи з версії YunoHost 4.2, доступний новий глобальний параметр 'security.ssh.port', що дозволяє уникнути ручного редагування конфігурації.", + "domain_created": "Домен створено", + "domain_cert_gen_failed": "Не вдалося утворити сертифікат", + "domain_cannot_remove_main_add_new_one": "Ви не можете видалити '{domain}', так як це основний домен і ваш єдиний домен, вам потрібно спочатку додати інший домен за допомогою 'yunohost domain add ', потім встановити його як основний домен за допомогою 'yunohost domain main-domain -n ' і потім ви можете вилучити домен '{domain}' за допомогою 'yunohost domain remove {domain}'.'", + "domain_cannot_add_xmpp_upload": "Ви не можете додавати домени, що починаються з 'xmpp-upload.'. Таку назву зарезервовано для функції XMPP upload, вбудованої в YunoHost.", + "domain_cannot_remove_main": "Ви не можете вилучити '{domain}', бо це основний домен, спочатку вам потрібно встановити інший домен в якості основного за допомогою 'yunohost domain main-domain -n '; ось список доменів-кандидатів: {other_domains}", + "disk_space_not_sufficient_update": "Недостатньо місця на диску для оновлення цього застосунку", + "disk_space_not_sufficient_install": "Недостатньо місця на диску для встановлення цього застосунку", + "diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте команду yunohost settings set security.ssh.port -v YOUR_SSH_PORT, щоб визначити порт SSH, і перевіртеyunohost tools regen-conf ssh --dry-run --with-diff і yunohost tools regen-conf ssh --force, щоб скинути ваш конфіг на рекомендований YunoHost.", + "diagnosis_sshd_config_inconsistent": "Схоже, що порт SSH був уручну змінений в /etc/ssh/sshd_config. Починаючи з версії YunoHost 4.2, доступний новий глобальний параметр 'security.ssh.port', що дозволяє уникнути ручного редагування конфігурації.", "diagnosis_sshd_config_insecure": "Схоже, що конфігурація SSH була змінена вручну і є небезпечною, оскільки не містить директив 'AllowGroups' або 'AllowUsers' для обмеження доступу авторизованих користувачів.", - "diagnosis_processes_killed_by_oom_reaper": "Деякі процеси були недавно вбито системою через брак пам'яті. Зазвичай це є симптомом нестачі пам'яті в системі або процесу, який з'їв дуже багато пам'яті. Зведення убитих процесів: {kills_summary}", - "diagnosis_never_ran_yet": "Схоже, що цей сервер був налаштований недавно, і поки немає звіту про діагностику. Вам слід почати з повної діагностики, або з веб-адміністратора, або використовуючи 'yunohost diagnosis run' з командного рядка.", + "diagnosis_processes_killed_by_oom_reaper": "Деякі процеси було недавно вбито системою через брак пам'яті. Зазвичай це є симптомом нестачі пам'яті в системі або процесу, який з'їв дуже багато пам'яті. Зведення убитих процесів:\n{kills_summary}", + "diagnosis_never_ran_yet": "Схоже, що цей сервер був налаштований недавно, і поки немає звіту про діагностику. Вам слід почати з повної діагностики, або з вебадміністрації, або використовуючи 'yunohost diagnosis run' з командного рядка.", "diagnosis_unknown_categories": "Наступні категорії невідомі: {categories}", - "diagnosis_http_nginx_conf_not_up_to_date_details": "Щоб виправити ситуацію, перевірте різницю за допомогою командного рядка, використовуючи yunohost tools regen-conf nginx --dry-run --with-diff , і якщо все в порядку, застосуйте зміни за допомогою yunohost tools regen-conf nginx --force .", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Щоб виправити становище, перевірте різницю за допомогою командного рядка, використовуючи yunohost tools regen-conf nginx --dry-run --with-diff, і якщо все в порядку, застосуйте зміни за допомогою команди yunohost tools regen-conf nginx --force.", "diagnosis_http_nginx_conf_not_up_to_date": "Схоже, що конфігурація nginx цього домену була змінена вручну, що не дозволяє YunoHost визначити, чи доступний він по HTTP.", - "diagnosis_http_partially_unreachable": "Домен {domain} здається недоступним по HTTP ззовні локальної мережі в IPv {failed}, хоча він працює в IPv {passed}.", - "diagnosis_http_unreachable": "Домен {domain} здається недоступним через HTTP ззовні локальної мережі.", - "diagnosis_http_bad_status_code": "Схоже, що замість вашого сервера відповіла інша машина (можливо, ваш маршрутизатор).
1. Найбільш поширеною причиною цієї проблеми є те, що порт 80 (і 443) неправильно перенаправлений на ваш сервер .
2. На більш складних установках: переконайтеся, що немає брандмауера або зворотного проксі.", - "diagnosis_http_connection_error": "Помилка підключення: не вдалося підключитися до запитуваного домену, швидше за все, він недоступний.", - "diagnosis_http_timeout": "При спробі зв'язатися з вашим сервером ззовні стався тайм-аут. Він здається недоступним.
1. Найбільш поширеною причиною цієї проблеми є те, що порт 80 (і 443) неправильно перенаправлений на ваш сервер .
2. Ви також повинні переконатися, що служба nginx запущена
3. На більш складних установках: переконайтеся, що немає брандмауера або зворотного проксі.", - "diagnosis_http_ok": "Домен {domain} доступний по HTTP ззовні локальної мережі.", - "diagnosis_http_localdomain": "Домен {domain} з доменом .local TLD не може бути доступний ззовні локальної мережі.", + "diagnosis_http_partially_unreachable": "Домен {domain} здається недоступним по HTTP поза локальною мережею в IPv{failed}, хоча він працює в IPv{passed}.", + "diagnosis_http_unreachable": "Домен {domain} здається недоступним через HTTP поза локальною мережею.", + "diagnosis_http_bad_status_code": "Схоже, що замість вашого сервера відповіла інша машина (можливо, ваш маршрутизатор).
1. Найбільш поширеною причиною цієї проблеми є те, що порт 80 (і 443) неправильно перенаправлено на ваш сервер .
2. На більш складних установках: переконайтеся, що немає фаєрвола або зворотного проксі.", + "diagnosis_http_connection_error": "Помилка з'єднання: не вдалося з'єднатися із запитуваним доменом, швидше за все, він недоступний.", + "diagnosis_http_timeout": "При спробі зв'язатися з вашим сервером ззовні стався тайм-аут. Він здається недоступним.
1. Найбільш поширеною причиною цієї проблеми є те, що порт 80 (і 443) неправильно перенаправлено на ваш сервер .
2. Ви також повинні переконатися, що служба nginx запущена
3.На більш складних установках: переконайтеся, що немає фаєрвола або зворотного проксі.", + "diagnosis_http_ok": "Домен {domain} доступний по HTTP поза локальною мережею.", "diagnosis_http_could_not_diagnose_details": "Помилка: {error}", - "diagnosis_http_could_not_diagnose": "Не вдалося діагностувати досяжність доменів ззовні в IPv {ipversion}.", - "diagnosis_http_hairpinning_issue_details": "Можливо, це пов'язано з блоком/маршрутизатором вашого інтернет-провайдера. В результаті, люди ззовні вашої локальної мережі зможуть отримати доступ до вашого сервера, як і очікувалося, але не люди зсередини локальної мережі (як ви, ймовірно?) При використанні доменного імені або глобального IP. Можливо, ви зможете поліпшити ситуацію, глянувши на https://yunohost.org/dns_local_network .", - "diagnosis_http_hairpinning_issue": "Схоже, що у вашій локальній мережі не включена проброска.", - "diagnosis_ports_forwarding_tip": "Щоб вирішити цю проблему, вам, швидше за все, потрібно налаштувати кидок портів на вашому інтернет-маршрутизатор, як описано в https://yunohost.org/isp_box_config.", - "diagnosis_ports_needed_by": "Відкриття цього порту необхідно для функцій {category} (служба {service}).", + "diagnosis_http_could_not_diagnose": "Не вдалося діагностувати досяжність доменів ззовні в IPv{ipversion}.", + "diagnosis_http_hairpinning_issue_details": "Можливо, це пов'язано з коробкою/маршрутизатором вашого інтернет-провайдера. В результаті, люди ззовні вашої локальної мережі зможуть отримати доступ до вашого сервера, як і очікувалося, але не люди зсередини локальної мережі (як ви, ймовірно?) При використанні доменного імені або глобального IP. Можливо, ви зможете поліпшити ситуацію, глянувши https://yunohost.org/dns_local_network ", + "diagnosis_http_hairpinning_issue": "Схоже, що у вашій локальній мережі не увімкнено шпилькування (hairpinning).", + "diagnosis_ports_forwarding_tip": "Щоб вирішити цю проблему, вам, швидше за все, потрібно налаштувати пересилання портів на вашому інтернет-маршрутизаторі, як описано в https://yunohost.org/isp_box_config", + "diagnosis_ports_needed_by": "Відкриття цього порту необхідне для функцій {category} (служба {service})", "diagnosis_ports_ok": "Порт {port} доступний ззовні.", - "diagnosis_ports_partially_unreachable": "Порт {port} не доступний ззовні в IPv {failed}.", + "diagnosis_ports_partially_unreachable": "Порт {port} не доступний ззовні в IPv{failed}.", "diagnosis_ports_unreachable": "Порт {port} недоступний ззовні.", "diagnosis_ports_could_not_diagnose_details": "Помилка: {error}", - "diagnosis_ports_could_not_diagnose": "Не вдалося діагностувати досяжність портів ззовні в IPv {ipversion}.", + "diagnosis_ports_could_not_diagnose": "Не вдалося діагностувати досяжність портів ззовні в IPv{ipversion}.", "diagnosis_description_regenconf": "Конфігурації системи", - "diagnosis_description_mail": "Електронна пошта", - "diagnosis_description_ports": "виявлення портів", + "diagnosis_description_mail": "Е-пошта", + "diagnosis_description_ports": "Виявлення портів", "diagnosis_description_systemresources": "Системні ресурси", "diagnosis_description_services": "Перевірка стану служб", - "diagnosis_description_dnsrecords": "записи DNS", + "diagnosis_description_dnsrecords": "DNS-записи", "diagnosis_description_ip": "Інтернет-з'єднання", - "diagnosis_description_basesystem": "Базова система", - "diagnosis_security_vulnerable_to_meltdown_details": "Щоб виправити це, вам слід оновити систему і перезавантажитися, щоб завантажити нове ядро linux (або звернутися до вашого серверного провайдеру, якщо це не спрацює). Додаткову інформацію див. На сайті https://meltdownattack.com/.", - "diagnosis_security_vulnerable_to_meltdown": "Схоже, що ви уразливі до критичної уразливості безпеки Meltdown.", - "diagnosis_rootfstotalspace_critical": "Коренева файлова система має тільки {space}, що вельми тривожно! Швидше за все, дисковий простір закінчиться дуже швидко! Рекомендується мати не менше 16 ГБ для кореневої файлової системи.", - "diagnosis_rootfstotalspace_warning": "Коренева файлова система має тільки {space}. Це може бути нормально, але будьте обережні, тому що в кінцевому підсумку дисковий простір може швидко закінчитися... Рекомендується мати не менше 16 ГБ для кореневої файлової системи.", - "diagnosis_regenconf_manually_modified_details": "Це можливо нормально, якщо ви знаєте, що робите! YunoHost перестане оновлювати цей файл автоматично... Але врахуйте, що поновлення YunoHost можуть містити важливі рекомендовані зміни. Якщо ви хочете, ви можете перевірити відмінності за допомогою команди yunohost tools regen-conf {category} --dry-run --with-diff і примусово повернути рекомендовану конфігурацію за допомогою yunohost tools regen- conf {category} --force .", - "diagnosis_regenconf_manually_modified": "Конфігураційний файл {file} , схоже, був змінений вручну.", - "diagnosis_regenconf_allgood": "Всі конфігураційні файли відповідають рекомендованої конфігурації!", - "diagnosis_mail_queue_too_big": "Занадто багато відкладених листів в поштовій черзі ({nb_pending} emails)", + "diagnosis_description_basesystem": "Основна система", + "diagnosis_security_vulnerable_to_meltdown_details": "Щоб виправити це, вам слід оновити систему і перезавантажитися, щоб завантажити нове ядро Linux (або звернутися до вашого серверного провайдера, якщо це не спрацює). Докладніше див. на сайті https://meltdownattack.com/.", + "diagnosis_security_vulnerable_to_meltdown": "Схоже, що ви вразливі до критичної вразливості безпеки Meltdown", + "diagnosis_rootfstotalspace_critical": "Коренева файлова система має тільки {space}, що дуже тривожно! Скоріше за все, дисковий простір закінчиться дуже скоро! Рекомендовано мати не менше 16 ГБ для кореневої файлової системи.", + "diagnosis_rootfstotalspace_warning": "Коренева файлова система має тільки {space}. Можливо це нормально, але будьте обережні, тому що в кінцевому підсумку дисковий простір може швидко закінчитися... Рекомендовано мати не менше 16 ГБ для кореневої файлової системи.", + "diagnosis_regenconf_manually_modified_details": "Можливо це нормально, якщо ви знаєте, що робите! YunoHost перестане оновлювати цей файл автоматично. Але врахуйте, що оновлення YunoHost можуть містити важливі рекомендовані зміни. Якщо хочете, ви можете перевірити відмінності за допомогою команди yunohost tools regen-conf {category} --dry-run --with-diff і примусово повернути рекомендовану конфігурацію за допомогою команди yunohost tools regen-conf {category} --force", + "diagnosis_regenconf_manually_modified": "Конфігураційний файл {file}, схоже, було змінено вручну.", + "diagnosis_regenconf_allgood": "Усі конфігураційні файли відповідають рекомендованій конфігурації!", + "diagnosis_mail_queue_too_big": "Занадто багато відкладених листів у поштовій черзі (листів: {nb_pending})", "diagnosis_mail_queue_unavailable_details": "Помилка: {error}", - "diagnosis_mail_queue_unavailable": "Неможливо дізнатися кількість очікують листів в черзі", - "diagnosis_mail_queue_ok": "{nb_pending} відкладені листи в поштових чергах", - "diagnosis_mail_blacklist_website": "Після визначення причини, по якій ви потрапили в чорний список, і її усунення, ви можете попросити видалити ваш IP або домен на {blacklist_website}.", + "diagnosis_mail_queue_unavailable": "Неможливо дізнатися кількість очікувальних листів у черзі", + "diagnosis_mail_queue_ok": "Відкладених електронних листів у поштових чергах: {nb_pending}", + "diagnosis_mail_blacklist_website": "Після визначення причини, з якої ви потрапили в чорний список, і її усунення, ви можете попросити видалити ваш IP або домен на {blacklist_website}", "diagnosis_mail_blacklist_reason": "Причина внесення в чорний список: {reason}", - "diagnosis_mail_blacklist_listed_by": "Ваш IP або домен {item} знаходиться в чорному списку {blacklist_name}.", + "diagnosis_mail_blacklist_listed_by": "Ваш IP або домен {item} знаходиться в чорному списку {blacklist_name}", "diagnosis_mail_blacklist_ok": "IP-адреси і домени, які використовуються цим сервером, не внесені в чорний список", - "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Поточний зворотний DNS: {rdns_domain}
Очікуване значення: {ehlo_domain} .", - "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Зворотний DNS неправильно налаштований в IPv {ipversion}. Деякі електронні листи можуть бути не доставлені або можуть бути відзначені як спам.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Деякі провайдери не дозволяють вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ваш зворотний DNS правильно налаштований для IPv4, ви можете спробувати відключити використання IPv6 при відправці листів, виконавши команду yunohost settings set smtp.allow_ipv6 -v off . Примітка: останнє рішення означає, що ви не зможете відправляти або отримувати електронну пошту з нечисленних серверів, що використовують тільки IPv6.", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ви відчуваєте проблеми з-за цього, розгляньте наступні рішення:
- Деякі провайдери надають альтернативу використання ретранслятора поштового сервера , хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN * з виділеним публічним IP * для обходу подібних обмежень. Дивіться https://yunohost.org/#/vpn_advantage
- Або можна переключитися на іншого провайдера .", - "diagnosis_mail_fcrdns_nok_details": "Спочатку спробуйте налаштувати зворотний DNS з {ehlo_domain} в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм тікет підтримки для цього).", - "diagnosis_mail_fcrdns_dns_missing": "У IPv {ipversion} не визначений зворотний DNS. Деякі листи можуть не доставлятися або позначатися як спам.", - "diagnosis_mail_fcrdns_ok": "Ваш зворотний DNS налаштований правильно!", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Поточний зворотний DNS:{rdns_domain}
Очікуване значення: {ehlo_domain}", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Зворотний DNS неправильно налаштований в IPv{ipversion}. Деякі електронні листи можуть бути не доставлені або можуть бути відзначені як спам.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ваш зворотний DNS правильно налаштований для IPv4, ви можете спробувати вимкнути використання IPv6 при надсиланні листів, виконавши команду yunohost settings set smtp.allow_ipv6 -v off. Примітка: останнє рішення означає, що ви не зможете надсилати або отримувати електронні листи з нечисленних серверів, що використовують тільки IPv6.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ви відчуваєте проблеми через це, розгляньте наступні рішення:
- Деякі провайдери надають альтернативу використання ретранслятора поштового сервера, хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN *з виділеним загальнодоступним IP* для обходу подібних обмежень. Дивіться https://yunohost.org/#/vpn_advantage
- Або можна переключитися на іншого провайдера", + "diagnosis_mail_fcrdns_nok_details": "Спочатку спробуйте налаштувати зворотний DNS з {ehlo_domain} в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм запит у підтримку для цього).", + "diagnosis_mail_fcrdns_dns_missing": "У IPv{ipversion} не визначений зворотний DNS. Деякі листи можуть не доставлятися або позначатися як спам.", + "diagnosis_mail_fcrdns_ok": "Ваш зворотний DNS налаштовано правильно!", "diagnosis_mail_ehlo_could_not_diagnose_details": "Помилка: {error}", - "diagnosis_mail_ehlo_could_not_diagnose": "Не вдалося діагностувати наявність певної поштовий сервер postfix ззовні в IPv {ipversion}.", - "diagnosis_mail_ehlo_wrong_details": "EHLO, отриманий віддаленим діагностичним центром в IPv {ipversion}, відрізняється від домену вашого сервера.
Отриманий EHLO: {wrong_ehlo}
Очікуваний: {right_ehlo} < br> найпоширенішою причиною цієї проблеми є те, що порт 25 неправильно перенаправлений на ваш сервер . Крім того, переконайтеся, що в роботу сервера не втручається брандмауер або зворотний проксі-сервер.", - "diagnosis_mail_ehlo_wrong": "Інший поштовий SMTP-сервер відповідає на IPv {ipversion}. Ваш сервер, ймовірно, не зможе отримувати електронну пошту.", + "diagnosis_mail_ehlo_could_not_diagnose": "Не вдалося діагностувати, чи доступний поштовий сервер postfix ззовні в IPv{ipversion}.", + "diagnosis_mail_ehlo_wrong_details": "EHLO, отриманий віддаленим діагностичним центром в IPv{ipversion}, відрізняється від домену вашого сервера.
Отриманий EHLO: {wrong_ehlo}
Очікуваний: {right_ehlo}< br>Найпоширенішою причиною цієї проблеми є те, що порт 25 неправильно перенаправлений на ваш сервер. Крім того, переконайтеся, що в роботу сервера не втручається фаєрвол або зворотний проксі-сервер.", + "diagnosis_mail_ehlo_wrong": "Інший поштовий SMTP-сервер відповідає на IPv{ipversion}. Ваш сервер, ймовірно, не зможе отримувати електронні листи.", "diagnosis_mail_ehlo_bad_answer_details": "Це може бути викликано тим, що замість вашого сервера відповідає інша машина.", - "diagnosis_mail_ehlo_bad_answer": "Ні-SMTP служба відповідає на порту 25 на IPv {ipversion}.", - "diagnosis_mail_ehlo_unreachable_details": "Не вдалося відкрити з'єднання по порту 25 з вашим сервером на IPv {ipversion}. Він здається недоступним.
1. Найбільш поширеною причиною цієї проблеми є те, що порт 25 неправильно перенаправлений на ваш сервер .
2. Ви також повинні переконатися, що служба postfix запущена.
3. На більш складних установках: переконайтеся, що немає брандмауера або зворотного проксі.", - "diagnosis_mail_ehlo_unreachable": "Поштовий сервер SMTP недоступний ззовні по IPv {ipversion}. Він не зможе отримувати повідомлення електронної пошти.", - "diagnosis_mail_ehlo_ok": "Поштовий сервер SMTP доступний ззовні і тому може отримувати електронну пошту!", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Деякі провайдери не дозволять вам розблокувати вихідний порт 25, тому що вони не піклуються про Net Neutrality.
- Деякі з них пропонують альтернативу використання ретранслятора поштового сервера , хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN * з виділеним публічним IP * для обходу такого роду обмежень. Дивіться https://yunohost.org/#/vpn_advantage
- Ви також можете розглянути можливість переходу на более дружнього до мережевого нейтралітету провайдера .", + "diagnosis_mail_ehlo_bad_answer": "Не-SMTP служба відповіла на порту 25 на IPv{ipversion}", + "diagnosis_mail_ehlo_unreachable_details": "Не вдалося відкрити з'єднання за портом 25 з вашим сервером на IPv{ipversion}. Він здається недоступним.
1. Найбільш поширеною причиною цієї проблеми є те, що порт 25 неправильно перенаправлений на ваш сервер.
2. Ви також повинні переконатися, що служба postfix запущена.
3. На більш складних установках: переконайтеся, що немає фаєрвола або зворотного проксі.", + "diagnosis_mail_ehlo_unreachable": "Поштовий сервер SMTP недоступний ззовні по IPv{ipversion}. Він не зможе отримувати листи електронної пошти.", + "diagnosis_mail_ehlo_ok": "Поштовий сервер SMTP доступний ззовні і тому може отримувати електронні листи!", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Деякі провайдери не дозволять вам розблокувати вихідний порт 25, тому що вони не піклуються про мережевий нейтралітет (Net Neutrality).
- Деякі з них пропонують альтернативу використання ретранслятора поштового сервера, хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN *з виділеним загальнодоступним IP* для обходу такого роду обмежень. Дивіться https://yunohost.org/#/vpn_advantage
- Ви також можете розглянути можливість переходу на більш дружнього до мережевого нейтралітету провайдера", "diagnosis_mail_outgoing_port_25_blocked_details": "Спочатку спробуйте розблокувати вихідний порт 25 в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм заявку в службу підтримки).", - "diagnosis_mail_outgoing_port_25_blocked": "Поштовий сервер SMTP не може відправляти електронні листи на інші сервери, оскільки вихідний порт 25 заблокований в IPv {ipversion}.", + "diagnosis_mail_outgoing_port_25_blocked": "Поштовий сервер SMTP не може відправляти електронні листи на інші сервери, оскільки вихідний порт 25 заблоковано в IPv{ipversion}.", "app_manifest_install_ask_path": "Оберіть шлях URL (після домену), за яким має бути встановлено цей застосунок", - "yunohost_postinstall_end_tip": "Постінсталляція завершена! Щоб завершити установку, будь ласка, розгляньте наступні варіанти: - додавання першого користувача через розділ 'Користувачі' веб-адміністратора (або 'yunohost user create ' в командному рядку); - діагностику можливих проблем через розділ 'Діагностика' веб-адміністратора (або 'yunohost diagnosis run' в командному рядку); - читання розділів 'Завершення установки' і 'Знайомство з YunoHost' в документації адміністратора: https://yunohost.org/admindoc.", - "yunohost_not_installed": "YunoHost встановлений неправильно. Будь ласка, запустіть 'yunohost tools postinstall'.", + "yunohost_postinstall_end_tip": "Післявстановлення завершено! Щоб завершити доналаштування, будь ласка, розгляньте наступні варіанти:\n - додавання першого користувача через розділ 'Користувачі' вебадміністрації (або 'yunohost user create ' в командному рядку);\n - діагностика можливих проблем через розділ 'Діагностика' вебадміністрації (або 'yunohost diagnosis run' в командному рядку);\n - прочитання розділів 'Завершення встановлення' і 'Знайомство з YunoHost' у документації адміністратора: https://yunohost.org/admindoc.", + "yunohost_not_installed": "YunoHost установлений неправильно. Будь ласка, запустіть 'yunohost tools postinstall'", "yunohost_installing": "Установлення YunoHost...", - "yunohost_configured": "YunoHost вже налаштований", + "yunohost_configured": "YunoHost вже налаштовано", "yunohost_already_installed": "YunoHost вже встановлено", - "user_updated": "Інформація про користувача змінена", + "user_updated": "Відомості про користувача змінено", "user_update_failed": "Не вдалося оновити користувача {user}: {error}", "user_unknown": "Невідомий користувач: {user}", - "user_home_creation_failed": "Не вдалося створити домашню папку для користувача", + "user_home_creation_failed": "Не вдалося створити каталог домівки для користувача", "user_deletion_failed": "Не вдалося видалити користувача {user}: {error}", - "user_deleted": "користувача видалено", + "user_deleted": "Користувача видалено", "user_creation_failed": "Не вдалося створити користувача {user}: {error}", - "user_created": "Аккаунт було створено", + "user_created": "Користувача створено", "user_already_exists": "Користувач '{user}' вже існує", "upnp_port_open_failed": "Не вдалося відкрити порт через UPnP", - "upnp_enabled": "UPnP включено", + "upnp_enabled": "UPnP увімкнено", "upnp_disabled": "UPnP вимкнено", - "upnp_dev_not_found": "UPnP-пристрій, не знайдено", + "upnp_dev_not_found": "UPnP-пристрій не знайдено", "upgrading_packages": "Оновлення пакетів...", - "upgrade_complete": "оновлення завершено", - "updating_apt_cache": "Вибірка доступних оновлень для системних пакетів...", - "update_apt_cache_warning": "Щось пішло не так при оновленні кеша APT (менеджера пакунків Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки: {sourceslist}", - "update_apt_cache_failed": "Неможливо оновити кеш APT (менеджер пакетів Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки: {sourceslist}", - "unrestore_app": "{app} не буде поновлено", - "unlimit": "немає квоти", + "upgrade_complete": "Оновлення завершено", + "updating_apt_cache": "Завантаження доступних оновлень для системних пакетів...", + "update_apt_cache_warning": "Щось пішло не так при оновленні кеша APT (менеджера пакунків Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки:\n{sourceslist}", + "update_apt_cache_failed": "Неможливо оновити кеш APT (менеджер пакетів Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки:\n{sourceslist}", + "unrestore_app": "{app} не буде оновлено", + "unlimit": "Квоти немає", "unknown_main_domain_path": "Невідомий домен або шлях для '{app}'. Вам необхідно вказати домен і шлях, щоб мати можливість вказати URL для дозволу.", "unexpected_error": "Щось пішло не так: {error}", - "unbackup_app": "{app} НЕ буде збережений", - "tools_upgrade_special_packages_completed": "Оновлення пакета YunoHost завершено. Натисніть [Enter] для повернення командного рядка", - "tools_upgrade_special_packages_explanation": "Спеціальне оновлення триватиме у фоновому режимі. Будь ласка, не запускайте ніяких інших дій на вашому сервері протягом наступних ~ 10 хвилин (в залежності від швидкості обладнання). Після цього вам, можливо, доведеться заново увійти в веб-адмін. Журнал поновлення буде доступний в Інструменти → Журнал (в веб-адміном) або за допомогою 'yunohost log list' (з командного рядка).", - "tools_upgrade_special_packages": "Тепер оновлюємо \"спеціальні\" (пов'язані з yunohost) пакети…", - "tools_upgrade_regular_packages_failed": "Не вдалося оновити пакети: {packages_list}", - "tools_upgrade_regular_packages": "Тепер оновлюємо \"звичайні\" (не пов'язані з yunohost) пакети…", - "tools_upgrade_cant_unhold_critical_packages": "Не вдалося утримати критичні пакети…", - "tools_upgrade_cant_hold_critical_packages": "Не вдалося утримати критичні пакети…", - "tools_upgrade_cant_both": "Неможливо оновити систему і програми одночасно", - "tools_upgrade_at_least_one": "Будь ласка, вкажіть 'apps', або 'system'.", - "this_action_broke_dpkg": "Ця дія порушило dpkg/APT (системні менеджери пакетів)... Ви можете спробувати вирішити цю проблему, підключившись по SSH і запустивши `sudo apt install --fix-broken` і/або` sudo dpkg --configure -a`.", + "unbackup_app": "{app} НЕ буде збережено", + "this_action_broke_dpkg": "Ця дія порушила dpkg/APT (системні менеджери пакетів)... Ви можете спробувати вирішити цю проблему, під'єднавшись по SSH і запустивши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`.", "system_username_exists": "Ім'я користувача вже існує в списку користувачів системи", - "system_upgraded": "система оновлена", - "ssowat_conf_updated": "Конфігурація SSOwat оновлена", - "ssowat_conf_generated": "Регенерувати конфігурація SSOwat", - "show_tile_cant_be_enabled_for_regex": "Ви не можете включити 'show_tile' прямо зараз, тому що URL для дозволу '{permission}' являє собою регекс", - "show_tile_cant_be_enabled_for_url_not_defined": "Ви не можете включити 'show_tile' прямо зараз, тому що спочатку ви повинні визначити URL для дозволу '{permission}'", + "system_upgraded": "Систему оновлено", + "ssowat_conf_generated": "Конфігурацію SSOwat перестворено", + "show_tile_cant_be_enabled_for_regex": "Ви не можете увімкнути 'show_tile' прямо зараз, тому що URL для дозволу '{permission}' являє собою регулярний вираз", + "show_tile_cant_be_enabled_for_url_not_defined": "Ви не можете увімкнути 'show_tile' прямо зараз, тому що спочатку ви повинні визначити URL для дозволу '{permission}'", "service_unknown": "Невідома служба '{service}'", - "service_stopped": "Служба '{service}' зупинена", - "service_stop_failed": "Неможливо зупинити службу '{service}' Недавні журнали служб: {logs}", - "service_started": "Служба '{service}' запущена", - "service_start_failed": "Не вдалося запустити службу '{service}' Recent service logs: {logs}", - "diagnosis_mail_outgoing_port_25_ok": "Поштовий сервер SMTP може відправляти електронні листи (вихідний порт 25 не заблокований).", - "diagnosis_swap_tip": "Будь ласка, будьте обережні і знайте, що якщо сервер розміщує своп на SD-карті або SSD-накопичувачі, це може різко скоротити термін служби устройства`.", - "diagnosis_swap_ok": "Система має {total} свопу!", - "diagnosis_swap_notsomuch": "Система має тільки {total} свопу. Щоб уникнути ситуацій, коли в системі закінчується пам'ять, слід передбачити наявність не менше {recommended} обсягу підкачки.", - "diagnosis_swap_none": "В системі повністю відсутній своп. Ви повинні розглянути можливість додавання принаймні {recommended} обсягу підкачки, щоб уникнути ситуацій, коли системі не вистачає пам'яті.", + "service_stopped": "Службу '{service}' зупинено", + "service_stop_failed": "Неможливо зупинити службу '{service}' \n\nНедавні журнали служби: {logs}", + "service_started": "Службу '{service}' запущено", + "service_start_failed": "Не вдалося запустити службу '{service}' \n\nНедавні журнали служби: {logs}", + "diagnosis_mail_outgoing_port_25_ok": "Поштовий сервер SMTP може відправляти електронні листи (вихідний порт 25 не заблоковано).", + "diagnosis_swap_tip": "Будь ласка, будьте обережні і знайте, що якщо сервер розміщує обсяг підкачки на SD-карті або SSD-накопичувачі, це може різко скоротити строк служби пристрою`.", + "diagnosis_swap_ok": "Система має {total} обсягу підкачки!", + "diagnosis_swap_notsomuch": "Система має тільки {total} обсягу підкачки. Щоб уникнути станоаищ, коли в системі закінчується пам'ять, слід передбачити наявність не менше {recommended} обсягу підкачки.", + "diagnosis_swap_none": "В системі повністю відсутня підкачка. Ви повинні розглянути можливість додавання принаймні {recommended} обсягу підкачки, щоб уникнути ситуацій, коли системі не вистачає пам'яті.", "diagnosis_ram_ok": "Система все ще має {available} ({available_percent}%) оперативної пам'яті з {total}.", - "diagnosis_ram_low": "У системі є {available} ({available_percent}%) оперативної пам'яті (з {total}). Будьте уважні.", - "diagnosis_ram_verylow": "Система має тільки {available} ({available_percent}%) оперативної пам'яті! (З {total})", - "diagnosis_diskusage_ok": "У сховищі {mountpoint} (на пристрої {device} ) залишилося {free} ({free_percent}%) вільного місця (з {total})!", - "diagnosis_diskusage_low": "Сховище {mountpoint} (на пристрої {device} ) має тільки {free} ({free_percent}%) вільного місця (з {total}). Будьте уважні.", - "diagnosis_diskusage_verylow": "Сховище {mountpoint} (на пристрої {device} ) має тільки {free} ({free_percent}%) вільного місця (з {total}). Вам дійсно варто подумати про очищення простору!", - "diagnosis_services_bad_status_tip": "Ви можете спробувати перезапустити службу , а якщо це не допоможе, подивіться журнали служби в webadmin (з командного рядка це можна зробити за допомогою yunohost service restart {service} yunohost service log {service} ).", - "diagnosis_services_bad_status": "Сервіс {service} знаходиться в {status} :(", - "diagnosis_services_conf_broken": "Конфігурація порушена для служби {service}!", - "diagnosis_services_running": "Служба {service} запущена!", - "diagnosis_domain_expires_in": "Термін дії {domain} закінчується через {days} днів.", - "diagnosis_domain_expiration_error": "Термін дії деяких доменів закінчується ДУЖЕ СКОРО!", - "diagnosis_domain_expiration_warning": "Термін дії деяких доменів закінчиться найближчим часом!", + "diagnosis_ram_low": "У системі наявно {available} ({available_percent}%) оперативної пам'яті (з {total}). Будьте уважні.", + "diagnosis_ram_verylow": "Система має тільки {available} ({available_percent}%) оперативної пам'яті! (з {total})", + "diagnosis_diskusage_ok": "У сховищі {mountpoint} (на пристрої {device}) залишилося {free} ({free_percent}%) вільного місця (з {total})!", + "diagnosis_diskusage_low": "Сховище {mountpoint} (на пристрої {device}) має тільки {free} ({free_percent}%) вільного місця (з {total}). Будьте уважні.", + "diagnosis_diskusage_verylow": "Сховище {mountpoint} (на пристрої {device}) має тільки {free} ({free_percent}%) вільного місця (з {total}). Вам дійсно варто подумати про очищення простору!", + "diagnosis_services_bad_status_tip": "Ви можете спробувати перезапустити службу, а якщо це не допоможе, подивіться журнали служби в вебадміністрації (з командного рядка це можна зробити за допомогою yunohost service restart {service} і yunohost service log {service}).", + "diagnosis_services_bad_status": "Служба {service} у стані {status} :(", + "diagnosis_services_conf_broken": "Для служби {service} порушена конфігурація!", + "diagnosis_services_running": "Службу {service} запущено!", + "diagnosis_domain_expires_in": "Строк дії {domain} спливе через {days} днів.", + "diagnosis_domain_expiration_error": "Строк дії деяких доменів НЕЗАБАРОМ спливе!", + "diagnosis_domain_expiration_warning": "Строк дії деяких доменів спливе найближчим часом!", "diagnosis_domain_expiration_success": "Ваші домени зареєстровані і не збираються спливати найближчим часом.", - "diagnosis_domain_expiration_not_found_details": "Інформація WHOIS для домену {domain} не містить інформації про термін дії?", + "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}", - "diagnosis_dns_missing_record": "Згідно рекомендованої конфігурації DNS, ви повинні додати запис DNS з наступними відомостями.\n
Тип: {type}\n
Назва: {name}\n
Значення: {value}", + "diagnosis_dns_missing_record": "Згідно рекомендованої конфігурації DNS, ви повинні додати запис DNS з наступними відомостями.
Тип: {type}
Назва: {name}
Значення: {value}", "diagnosis_dns_bad_conf": "Деякі DNS-записи відсутні або неправильні для домену {domain} (категорія {category})", "diagnosis_dns_good_conf": "DNS-записи правильно налаштовані для домену {domain} (категорія {category})", "diagnosis_ip_weird_resolvconf_details": "Файл /etc/resolv.conf повинен бути символічним посиланням на /etc/resolvconf/run/resolv.conf, що вказує на 127.0.0.1(dnsmasq). Якщо ви хочете вручну налаштувати DNS вирішувачі (resolvers), відредагуйте /etc/resolv.dnsmasq.conf.", @@ -500,16 +448,16 @@ "diagnosis_ip_broken_dnsresolution": "Роздільність доменних імен, схоже, з якоїсь причини не працює... Фаєрвол блокує DNS-запити?", "diagnosis_ip_dnsresolution_working": "Роздільність доменних імен працює!", "diagnosis_ip_not_connected_at_all": "Здається, сервер взагалі не під'єднаний до Інтернету!?", - "diagnosis_ip_local": "Локальний IP: {local}.", + "diagnosis_ip_local": "Локальний IP: {local}", "diagnosis_ip_global": "Глобальний IP: {global}", - "diagnosis_ip_no_ipv6_tip": "Наявність робочого IPv6 не є обов'язковим для роботи вашого сервера, але це краще для здоров'я Інтернету в цілому. IPv6 зазвичай автоматично налаштовується системою або вашим провайдером, якщо він доступний. В іншому випадку вам, можливо, доведеться налаштувати деякі речі вручну, як пояснюється в документації тут: https://yunohost.org/#/ipv6. Якщо ви не можете увімкнути IPv6 або якщо це здається вам занадто технічним, ви також можете сміливо нехтувати цим попередженням.", + "diagnosis_ip_no_ipv6_tip": "Наявність робочого IPv6 не є обов'язковим для роботи вашого сервера, але це краще для здоров'я Інтернету в цілому. IPv6 зазвичай автоматично налаштовується системою або вашим провайдером, якщо він доступний. В іншому випадку вам, можливо, доведеться налаштувати деякі речі вручну, як пояснюється в документації тут: https://yunohost.org/#/ipv6. Якщо ви не можете увімкнути IPv6 або якщо це здається вам занадто технічним, ви також можете сміливо нехтувати цим попередженням.", "diagnosis_ip_no_ipv6": "Сервер не має робочого IPv6.", "diagnosis_ip_connected_ipv6": "Сервер під'єднаний до Інтернету через IPv6!", "diagnosis_ip_no_ipv4": "Сервер не має робочого IPv4.", "diagnosis_ip_connected_ipv4": "Сервер під'єднаний до Інтернету через IPv4!", - "diagnosis_no_cache": "Для категорії \"{category} 'ще немає кеша діагностики.", + "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}!", @@ -517,8 +465,8 @@ "diagnosis_cant_run_because_of_dep": "Неможливо запустити діагностику для {category}, поки є важливі проблеми, пов'язані з {dep}.", "diagnosis_cache_still_valid": "(Кеш все ще дійсний для діагностики {category}. Повторна діагностика поки не проводиться!)", "diagnosis_failed_for_category": "Не вдалося провести діагностику для категорії '{category}': {error}", - "diagnosis_display_tip": "Щоб побачити знайдені проблеми, ви можете перейти в розділ Діагностика в вебадміністраторі або виконати команду 'yunohost diagnosis show --issues --human-readable' з командного рядка.", - "diagnosis_package_installed_from_sury_details": "Деякі пакети були ненавмисно встановлені зі стороннього репозиторію під назвою Sury. Команда YunoHost поліпшила стратегію роботи з цими пакетами, але очікується, що в деяких системах, які встановили застосунки PHP7.3 ще на Stretch, залишаться деякі невідповідності. Щоб виправити це становище, спробуйте виконати наступну команду: {cmd_to_fix}.", + "diagnosis_display_tip": "Щоб побачити знайдені проблеми, ви можете перейти в розділ Діагностика в вебадміністрації або виконати команду 'yunohost diagnosis show --issues --human-readable' з командного рядка.", + "diagnosis_package_installed_from_sury_details": "Деякі пакети були ненавмисно встановлені зі стороннього репозиторію під назвою Sury. Команда YunoHost поліпшила стратегію роботи з цими пакетами, але очікується, що в деяких системах, які встановили застосунки PHP7.3 ще на Stretch, залишаться деякі невідповідності. Щоб виправити це становище, спробуйте виконати наступну команду: {cmd_to_fix}", "diagnosis_package_installed_from_sury": "Деякі системні пакети мають бути зістарені у версії", "diagnosis_backports_in_sources_list": "Схоже, що apt (менеджер пакетів) налаштований на використання репозиторія backports. Якщо ви не знаєте, що робите, ми наполегливо не радимо встановлювати пакети з backports, тому що це може привести до нестабільності або конфліктів у вашій системі.", "diagnosis_basesystem_ynh_inconsistent_versions": "Ви використовуєте несумісні версії пакетів YunoHost... швидше за все, через невдале або часткове оновлення.", @@ -528,14 +476,14 @@ "diagnosis_basesystem_host": "Сервер працює під управлінням Debian {debian_version}", "diagnosis_basesystem_hardware_model": "Модель сервера - {model}", "diagnosis_basesystem_hardware": "Архітектура апаратного забезпечення сервера - {virt} {arch}", - "custom_app_url_required": "Ви повинні надати URL-адресу для оновлення вашого користувацького застосунку {app}.", - "confirm_app_install_thirdparty": "НЕБЕЗПЕЧНО! Цей застосунок не входить в каталог застосунків YunoHost. Установлення сторонніх застосунків може порушити цілісність і безпеку вашої системи. Вам не слід встановлювати його, якщо ви не знаєте, що робите. НІЯКОЇ ПІДТРИМКИ НЕ БУДЕ, якщо цей застосунок не буде працювати або зламає вашу систему... Якщо ви все одно готові піти на такий ризик, введіть '{answers}'.", - "confirm_app_install_danger": "НЕБЕЗПЕЧНО! Відомо, що цей застосунок все ще експериментальний (якщо не сказати, що він явно не працює)! Вам не слід встановлювати його, якщо ви не знаєте, що робите. Ніякої підтримки не буде надано, якщо цей застосунок не буде працювати або зламає вашу систему... Якщо ви все одно готові ризикнути, введіть '{answers}'.", + "custom_app_url_required": "Ви повинні надати URL-адресу для оновлення вашого користувацького застосунку {app}", + "confirm_app_install_thirdparty": "НЕБЕЗПЕЧНО! Цей застосунок не входить у каталог застосунків YunoHost. Установлення сторонніх застосунків може порушити цілісність і безпеку вашої системи. Вам не слід встановлювати його, якщо ви не знаєте, що робите. НІЯКОЇ ПІДТРИМКИ НЕ БУДЕ, якщо цей застосунок не буде працювати або зламає вашу систему... Якщо ви все одно готові піти на такий ризик, введіть '{answers}'", + "confirm_app_install_danger": "НЕБЕЗПЕЧНО! Відомо, що цей застосунок все ще експериментальний (якщо не сказати, що він явно не працює)! Вам не слід встановлювати його, якщо ви не знаєте, що робите. Ніякої підтримки не буде надано, якщо цей застосунок не буде працювати або зламає вашу систему... Якщо ви все одно готові ризикнути, введіть '{answers}'", "confirm_app_install_warning": "Попередження: Цей застосунок може працювати, але він не дуже добре інтегрований в YunoHost. Деякі функції, такі як єдина реєстрація та резервне копіювання/відновлення, можуть бути недоступні. Все одно встановити? [{answers}]. ", "certmanager_unable_to_parse_self_CA_name": "Не вдалося розібрати назву самопідписного центру (файл: {file})", "certmanager_self_ca_conf_file_not_found": "Не вдалося знайти файл конфігурації для самопідписного центру (файл: {file})", "certmanager_no_cert_file": "Не вдалося розпізнати файл сертифіката для домену {domain} (файл: {file})", - "certmanager_hit_rate_limit": "Для цього набору доменів {domain} недавно було випущено дуже багато сертифікатів. Будь ласка, спробуйте ще раз пізніше. Див. https://letsencrypt.org/docs/rate-limits/ для отримання подробиць.", + "certmanager_hit_rate_limit": "Для цього набору доменів {domain} недавно було випущено дуже багато сертифікатів. Будь ласка, спробуйте ще раз пізніше. Див. https://letsencrypt.org/docs/rate-limits/ для отримання подробиць", "certmanager_warning_subdomain_dns_record": "Піддомен '{subdomain}' не дозволяється на тій же IP-адресі, що і '{domain}'. Деякі функції будуть недоступні, поки ви не виправите це і не перестворите сертифікат.", "certmanager_domain_http_not_working": "Домен {domain}, схоже, не доступний через HTTP. Будь ласка, перевірте категорію 'Мережа' в діагностиці для отримання додаткових даних. (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки).", "certmanager_domain_dns_ip_differs_from_public_ip": "DNS-записи для домену '{domain}' відрізняються від IP цього сервера. Будь ласка, перевірте категорію 'DNS-записи' (основні) в діагностиці для отримання додаткових даних. Якщо ви недавно змінили запис A, будь ласка, зачекайте, поки він пошириться (деякі програми перевірки поширення DNS доступні в Інтернеті). (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки).", @@ -554,13 +502,13 @@ "backup_with_no_restore_script_for_app": "{app} не має скрипта відновлення, ви не зможете автоматично відновити резервну копію цього застосунку.", "backup_with_no_backup_script_for_app": "Застосунок '{app}' не має скрипта резервного копіювання. Нехтую ним.", "backup_unable_to_organize_files": "Неможливо використовувати швидкий спосіб для організації файлів в архіві", - "backup_system_part_failed": "Не вдалося створити резервну копію системної частини '{part}'.", + "backup_system_part_failed": "Не вдалося створити резервну копію системної частини '{part}'", "backup_running_hooks": "Запуск гачків (hook) резервного копіювання...", "backup_permission": "Дозвіл на резервне копіювання для {app}", "backup_output_symlink_dir_broken": "Ваш архівний каталог '{path}' є неробочим символічним посиланням. Можливо, ви забули перемонтувати або підключити носій, на який вона вказує.", "backup_output_directory_required": "Ви повинні вказати вихідний каталог для резервного копіювання", "backup_output_directory_not_empty": "Ви повинні вибрати порожній вихідний каталог", - "backup_output_directory_forbidden": "Виберіть інший вихідний каталог. Резервні копії не можуть бути створені в підкаталогах /bin,/boot,/dev,/etc,/lib,/root,/run,/sbin,/sys,/usr,/var або /home/yunohost.backup/archives.", + "backup_output_directory_forbidden": "Виберіть інший вихідний каталог. Резервні копії не можуть бути створені в підкаталогах /bin,/boot,/dev,/etc,/lib,/root,/run,/sbin,/sys,/usr,/var або /home/yunohost.backup/archives", "backup_nothings_done": "Нема що зберігати", "backup_no_uncompress_archive_dir": "Немає такого каталогу нестислого архіву", "backup_mount_archive_for_restore": "Підготовлення архіву для відновлення...", @@ -582,7 +530,7 @@ "backup_cleaning_failed": "Не вдалося очистити тимчасовий каталог резервного копіювання", "backup_cant_mount_uncompress_archive": "Не вдалося змонтувати нестислий архів як захищений від запису", "backup_ask_for_copying_if_needed": "Ви бажаєте тимчасово виконати резервне копіювання з використанням {size} МБ? (Цей спосіб використовується, оскільки деякі файли не можуть бути підготовлені дієвіше).", - "backup_archive_writing_error": "Не вдалося додати файли '{source}' (названі в архіві '{dest}') для резервного копіювання в стислий архів '{archive}'.", + "backup_archive_writing_error": "Не вдалося додати файли '{source}' (названі в архіві '{dest}') для резервного копіювання в стислий архів '{archive}'", "backup_archive_system_part_not_available": "Системна частина '{part}' недоступна в цій резервній копії", "backup_archive_corrupted": "Схоже, що архів резервної копії '{archive}' пошкоджений: {error}", "backup_archive_cant_retrieve_info_json": "Не вдалося завантажити відомості для архіву '{archive}'... info.json не може бути отриманий(або не є правильним json).", @@ -600,7 +548,7 @@ "ask_password": "Пароль", "ask_new_path": "Новий шлях", "ask_new_domain": "Новий домен", - "ask_new_admin_password": "Новий пароль адміністратора", + "ask_new_admin_password": "Новий пароль адміністрації", "ask_main_domain": "Основний домен", "ask_lastname": "Прізвище", "ask_firstname": "Ім'я", @@ -618,7 +566,7 @@ "app_upgrade_failed": "Не вдалося оновити {app}: {error}", "app_upgrade_app_name": "Зараз оновлюємо {app}...", "app_upgrade_several_apps": "Наступні застосунки буде оновлено: {apps}", - "app_unsupported_remote_type": "Для застосунку використовується непідтримуваний віддалений тип.", + "app_unsupported_remote_type": "Для застосунку використовується непідтримуваний віддалений тип", "app_unknown": "Невідомий застосунок", "app_start_restore": "Відновлення {app}...", "app_start_backup": "Збирання файлів для резервного копіювання {app}...", @@ -628,7 +576,7 @@ "app_restore_script_failed": "Сталася помилка всередині скрипта відновлення застосунку", "app_restore_failed": "Не вдалося відновити {app}: {error}", "app_remove_after_failed_install": "Вилучення застосунку після збою встановлення...", - "app_requirements_unmeet": "Вимоги не виконані для {app}, пакет {pkgname} ({version}) повинен бути {spec}.", + "app_requirements_unmeet": "Вимоги не виконані для {app}, пакет {pkgname} ({version}) повинен бути {spec}", "app_requirements_checking": "Перевіряння необхідних пакетів для {app}...", "app_removed": "{app} видалено", "app_not_properly_removed": "{app} не було видалено належним чином", @@ -637,5 +585,104 @@ "app_not_upgraded": "Застосунок '{failed_app}' не вдалося оновити, і, як наслідок, оновлення таких застосунків було скасовано: {apps}", "app_manifest_install_ask_is_public": "Чи має цей застосунок бути відкритим для анонімних відвідувачів?", "app_manifest_install_ask_admin": "Виберіть користувача-адміністратора для цього застосунку", - "app_manifest_install_ask_password": "Виберіть пароль адміністратора для цього застосунку" -} + "app_manifest_install_ask_password": "Виберіть пароль адміністрації для цього застосунку", + "diagnosis_description_apps": "Застосунки", + "user_import_success": "Користувачів успішно імпортовано", + "user_import_nothing_to_do": "Не потрібно імпортувати жодного користувача", + "user_import_failed": "Операція імпорту користувачів цілковито не вдалася", + "user_import_partial_failed": "Операція імпорту користувачів частково не вдалася", + "user_import_missing_columns": "Відсутні такі стовпці: {columns}", + "user_import_bad_file": "Ваш файл CSV неправильно відформатовано, він буде знехтуваний, щоб уникнути потенційної втрати даних", + "user_import_bad_line": "Неправильний рядок {line}: {details}", + "invalid_password": "Недійсний пароль", + "log_user_import": "Імпорт користувачів", + "ldap_server_is_down_restart_it": "Службу LDAP вимкнено, спробуйте перезапустити її...", + "ldap_server_down": "Не вдається під'єднатися до сервера LDAP", + "global_settings_setting_security_experimental_enabled": "Увімкнути експериментальні функції безпеки (не вмикайте це, якщо ви не знаєте, що робите!)", + "diagnosis_apps_deprecated_practices": "Установлена версія цього застосунку все ще використовує деякі надзастарілі практики упакування. Вам дійсно варто подумати про його оновлення.", + "diagnosis_apps_outdated_ynh_requirement": "Установлена версія цього застосунку вимагає лише Yunohost >= 2.x, що, як правило, вказує на те, що воно не відповідає сучасним рекомендаційним практикам упакування та порадникам. Вам дійсно варто подумати про його оновлення.", + "diagnosis_apps_bad_quality": "Цей застосунок наразі позначено як зламаний у каталозі застосунків YunoHost. Це може бути тимчасовою проблемою, поки організатори намагаються вирішити цю проблему. Тим часом оновлення цього застосунку вимкнено.", + "diagnosis_apps_broken": "Цей застосунок наразі позначено як зламаний у каталозі застосунків YunoHost. Це може бути тимчасовою проблемою, поки організатори намагаються вирішити цю проблему. Тим часом оновлення цього застосунку вимкнено.", + "diagnosis_apps_not_in_app_catalog": "Цей застосунок не міститься у каталозі застосунків YunoHost. Якщо він був у минулому і був видалений, вам слід подумати про видалення цього застосунку, оскільки він не отримає оновлення, і це може поставити під загрозу цілісність та безпеку вашої системи.", + "diagnosis_apps_issue": "Виявлено проблему із застосунком {app}", + "diagnosis_apps_allgood": "Усі встановлені застосунки дотримуються основних способів упакування", + "diagnosis_high_number_auth_failures": "Останнім часом сталася підозріло велика кількість помилок автентифікації. Ви можете переконатися, що fail2ban працює і правильно налаштований, або скористатися власним портом для SSH, як описано в https://yunohost.org/security.", + "global_settings_setting_security_nginx_redirect_to_https": "Типово переспрямовувати HTTP-запити до HTTP (НЕ ВИМИКАЙТЕ, якщо ви дійсно не знаєте, що робите!)", + "app_config_unable_to_apply": "Не вдалося застосувати значення панелі конфігурації.", + "app_config_unable_to_read": "Не вдалося розпізнати значення панелі конфігурації.", + "config_apply_failed": "Не вдалося застосувати нову конфігурацію: {error}", + "config_cant_set_value_on_section": "Ви не можете встановити одне значення на весь розділ конфігурації.", + "config_forbidden_keyword": "Ключове слово '{keyword}' зарезервовано, ви не можете створити або використовувати панель конфігурації з запитом із таким ID.", + "config_no_panel": "Панель конфігурації не знайдено.", + "config_unknown_filter_key": "Ключ фільтра '{filter_key}' недійсний.", + "config_validate_color": "Колір RGB має бути дійсним шістнадцятковим кольоровим кодом", + "config_validate_date": "Дата має бути дійсною, наприклад, у форматі РРРР-ММ-ДД", + "config_validate_email": "Е-пошта має бути дійсною", + "config_validate_time": "Час має бути дійсним, наприклад ГГ:ХХ", + "config_validate_url": "Вебадреса має бути дійсною", + "config_version_not_supported": "Версії конфігураційної панелі '{version}' не підтримуються.", + "danger": "Небезпека:", + "invalid_number_min": "Має бути більшим за {min}", + "invalid_number_max": "Має бути меншим за {max}", + "log_app_config_set": "Застосувати конфігурацію до застосунку '{}'", + "service_not_reloading_because_conf_broken": "Неможливо перезавантажити/перезапустити службу '{name}', тому що її конфігурацію порушено: {errors}", + "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.", + "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": "Записи вже оновлені, нічого не потрібно робити.", + "domain_unknown": "Домен '{domain}' є невідомим", + "migration_0021_start": "Початок міграції на Bullseye", + "migration_0021_patching_sources_list": "Виправлення sources.lists...", + "migration_0021_main_upgrade": "Початок основного оновлення...", + "migration_0021_yunohost_upgrade": "Початок оновлення ядра YunoHost...", + "migration_0021_not_buster": "Поточний дистрибутив Debian не є Buster!", + "migration_0021_problematic_apps_warning": "Зверніть увагу, що були виявлені наступні, ймовірно проблемні встановлені застосунки. Схоже, що вони не були встановлені з каталогу застосунків YunoHost або не зазначені як «робочі». Отже, не можна гарантувати, що вони будуть працювати після оновлення: {problematic_apps}", + "migration_0021_modified_files": "Зверніть увагу, що такі файли були змінені вручну і можуть бути перезаписані після оновлення: {manually_modified_files}", + "migration_0021_cleaning_up": "Очищення кеш-пам'яті і пакетів, які більше не потрібні...", + "migration_0021_patch_yunohost_conflicts": "Застосування виправлення для вирішення проблеми конфлікту...", + "migration_0021_still_on_buster_after_main_upgrade": "Щось пішло не так під час основного оновлення, здається, що система все ще працює на Debian Buster", + "migration_0021_not_enough_free_space": "Вільного місця в /var/ досить мало! У вас повинно бути не менше 1 ГБ вільного місця, щоб запустити цю міграцію.", + "migration_0021_system_not_fully_up_to_date": "Ваша система не повністю оновлена. Будь ласка, виконайте регулярне оновлення перед запуском міграції на Bullseye.", + "migration_0021_general_warning": "Будь ласка, зверніть увагу, що ця міграція є делікатною операцією. Команда YunoHost зробила все можливе, щоб перевірити і протестувати її, але міграція все ще може порушити частину системи або її застосунків.\n\nТому рекомендовано:\n - Виконати резервне копіювання всіх важливих даних або застосунків. Подробиці на сайті https://yunohost.org/backup; \n - Наберіться терпіння після запуску міграції: В залежності від вашого з'єднання з Інтернетом і апаратного забезпечення, оновлення може зайняти до декількох годин.", + "migration_description_0021_migrate_to_bullseye": "Оновлення системи до Debian Bullseye і YunoHost 11.x", + "global_settings_setting_security_ssh_password_authentication": "Дозволити автентифікацію паролем для SSH", + "service_description_postgresql": "Зберігає дані застосунків (база даних SQL)", + "domain_config_default_app": "Типовий застосунок", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 встановлено, але не PostgreSQL 13!? У вашій системі могло статися щось неприємне :(...", + "migration_description_0023_postgresql_11_to_13": "Перенесення баз даних з PostgreSQL 11 на 13", + "tools_upgrade": "Оновлення системних пакетів", + "tools_upgrade_failed": "Не вдалося оновити наступні пакети: {packages_list}", + "migration_0023_not_enough_space": "Звільніть достатньо місця в {path} для виконання міграції.", + "migration_0023_postgresql_11_not_installed": "PostgreSQL не було встановлено у вашій системі. Нічого робити.", + "migration_description_0022_php73_to_php74_pools": "Перенесення конфігураційних файлів php7.3-fpm 'pool' на php7.4" +} \ No newline at end of file diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index 560ee0db0..2daf45483 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -137,7 +137,6 @@ "additional_urls_already_removed": "权限'{permission}'的其他URL中已经删除了附加URL'{url}'", "app_manifest_install_ask_path": "选择安装此应用的路径", "app_manifest_install_ask_domain": "选择应安装此应用程序的域", - "app_manifest_invalid": "应用清单错误: {error}", "app_location_unavailable": "该URL不可用,或与已安装的应用冲突:\n{apps}", "app_label_deprecated": "不推荐使用此命令!请使用新命令 'yunohost user permission update'来管理应用标签。", "app_make_default_location_already_used": "无法将'{app}' 设置为域上的默认应用,'{other_app}'已在使用'{domain}'", @@ -150,7 +149,6 @@ "app_change_url_success": "{app} URL现在为 {domain}{path}", "app_change_url_no_script": "应用程序'{app_name}'尚不支持URL修改. 也许您应该升级它。", "app_change_url_identical_domains": "新旧domain / url_path是相同的('{domain}{path}'),无需执行任何操作。", - "app_change_url_failed_nginx_reload": "无法重新加载NGINX. 这是'nginx -t'的输出:\n{nginx_errors}", "app_argument_required": "参数'{name}'为必填项", "app_argument_password_no_default": "解析密码参数'{name}'时出错:出于安全原因,密码参数不能具有默认值", "app_argument_invalid": "为参数'{name}'选择一个有效值: {error}", @@ -219,7 +217,6 @@ "service_description_rspamd": "过滤垃圾邮件和其他与电子邮件相关的功能", "service_description_redis-server": "用于快速数据访问,任务队列和程序之间通信的专用数据库", "service_description_postfix": "用于发送和接收电子邮件", - "service_description_php7.3-fpm": "使用NGINX运行用PHP编写的应用程序", "service_description_nginx": "为你的服务器上托管的所有网站提供服务或访问", "service_description_mysql": "存储应用程序数据(SQL数据库)", "service_description_metronome": "管理XMPP即时消息传递帐户", @@ -236,20 +233,13 @@ "service_reload_failed": "无法重新加载服务'{service}'\n\n最近的服务日志:{logs}", "service_removed": "服务 '{service}' 已删除", "service_remove_failed": "无法删除服务'{service}'", - "service_regen_conf_is_deprecated": "不建议使用'yunohost service regen-conf' ! 请改用'yunohost tools regen-conf'。", "service_enabled": "现在,服务'{service}' 将在系统引导过程中自动启动。", "service_enable_failed": "无法使服务 '{service}'在启动时自动启动。\n\n最近的服务日志:{logs}", "service_disabled": "系统启动时,服务 '{service}' 将不再启动。", "service_disable_failed": "服务'{service}'在启动时无法启动。\n\n最近的服务日志:{logs}", - "tools_upgrade_regular_packages": "现在正在升级 'regular' (与yunohost无关)的软件包…", - "tools_upgrade_cant_unhold_critical_packages": "无法解压关键软件包…", - "tools_upgrade_cant_hold_critical_packages": "无法保存重要软件包…", - "tools_upgrade_cant_both": "无法同时升级系统和应用程序", - "tools_upgrade_at_least_one": "请指定'apps', 或 'system'", "this_action_broke_dpkg": "此操作破坏了dpkg / APT(系统软件包管理器)...您可以尝试通过SSH连接并运行`sudo apt install --fix-broken`和/或`sudo dpkg --configure -a`来解决此问题。", "system_username_exists": "用户名已存在于系统用户列表中", "system_upgraded": "系统升级", - "ssowat_conf_updated": "SSOwat配置已更新", "ssowat_conf_generated": "SSOwat配置已重新生成", "show_tile_cant_be_enabled_for_regex": "你不能启用'show_tile',因为权限'{permission}'的URL是一个重合词", "show_tile_cant_be_enabled_for_url_not_defined": "您现在无法启用 'show_tile' ,因为您必须先为权限'{permission}'定义一个URL", @@ -267,10 +257,6 @@ "unknown_main_domain_path": "'{app}'的域或路径未知。您需要指定一个域和一个路径,以便能够指定用于许可的URL。", "unexpected_error": "出乎意料的错误: {error}", "unbackup_app": "{app} 将不会保存", - "tools_upgrade_special_packages_completed": "YunoHost软件包升级完成。\n按[Enter]返回命令行", - "tools_upgrade_special_packages_explanation": "特殊升级将在后台继续。请在接下来的10分钟内(取决于硬件速度)在服务器上不要执行任何其他操作。此后,您可能必须重新登录Webadmin。升级日志将在“工具”→“日志”(在Webadmin中)或使用'yunohost log list'(从命令行)中可用。", - "tools_upgrade_special_packages": "现在正在升级'special'(与yunohost相关的)程序包…", - "tools_upgrade_regular_packages_failed": "无法升级软件包: {packages_list}", "yunohost_installing": "正在安装YunoHost ...", "yunohost_configured": "现在已配置YunoHost", "yunohost_already_installed": "YunoHost已经安装", @@ -354,14 +340,11 @@ "dyndns_ip_updated": "在DynDNS上更新了您的IP", "dyndns_ip_update_failed": "无法将IP地址更新到DynDNS", "dyndns_could_not_check_available": "无法检查{provider}上是否可用 {domain}。", - "dyndns_could_not_check_provide": "无法检查{provider}是否可以提供 {domain}.", "dpkg_lock_not_available": "该命令现在无法运行,因为另一个程序似乎正在使用dpkg锁(系统软件包管理器)", "dpkg_is_broken": "您现在不能执行此操作,因为dpkg / APT(系统软件包管理器)似乎处于损坏状态……您可以尝试通过SSH连接并运行sudo apt install --fix-broken和/或 sudo dpkg --configure-a 来解决此问题.", "downloading": "下载中…", "done": "完成", "domains_available": "可用域:", - "domain_unknown": "未知网域", - "domain_name_unknown": "域'{domain}'未知", "domain_uninstall_app_first": "这些应用程序仍安装在您的域中:\n{apps}\n\n请先使用 'yunohost app remove the_app_id' 将其卸载,或使用 'yunohost app change-url the_app_id'将其移至另一个域,然后再继续删除域", "domain_remove_confirm_apps_removal": "删除该域将删除这些应用程序:\n{apps}\n\n您确定要这样做吗? [{answers}]", "domain_hostname_failed": "无法设置新的主机名。稍后可能会引起问题(可能没问题)。", @@ -499,8 +482,6 @@ "diagnosis_dns_discrepancy": "以下DNS记录似乎未遵循建议的配置:
类型: {type}
名称: {name}
代码> 当前值: {current}期望值: {value}", "log_backup_create": "创建备份档案", "log_available_on_yunopaste": "现在可以通过{url}使用此日志", - "log_app_config_apply": "将配置应用于 '{}' 应用", - "log_app_config_show_panel": "显示 '{}' 应用的配置面板", "log_app_action_run": "运行 '{}' 应用的操作", "log_app_makedefault": "将 '{}' 设为默认应用", "log_app_upgrade": "升级 '{}' 应用", @@ -511,7 +492,7 @@ "log_does_exists": "没有名称为'{log}'的操作日志,请使用 'yunohost log list' 查看所有可用的操作日志", "log_help_to_get_failed_log": "操作'{desc}'无法完成。请使用命令'yunohost log share {name}' 共享此操作的完整日志以获取帮助", "log_link_to_failed_log": "无法完成操作 '{desc}'。请通过单击此处提供此操作的完整日志以获取帮助", - "log_help_to_get_log": "要查看操作'{desc}'的日志,请使用命令'yunohost log show {name}{name}'", + "log_help_to_get_log": "要查看操作'{desc}'的日志,请使用命令'yunohost log show {name}'", "log_link_to_log": "此操作的完整日志: '{desc}'", "log_corrupted_md_file": "与日志关联的YAML元数据文件已损坏: '{md_file}\n错误: {error}'", "iptables_unavailable": "你不能在这里使用iptables。你要么在一个容器中,要么你的内核不支持它", @@ -541,7 +522,6 @@ "permission_already_allowed": "群组 '{group}' 已启用权限'{permission}'", "pattern_password_app": "抱歉,密码不能包含以下字符: {forbidden_chars}", "pattern_username": "只能为小写字母数字和下划线字符", - "pattern_positive_number": "必须为正数", "pattern_port_or_range": "必须是有效的端口号(即0-65535)或端口范围(例如100:200)", "pattern_password": "必须至少3个字符长", "pattern_mailbox_quota": "必须为带b/k/M/G/T 后缀的大小或0,才能没有配额", @@ -555,7 +535,6 @@ "password_too_simple_3": "密码长度至少为8个字符,并且包含数字,大写,小写和特殊字符", "password_too_simple_2": "密码长度至少为8个字符,并且包含数字,大写和小写字符", "password_listed": "该密码是世界上最常用的密码之一。 请选择一些更独特的东西。", - "packages_upgrade_failed": "无法升级所有软件包", "invalid_number": "必须是数字", "not_enough_disk_space": "'{path}'上的可用空间不足", "migrations_to_be_ran_manually": "迁移{id}必须手动运行。请转到webadmin页面上的工具→迁移,或运行`yunohost tools migrations run`。", @@ -574,41 +553,11 @@ "migrations_exclusive_options": "'--auto', '--skip',和'--force-rerun'是互斥的选项。", "migrations_failed_to_load_migration": "无法加载迁移{id}: {error}", "migrations_dependencies_not_satisfied": "在迁移{id}之前运行以下迁移: '{dependencies_id}'。", - "migrations_cant_reach_migration_file": "无法访问路径'%s'处的迁移文件", "migrations_already_ran": "这些迁移已经完成: {ids}", - "migration_0019_slapd_config_will_be_overwritten": "好像您手动编辑了slapd配置。对于此关键迁移,YunoHost需要强制更新slapd配置。原始文件将备份在{conf_backup_folder}中。", - "migration_0019_add_new_attributes_in_ldap": "在LDAP数据库中添加权限的新属性", - "migration_0018_failed_to_reset_legacy_rules": "无法重置旧版iptables规则: {error}", - "migration_0018_failed_to_migrate_iptables_rules": "无法将旧的iptables规则迁移到nftables: {error}", - "migration_0017_not_enough_space": "在{path}中提供足够的空间来运行迁移。", - "migration_0017_postgresql_11_not_installed": "已安装PostgreSQL 9.6,但未安装PostgreSQL11?您的系统上可能发生了一些奇怪的事情:(...", - "migration_0017_postgresql_96_not_installed": "PostgreSQL未安装在您的系统上。无事可做。", - "migration_0015_weak_certs": "发现以下证书仍然使用弱签名算法,并且必须升级以与下一版本的nginx兼容: {certs}", - "migration_0015_cleaning_up": "清理不再有用的缓存和软件包...", - "migration_0015_specific_upgrade": "开始升级需要独立升级的系统软件包...", - "migration_0015_modified_files": "请注意,发现以下文件是手动修改的,并且在升级后可能会被覆盖: {manually_modified_files}", - "migration_0015_problematic_apps_warning": "请注意,已检测到以下可能有问题的已安装应用程序。看起来好像那些不是从YunoHost应用程序目录中安装的,或者没有标记为“正在运行”。因此,不能保证它们在升级后仍然可以使用: {problematic_apps}", - "migration_0015_general_warning": "请注意,此迁移是一项微妙的操作。YunoHost团队竭尽全力对其进行检查和测试,但迁移仍可能会破坏系统或其应用程序的某些部分。\n\n因此,建议:\n -对任何关键数据或应用程序执行备份。有关更多信息,请访问https://yunohost.org/backup;\n -启动迁移后要耐心:根据您的Internet连接和硬件,升级所有内容最多可能需要几个小时。", - "migration_0015_system_not_fully_up_to_date": "您的系统不是最新的。请先执行常规升级,然后再运行向Buster的迁移。", - "migration_0015_not_enough_free_space": "/var/中的可用空间非常低!您应该至少有1GB的可用空间来运行此迁移。", - "migration_0015_not_stretch": "当前的Debian发行版不是Stretch!", - "migration_0015_yunohost_upgrade": "正在启动YunoHost核心升级...", - "migration_0015_still_on_stretch_after_main_upgrade": "在主要升级期间出了点问题,系统似乎仍在Debian Stretch上", - "migration_0015_main_upgrade": "正在开始主要升级...", - "migration_0015_patching_sources_list": "修补sources.lists ...", - "migration_0015_start": "开始迁移至Buster", - "migration_update_LDAP_schema": "正在更新LDAP模式...", "migration_ldap_rollback_success": "系统回滚。", "migration_ldap_migration_failed_trying_to_rollback": "无法迁移...试图回滚系统。", "migration_ldap_can_not_backup_before_migration": "迁移失败之前,无法完成系统的备份。错误: {error}", "migration_ldap_backup_before_migration": "在实际迁移之前,请创建LDAP数据库和应用程序设置的备份。", - "migration_description_0020_ssh_sftp_permissions": "添加SSH和SFTP权限支持", - "migration_description_0019_extend_permissions_features": "扩展/修改应用程序的权限管理系统", - "migration_description_0018_xtable_to_nftable": "将旧的网络流量规则迁移到新的nftable系统", - "migration_description_0017_postgresql_9p6_to_11": "将数据库从PostgreSQL 9.6迁移到11", - "migration_description_0016_php70_to_php73_pools": "将php7.0-fpm'pool'conf文件迁移到php7.3", - "migration_description_0015_migrate_to_buster": "将系统升级到Debian Buster和YunoHost 4.x", - "migrating_legacy_permission_settings": "正在迁移旧版权限设置...", "main_domain_changed": "主域已更改", "main_domain_change_failed": "无法更改主域", "mail_unavailable": "该电子邮件地址是保留的,并且将自动分配给第一个用户", @@ -630,5 +579,30 @@ "log_user_group_delete": "删除组'{}'", "log_user_group_create": "创建组'{}'", "log_user_delete": "删除用户'{}'", - "log_user_create": "添加用户'{}'" + "log_user_create": "添加用户'{}'", + "domain_registrar_is_not_configured": "尚未为域 {domain} 配置注册商。", + "domain_dns_push_not_applicable": "的自动DNS配置的特征是不适用域{domain}。您应该按照 https://yunohost.org/dns_config 上的文档手动配置DNS 记录。", + "disk_space_not_sufficient_update": "没有足够的磁盘空间来更新此应用程序", + "diagnosis_high_number_auth_failures": "最近出现了大量可疑的失败身份验证。您的fail2ban正在运行且配置正确,或使用自定义端口的SSH作为https://yunohost.org/解释的安全性。", + "diagnosis_apps_not_in_app_catalog": "此应用程序不在 YunoHost 的应用程序目录中。如果它过去有被删除过,您应该考虑卸载此应用程,因为它不会更新,并且可能会损害您系统的完整和安全性。", + "app_config_unable_to_apply": "无法应用配置面板值。", + "app_config_unable_to_read": "无法读取配置面板值。", + "config_forbidden_keyword": "关键字“{keyword}”是保留的,您不能创建或使用带有此 ID 的问题的配置面板。", + "config_no_panel": "未找到配置面板。", + "config_unknown_filter_key": "该过滤器钥匙“{filter_key}”有误。", + "diagnosis_apps_outdated_ynh_requirement": "此应用程序的安装 版本只需要 yunohost >= 2.x,这往往表明它与推荐的打包实践和帮助程序不是最新的。你真的应该考虑更新它。", + "disk_space_not_sufficient_install": "没有足够的磁盘空间来安装此应用程序", + "config_apply_failed": "应用新配置 失败:{error}", + "config_cant_set_value_on_section": "无法在整个配置部分设置单个值 。", + "config_validate_color": "是有效的 RGB 十六进制颜色", + "config_validate_date": "有效日期格式为YYYY-MM-DD", + "config_validate_email": "是有效的电子邮件", + "config_validate_time": "应该是像 HH:MM 这样的有效时间", + "config_validate_url": "应该是有效的URL", + "config_version_not_supported": "不支持配置面板版本“{ version }”。", + "danger": "警告:", + "diagnosis_apps_allgood": "所有已安装的应用程序都遵守基本的打包原则", + "diagnosis_apps_deprecated_practices": "此应用程序的安装 版本仍然使用一些超旧的弃用打包原则。推荐您升级它。", + "diagnosis_apps_issue": "发现应用{ app } 存在问题", + "diagnosis_description_apps": "应用" } \ No newline at end of file diff --git a/maintenance/autofix_locale_format.py b/maintenance/autofix_locale_format.py new file mode 100644 index 000000000..1c56ea386 --- /dev/null +++ b/maintenance/autofix_locale_format.py @@ -0,0 +1,170 @@ +import os +import re +import json +import glob +from collections import OrderedDict + +ROOT = os.path.dirname(__file__) + "/../" +LOCALE_FOLDER = ROOT + "/locales/" + +# List all locale files (except en.json being the ref) +TRANSLATION_FILES = glob.glob(LOCALE_FOLDER + "*.json") +TRANSLATION_FILES = [filename.split("/")[-1] for filename in TRANSLATION_FILES] +print(LOCALE_FOLDER) +TRANSLATION_FILES.remove("en.json") + +REFERENCE_FILE = LOCALE_FOLDER + "en.json" + + +def autofix_i18n_placeholders(): + def _autofix_i18n_placeholders(locale_file): + """ + This tries for magically fix mismatch between en.json format and other.json format + e.g. an i18n string with: + source: "Lorem ipsum {some_var}" + fr: "Lorem ipsum {une_variable}" + (ie the keyword in {} was translated but shouldnt have been) + """ + + this_locale = json.loads(open(LOCALE_FOLDER + locale_file).read()) + fixed_stuff = False + reference = json.loads(open(REFERENCE_FILE).read()) + + # We iterate over all keys/string in en.json + for key, string in reference.items(): + + # Ignore check if there's no translation yet for this key + if key not in this_locale: + continue + + # Then we check that every "{stuff}" (for python's .format()) + # should also be in the translated string, otherwise the .format + # will trigger an exception! + subkeys_in_ref = [k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)] + subkeys_in_this_locale = [ + k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key]) + ] + + if set(subkeys_in_ref) != set(subkeys_in_this_locale) and ( + len(subkeys_in_ref) == len(subkeys_in_this_locale) + ): + for i, subkey in enumerate(subkeys_in_ref): + this_locale[key] = this_locale[key].replace( + "{%s}" % subkeys_in_this_locale[i], "{%s}" % subkey + ) + fixed_stuff = True + + # Validate that now it's okay ? + subkeys_in_ref = [k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)] + subkeys_in_this_locale = [ + k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key]) + ] + if any(k not in subkeys_in_ref for k in subkeys_in_this_locale): + raise Exception( + """\n +========================== +Format inconsistency for string {key} in {locale_file}:" +en.json -> {string} +{locale_file} -> {translated_string} +Please fix it manually ! + """.format( + key=key, + string=string.encode("utf-8"), + locale_file=locale_file, + translated_string=this_locale[key].encode("utf-8"), + ) + ) + + if fixed_stuff: + json.dump( + this_locale, + open(LOCALE_FOLDER + locale_file, "w"), + indent=4, + ensure_ascii=False, + ) + + for locale_file in TRANSLATION_FILES: + _autofix_i18n_placeholders(locale_file) + + +def autofix_orthotypography_and_standardized_words(): + def reformat(lang, transformations): + + locale = open(f"{LOCALE_FOLDER}{lang}.json").read() + for pattern, replace in transformations.items(): + locale = re.compile(pattern).sub(replace, locale) + + open(f"{LOCALE_FOLDER}{lang}.json", "w").write(locale) + + ###################################################### + + godamn_spaces_of_hell = [ + "\u00a0", + "\u2000", + "\u2001", + "\u2002", + "\u2003", + "\u2004", + "\u2005", + "\u2006", + "\u2007", + "\u2008", + "\u2009", + "\u200A", + "\u202f", + "\u202F", + "\u3000", + ] + + transformations = {s: " " for s in godamn_spaces_of_hell} + transformations.update( + { + "…": "...", + } + ) + + reformat("en", transformations) + + ###################################################### + + transformations.update( + { + "courriel": "email", + "e-mail": "email", + "Courriel": "Email", + "E-mail": "Email", + "« ": "'", + "«": "'", + " »": "'", + "»": "'", + "’": "'", + # r"$(\w{1,2})'|( \w{1,2})'": r"\1\2’", + } + ) + + reformat("fr", transformations) + + +def remove_stale_translated_strings(): + + reference = json.loads(open(LOCALE_FOLDER + "en.json").read()) + + for locale_file in TRANSLATION_FILES: + + print(locale_file) + this_locale = json.loads( + open(LOCALE_FOLDER + locale_file).read(), object_pairs_hook=OrderedDict + ) + this_locale_fixed = {k: v for k, v in this_locale.items() if k in reference} + + json.dump( + this_locale_fixed, + open(LOCALE_FOLDER + locale_file, "w"), + indent=4, + ensure_ascii=False, + ) + + +autofix_orthotypography_and_standardized_words() +remove_stale_translated_strings() +autofix_i18n_placeholders() diff --git a/maintenance/make_changelog.sh b/maintenance/make_changelog.sh new file mode 100644 index 000000000..44171c5b6 --- /dev/null +++ b/maintenance/make_changelog.sh @@ -0,0 +1,36 @@ +VERSION="?" +RELEASE="testing" +REPO=$(basename $(git rev-parse --show-toplevel)) +REPO_URL=$(git remote get-url origin) +ME=$(git config --global --get user.name) +EMAIL=$(git config --global --get user.email) + +LAST_RELEASE=$(git tag --list 'debian/11.*' | tail -n 1) + +echo "$REPO ($VERSION) $RELEASE; urgency=low" +echo "" + +git log $LAST_RELEASE.. -n 10000 --first-parent --pretty=tformat:' - %b%s (%h)' \ +| sed -E "s@Merge .*#([0-9]+).*\$@ \([#\1]\($REPO_URL/pull/\1\)\)@g" \ +| grep -v "Update from Weblate" \ +| tac + +TRANSLATIONS=$(git log $LAST_RELEASE... -n 10000 --pretty=format:"%s" \ + | grep "Translated using Weblate" \ + | sed -E "s/Translated using Weblate \((.*)\)/\1/g" \ + | sort | uniq | tr '\n' ', ' | sed -e 's/,$//g' -e 's/,/, /g') +[[ -z "$TRANSLATIONS" ]] || echo " - [i18n] Translations updated for $TRANSLATIONS" + +echo "" +CONTRIBUTORS=$(git logc $LAST_RELEASE... -n 10000 --pretty=format:"%an" \ + | sort | uniq | grep -v "$ME" \ + | tr '\n' ', ' | sed -e 's/,$//g' -e 's/,/, /g') +[[ -z "$CONTRIBUTORS" ]] || echo " Thanks to all contributors <3 ! ($CONTRIBUTORS)" +echo "" +echo " -- $ME <$EMAIL> $(date -R)" +echo "" + + + +# PR links can be converted to regular texts using : sed -E 's@\[(#[0-9]*)\]\([^ )]*\)@\1@g' +# Or readded with sed -E 's@#([0-9]*)@[YunoHost#\1](https://github.com/yunohost/yunohost/pull/\1)@g' | sed -E 's@\((\w+)\)@([YunoHost/\1](https://github.com/yunohost/yunohost/commit/\1))@g' diff --git a/tests/test_i18n_keys.py b/maintenance/missing_i18n_keys.py similarity index 63% rename from tests/test_i18n_keys.py rename to maintenance/missing_i18n_keys.py index 33c1f7b65..3dbca8027 100644 --- a/tests/test_i18n_keys.py +++ b/maintenance/missing_i18n_keys.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- +import toml import os import re import glob import json import yaml import subprocess +import sys -ignore = [ - "password_too_simple_", - "password_listed", - "backup_method_", - "backup_applying_method_", - "confirm_app_install_", -] +ROOT = os.path.dirname(__file__) + "/../" +LOCALE_FOLDER = ROOT + "/locales/" +REFERENCE_FILE = LOCALE_FOLDER + "en.json" ############################################################################### # Find used keys in python code # @@ -32,12 +30,12 @@ def find_expected_string_keys(): p3 = re.compile(r"YunohostValidationError\(\n*\s*[\'\"](\w+)[\'\"]") p4 = re.compile(r"# i18n: [\'\"]?(\w+)[\'\"]?") - python_files = glob.glob("src/yunohost/*.py") - python_files.extend(glob.glob("src/yunohost/utils/*.py")) - python_files.extend(glob.glob("src/yunohost/data_migrations/*.py")) - python_files.extend(glob.glob("src/yunohost/authenticators/*.py")) - python_files.extend(glob.glob("data/hooks/diagnosis/*.py")) - python_files.append("bin/yunohost") + python_files = glob.glob(ROOT + "src/*.py") + python_files.extend(glob.glob(ROOT + "src/utils/*.py")) + python_files.extend(glob.glob(ROOT + "src/migrations/*.py")) + python_files.extend(glob.glob(ROOT + "src/authenticators/*.py")) + python_files.extend(glob.glob(ROOT + "src/diagnosers/*.py")) + python_files.append(ROOT + "bin/yunohost") for python_file in python_files: content = open(python_file).read() @@ -59,7 +57,9 @@ def find_expected_string_keys(): # For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries) # Also we expect to have "diagnosis_description_" for each diagnosis p3 = re.compile(r"[\"\'](diagnosis_[a-z]+_\w+)[\"\']") - for python_file in glob.glob("data/hooks/diagnosis/*.py"): + for python_file in glob.glob(ROOT + "src/diagnosers/*.py"): + if "__init__.py" in python_file: + continue content = open(python_file).read() for m in p3.findall(content): if m.endswith("_"): @@ -71,14 +71,14 @@ def find_expected_string_keys(): ] # For each migration, expect to find "migration_description_" - for path in glob.glob("src/yunohost/data_migrations/*.py"): + for path in glob.glob(ROOT + "src/migrations/*.py"): if "__init__" in path: continue yield "migration_description_" + os.path.basename(path)[:-3] # For each default service, expect to find "service_description_" for service, info in yaml.safe_load( - open("data/templates/yunohost/services.yml") + open(ROOT + "conf/yunohost/services.yml") ).items(): if info is None: continue @@ -87,7 +87,7 @@ def find_expected_string_keys(): # For all unit operations, expect to find "log_" # A unit operation is created either using the @is_unit_operation decorator # or using OperationLogger( - cmd = "grep -hr '@is_unit_operation' src/yunohost/ -A3 2>/dev/null | grep '^def' | sed -E 's@^def (\\w+)\\(.*@\\1@g'" + cmd = f"grep -hr '@is_unit_operation' {ROOT}/src/ -A3 2>/dev/null | grep '^def' | sed -E 's@^def (\\w+)\\(.*@\\1@g'" for funcname in ( subprocess.check_output(cmd, shell=True).decode("utf-8").strip().split("\n") ): @@ -102,14 +102,14 @@ def find_expected_string_keys(): # Global settings descriptions # Will be on a line like : ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", ... p5 = re.compile(r" \(\n*\s*[\"\'](\w[\w\.]+)[\"\'],") - content = open("src/yunohost/settings.py").read() + content = open(ROOT + "src/settings.py").read() for m in ( "global_settings_setting_" + s.replace(".", "_") for s in p5.findall(content) ): yield m # Keys for the actionmap ... - for category in yaml.safe_load(open("data/actionsmap/yunohost.yml")).values(): + for category in yaml.safe_load(open(ROOT + "share/actionsmap.yml")).values(): if "actions" not in category.keys(): continue for action in category["actions"].values(): @@ -137,70 +137,72 @@ def find_expected_string_keys(): yield "backup_applying_method_%s" % method yield "backup_method_%s_finished" % method - for level in ["danger", "thirdparty", "warning"]: - yield "confirm_app_install_%s" % level + registrars = toml.load(open(ROOT + "share/registrar_list.toml")) + supported_registrars = ["ovh", "gandi", "godaddy"] + for registrar in supported_registrars: + for key in registrars[registrar].keys(): + yield f"domain_config_{key}" - for errortype in ["not_found", "error", "warning", "success", "not_found_details"]: - yield "diagnosis_domain_expiration_%s" % errortype - yield "diagnosis_domain_not_found_details" - - for errortype in ["bad_status_code", "connection_error", "timeout"]: - yield "diagnosis_http_%s" % errortype - - yield "password_listed" - for i in [1, 2, 3, 4]: - yield "password_too_simple_%s" % i - - checks = [ - "outgoing_port_25_ok", - "ehlo_ok", - "fcrdns_ok", - "blacklist_ok", - "queue_ok", - "ehlo_bad_answer", - "ehlo_unreachable", - "ehlo_bad_answer_details", - "ehlo_unreachable_details", - ] - for check in checks: - yield "diagnosis_mail_%s" % check - - -############################################################################### -# Load en locale json keys # -############################################################################### - - -def keys_defined_for_en(): - return json.loads(open("locales/en.json").read()).keys() + domain_config = toml.load(open(ROOT + "share/config_domain.toml")) + for panel in domain_config.values(): + if not isinstance(panel, dict): + continue + for section in panel.values(): + if not isinstance(section, dict): + continue + for key, values in section.items(): + if not isinstance(values, dict): + continue + yield f"domain_config_{key}" ############################################################################### # Compare keys used and keys defined # ############################################################################### +if len(sys.argv) <= 1 or sys.argv[1] not in ["--check", "--fix"]: + print("Please specify --check or --fix") + sys.exit(1) expected_string_keys = set(find_expected_string_keys()) -keys_defined = set(keys_defined_for_en()) +keys_defined_for_en = json.loads(open(REFERENCE_FILE).read()).keys() +keys_defined = set(keys_defined_for_en) +unused_keys = keys_defined.difference(expected_string_keys) +unused_keys = sorted(unused_keys) -def test_undefined_i18n_keys(): - undefined_keys = expected_string_keys.difference(keys_defined) - undefined_keys = sorted(undefined_keys) +undefined_keys = expected_string_keys.difference(keys_defined) +undefined_keys = sorted(undefined_keys) + +mode = sys.argv[1].strip("-") +if mode == "check": + + # Unused keys are not too problematic, will be automatically + # removed by the other autoreformat script, + # but still informative to display them + if unused_keys: + print( + "Those i18n keys appears unused:\n" " - " + "\n - ".join(unused_keys) + ) if undefined_keys: - raise Exception( + print( "Those i18n keys should be defined in en.json:\n" " - " + "\n - ".join(undefined_keys) ) + sys.exit(1) +elif mode == "fix": + j = json.loads(open(REFERENCE_FILE).read()) + for key in undefined_keys: + j[key] = "FIXME" + for key in unused_keys: + del j[key] -def test_unused_i18n_keys(): - - unused_keys = keys_defined.difference(expected_string_keys) - unused_keys = sorted(unused_keys) - - if unused_keys: - raise Exception( - "Those i18n keys appears unused:\n" " - " + "\n - ".join(unused_keys) - ) + json.dump( + j, + open(REFERENCE_FILE, "w"), + indent=4, + ensure_ascii=False, + sort_keys=True, + ) diff --git a/sbin/yunohost-reset-ldap-password b/sbin/yunohost-reset-ldap-password deleted file mode 100755 index 95f84875f..000000000 --- a/sbin/yunohost-reset-ldap-password +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -echo "Warning: this script is now deprecated. You can simply type 'yunohost tools adminpw' to change the root/admin password." -yunohost tools adminpw diff --git a/data/other/password/100000-most-used.txt.gz b/share/100000-most-used-passwords.txt.gz similarity index 100% rename from data/other/password/100000-most-used.txt.gz rename to share/100000-most-used-passwords.txt.gz diff --git a/data/actionsmap/yunohost.yml b/share/actionsmap.yml similarity index 89% rename from data/actionsmap/yunohost.yml rename to share/actionsmap.yml index 052d86122..89c6e914d 100644 --- a/data/actionsmap/yunohost.yml +++ b/share/actionsmap.yml @@ -33,7 +33,7 @@ # Global parameters # ############################# _global: - name: yunohost.admin + namespace: yunohost authentication: api: ldap_admin cli: null @@ -89,9 +89,6 @@ user: pattern: &pattern_lastname - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - "pattern_lastname" - -m: - full: --mail - help: (Deprecated, see --domain) Main unique email address -p: full: --password help: User password @@ -479,21 +476,17 @@ domain: help: Do not ask confirmation to remove apps action: store_true + ### domain_dns_conf() dns-conf: + deprecated: true action_help: Generate sample DNS configuration for a domain - api: GET /domains//dns arguments: domain: help: Target domain - -t: - full: --ttl - help: Time To Live (TTL) in second before DNS servers update. Default is 3600 seconds (i.e. 1 hour). extra: - pattern: - - !!str ^[0-9]+$ - - "pattern_positive_number" - + pattern: *pattern_domain + ### domain_maindomain() main-domain: action_help: Check the current main domain, or change it @@ -511,8 +504,8 @@ domain: ### certificate_status() cert-status: + deprecated: true action_help: List status of current certificates (all by default). - api: GET /domains//cert arguments: domain_list: help: Domains to check @@ -523,8 +516,8 @@ domain: ### certificate_install() cert-install: + deprecated: true action_help: Install Let's Encrypt certificates for given domains (all by default). - api: PUT /domains//cert arguments: domain_list: help: Domains for which to install the certificates @@ -538,14 +531,11 @@ domain: --self-signed: help: Install self-signed certificate instead of Let's Encrypt action: store_true - --staging: - help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. - action: store_true ### certificate_renew() cert-renew: + deprecated: true action_help: Renew the Let's Encrypt certificates for given domains (all by default). - api: PUT /domains//cert/renew arguments: domain_list: help: Domains for which to renew the certificates @@ -559,9 +549,6 @@ domain: --no-checks: help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) action: store_true - --staging: - help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. - action: store_true ### domain_url_available() url-available: @@ -575,6 +562,135 @@ domain: path: help: The path to check (e.g. /coffee) + subcategories: + + config: + subcategory_help: Domain settings + actions: + + ### domain_config_get() + get: + action_help: Display a domain configuration + api: GET /domains//config + arguments: + domain: + help: Domain name + key: + help: A specific panel, section or a question identifier + nargs: '?' + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true + -e: + full: --export + help: Only export key/values, meant to be reimported using "config set --args-file" + action: store_true + + ### domain_config_set() + set: + action_help: Apply a new configuration + api: PUT /domains//config + arguments: + domain: + help: Domain name + key: + help: The question or form key + nargs: '?' + -v: + full: --value + help: new value + -a: + full: --args + help: Serialized arguments for new configuration (i.e. "mail_in=0&mail_out=0") + + dns: + subcategory_help: Manage domains DNS + actions: + ### domain_dns_conf() + suggest: + action_help: Generate sample DNS configuration for a domain + api: + - GET /domains//dns + - GET /domains//dns/suggest + arguments: + domain: + help: Target domain + extra: + pattern: *pattern_domain + + ### domain_dns_push() + push: + action_help: Push DNS records to registrar + api: POST /domains//dns/push + arguments: + domain: + help: Domain name to push DNS conf for + extra: + pattern: *pattern_domain + -d: + full: --dry-run + help: Only display what's to be pushed + action: store_true + --force: + help: Also update/remove records which were not originally set by Yunohost, or which have been manually modified + action: store_true + --purge: + help: Delete all records + action: store_true + + cert: + subcategory_help: Manage domain certificates + actions: + ### certificate_status() + status: + action_help: List status of current certificates (all by default). + api: GET /domains//cert + arguments: + domain_list: + help: Domains to check + nargs: "*" + --full: + help: Show more details + action: store_true + + ### certificate_install() + install: + action_help: Install Let's Encrypt certificates for given domains (all by default). + api: PUT /domains//cert + arguments: + domain_list: + help: Domains for which to install the certificates + nargs: "*" + --force: + help: Install even if current certificate is not self-signed + action: store_true + --no-checks: + help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to install. (Not recommended) + action: store_true + --self-signed: + help: Install self-signed certificate instead of Let's Encrypt + action: store_true + + ### certificate_renew() + renew: + action_help: Renew the Let's Encrypt certificates for given domains (all by default). + api: PUT /domains//cert/renew + arguments: + domain_list: + help: Domains for which to renew the certificates + nargs: "*" + --force: + help: Ignore the validity threshold (30 days) + action: store_true + --email: + help: Send an email to root with logs if some renewing fails + action: store_true + --no-checks: + help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) + action: store_true + + ############################# # App # ############################# @@ -610,9 +726,6 @@ app: app: help: Name, local path or git URL of the app to fetch the manifest of - fetchlist: - deprecated: true - ### app_list() list: action_help: List installed apps @@ -622,12 +735,10 @@ app: full: --full help: Display all details, including the app manifest and various other infos action: store_true - -i: - full: --installed - help: Dummy argument, does nothing anymore (still there only for backward compatibility) + -u: + full: --upgradable + help: List only apps that can upgrade to a newer version action: store_true - filter: - nargs: '?' ### app_info() info: @@ -777,6 +888,10 @@ app: -d: full: --domain help: Specific domain to put app on (the app domain by default) + -u: + full: --undo + help: Undo redirection + action: store_true ### app_ssowatconf() ssowatconf: @@ -792,36 +907,6 @@ app: new_label: help: New app label - ### app_addaccess() TODO: Write help - addaccess: - action_help: Grant access right to users (everyone by default) - deprecated: true - arguments: - apps: - nargs: "+" - -u: - full: --users - nargs: "*" - - ### app_removeaccess() TODO: Write help - removeaccess: - action_help: Revoke access right to users (everyone by default) - deprecated: true - arguments: - apps: - nargs: "+" - -u: - full: --users - nargs: "*" - - ### app_clearaccess() - clearaccess: - action_help: Reset access rights for the app - deprecated: true - arguments: - apps: - nargs: "+" - subcategories: action: @@ -853,24 +938,45 @@ app: subcategory_help: Applications configuration panel actions: - ### app_config_show_panel() - show-panel: - action_help: show config panel for the application + ### app_config_get() + get: + action_help: Display an app configuration api: GET /apps//config-panel arguments: - app: - help: App name + app: + help: App name + key: + help: A specific panel, section or a question identifier + nargs: '?' + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true + -e: + full: --export + help: Only export key/values, meant to be reimported using "config set --args-file" + action: store_true - ### app_config_apply() - apply: - action_help: apply the new configuration + ### app_config_set() + set: + action_help: Apply a new configuration api: PUT /apps//config arguments: - app: - help: App name - -a: - full: --args - help: Serialized arguments for new configuration (i.e. "domain=domain.tld&path=/path") + app: + help: App name + key: + help: The question or panel key + nargs: '?' + -v: + full: --value + help: new value + -a: + full: --args + help: Serialized arguments for new configuration (i.e. "domain=domain.tld&path=/path") + -f: + full: --args-file + help: YAML or JSON file with key/value couples + type: open ############################# # Backup # @@ -1045,13 +1151,6 @@ service: full: --log help: Absolute path to log file to display nargs: "+" - -t: - full: --log_type - help: Type of the log (file or systemd) - nargs: "+" - choices: - - file - - systemd --test_status: help: Specify a custom bash command to check the status of the service. Note that it only makes sense to specify this if the corresponding systemd service does not return the proper information already. --test_conf: @@ -1065,9 +1164,6 @@ service: full: --need_lock help: Use this option to prevent deadlocks if the service does invoke yunohost commands. action: store_true - -s: - full: --status - help: Deprecated, old option. Does nothing anymore. Possibly check the --test_status option. ### service_remove() remove: @@ -1169,35 +1265,6 @@ service: default: 50 type: int - ### service_regen_conf() - regen-conf: - action_help: Regenerate the configuration file(s) for a service - deprecated_alias: - - regenconf - arguments: - names: - help: Services name to regenerate configuration of - nargs: "*" - metavar: NAME - -d: - full: --with-diff - help: Show differences in case of configuration changes - action: store_true - -f: - full: --force - help: > - Override all manual modifications in configuration - files - action: store_true - -n: - full: --dry-run - help: Show what would have been regenerated - action: store_true - -p: - full: --list-pending - help: List pending configuration files and exit - action: store_true - ############################# # Firewall # ############################# @@ -1331,9 +1398,6 @@ dyndns: subscribe: action_help: Subscribe to a DynDNS service arguments: - --subscribe-host: - help: Dynette HTTP API to subscribe to - default: "dyndns.yunohost.org" -d: full: --domain help: Full domain to subscribe with @@ -1347,20 +1411,11 @@ dyndns: update: action_help: Update IP on DynDNS platform arguments: - --dyn-host: - help: Dynette DNS server to inform - default: "dyndns.yunohost.org" -d: full: --domain help: Full domain to update extra: pattern: *pattern_domain - -k: - full: --key - help: Public DNS key - -i: - full: --ipv4 - help: IP address to send -f: full: --force help: Force the update (for debugging only) @@ -1369,17 +1424,6 @@ dyndns: full: --dry-run help: Only display the generated zone action: store_true - -6: - full: --ipv6 - help: IPv6 address to send - - ### dyndns_installcron() - installcron: - deprecated: true - - ### dyndns_removecron() - removecron: - deprecated: true ############################# @@ -1460,12 +1504,6 @@ tools: nargs: "?" metavar: TARGET default: all - --apps: - help: (Deprecated, see first positional arg) Fetch the application list to check which apps can be upgraded - action: store_true - --system: - help: (Deprecated, see first positional arg) Fetch available system packages upgrades (equivalent to apt update) - action: store_true ### tools_upgrade() upgrade: @@ -1478,12 +1516,6 @@ tools: - apps - system nargs: "?" - --apps: - help: (Deprecated, see first positional arg) Upgrade all applications - action: store_true - --system: - help: (Deprecated, see first positional arg) Upgrade only the system packages - action: store_true ### tools_shell() shell: diff --git a/share/config_domain.toml b/share/config_domain.toml new file mode 100644 index 000000000..65e755365 --- /dev/null +++ b/share/config_domain.toml @@ -0,0 +1,60 @@ +version = "1.0" +i18n = "domain_config" + +# +# Other things we may want to implement in the future: +# +# - maindomain handling +# - default app +# - autoredirect www in nginx conf +# - ? +# + +[feature] + [feature.app] + [feature.app.default_app] + type = "app" + filter = "is_webapp" + default = "_none" + + [feature.mail] + #services = ['postfix', 'dovecot'] + + [feature.mail.features_disclaimer] + type = "alert" + style = "warning" + icon = "warning" + + [feature.mail.mail_out] + type = "boolean" + default = 1 + + [feature.mail.mail_in] + type = "boolean" + default = 1 + + #[feature.mail.backup_mx] + #type = "tags" + #default = [] + #pattern.regexp = '^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$' + #pattern.error = "pattern_error" + + [feature.xmpp] + + [feature.xmpp.xmpp] + type = "boolean" + default = 0 + +[dns] + + [dns.registrar] + optional = true + + # This part is automatically generated in DomainConfigPanel + +# [dns.advanced] +# +# [dns.advanced.ttl] +# type = "number" +# min = 0 +# default = 3600 diff --git a/data/other/dnsbl_list.yml b/share/dnsbl_list.yml similarity index 81% rename from data/other/dnsbl_list.yml rename to share/dnsbl_list.yml index 1dc0175a3..ad86fd2a6 100644 --- a/data/other/dnsbl_list.yml +++ b/share/dnsbl_list.yml @@ -5,138 +5,161 @@ ipv4: true ipv6: true domain: false + non_blacklisted_return_code: [] - name: Barracuda Reputation Block List dns_server: b.barracudacentral.org website: https://barracudacentral.org/rbl/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: Hostkarma dns_server: hostkarma.junkemailfilter.com website: https://ipadmin.junkemailfilter.com/remove.php ipv4: true ipv6: false domain: false + non_blacklisted_return_code: ['127.0.0.1', '127.0.0.5'] - name: ImproWare IP based spamlist dns_server: spamrbl.imp.ch website: https://antispam.imp.ch/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: ImproWare IP based wormlist dns_server: wormrbl.imp.ch website: https://antispam.imp.ch/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: Backscatterer.org dns_server: ips.backscatterer.org website: http://www.backscatterer.org/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: inps.de dns_server: dnsbl.inps.de website: http://dnsbl.inps.de/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: LASHBACK dns_server: ubl.unsubscore.com website: https://blacklist.lashback.com/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: Mailspike.org dns_server: bl.mailspike.net website: http://www.mailspike.net/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: NiX Spam dns_server: ix.dnsbl.manitu.net website: http://www.dnsbl.manitu.net/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: REDHAWK dns_server: access.redhawk.org website: https://www.redhawk.org/SpamHawk/query.php ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: SORBS Open SMTP relays dns_server: smtp.dnsbl.sorbs.net website: http://www.sorbs.net/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: SORBS Spamhost (last 28 days) dns_server: recent.spam.dnsbl.sorbs.net website: http://www.sorbs.net/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: SORBS Spamhost (last 48 hours) dns_server: new.spam.dnsbl.sorbs.net website: http://www.sorbs.net/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: SpamCop Blocking List dns_server: bl.spamcop.net website: https://www.spamcop.net/bl.shtml ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: Spam Eating Monkey SEM-BACKSCATTER dns_server: backscatter.spameatingmonkey.net website: https://spameatingmonkey.com/services ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: Spam Eating Monkey SEM-BLACK dns_server: bl.spameatingmonkey.net website: https://spameatingmonkey.com/services ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: Spam Eating Monkey SEM-IPV6BL dns_server: bl.ipv6.spameatingmonkey.net website: https://spameatingmonkey.com/services ipv4: false ipv6: true domain: false + non_blacklisted_return_code: [] - name: SpamRATS! all dns_server: all.spamrats.com website: http://www.spamrats.com/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: PSBL (Passive Spam Block List) dns_server: psbl.surriel.com website: http://psbl.surriel.com/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: SWINOG dns_server: dnsrbl.swinog.ch website: https://antispam.imp.ch/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: GBUdb Truncate dns_server: truncate.gbudb.net website: http://www.gbudb.com/truncate/index.jsp ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] - name: Weighted Private Block List dns_server: db.wpbl.info website: http://www.wpbl.info/ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] # Used by GAFAM - name: Composite Blocking List dns_server: cbl.abuseat.org @@ -144,6 +167,7 @@ ipv4: true ipv6: false domain: false + non_blacklisted_return_code: [] # Used by GAFAM - name: SenderScore Blacklist dns_server: bl.score.senderscore.com @@ -152,18 +176,21 @@ ipv6: false domain: false # Added cause it supports IPv6 + non_blacklisted_return_code: [] - name: AntiCaptcha.NET IPv6 dns_server: dnsbl6.anticaptcha.net website: http://anticaptcha.net/ ipv4: false ipv6: true domain: false + non_blacklisted_return_code: [] - name: Suomispam Blacklist dns_server: bl.suomispam.net website: http://suomispam.net/ ipv4: true ipv6: true domain: false + non_blacklisted_return_code: [] - name: NordSpam dns_server: bl.nordspam.com website: https://www.nordspam.com/ diff --git a/data/other/ffdhe2048.pem b/share/ffdhe2048.pem similarity index 100% rename from data/other/ffdhe2048.pem rename to share/ffdhe2048.pem diff --git a/data/helpers b/share/helpers similarity index 100% rename from data/helpers rename to share/helpers diff --git a/share/registrar_list.toml b/share/registrar_list.toml new file mode 100644 index 000000000..afb213aa1 --- /dev/null +++ b/share/registrar_list.toml @@ -0,0 +1,649 @@ +[aliyun] + [aliyun.auth_key_id] + type = "string" + redact = true + + [aliyun.auth_secret] + type = "string" + redact = true + +[aurora] + [aurora.auth_api_key] + type = "string" + redact = true + + [aurora.auth_secret_key] + type = "string" + redact = true + +[azure] + [azure.auth_client_id] + type = "string" + redact = true + + [azure.auth_client_secret] + type = "string" + redact = true + + [azure.auth_tenant_id] + type = "string" + redact = true + + [azure.auth_subscription_id] + type = "string" + redact = true + + [azure.resource_group] + type = "string" + redact = true + +[cloudflare] + [cloudflare.auth_username] + type = "string" + redact = true + + [cloudflare.auth_token] + type = "string" + redact = true + + [cloudflare.zone_id] + type = "string" + redact = true + +[cloudns] + [cloudns.auth_id] + type = "string" + redact = true + + [cloudns.auth_subid] + type = "string" + redact = true + + [cloudns.auth_subuser] + type = "string" + redact = true + + [cloudns.auth_password] + type = "password" + + [cloudns.weight] + type = "number" + + [cloudns.port] + type = "number" +[cloudxns] + [cloudxns.auth_username] + type = "string" + redact = true + + [cloudxns.auth_token] + type = "string" + redact = true + +[conoha] + [conoha.auth_region] + type = "string" + redact = true + + [conoha.auth_token] + type = "string" + redact = true + + [conoha.auth_username] + type = "string" + redact = true + + [conoha.auth_password] + type = "password" + + [conoha.auth_tenant_id] + type = "string" + redact = true + +[constellix] + [constellix.auth_username] + type = "string" + redact = true + + [constellix.auth_token] + type = "string" + redact = true + +[digitalocean] + [digitalocean.auth_token] + type = "string" + redact = true + +[dinahosting] + [dinahosting.auth_username] + type = "string" + redact = true + + [dinahosting.auth_password] + type = "password" + +[directadmin] + [directadmin.auth_password] + type = "password" + + [directadmin.auth_username] + type = "string" + redact = true + + [directadmin.endpoint] + type = "string" + redact = true + +[dnsimple] + [dnsimple.auth_token] + type = "string" + redact = true + + [dnsimple.auth_username] + type = "string" + redact = true + + [dnsimple.auth_password] + type = "password" + + [dnsimple.auth_2fa] + type = "string" + redact = true + +[dnsmadeeasy] + [dnsmadeeasy.auth_username] + type = "string" + redact = true + + [dnsmadeeasy.auth_token] + type = "string" + redact = true + +[dnspark] + [dnspark.auth_username] + type = "string" + redact = true + + [dnspark.auth_token] + type = "string" + redact = true + +[dnspod] + [dnspod.auth_username] + type = "string" + redact = true + + [dnspod.auth_token] + type = "string" + redact = true + +[dreamhost] + [dreamhost.auth_token] + type = "string" + redact = true + +[dynu] + [dynu.auth_token] + type = "string" + redact = true + +[easydns] + [easydns.auth_username] + type = "string" + redact = true + + [easydns.auth_token] + type = "string" + redact = true + +[easyname] + [easyname.auth_username] + type = "string" + redact = true + + [easyname.auth_password] + type = "password" + +[euserv] + [euserv.auth_username] + type = "string" + redact = true + + [euserv.auth_password] + type = "password" + +[exoscale] + [exoscale.auth_key] + type = "string" + redact = true + + [exoscale.auth_secret] + type = "string" + redact = true + +[gandi] + [gandi.auth_token] + type = "string" + redact = true + + [gandi.api_protocol] + type = "string" + choices.rpc = "RPC" + choices.rest = "REST" + default = "rest" + visible = "false" + +[gehirn] + [gehirn.auth_token] + type = "string" + redact = true + + [gehirn.auth_secret] + type = "string" + redact = true + +[glesys] + [glesys.auth_username] + type = "string" + redact = true + + [glesys.auth_token] + type = "string" + redact = true + +[godaddy] + [godaddy.auth_key] + type = "string" + redact = true + + [godaddy.auth_secret] + type = "string" + redact = true + +[googleclouddns] + [goggleclouddns.auth_service_account_info] + type = "string" + redact = true + +[gransy] + [gransy.auth_username] + type = "string" + redact = true + + [gransy.auth_password] + type = "password" + +[gratisdns] + [gratisdns.auth_username] + type = "string" + redact = true + + [gratisdns.auth_password] + type = "password" + +[henet] + [henet.auth_username] + type = "string" + redact = true + + [henet.auth_password] + type = "password" + +[hetzner] + [hetzner.auth_token] + type = "string" + redact = true + +[hostingde] + [hostingde.auth_token] + type = "string" + redact = true + +[hover] + [hover.auth_username] + type = "string" + redact = true + + [hover.auth_password] + type = "password" + +[infoblox] + [infoblox.auth_user] + type = "string" + redact = true + + [infoblox.auth_psw] + type = "password" + + [infoblox.ib_view] + type = "string" + redact = true + + [infoblox.ib_host] + type = "string" + redact = true + +[infomaniak] + [infomaniak.auth_token] + type = "string" + redact = true + +[internetbs] + [internetbs.auth_key] + type = "string" + redact = true + + [internetbs.auth_password] + type = "string" + redact = true + +[inwx] + [inwx.auth_username] + type = "string" + redact = true + + [inwx.auth_password] + type = "password" + +[joker] + [joker.auth_token] + type = "string" + redact = true + +[linode] + [linode.auth_token] + type = "string" + redact = true + +[linode4] + [linode4.auth_token] + type = "string" + redact = true + +[localzone] + [localzone.filename] + type = "string" + redact = true + +[luadns] + [luadns.auth_username] + type = "string" + redact = true + + [luadns.auth_token] + type = "string" + redact = true + +[memset] + [memset.auth_token] + type = "string" + redact = true + +[mythicbeasts] + [mythicbeasts.auth_username] + type = "string" + redact = true + + [mythicbeasts.auth_password] + type = "password" + + [mythicbeasts.auth_token] + type = "string" + redact = true + +[namecheap] + [namecheap.auth_token] + type = "string" + redact = true + + [namecheap.auth_username] + type = "string" + redact = true + + [namecheap.auth_client_ip] + type = "string" + redact = true + + [namecheap.auth_sandbox] + type = "string" + redact = true + +[namesilo] + [namesilo.auth_token] + type = "string" + redact = true + +[netcup] + [netcup.auth_customer_id] + type = "string" + redact = true + + [netcup.auth_api_key] + type = "string" + redact = true + + [netcup.auth_api_password] + type = "string" + redact = true + +[nfsn] + [nfsn.auth_username] + type = "string" + redact = true + + [nfsn.auth_token] + type = "string" + redact = true + +[njalla] + [njalla.auth_token] + type = "string" + redact = true + +[nsone] + [nsone.auth_token] + type = "string" + redact = true + +[onapp] + [onapp.auth_username] + type = "string" + redact = true + + [onapp.auth_token] + type = "string" + redact = true + + [onapp.auth_server] + type = "string" + redact = true + +[online] + [online.auth_token] + type = "string" + redact = true + +[ovh] + [ovh.auth_entrypoint] + type = "select" + choices = ["ovh-eu", "ovh-ca", "soyoustart-eu", "soyoustart-ca", "kimsufi-eu", "kimsufi-ca"] + default = "ovh-eu" + + [ovh.auth_application_key] + type = "string" + redact = true + + [ovh.auth_application_secret] + type = "string" + redact = true + + [ovh.auth_consumer_key] + type = "string" + redact = true + +[plesk] + [plesk.auth_username] + type = "string" + redact = true + + [plesk.auth_password] + type = "password" + + [plesk.plesk_server] + type = "string" + redact = true + +[pointhq] + [pointhq.auth_username] + type = "string" + redact = true + + [pointhq.auth_token] + type = "string" + redact = true + +[powerdns] + [powerdns.auth_token] + type = "string" + redact = true + + [powerdns.pdns_server] + type = "string" + redact = true + + [powerdns.pdns_server_id] + type = "string" + redact = true + + [powerdns.pdns_disable_notify] + type = "boolean" + +[rackspace] + [rackspace.auth_account] + type = "string" + redact = true + + [rackspace.auth_username] + type = "string" + redact = true + + [rackspace.auth_api_key] + type = "string" + redact = true + + [rackspace.auth_token] + type = "string" + redact = true + + [rackspace.sleep_time] + type = "string" + redact = true + +[rage4] + [rage4.auth_username] + type = "string" + redact = true + + [rage4.auth_token] + type = "string" + redact = true + +[rcodezero] + [rcodezero.auth_token] + type = "string" + redact = true + +[route53] + [route53.auth_access_key] + type = "string" + redact = true + + [route53.auth_access_secret] + type = "string" + redact = true + + [route53.private_zone] + type = "string" + redact = true + + [route53.auth_username] + type = "string" + redact = true + + [route53.auth_token] + type = "string" + redact = true + +[safedns] + [safedns.auth_token] + type = "string" + redact = true + +[sakuracloud] + [sakuracloud.auth_token] + type = "string" + redact = true + + [sakuracloud.auth_secret] + type = "string" + redact = true + +[softlayer] + [softlayer.auth_username] + type = "string" + redact = true + + [softlayer.auth_api_key] + type = "string" + redact = true + +[transip] + [transip.auth_username] + type = "string" + redact = true + + [transip.auth_api_key] + type = "string" + redact = true + +[ultradns] + [ultradns.auth_token] + type = "string" + redact = true + + [ultradns.auth_username] + type = "string" + redact = true + + [ultradns.auth_password] + type = "password" + +[vultr] + [vultr.auth_token] + type = "string" + redact = true + +[yandex] + [yandex.auth_token] + type = "string" + redact = true + +[zeit] + [zeit.auth_token] + type = "string" + redact = true + +[zilore] + [zilore.auth_key] + type = "string" + redact = true + +[zonomi] + [zonomy.auth_token] + type = "string" + redact = true + + [zonomy.auth_entrypoint] + type = "string" + redact = true + diff --git a/src/yunohost/__init__.py b/src/__init__.py similarity index 92% rename from src/yunohost/__init__.py rename to src/__init__.py index dad73e2a4..608917185 100644 --- a/src/yunohost/__init__.py +++ b/src/__init__.py @@ -22,7 +22,14 @@ def cli(debug, quiet, output_as, timeout, args, parser): if not is_installed(): check_command_is_valid_before_postinstall(args) - ret = moulinette.cli(args, output_as=output_as, timeout=timeout, top_parser=parser) + ret = moulinette.cli( + args, + actionsmap="/usr/share/yunohost/actionsmap.yml", + locales_dir="/usr/share/yunohost/locales/", + output_as=output_as, + timeout=timeout, + top_parser=parser, + ) sys.exit(ret) @@ -39,6 +46,8 @@ def api(debug, host, port): ret = moulinette.api( host=host, port=port, + actionsmap="/usr/share/yunohost/actionsmap.yml", + locales_dir="/usr/share/yunohost/locales/", routes={("GET", "/installed"): is_installed_api}, ) sys.exit(ret) @@ -78,7 +87,7 @@ def init(interface="cli", debug=False, quiet=False, logdir="/var/log/yunohost"): def init_i18n(): # This should only be called when not willing to go through moulinette.cli # or moulinette.api but still willing to call m18n.n/g... - m18n.load_namespace("yunohost") + m18n.set_locales_dir("/usr/share/yunohost/locales/") m18n.set_locale(get_locale()) diff --git a/src/yunohost/app.py b/src/app.py similarity index 50% rename from src/yunohost/app.py rename to src/app.py index f3d891081..3b60deb6c 100644 --- a/src/yunohost/app.py +++ b/src/app.py @@ -31,15 +31,12 @@ import yaml import time import re import subprocess -import glob -import urllib.parse import tempfile from collections import OrderedDict +from typing import List, Tuple, Dict, Any 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,151 +45,84 @@ from moulinette.utils.filesystem import ( read_yaml, write_to_file, write_to_json, - write_to_yaml, - mkdir, + cp, + rm, + chown, + chmod, ) -from yunohost.service import service_status, _run_service_command from yunohost.utils import packages +from yunohost.utils.config import ( + ConfigPanel, + ask_questions_and_parse_answers, + DomainQuestion, + PathQuestion, + hydrate_questions_with_choices, +) +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 ( # noqa + app_catalog, + app_search, + _load_apps_catalog, +) 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]*))?$" ) +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)?/?$" +) -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"]} +APP_FILES_TO_COPY = [ + "manifest.json", + "manifest.toml", + "actions.json", + "actions.toml", + "config_panel.toml", + "scripts", + "conf", + "hooks", + "doc", +] -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): +def app_list(full=False, upgradable=False): """ List installed apps """ - # Old legacy argument ... app_list was a combination of app_list and - # app_catalog before 3.8 ... - if installed: - logger.warning( - "Argument --installed ain't needed anymore when using 'yunohost app list'. It directly returns the list of installed apps.." - ) - - # Filter is a deprecated option... - if filter: - logger.warning( - "Using -f $appname in 'yunohost app list' is deprecated. Just use 'yunohost app list | grep -q 'id: $appname' to check a specific app is installed" - ) - out = [] for app_id in sorted(_installed_apps()): - - if filter and not app_id.startswith(filter): - continue - try: - app_info_dict = app_info(app_id, full=full) + app_info_dict = app_info(app_id, full=full, upgradable=upgradable) except Exception as e: - logger.error("Failed to read info for %s : %s" % (app_id, e)) + logger.error(f"Failed to read info for {app_id} : {e}") continue app_info_dict["id"] = app_id + if upgradable and app_info_dict.get("upgradable") != "yes": + continue out.append(app_info_dict) return {"apps": out} -def app_info(app, full=False): +def app_info(app, full=False, upgradable=False): """ Get info for a specific app """ from yunohost.permission import user_permission_list + from yunohost.domain import domain_config_get - if not _is_installed(app): - raise YunohostValidationError( - "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() - ) + _assert_is_installed(app) setting_path = os.path.join(APPS_SETTING_PATH, app) local_manifest = _get_manifest_of_app(setting_path) @@ -211,6 +141,25 @@ def app_info(app, full=False): if "domain" in settings and "path" in settings: ret["domain_path"] = settings["domain"] + settings["path"] + if not upgradable and not full: + return ret + + absolute_app_name, _ = _parse_app_instance_name(app) + from_catalog = _load_apps_catalog()["apps"].get(absolute_app_name, {}) + + ret["upgradable"] = _app_upgradable({**ret, "from_catalog": from_catalog}) + + if ret["upgradable"] == "yes": + ret["current_version"] = ret.get("version", "?") + ret["new_version"] = from_catalog.get("manifest", {}).get("version", "?") + + if ret["current_version"] == ret["new_version"]: + current_revision = settings.get("current_revision", "?")[:7] + new_revision = from_catalog.get("git", {}).get("revision", "?")[:7] + + ret["current_version"] = f" ({current_revision})" + ret["new_version"] = f" ({new_revision})" + if not full: return ret @@ -221,9 +170,15 @@ def app_info(app, full=False): ) ret["settings"] = settings - 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["from_catalog"] = from_catalog + + ret["is_webapp"] = "domain" in settings and "path" in settings + + if ret["is_webapp"]: + ret["is_default"] = ( + domain_config_get(settings["domain"], "feature.app.default_app") == app + ) + ret["supports_change_url"] = os.path.exists( os.path.join(setting_path, "scripts", "change_url") ) @@ -233,12 +188,16 @@ def app_info(app, full=False): ret["supports_multi_instance"] = is_true( local_manifest.get("multi_instance", False) ) + ret["supports_config_panel"] = os.path.exists( + os.path.join(setting_path, "config_panel.toml") + ) ret["permissions"] = permissions ret["label"] = permissions.get(app + ".main", {}).get("label") if not ret["label"]: - logger.warning("Failed to get label for app %s ?" % app) + logger.warning(f"Failed to get label for app {app} ?") + ret["label"] = local_manifest["name"] return ret @@ -334,8 +293,8 @@ def app_map(app=None, raw=False, user=None): permissions = user_permission_list(full=True, absolute_urls=True, apps=apps)[ "permissions" ] - for app_id in apps: - app_settings = _get_app_settings(app_id) + for app in apps: + app_settings = _get_app_settings(app) if not app_settings: continue if "domain" not in app_settings: @@ -351,20 +310,19 @@ def app_map(app=None, raw=False, user=None): continue # Users must at least have access to the main permission to have access to extra permissions if user: - if not app_id + ".main" in permissions: + if not app + ".main" in permissions: logger.warning( - "Uhoh, no main permission was found for app %s ... sounds like an app was only partially removed due to another bug :/" - % app_id + f"Uhoh, no main permission was found for app {app} ... sounds like an app was only partially removed due to another bug :/" ) continue - main_perm = permissions[app_id + ".main"] + main_perm = permissions[app + ".main"] if user not in main_perm["corresponding_users"]: continue this_app_perms = { p: i for p, i in permissions.items() - if p.startswith(app_id + ".") and (i["url"] or i["additional_urls"]) + if p.startswith(app + ".") and (i["url"] or i["additional_urls"]) } for perm_name, perm_info in this_app_perms.items(): @@ -404,7 +362,7 @@ def app_map(app=None, raw=False, user=None): perm_path = "/" if perm_domain not in result: result[perm_domain] = {} - result[perm_domain][perm_path] = {"label": perm_label, "id": app_id} + result[perm_domain][perm_path] = {"label": perm_label, "id": app} return result @@ -421,6 +379,7 @@ def app_change_url(operation_logger, app, domain, path): """ from yunohost.hook import hook_exec, hook_callback + from yunohost.service import service_reload_or_restart installed = _is_installed(app) if not installed: @@ -437,25 +396,27 @@ 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( "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 + ) - 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, 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 @@ -466,13 +427,12 @@ def app_change_url(operation_logger, app, domain, path): operation_logger.extra.update({"env": env_dict}) operation_logger.start() - tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) change_url_script = os.path.join(tmp_workdir_for_app, "scripts/change_url") # Execute App change_url script ret = hook_exec(change_url_script, env=env_dict)[0] if ret != 0: - msg = "Failed to change '%s' url." % app + msg = f"Failed to change '{app}' url." logger.error(msg) operation_logger.error(msg) @@ -489,15 +449,7 @@ def app_change_url(operation_logger, app, domain, path): app_ssowatconf() - # avoid common mistakes - if _run_service_command("reload", "nginx") is False: - # grab nginx errors - # the "exit 0" is here to avoid check_output to fail because 'nginx -t' - # will return != 0 since we are in a failed state - nginx_errors = check_output("nginx -t; exit 0") - raise YunohostError( - "app_change_url_failed_nginx_reload", nginx_errors=nginx_errors - ) + service_reload_or_restart("nginx") logger.success(m18n.n("app_change_url_success", app=app, domain=domain, path=path)) @@ -516,9 +468,15 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False """ from packaging import version - from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback + from yunohost.hook import ( + hook_add, + hook_remove, + hook_callback, + hook_exec_with_script_debug_if_failure, + ) 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 @@ -536,10 +494,8 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False apps = [app_ for i, app_ in enumerate(apps) if app_ not in apps[:i]] # Abort if any of those app is in fact not installed.. - for app in [app_ for app_ in apps if not _is_installed(app_)]: - raise YunohostValidationError( - "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() - ) + for app_ in apps: + _assert_is_installed(app_) if len(apps) == 0: raise YunohostValidationError("apps_already_up_to_date") @@ -553,22 +509,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["manifest"]["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 @@ -607,17 +563,15 @@ 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) - # 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, 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) @@ -640,36 +594,18 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False # Execute the app upgrade script upgrade_failed = True try: - upgrade_retcode = hook_exec( - extracted_app_folder + "/scripts/upgrade", env=env_dict - )[0] - - upgrade_failed = True if upgrade_retcode != 0 else False - if upgrade_failed: - error = m18n.n("app_upgrade_script_failed") - logger.error( - m18n.n("app_upgrade_failed", app=app_instance_name, error=error) - ) - failure_message_with_debug_instructions = operation_logger.error(error) - if Moulinette.interface.type != "api": - dump_app_log_extract_for_debugging(operation_logger) - # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception - except (KeyboardInterrupt, EOFError): - upgrade_retcode = -1 - error = m18n.n("operation_interrupted") - logger.error( - m18n.n("app_upgrade_failed", app=app_instance_name, error=error) + ( + upgrade_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( + extracted_app_folder + "/scripts/upgrade", + env=env_dict, + operation_logger=operation_logger, + error_message_if_script_failed=m18n.n("app_upgrade_script_failed"), + error_message_if_failed=lambda e: m18n.n( + "app_upgrade_failed", app=app_instance_name, error=e + ), ) - failure_message_with_debug_instructions = operation_logger.error(error) - # Something wrong happened in Yunohost's code (most probably hook_exec) - except Exception: - import traceback - - error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) - logger.error( - m18n.n("app_install_failed", app=app_instance_name, error=error) - ) - failure_message_with_debug_instructions = operation_logger.error(error) finally: # Whatever happened (install success or failure) we check if it broke the system # and warn the user about it @@ -729,45 +665,21 @@ 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.json", - "config_panel.toml", - "conf", - ]: + # 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) 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) + 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)) @@ -782,17 +694,13 @@ 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"] - - 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) + raw_questions = manifest.get("arguments", {}).get("install", []) + manifest["arguments"]["install"] = hydrate_questions_with_choices(raw_questions) + return manifest @@ -816,7 +724,13 @@ def app_install( force -- Do not ask for confirmation when installing experimental / low-quality apps """ - from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback + from yunohost.hook import ( + hook_add, + hook_remove, + hook_callback, + hook_exec, + hook_exec_with_script_debug_if_failure, + ) from yunohost.log import OperationLogger from yunohost.permission import ( user_permission_list, @@ -825,16 +739,30 @@ 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: + raise YunohostValidationError("disk_space_not_sufficient_install") + + def confirm_install(app): - def confirm_install(confirm): # 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 - if confirm in ["danger", "thirdparty"]: + quality = _app_quality(app) + if quality == "success": + return + + # i18n: confirm_app_install_warning + # i18n: confirm_app_install_danger + # i18n: confirm_app_install_thirdparty + + if quality in ["danger", "thirdparty"]: answer = Moulinette.prompt( - m18n.n("confirm_app_install_" + confirm, answers="Yes, I understand"), + m18n.n("confirm_app_install_" + quality, answers="Yes, I understand"), color="red", ) if answer != "Yes, I understand": @@ -842,51 +770,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"]: @@ -896,11 +786,11 @@ 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 - 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) @@ -911,13 +801,17 @@ 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) + 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_odict, extracted_app_folder) + 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) @@ -928,19 +822,6 @@ def app_install( # We'll check that the app didn't brutally edit some system configuration manually_modified_files_before_install = manually_modified_files() - # Tell the operation_logger to redact all password-type args - # Also redact the % escaped version of the password that might appear in - # the 'args' section of metadata (relevant for password with non-alphanumeric char) - data_to_redact = [ - value[0] for value in args_odict.values() if value[1] == "password" - ] - data_to_redact += [ - urllib.parse.quote(data) - for data in data_to_redact - if urllib.parse.quote(data) != data - ] - operation_logger.data_to_redact.extend(data_to_redact) - operation_logger.related_to = [ s for s in operation_logger.related_to if s[0] != "app" ] @@ -964,23 +845,12 @@ 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.json", - "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) + cp( + f"{extracted_app_folder}/{file_to_copy}", + f"{app_setting_path}/{file_to_copy}", + recursive=True, ) # Initialize the main permission for the app @@ -996,41 +866,33 @@ 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, workdir=extracted_app_folder + ) env_dict_for_logging = env_dict.copy() - for arg_name, arg_value_and_type in args_odict.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[f"YNH_APP_ARG_{question.name.upper()}"] operation_logger.extra.update({"env": env_dict_for_logging}) # Execute the app install script install_failed = True try: - install_retcode = hook_exec( - os.path.join(extracted_app_folder, "scripts/install"), env=env_dict - )[0] - # "Common" app install failure : the script failed and returned exit code != 0 - install_failed = True if install_retcode != 0 else False - if install_failed: - error = m18n.n("app_install_script_failed") - logger.error(m18n.n("app_install_failed", app=app_id, error=error)) - failure_message_with_debug_instructions = operation_logger.error(error) - if Moulinette.interface.type != "api": - dump_app_log_extract_for_debugging(operation_logger) - # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception - except (KeyboardInterrupt, EOFError): - error = m18n.n("operation_interrupted") - logger.error(m18n.n("app_install_failed", app=app_id, error=error)) - failure_message_with_debug_instructions = operation_logger.error(error) - # Something wrong happened in Yunohost's code (most probably hook_exec) - except Exception: - import traceback - - error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) - logger.error(m18n.n("app_install_failed", app=app_id, error=error)) - failure_message_with_debug_instructions = operation_logger.error(error) + ( + install_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( + os.path.join(extracted_app_folder, "scripts/install"), + env=env_dict, + operation_logger=operation_logger, + error_message_if_script_failed=m18n.n("app_install_script_failed"), + error_message_if_failed=lambda e: m18n.n( + "app_install_failed", app=app_id, error=e + ), + ) finally: # If success so far, validate that app didn't break important stuff if not install_failed: @@ -1059,19 +921,16 @@ def app_install( # This option is meant for packagers to debug their apps more easily if no_remove_on_failure: raise YunohostError( - "The installation of %s failed, but was not cleaned up as requested by --no-remove-on-failure." - % app_id, + f"The installation of {app_id} failed, but was not cleaned up as requested by --no-remove-on-failure.", raw_msg=True, ) else: 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, workdir=extracted_app_folder + ) # Execute remove script operation_logger_remove = OperationLogger( @@ -1134,62 +993,15 @@ 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")) hook_callback("post_app_install", env=env_dict) -def dump_app_log_extract_for_debugging(operation_logger): - - with open(operation_logger.log_path, "r") as f: - lines = f.readlines() - - filters = [ - r"set [+-]x$", - r"set [+-]o xtrace$", - r"local \w+$", - r"local legacy_args=.*$", - r".*Helper used in legacy mode.*", - r"args_array=.*$", - r"local -A args_array$", - r"ynh_handle_getopts_args", - r"ynh_script_progression", - ] - - filters = [re.compile(f_) for f_ in filters] - - lines_to_display = [] - for line in lines: - - if ": " not in line.strip(): - continue - - # A line typically looks like - # 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo - # And we just want the part starting by "DEBUG - " - line = line.strip().split(": ", 1)[1] - - if any(filter_.search(line) for filter_ in filters): - continue - - lines_to_display.append(line) - - if line.endswith("+ ynh_exit_properly") or " + ynh_die " in line: - break - elif len(lines_to_display) > 20: - lines_to_display.pop(0) - - logger.warning( - "Here's an extract of the logs before the crash. It might help debugging the error:" - ) - for line in lines_to_display: - logger.info(line) - - @is_unit_operation() def app_remove(operation_logger, app, purge=False): """ @@ -1200,12 +1012,14 @@ 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, permission_delete, permission_sync_to_user, ) + from yunohost.domain import domain_list, domain_config_get, domain_config_set if not _is_installed(app): raise YunohostValidationError( @@ -1230,12 +1044,9 @@ def app_remove(operation_logger, app, purge=False): remove_script = f"{tmp_workdir_for_app}/scripts/remove" 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(purge) + 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}) operation_logger.flush() @@ -1268,70 +1079,16 @@ def app_remove(operation_logger, app, purge=False): hook_remove(app) + for domain in domain_list()["domains"]: + if domain_config_get(domain, "feature.app.default_app") == app: + domain_config_set(domain, "feature.app.default_app", "_none") + permission_sync_to_user() _assert_system_is_sane_for_app(manifest, "post") -def app_addaccess(apps, users=[]): - """ - Grant access right to users (everyone by default) - - Keyword argument: - users - apps - - """ - from yunohost.permission import user_permission_update - - output = {} - for app in apps: - permission = user_permission_update( - app + ".main", add=users, remove="all_users" - ) - output[app] = permission["corresponding_users"] - - return {"allowed_users": output} - - -def app_removeaccess(apps, users=[]): - """ - Revoke access right to users (everyone by default) - - Keyword argument: - users - apps - - """ - from yunohost.permission import user_permission_update - - output = {} - for app in apps: - permission = user_permission_update(app + ".main", remove=users) - output[app] = permission["corresponding_users"] - - return {"allowed_users": output} - - -def app_clearaccess(apps): - """ - Reset access rights for the app - - Keyword argument: - apps - - """ - from yunohost.permission import user_permission_reset - - output = {} - for app in apps: - permission = user_permission_reset(app + ".main") - output[app] = permission["corresponding_users"] - - return {"allowed_users": output} - - @is_unit_operation() -def app_makedefault(operation_logger, app, domain=None): +def app_makedefault(operation_logger, app, domain=None, undo=False): """ Redirect domain root to an app @@ -1340,48 +1097,24 @@ def app_makedefault(operation_logger, app, domain=None): domain """ - from yunohost.domain import domain_list + from yunohost.domain import _assert_domain_exists, domain_config_set app_settings = _get_app_settings(app) app_domain = app_settings["domain"] - app_path = app_settings["path"] if domain is None: domain = app_domain - operation_logger.related_to.append(("domain", domain)) - elif domain not in domain_list()["domains"]: - raise YunohostValidationError("domain_name_unknown", domain=domain) - if "/" in app_map(raw=True)[domain]: - raise YunohostValidationError( - "app_make_default_location_already_used", - app=app, - domain=app_domain, - other_app=app_map(raw=True)[domain]["/"]["id"], - ) + _assert_domain_exists(domain) + + operation_logger.related_to.append(("domain", domain)) operation_logger.start() - # TODO / FIXME : current trick is to add this to conf.json.persisten - # This is really not robust and should be improved - # e.g. have a flag in /etc/yunohost/apps/$app/ to say that this is the - # default app or idk... - if not os.path.exists("/etc/ssowat/conf.json.persistent"): - ssowat_conf = {} + if undo: + domain_config_set(domain, "feature.app.default_app", "_none") else: - ssowat_conf = read_json("/etc/ssowat/conf.json.persistent") - - if "redirected_urls" not in ssowat_conf: - ssowat_conf["redirected_urls"] = {} - - ssowat_conf["redirected_urls"][domain + "/"] = app_domain + app_path - - write_to_json( - "/etc/ssowat/conf.json.persistent", ssowat_conf, sort_keys=True, indent=4 - ) - os.system("chmod 644 /etc/ssowat/conf.json.persistent") - - logger.success(m18n.n("ssowat_conf_updated")) + domain_config_set(domain, "feature.app.default_app", app) def app_setting(app, key, value=None, delete=False): @@ -1417,7 +1150,8 @@ def app_setting(app, key, value=None, delete=False): ) permissions = user_permission_list(full=True, apps=[app])["permissions"] - permission_name = "%s.legacy_%s_uris" % (app, key.split("_")[0]) + key_ = key.split("_")[0] + permission_name = f"{app}.legacy_{key_}_uris" permission = permissions.get(permission_name) # GET @@ -1545,7 +1279,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... @@ -1578,7 +1313,7 @@ def app_ssowatconf(): """ - from yunohost.domain import domain_list, _get_maindomain + from yunohost.domain import domain_list, _get_maindomain, domain_config_get from yunohost.permission import user_permission_list main_domain = _get_maindomain() @@ -1616,6 +1351,23 @@ def app_ssowatconf(): redirected_urls.update(app_settings.get("redirected_urls", {})) redirected_regex.update(app_settings.get("redirected_regex", {})) + from .utils.legacy import ( + translate_legacy_default_app_in_ssowant_conf_json_persistent, + ) + + translate_legacy_default_app_in_ssowant_conf_json_persistent() + + for domain in domains: + default_app = domain_config_get(domain, "feature.app.default_app") + if default_app != "_none" and _is_installed(default_app): + app_settings = _get_app_settings(default_app) + app_domain = app_settings["domain"] + app_path = app_settings["path"] + + # Prevent infinite redirect loop... + if domain + "/" != app_domain + app_path: + redirected_urls[domain + "/"] = app_domain + app_path + # New permission system for perm_name, perm_info in all_permissions.items(): @@ -1657,10 +1409,6 @@ def app_ssowatconf(): write_to_json("/etc/ssowat/conf.json", conf_dict, sort_keys=True, indent=4) - from .utils.legacy import translate_legacy_rules_in_ssowant_conf_json_persistent - - translate_legacy_rules_in_ssowant_conf_json_persistent() - logger.debug(m18n.n("ssowat_conf_generated")) @@ -1704,9 +1452,9 @@ def app_action_run(operation_logger, app, action, args=None): actions = {x["id"]: x for x in actions} if action not in actions: + available_actions = (", ".join(actions.keys()),) raise YunohostValidationError( - "action '%s' not available for app '%s', available actions are: %s" - % (action, app, ", ".join(actions.keys())), + f"action '{action}' not available for app '{app}', available actions are: {available_actions}", raw_msg=True, ) @@ -1715,232 +1463,155 @@ 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) + raw_questions = actions[action].get("arguments", {}) + questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) + args = { + question.name: question.value + for question in questions + if question.value is not None + } + + tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) env_dict = _make_environment_for_app_script( - app, args=args_odict, args_prefix="ACTION_" + app, args=args, args_prefix="ACTION_", workdir=tmp_workdir_for_app ) env_dict["YNH_ACTION"] = action - _, path = tempfile.mkstemp() + _, action_script = tempfile.mkstemp(dir=tmp_workdir_for_app) - with open(path, "w") as script: + with open(action_script, "w") as script: script.write(action_declaration["command"]) - os.chmod(path, 700) - if action_declaration.get("cwd"): cwd = action_declaration["cwd"].replace("$app", app) else: - cwd = os.path.join(APPS_SETTING_PATH, app) + cwd = tmp_workdir_for_app - # FIXME: this should probably be ran in a tmp workdir... - retcode = hook_exec( - path, - env=env_dict, - chdir=cwd, - user=action_declaration.get("user", "root"), - )[0] + try: + retcode = hook_exec( + action_script, + env=env_dict, + chdir=cwd, + user=action_declaration.get("user", "root"), + )[0] + # Calling hook_exec could fail miserably, or get + # manually interrupted (by mistake or because script was stuck) + # In that case we still want to delete the tmp work dir + except (KeyboardInterrupt, EOFError, Exception): + retcode = -1 + import traceback + + logger.error(m18n.n("unexpected_error", error="\n" + traceback.format_exc())) + finally: + shutil.rmtree(tmp_workdir_for_app) if retcode not in action_declaration.get("accepted_return_codes", [0]): - msg = "Error while executing action '%s' of app '%s': return code %s" % ( - action, - app, - retcode, - ) + msg = f"Error while executing action '{action}' of app '{app}': return code {retcode}" operation_logger.error(msg) raise YunohostError(msg, raw_msg=True) - os.remove(path) - operation_logger.success() return logger.success("Action successed!") -# Config panel todo list: -# * docstrings -# * merge translations on the json once the workflow is in place -@is_unit_operation() -def app_config_show_panel(operation_logger, app): - logger.warning(m18n.n("experimental_feature")) - - from yunohost.hook import hook_exec - - # this will take care of checking if the app is installed - app_info_dict = app_info(app) - - operation_logger.start() - config_panel = _get_app_config_panel(app) - config_script = os.path.join(APPS_SETTING_PATH, app, "scripts", "config") - - app_id, app_instance_nb = _parse_app_instance_name(app) - - if not config_panel or not os.path.exists(config_script): - return { - "app_id": app_id, - "app": app, - "app_name": app_info_dict["name"], - "config_panel": [], - } - - env = { - "YNH_APP_ID": app_id, - "YNH_APP_INSTANCE_NAME": app, - "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), - } - - # FIXME: this should probably be ran in a tmp workdir... - return_code, parsed_values = hook_exec( - config_script, args=["show"], env=env, return_format="plain_dict" - ) - - if return_code != 0: - raise Exception( - "script/config show return value code: %s (considered as an error)", - return_code, +def app_config_get(app, key="", full=False, export=False): + """ + Display an app configuration in classic, full or export mode + """ + if full and export: + raise YunohostValidationError( + "You can't use --full and --export together.", raw_msg=True ) - logger.debug("Generating global variables:") - for tab in config_panel.get("panel", []): - tab_id = tab["id"] # this makes things easier to debug on crash - for section in tab.get("sections", []): - section_id = section["id"] - for option in section.get("options", []): - option_name = option["name"] - generated_name = ( - "YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_name) - ).upper() - option["name"] = generated_name - logger.debug( - " * '%s'.'%s'.'%s' -> %s", - tab.get("name"), - section.get("name"), - option.get("name"), - generated_name, + if full: + mode = "full" + elif export: + mode = "export" + else: + mode = "classic" + + config_ = AppConfigPanel(app) + return config_.get(key, mode) + + +@is_unit_operation() +def app_config_set( + operation_logger, app, key=None, value=None, args=None, args_file=None +): + """ + Apply a new app configuration + """ + + config_ = AppConfigPanel(app) + + return config_.set(key, value, args, args_file, operation_logger=operation_logger) + + +class AppConfigPanel(ConfigPanel): + entity_type = "app" + save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml") + config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.toml") + + def _load_current_values(self): + self.values = self._call_config_script("show") + + def _apply(self): + env = {key: str(value) for key, value in self.new_values.items()} + return_content = self._call_config_script("apply", env=env) + + # If the script returned validation error + # raise a ValidationError exception using + # the first key + if return_content: + for key, message in return_content.get("validation_errors").items(): + raise YunohostValidationError( + "app_argument_invalid", + name=key, + error=message, ) - if generated_name in parsed_values: - # code is not adapted for that so we have to mock expected format :/ - if option.get("type") == "boolean": - if parsed_values[generated_name].lower() in ("true", "1", "y"): - option["default"] = parsed_values[generated_name] - else: - del option["default"] - else: - option["default"] = parsed_values[generated_name] + def _call_config_script(self, action, env=None): + from yunohost.hook import hook_exec - args_dict = _parse_args_in_yunohost_format( - {option["name"]: parsed_values[generated_name]}, [option] - ) - option["default"] = args_dict[option["name"]][0] - else: - logger.debug( - "Variable '%s' is not declared by config script, using default", - generated_name, - ) - # do nothing, we'll use the default if present + if env is None: + env = {} - return { - "app_id": app_id, - "app": app, - "app_name": app_info_dict["name"], - "config_panel": config_panel, - "logs": operation_logger.success(), - } + # Add default config script if needed + config_script = os.path.join( + APPS_SETTING_PATH, self.entity, "scripts", "config" + ) + if not os.path.exists(config_script): + logger.debug("Adding a default config script") + default_script = """#!/bin/bash +source /usr/share/yunohost/helpers +ynh_abort_if_errors +ynh_app_config_run $1 +""" + write_to_file(config_script, default_script) - -@is_unit_operation() -def app_config_apply(operation_logger, app, args): - logger.warning(m18n.n("experimental_feature")) - - from yunohost.hook import hook_exec - - installed = _is_installed(app) - if not installed: - raise YunohostValidationError( - "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() + # Call config script to extract current values + logger.debug(f"Calling '{action}' action from config script") + app = self.entity + app_id, app_instance_nb = _parse_app_instance_name(app) + settings = _get_app_settings(app) + env.update( + { + "app_id": app_id, + "app": app, + "app_instance_nb": str(app_instance_nb), + "final_path": settings.get("final_path", ""), + "YNH_APP_BASEDIR": os.path.join(APPS_SETTING_PATH, app), + } ) - config_panel = _get_app_config_panel(app) - config_script = os.path.join(APPS_SETTING_PATH, app, "scripts", "config") - - if not config_panel or not os.path.exists(config_script): - # XXX real exception - raise Exception("Not config-panel.json nor scripts/config") - - operation_logger.start() - app_id, app_instance_nb = _parse_app_instance_name(app) - env = { - "YNH_APP_ID": app_id, - "YNH_APP_INSTANCE_NAME": app, - "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), - } - args = dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {} - - for tab in config_panel.get("panel", []): - tab_id = tab["id"] # this makes things easier to debug on crash - for section in tab.get("sections", []): - section_id = section["id"] - for option in section.get("options", []): - option_name = option["name"] - generated_name = ( - "YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_name) - ).upper() - - if generated_name in args: - logger.debug( - "include into env %s=%s", generated_name, args[generated_name] - ) - env[generated_name] = args[generated_name] - else: - logger.debug("no value for key id %s", generated_name) - - # for debug purpose - for key in args: - if key not in env: - logger.warning( - "Ignore key '%s' from arguments because it is not in the config", key - ) - - # FIXME: this should probably be ran in a tmp workdir... - return_code = hook_exec( - config_script, - args=["apply"], - env=env, - )[0] - - if return_code != 0: - msg = ( - "'script/config apply' return value code: %s (considered as an error)" - % return_code - ) - operation_logger.error(msg) - raise Exception(msg) - - logger.success("Config updated as expected") - return { - "app": app, - "logs": operation_logger.success(), - } - - -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 + ret, values = hook_exec(config_script, args=[action], env=env) + if ret != 0: + if action == "show": + raise YunohostError("app_config_unable_to_read") + else: + raise YunohostError("app_config_unable_to_apply") + return values def _get_app_actions(app_id): @@ -2028,159 +1699,20 @@ def _get_app_actions(app_id): return None -def _get_app_config_panel(app_id): - "Get app config panel stored in json or in toml" - config_panel_toml_path = os.path.join( - APPS_SETTING_PATH, app_id, "config_panel.toml" - ) - config_panel_json_path = os.path.join( - APPS_SETTING_PATH, app_id, "config_panel.json" - ) - - # sample data to get an idea of what is going on - # this toml extract: - # - # version = "0.1" - # name = "Unattended-upgrades configuration panel" - # - # [main] - # name = "Unattended-upgrades configuration" - # - # [main.unattended_configuration] - # name = "50unattended-upgrades configuration file" - # - # [main.unattended_configuration.upgrade_level] - # name = "Choose the sources of packages to automatically upgrade." - # default = "Security only" - # type = "text" - # help = "We can't use a choices field for now. In the meantime please choose between one of this values:
Security only, Security and updates." - # # choices = ["Security only", "Security and updates"] - - # [main.unattended_configuration.ynh_update] - # name = "Would you like to update YunoHost packages automatically ?" - # type = "bool" - # default = true - # - # will be parsed into this: - # - # OrderedDict([(u'version', u'0.1'), - # (u'name', u'Unattended-upgrades configuration panel'), - # (u'main', - # OrderedDict([(u'name', u'Unattended-upgrades configuration'), - # (u'unattended_configuration', - # OrderedDict([(u'name', - # u'50unattended-upgrades configuration file'), - # (u'upgrade_level', - # OrderedDict([(u'name', - # u'Choose the sources of packages to automatically upgrade.'), - # (u'default', - # u'Security only'), - # (u'type', u'text'), - # (u'help', - # u"We can't use a choices field for now. In the meantime please choose between one of this values:
Security only, Security and updates.")])), - # (u'ynh_update', - # OrderedDict([(u'name', - # u'Would you like to update YunoHost packages automatically ?'), - # (u'type', u'bool'), - # (u'default', True)])), - # - # and needs to be converted into this: - # - # {u'name': u'Unattended-upgrades configuration panel', - # u'panel': [{u'id': u'main', - # u'name': u'Unattended-upgrades configuration', - # u'sections': [{u'id': u'unattended_configuration', - # u'name': u'50unattended-upgrades configuration file', - # u'options': [{u'//': u'"choices" : ["Security only", "Security and updates"]', - # u'default': u'Security only', - # u'help': u"We can't use a choices field for now. In the meantime please choose between one of this values:
Security only, Security and updates.", - # u'id': u'upgrade_level', - # u'name': u'Choose the sources of packages to automatically upgrade.', - # u'type': u'text'}, - # {u'default': True, - # u'id': u'ynh_update', - # u'name': u'Would you like to update YunoHost packages automatically ?', - # u'type': u'bool'}, - - if os.path.exists(config_panel_toml_path): - toml_config_panel = toml.load( - open(config_panel_toml_path, "r"), _dict=OrderedDict - ) - - # transform toml format into json format - config_panel = { - "name": toml_config_panel["name"], - "version": toml_config_panel["version"], - "panel": [], - } - - panels = [ - key_value - for key_value in toml_config_panel.items() - if key_value[0] not in ("name", "version") - and isinstance(key_value[1], OrderedDict) - ] - - for key, value in panels: - panel = { - "id": key, - "name": value["name"], - "sections": [], - } - - sections = [ - k_v1 - for k_v1 in value.items() - if k_v1[0] not in ("name",) and isinstance(k_v1[1], OrderedDict) - ] - - for section_key, section_value in sections: - section = { - "id": section_key, - "name": section_value["name"], - "options": [], - } - - options = [ - k_v - for k_v in section_value.items() - if k_v[0] not in ("name",) and isinstance(k_v[1], OrderedDict) - ] - - for option_key, option_value in options: - option = dict(option_value) - option["name"] = option_key - option["ask"] = {"en": option["ask"]} - if "help" in option: - option["help"] = {"en": option["help"]} - section["options"].append(option) - - panel["sections"].append(section) - - config_panel["panel"].append(panel) - - return config_panel - - elif os.path.exists(config_panel_json_path): - return json.load(open(config_panel_json_path)) - - return None - - -def _get_app_settings(app_id): +def _get_app_settings(app): """ Get settings of an installed app Keyword arguments: - app_id -- The app id + app -- The app id (like nextcloud__2) """ - if not _is_installed(app_id): + if not _is_installed(app): raise YunohostValidationError( - "app_not_installed", app=app_id, all_apps=_get_all_installed_apps_id() + "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() ) try: - with open(os.path.join(APPS_SETTING_PATH, app_id, "settings.yml")) as f: + with open(os.path.join(APPS_SETTING_PATH, app, "settings.yml")) as f: settings = yaml.safe_load(f) # If label contains unicode char, this may later trigger issues when building strings... # FIXME: this should be propagated to read_yaml so that this fix applies everywhere I think... @@ -2198,77 +1730,28 @@ def _get_app_settings(app_id): or not settings.get("path", "/").startswith("/") ): settings["path"] = "/" + settings["path"].strip("/") - _set_app_settings(app_id, settings) + _set_app_settings(app, settings) - if app_id == settings["id"]: + if app == settings["id"]: return settings except (IOError, TypeError, KeyError): - logger.error(m18n.n("app_not_correctly_installed", app=app_id)) + logger.error(m18n.n("app_not_correctly_installed", app=app)) return {} -def _set_app_settings(app_id, settings): +def _set_app_settings(app, settings): """ Set settings of an app Keyword arguments: - app_id -- The app id + app_id -- The app id (like nextcloud__2) settings -- Dict with app settings """ - with open(os.path.join(APPS_SETTING_PATH, app_id, "settings.yml"), "w") as f: + with open(os.path.join(APPS_SETTING_PATH, app, "settings.yml"), "w") as f: 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" @@ -2395,8 +1878,7 @@ def _get_manifest_of_app(path): manifest = read_json(os.path.join(path, "manifest.json")) else: raise YunohostError( - "There doesn't seem to be any manifest file in %s ... It looks like an app was not correctly installed/removed." - % path, + f"There doesn't seem to be any manifest file in {path} ... It looks like an app was not correctly installed/removed.", raw_msg=True, ) @@ -2451,149 +1933,207 @@ def _set_default_ask_questions(arguments): for question in questions_with_default ): # The key is for example "app_manifest_install_ask_domain" - key = "app_manifest_%s_ask_%s" % (script_name, arg["name"]) + arg_name = arg["name"] + key = f"app_manifest_{script_name}_ask_{arg_name}" arg["ask"] = m18n.n(key) + # Also it in fact doesn't make sense for any of those questions to have an example value nor a default value... + if arg.get("type") in ["domain", "user", "password"]: + if "example" in arg: + del arg["example"] + if "default" in arg: + del arg["default"] + return arguments -def _get_git_last_commit_hash(repository, reference="HEAD"): - """ - Attempt to retrieve the last commit hash of a git repository +def _is_app_repo_url(string: str) -> bool: - Keyword arguments: - repository -- The URL or path of the repository + string = string.strip() + # Dummy test for ssh-based stuff ... should probably be improved somehow + if "@" in string: + return True + + return bool(APP_REPO_URL.match(string)) + + +def _app_quality(src: str) -> str: """ - 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") + app may in fact be an app name, an url, or a path + """ + + raw_app_catalog = _load_apps_catalog()["apps"] + if src in raw_app_catalog or _is_app_repo_url(src): + + # If we got an app name directly (e.g. just "wordpress"), we gonna test this name + if src in raw_app_catalog: + app_name_to_test = src + # 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 src) or ("https://" in src): + app_name_to_test = src.strip("/").split("/")[-1].replace("_ynh", "") + else: + # FIXME : watdo if '@' in app ? + return "thirdparty" + + if app_name_to_test in raw_app_catalog: + + state = raw_app_catalog[app_name_to_test].get("state", "notworking") + level = raw_app_catalog[app_name_to_test].get("level", None) + if state in ["working", "validated"]: + 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(src): + return "thirdparty" else: - return commit.strip() + 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") -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 _is_app_repo_url(src): + url = src.strip().strip("/") + branch = "master" + revision = "HEAD" + # gitlab urls may look like 'https://domain/org/group/repo/-/tree/testing' + # compated to github urls looking like 'https://domain/org/repo/tree/testing' + if "/-/" in url: + url = url.replace("/-/", "/") + if "/tree/" in url: + url, branch = url.split("/tree/", 1) + return _extract_app_from_gitrepo(url, branch, revision, {}) + # App is a local folder + 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") + + +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 os.path.isdir(path): + shutil.rmtree(extracted_app_folder) + if path[-1] != "/": + path = path + "/" + cp(path, extracted_app_folder, recursive=True) + else: + try: + shutil.unpack_archive(path, extracted_app_folder) + except Exception: + 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(f"cannot get last commit hash because: {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) +# +# ############################### # +# Small utilities # +# ############################### # +# -def _is_installed(app): +def _is_installed(app: str) -> bool: """ Check if application is installed @@ -2607,35 +2147,34 @@ def _is_installed(app): return os.path.isdir(APPS_SETTING_PATH + app) -def _installed_apps(): +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() -> List[str]: return os.listdir(APPS_SETTING_PATH) -def _value_for_locale(values): +def _get_all_installed_apps_id(): """ - Return proper value for current locale - - Keyword arguments: - values -- A dict of values associated to their locale - - Returns: - An utf-8 encoded string - + Return something like: + ' * app1 + * app2 + * ...' """ - if not isinstance(values, dict): - return values - for lang in [m18n.locale, m18n.default_locale]: - try: - return values[lang] - except KeyError: - continue + all_apps_ids = sorted(_installed_apps()) - # Fallback to first value - return list(values.values())[0] + 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, app_instance_name): +def _check_manifest_requirements(manifest: Dict): """Check if required packages are met from the manifest""" packaging_format = int(manifest.get("packaging_format", 0)) @@ -2647,7 +2186,9 @@ def _check_manifest_requirements(manifest, app_instance_name): if not requirements: return - logger.debug(m18n.n("app_requirements_checking", app=app_instance_name)) + app = manifest.get("id", "?") + + logger.debug(m18n.n("app_requirements_checking", app=app)) # Iterate over requirements for pkgname, spec in requirements.items(): @@ -2658,374 +2199,31 @@ def _check_manifest_requirements(manifest, app_instance_name): pkgname=pkgname, version=version, spec=spec, - app=app_instance_name, + app=app, ) -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) - - -class Question: - "empty class to store questions information" - - -class YunoHostArgumentFormatParser(object): - hide_user_input_in_prompt = False - - def parse_question(self, question, user_answers): - parsed_question = Question() - - parsed_question.name = question["name"] - parsed_question.default = question.get("default", None) - parsed_question.choices = question.get("choices", []) - parsed_question.optional = question.get("optional", False) - parsed_question.ask = question.get("ask") - parsed_question.value = user_answers.get(parsed_question.name) - - if parsed_question.ask is None: - parsed_question.ask = "Enter value for '%s':" % parsed_question.name - - # Empty value is parsed as empty string - if parsed_question.default == "": - parsed_question.default = None - - return parsed_question - - def parse(self, question, user_answers): - question = self.parse_question(question, user_answers) - - if question.value is None: - text_for_user_input_in_cli = self._format_text_for_user_input_in_cli( - question - ) - - try: - question.value = Moulinette.prompt( - text_for_user_input_in_cli, self.hide_user_input_in_prompt - ) - except NotImplementedError: - question.value = None - - # we don't have an answer, check optional and default_value - if question.value is None or question.value == "": - if not question.optional and question.default is None: - raise YunohostValidationError( - "app_argument_required", name=question.name - ) - else: - question.value = ( - getattr(self, "default_value", None) - if question.default is None - else question.default - ) - - # we have an answer, do some post checks - if question.value is not None: - if question.choices and question.value not in question.choices: - self._raise_invalid_answer(question) - - # this is done to enforce a certain formating like for boolean - # by default it doesn't do anything - question.value = self._post_parse_value(question) - - return (question.value, self.argument_type) - - def _raise_invalid_answer(self, question): - raise YunohostValidationError( - "app_argument_choice_invalid", - name=question.name, - choices=", ".join(question.choices), - ) - - def _format_text_for_user_input_in_cli(self, question): - text_for_user_input_in_cli = _value_for_locale(question.ask) - - if question.choices: - text_for_user_input_in_cli += " [{0}]".format(" | ".join(question.choices)) - - if question.default is not None: - text_for_user_input_in_cli += " (default: {0})".format(question.default) - - return text_for_user_input_in_cli - - def _post_parse_value(self, question): - return question.value - - -class StringArgumentParser(YunoHostArgumentFormatParser): - argument_type = "string" - default_value = "" - - -class PasswordArgumentParser(YunoHostArgumentFormatParser): - hide_user_input_in_prompt = True - argument_type = "password" - default_value = "" - forbidden_chars = "{}" - - def parse_question(self, question, user_answers): - question = super(PasswordArgumentParser, self).parse_question( - question, user_answers - ) - - if question.default is not None: - raise YunohostValidationError( - "app_argument_password_no_default", name=question.name - ) - - return question - - def _post_parse_value(self, question): - if any(char in question.value for char in self.forbidden_chars): - raise YunohostValidationError( - "pattern_password_app", forbidden_chars=self.forbidden_chars - ) - - # If it's an optional argument the value should be empty or strong enough - if not question.optional or question.value: - from yunohost.utils.password import assert_password_is_strong_enough - - assert_password_is_strong_enough("user", question.value) - - return super(PasswordArgumentParser, self)._post_parse_value(question) - - -class PathArgumentParser(YunoHostArgumentFormatParser): - argument_type = "path" - default_value = "" - - -class BooleanArgumentParser(YunoHostArgumentFormatParser): - argument_type = "boolean" - default_value = False - - def parse_question(self, question, user_answers): - question = super(BooleanArgumentParser, self).parse_question( - question, user_answers - ) - - if question.default is None: - question.default = False - - return question - - def _format_text_for_user_input_in_cli(self, question): - text_for_user_input_in_cli = _value_for_locale(question.ask) - - text_for_user_input_in_cli += " [yes | no]" - - if question.default is not None: - formatted_default = "yes" if question.default else "no" - text_for_user_input_in_cli += " (default: {0})".format(formatted_default) - - return text_for_user_input_in_cli - - def _post_parse_value(self, question): - if isinstance(question.value, bool): - return 1 if question.value else 0 - - if str(question.value).lower() in ["1", "yes", "y", "true"]: - return 1 - - if str(question.value).lower() in ["0", "no", "n", "false"]: - return 0 - - raise YunohostValidationError( - "app_argument_choice_invalid", - name=question.name, - choices="yes, no, y, n, 1, 0", - ) - - -class DomainArgumentParser(YunoHostArgumentFormatParser): - argument_type = "domain" - - def parse_question(self, question, user_answers): - from yunohost.domain import domain_list, _get_maindomain - - question = super(DomainArgumentParser, self).parse_question( - question, user_answers - ) - - if question.default is None: - question.default = _get_maindomain() - - question.choices = domain_list()["domains"] - - return question - - def _raise_invalid_answer(self, question): - raise YunohostValidationError( - "app_argument_invalid", name=question.name, error=m18n.n("domain_unknown") - ) - - -class UserArgumentParser(YunoHostArgumentFormatParser): - argument_type = "user" - - def parse_question(self, question, user_answers): - from yunohost.user import user_list, user_info - from yunohost.domain import _get_maindomain - - question = super(UserArgumentParser, self).parse_question( - question, user_answers - ) - question.choices = user_list()["users"] - if question.default is None: - root_mail = "root@%s" % _get_maindomain() - for user in question.choices.keys(): - if root_mail in user_info(user).get("mail-aliases", []): - question.default = user - break - - return question - - def _raise_invalid_answer(self, question): - raise YunohostValidationError( - "app_argument_invalid", - name=question.name, - error=m18n.n("user_unknown", user=question.value), - ) - - -class NumberArgumentParser(YunoHostArgumentFormatParser): - argument_type = "number" - default_value = "" - - def parse_question(self, question, user_answers): - question = super(NumberArgumentParser, self).parse_question( - question, user_answers - ) - - if question.default is None: - question.default = 0 - - return question - - def _post_parse_value(self, question): - if isinstance(question.value, int): - return super(NumberArgumentParser, self)._post_parse_value(question) - - if isinstance(question.value, str) and question.value.isdigit(): - return int(question.value) - - raise YunohostValidationError( - "app_argument_invalid", name=question.name, error=m18n.n("invalid_number") - ) - - -class DisplayTextArgumentParser(YunoHostArgumentFormatParser): - argument_type = "display_text" - - def parse(self, question, user_answers): - print(question["ask"]) - - -ARGUMENTS_TYPE_PARSERS = { - "string": StringArgumentParser, - "password": PasswordArgumentParser, - "path": PathArgumentParser, - "boolean": BooleanArgumentParser, - "domain": DomainArgumentParser, - "user": UserArgumentParser, - "number": NumberArgumentParser, - "display_text": DisplayTextArgumentParser, -} - - -def _parse_args_in_yunohost_format(user_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) - 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: - parser = ARGUMENTS_TYPE_PARSERS[question.get("type", "string")]() - - answer = parser.parse(question=question, user_answers=user_answers) - if answer is not None: - parsed_answers_dict[question["name"]] = answer - - return parsed_answers_dict - - -def _validate_and_normalize_webpath(args_dict, app_folder): +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_args = [ - (name, value[0]) for name, value in args_dict.items() if value[1] == "domain" + 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_args = [ - (name, value[0]) for name, value in args_dict.items() if value[1] == "path" + path_questions = [ + question for question in raw_questions if question.get("type") == "path" ] - if len(domain_args) == 1 and len(path_args) == 1: - - domain = domain_args[0][1] - path = path_args[0][1] - domain, path = _normalize_domain_path(domain, 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 "" + 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 @@ -3035,36 +2233,30 @@ 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 - ) and re.search( - r"(ynh_webpath_register|yunohost app checkurl)", install_script_content - ): + r"\npath(_url)?=[\"']?/[\"']?", install_script_content + ) and re.search(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 _normalize_domain_path(domain, path): +def _validate_webpath_requirement( + args: Dict[str, Any], path_requirement: str, ignore_app=None +) -> None: - # We want url to be of the format : - # some.domain.tld/foo + domain = args.get("domain") + path = args.get("path") - # Remove http/https prefix if it's there - if domain.startswith("https://"): - domain = domain[len("https://") :] - elif domain.startswith("http://"): - domain = domain[len("http://") :] + if path_requirement == "domain_and_path": + _assert_no_conflicting_apps(domain, path, ignore_app=ignore_app) - # Remove trailing slashes - domain = domain.rstrip("/").lower() - path = "/" + path.strip("/") - - return domain, path + elif path_requirement == "full_domain": + _assert_no_conflicting_apps( + domain, "/", full_domain=True, ignore_app=ignore_app + ) def _get_conflicting_apps(domain, path, ignore_app=None): @@ -3077,13 +2269,13 @@ def _get_conflicting_apps(domain, path, ignore_app=None): ignore_app -- An optional app id to ignore (c.f. the change_url usecase) """ - from yunohost.domain import domain_list + 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 - if domain not in domain_list()["domains"]: - raise YunohostValidationError("domain_name_unknown", domain=domain) + _assert_domain_exists(domain) # Fetch apps map apps_map = app_map(raw=True) @@ -3112,14 +2304,7 @@ def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False if conflicts: apps = [] for path, app_id, app_label in conflicts: - apps.append( - " * {domain:s}{path:s} → {app_label:s} ({app_id:s})".format( - domain=domain, - path=path, - app_id=app_id, - app_label=app_label, - ) - ) + apps.append(f" * {domain}{path} → {app_label} ({app_id})") if full_domain: raise YunohostValidationError("app_full_domain_unavailable", domain=domain) @@ -3129,7 +2314,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_"): +def _make_environment_for_app_script( + app, args={}, args_prefix="APP_ARG_", workdir=None +): app_setting_path = os.path.join(APPS_SETTING_PATH, app) @@ -3141,216 +2328,63 @@ def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"): "YNH_APP_INSTANCE_NAME": app, "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), "YNH_APP_MANIFEST_VERSION": manifest.get("version", "?"), + "YNH_ARCH": check_output("dpkg --print-architecture"), } - 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] - ) + if workdir: + env_dict["YNH_APP_BASEDIR"] = workdir + + for arg_name, arg_value in args.items(): + arg_name_upper = arg_name.upper() + env_dict[f"YNH_{args_prefix}{arg_name_upper}"] = str(arg_value) 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 - >>> _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, "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) -# -# ############################### # -# Applications list management # -# ############################### # -# +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}__")] -def _initialize_apps_catalog_system(): - """ - This function is meant to intialize the apps_catalog system with YunoHost's default app catalog. - """ + # Find the list of ids, such as [1, 2, 4] + sibling_ids = [_parse_app_instance_name(a)[1] for a in sibling_app_ids] - 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 # -# ############################### # -# + # 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 def _make_tmp_workdir_for_app(app=None): @@ -3399,20 +2433,26 @@ def is_true(arg): elif isinstance(arg, str): return arg.lower() in ["yes", "true", "on"] else: - logger.debug("arg should be a boolean or a string, got %r", arg) + logger.debug(f"arg should be a boolean or a string, got {arg}") return True if arg else False def unstable_apps(): output = [] + deprecated_apps = ["mailman"] for infos in app_list(full=True)["apps"]: - if not infos.get("from_catalog") or infos.get("from_catalog").get("state") in [ - "inprogress", - "notworking", - ]: + if ( + not infos.get("from_catalog") + or infos.get("from_catalog").get("state") + in [ + "inprogress", + "notworking", + ] + or infos["id"] in deprecated_apps + ): output.append(infos["id"]) return output @@ -3420,14 +2460,16 @@ def unstable_apps(): def _assert_system_is_sane_for_app(manifest, when): + from yunohost.service import service_status + logger.debug("Checking that required services are up and running...") services = manifest.get("services", []) - # Some apps use php-fpm or php5-fpm which is now php7.0-fpm + # Some apps use php-fpm, php5-fpm or php7.x-fpm which is now php7.4-fpm def replace_alias(service): - if service in ["php-fpm", "php5-fpm", "php7.0-fpm"]: - return "php7.3-fpm" + if service in ["php-fpm", "php5-fpm", "php7.0-fpm", "php7.3-fpm"]: + return "php7.4-fpm" else: return service @@ -3436,7 +2478,7 @@ def _assert_system_is_sane_for_app(manifest, when): # We only check those, mostly to ignore "custom" services # (added by apps) and because those are the most popular # services - service_filter = ["nginx", "php7.3-fpm", "mysql", "postfix"] + service_filter = ["nginx", "php7.4-fpm", "mysql", "postfix"] services = [str(s) for s in services if s in service_filter] if "nginx" not in services: @@ -3446,6 +2488,7 @@ def _assert_system_is_sane_for_app(manifest, when): # Wait if a service is reloading test_nb = 0 + while test_nb < 16: if not any(s for s in services if service_status(s)["status"] == "reloading"): break @@ -3476,213 +2519,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/app_catalog.py b/src/app_catalog.py new file mode 100644 index 000000000..5ae8ef30b --- /dev/null +++ b/src/app_catalog.py @@ -0,0 +1,241 @@ +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" + + +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( + f"Could not initialize the apps catalog system... : {e}", raw_msg=True + ) + + 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( + f"Could not read the apps_catalog list ... : {e}", raw_msg=True + ) + + +def _actual_apps_catalog_api_url(base_url): + + return f"{base_url}/v{APPS_CATALOG_API_VERSION}/apps.json" + + +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 = f"{APPS_CATALOG_CACHE}/{apps_catalog_id}.json" + try: + write_to_json(cache_file, apps_catalog_content) + except Exception as e: + raise YunohostError( + f"Unable to write cache data for {apps_catalog_id} apps_catalog : {e}", + raw_msg=True, + ) + + 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 = f"{APPS_CATALOG_CACHE}/{apps_catalog_id}.json" + + try: + apps_catalog_content = ( + read_json(cache_file) if os.path.exists(cache_file) else None + ) + except Exception as e: + raise YunohostError( + f"Unable to read cache for apps_catalog {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"]: + other_catalog = merged_catalog["apps"][app]["repository"] + logger.warning( + f"Duplicate app {app} found between apps catalog {apps_catalog_id} and {other_catalog}" + ) + continue + + info["repository"] = apps_catalog_id + merged_catalog["apps"][app] = info + + # Annnnd categories + merged_catalog["categories"] += apps_catalog_content["categories"] + + return merged_catalog diff --git a/src/yunohost/authenticators/ldap_admin.py b/src/authenticators/ldap_admin.py similarity index 51% rename from src/yunohost/authenticators/ldap_admin.py rename to src/authenticators/ldap_admin.py index 94d68a8db..872dd3c8d 100644 --- a/src/yunohost/authenticators/ldap_admin.py +++ b/src/authenticators/ldap_admin.py @@ -8,10 +8,14 @@ import time from moulinette import m18n from moulinette.authentication import BaseAuthenticator -from yunohost.utils.error import YunohostError +from moulinette.utils.text import random_ascii + +from yunohost.utils.error import YunohostError, YunohostAuthenticationError logger = logging.getLogger("yunohost.authenticators.ldap_admin") +session_secret = random_ascii() + class Authenticator(BaseAuthenticator): @@ -66,3 +70,59 @@ class Authenticator(BaseAuthenticator): # Free the connection, we don't really need it to keep it open as the point is only to check authentication... if con: con.unbind_s() + + def set_session_cookie(self, infos): + + from bottle import response + + assert isinstance(infos, dict) + + # This allows to generate a new session id or keep the existing one + current_infos = self.get_session_cookie(raise_if_no_session_exists=False) + new_infos = {"id": current_infos["id"]} + new_infos.update(infos) + + response.set_cookie( + "yunohost.admin", + new_infos, + secure=True, + secret=session_secret, + httponly=True, + # samesite="strict", # Bottle 0.12 doesn't support samesite, to be added in next versions + ) + + def get_session_cookie(self, raise_if_no_session_exists=True): + + from bottle import request + + try: + # N.B. : here we implicitly reauthenticate the cookie + # because it's signed via the session_secret + # If no session exists (or if session is invalid?) + # it's gonna return the default empty dict, + # which we interpret as an authentication failure + infos = request.get_cookie( + "yunohost.admin", secret=session_secret, default={} + ) + except Exception: + if not raise_if_no_session_exists: + return {"id": random_ascii()} + raise YunohostAuthenticationError("unable_authenticate") + + if not infos and raise_if_no_session_exists: + raise YunohostAuthenticationError("unable_authenticate") + + if "id" not in infos: + infos["id"] = random_ascii() + + # FIXME: Here, maybe we want to re-authenticate the session via the authenticator + # For example to check that the username authenticated is still in the admin group... + + return infos + + def delete_session_cookie(self): + + from bottle import response + + response.set_cookie("yunohost.admin", "", max_age=-1) + response.delete_cookie("yunohost.admin") diff --git a/src/yunohost/backup.py b/src/backup.py similarity index 95% rename from src/yunohost/backup.py rename to src/backup.py index 09b35cb67..bba60b895 100644 --- a/src/yunohost/backup.py +++ b/src/backup.py @@ -44,15 +44,11 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, mkdir, write_to_yaml, read_yaml from moulinette.utils.process import check_output +import yunohost.domain from yunohost.app import ( app_info, _is_installed, _make_environment_for_app_script, - dump_app_log_extract_for_debugging, - _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 ( @@ -60,6 +56,7 @@ from yunohost.hook import ( hook_info, hook_callback, hook_exec, + hook_exec_with_script_debug_if_failure, CUSTOM_HOOK_FOLDER, ) from yunohost.tools import ( @@ -75,7 +72,7 @@ from yunohost.utils.filesystem import free_space_in_directory from yunohost.settings import settings_get BACKUP_PATH = "/home/yunohost.backup" -ARCHIVES_PATH = "%s/archives" % BACKUP_PATH +ARCHIVES_PATH = f"{BACKUP_PATH}/archives" APP_MARGIN_SPACE_SIZE = 100 # In MB CONF_MARGIN_SPACE_SIZE = 10 # IN MB POSTINSTALL_ESTIMATE_SPACE_SIZE = 5 # In MB @@ -83,7 +80,7 @@ MB_ALLOWED_TO_ORGANIZE = 10 logger = getActionLogger("yunohost.backup") -class BackupRestoreTargetsManager(object): +class BackupRestoreTargetsManager: """ BackupRestoreTargetsManager manage the targets @@ -405,7 +402,7 @@ class BackupManager: # backup and restore scripts for app in target_list: - app_script_folder = "/etc/yunohost/apps/%s/scripts" % app + app_script_folder = f"/etc/yunohost/apps/{app}/scripts" backup_script_path = os.path.join(app_script_folder, "backup") restore_script_path = os.path.join(app_script_folder, "restore") @@ -558,7 +555,7 @@ class BackupManager: self._compute_backup_size() # Create backup info file - with open("%s/info.json" % self.work_dir, "w") as f: + with open(f"{self.work_dir}/info.json", "w") as f: f.write(json.dumps(self.info)) def _get_env_var(self, app=None): @@ -707,6 +704,9 @@ class BackupManager: # Prepare environment env_dict = self._get_env_var(app) + env_dict["YNH_APP_BASEDIR"] = os.path.join( + self.work_dir, "apps", app, "settings" + ) tmp_app_bkp_dir = env_dict["YNH_APP_BACKUP_DIR"] settings_dir = os.path.join(self.work_dir, "apps", app, "settings") @@ -732,9 +732,10 @@ class BackupManager: logger.debug(m18n.n("backup_permission", app=app)) permissions = user_permission_list(full=True, apps=[app])["permissions"] this_app_permissions = {name: infos for name, infos in permissions.items()} - write_to_yaml("%s/permissions.yml" % settings_dir, this_app_permissions) + write_to_yaml(f"{settings_dir}/permissions.yml", this_app_permissions) - except Exception: + except Exception as e: + logger.debug(e) abs_tmp_app_dir = os.path.join(self.work_dir, "apps/", app) shutil.rmtree(abs_tmp_app_dir, ignore_errors=True) logger.error(m18n.n("backup_app_failed", app=app)) @@ -861,9 +862,13 @@ class RestoreManager: # FIXME this way to get the info is not compatible with copy or custom # backup methods self.info = backup_info(name, with_details=True) - if not self.info["from_yunohost_version"] or version.parse( - self.info["from_yunohost_version"] - ) < version.parse("3.8.0"): + + from_version = self.info.get("from_yunohost_version", "") + # Remove any '~foobar' in the version ... c.f ~alpha, ~beta version during + # early dev for next debian version + from_version = re.sub(r"~\w+", "", from_version) + + if not from_version or version.parse(from_version) < version.parse("4.2.0"): raise YunohostValidationError("restore_backup_too_old") self.archive_path = self.info["path"] @@ -916,7 +921,7 @@ class RestoreManager: if not os.path.isfile("/etc/yunohost/installed"): # Retrieve the domain from the backup try: - with open("%s/conf/ynh/current_host" % self.work_dir, "r") as f: + with open(f"{self.work_dir}/conf/ynh/current_host", "r") as f: domain = f.readline().rstrip() except IOError: logger.debug( @@ -999,7 +1004,7 @@ class RestoreManager: continue hook_paths = self.info["system"][system_part]["paths"] - hook_paths = ["hooks/restore/%s" % os.path.basename(p) for p in hook_paths] + hook_paths = [f"hooks/restore/{os.path.basename(p)}" for p in hook_paths] # Otherwise, add it from the archive to the system # FIXME: Refactor hook_add and use it instead @@ -1066,7 +1071,7 @@ class RestoreManager: ret = subprocess.call(["umount", self.work_dir]) if ret == 0: subprocess.call(["rmdir", self.work_dir]) - logger.debug("Unmount dir: {}".format(self.work_dir)) + logger.debug(f"Unmount dir: {self.work_dir}") else: raise YunohostError("restore_removing_tmp_dir_failed") elif os.path.isdir(self.work_dir): @@ -1075,7 +1080,7 @@ class RestoreManager: ) ret = subprocess.call(["rm", "-Rf", self.work_dir]) if ret == 0: - logger.debug("Delete dir: {}".format(self.work_dir)) + logger.debug(f"Delete dir: {self.work_dir}") else: raise YunohostError("restore_removing_tmp_dir_failed") @@ -1177,15 +1182,16 @@ class RestoreManager: self._restore_apps() except Exception as e: raise YunohostError( - "The following critical error happened during restoration: %s" % e + f"The following critical error happened during restoration: {e}" ) finally: self.clean() def _patch_legacy_php_versions_in_csv_file(self): """ - Apply dirty patch to redirect php5 and php7.0 files to php7.3 + Apply dirty patch to redirect php5 and php7.0 files to php7.4 """ + from yunohost.utils.legacy import LEGACY_PHP_VERSION_REPLACEMENTS backup_csv = os.path.join(self.work_dir, "backup.csv") @@ -1283,6 +1289,8 @@ class RestoreManager: else: operation_logger.success() + yunohost.domain.domain_list_cache = {} + regen_conf() _tools_migrations_run_after_system_restore( @@ -1345,6 +1353,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.user import user_group_list from yunohost.permission import ( permission_create, @@ -1416,20 +1429,19 @@ class RestoreManager: restore_script = os.path.join(tmp_workdir_for_app, "restore") # Restore permissions - if not os.path.isfile("%s/permissions.yml" % app_settings_new_path): + if not os.path.isfile(f"{app_settings_new_path}/permissions.yml"): raise YunohostError( "Didnt find a permssions.yml for the app !?", raw_msg=True ) - permissions = read_yaml("%s/permissions.yml" % app_settings_new_path) + permissions = read_yaml(f"{app_settings_new_path}/permissions.yml") existing_groups = user_group_list()["groups"] for permission_name, permission_infos in permissions.items(): if "allowed" not in permission_infos: logger.warning( - "'allowed' key corresponding to allowed groups for permission %s not found when restoring app %s … You might have to reconfigure permissions yourself." - % (permission_name, app_instance_name) + f"'allowed' key corresponding to allowed groups for permission {permission_name} not found when restoring app {app_instance_name} … You might have to reconfigure permissions yourself." ) should_be_allowed = ["all_users"] else: @@ -1454,7 +1466,7 @@ class RestoreManager: permission_sync_to_user() - os.remove("%s/permissions.yml" % app_settings_new_path) + os.remove(f"{app_settings_new_path}/permissions.yml") _tools_migrations_run_before_app_restore( backup_version=self.info["from_yunohost_version"], @@ -1479,7 +1491,11 @@ 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, @@ -1496,37 +1512,19 @@ class RestoreManager: # Execute the app install script restore_failed = True try: - restore_retcode = hook_exec( + ( + restore_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( restore_script, chdir=app_backup_in_archive, env=env_dict, - )[0] - # "Common" app restore failure : the script failed and returned exit code != 0 - restore_failed = True if restore_retcode != 0 else False - if restore_failed: - error = m18n.n("app_restore_script_failed") - logger.error( - m18n.n("app_restore_failed", app=app_instance_name, error=error) - ) - failure_message_with_debug_instructions = operation_logger.error(error) - if Moulinette.interface.type != "api": - dump_app_log_extract_for_debugging(operation_logger) - # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception - except (KeyboardInterrupt, EOFError): - error = m18n.n("operation_interrupted") - logger.error( - m18n.n("app_restore_failed", app=app_instance_name, error=error) + operation_logger=operation_logger, + error_message_if_script_failed=m18n.n("app_restore_script_failed"), + error_message_if_failed=lambda e: m18n.n( + "app_restore_failed", app=app_instance_name, error=e + ), ) - failure_message_with_debug_instructions = operation_logger.error(error) - # Something wrong happened in Yunohost's code (most probably hook_exec) - except Exception: - import traceback - - error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) - logger.error( - m18n.n("app_restore_failed", app=app_instance_name, error=error) - ) - failure_message_with_debug_instructions = operation_logger.error(error) finally: # Cleaning temporary scripts directory shutil.rmtree(tmp_workdir_for_app, ignore_errors=True) @@ -1541,8 +1539,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) - + 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)], @@ -1574,7 +1573,7 @@ class RestoreManager: # # Backup methods # # -class BackupMethod(object): +class BackupMethod: """ BackupMethod is an abstract class that represents a way to backup and @@ -1816,8 +1815,7 @@ class BackupMethod(object): # where everything is mapped to /dev/mapper/some-stuff # yet there are different devices behind it or idk ... logger.warning( - "Could not link %s to %s (%s) ... falling back to regular copy." - % (src, dest, str(e)) + f"Could not link {src} to {dest} ({e}) ... falling back to regular copy." ) else: # Success, go to next file to organize @@ -2383,8 +2381,8 @@ def backup_list(with_info=False, human_readable=False): """ # Get local archives sorted according to last modification time # (we do a realpath() to resolve symlinks) - archives = glob("%s/*.tar.gz" % ARCHIVES_PATH) + glob("%s/*.tar" % ARCHIVES_PATH) - archives = set([os.path.realpath(archive) for archive in archives]) + archives = glob(f"{ARCHIVES_PATH}/*.tar.gz") + glob(f"{ARCHIVES_PATH}/*.tar") + archives = {os.path.realpath(archive) for archive in archives} archives = sorted(archives, key=lambda x: os.path.getctime(x)) # Extract only filename without the extension @@ -2406,10 +2404,8 @@ def backup_list(with_info=False, human_readable=False): except Exception: import traceback - logger.warning( - "Could not check infos for archive %s: %s" - % (archive, "\n" + traceback.format_exc()) - ) + trace_ = "\n" + traceback.format_exc() + logger.warning(f"Could not check infos for archive {archive}: {trace_}") archives = d @@ -2424,7 +2420,7 @@ def backup_download(name): ) return - archive_file = "%s/%s.tar" % (ARCHIVES_PATH, name) + archive_file = f"{ARCHIVES_PATH}/{name}.tar" # Check file exist (even if it's a broken symlink) if not os.path.lexists(archive_file): @@ -2466,7 +2462,7 @@ def backup_info(name, with_details=False, human_readable=False): elif name.endswith(".tar"): name = name[: -len(".tar")] - archive_file = "%s/%s.tar" % (ARCHIVES_PATH, name) + archive_file = f"{ARCHIVES_PATH}/{name}.tar" # Check file exist (even if it's a broken symlink) if not os.path.lexists(archive_file): @@ -2484,7 +2480,7 @@ def backup_info(name, with_details=False, human_readable=False): "backup_archive_broken_link", path=archive_file ) - info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) + info_file = f"{ARCHIVES_PATH}/{name}.info.json" if not os.path.exists(info_file): tar = tarfile.open( @@ -2595,10 +2591,10 @@ def backup_delete(name): hook_callback("pre_backup_delete", args=[name]) - archive_file = "%s/%s.tar" % (ARCHIVES_PATH, name) - if os.path.exists(archive_file + ".gz"): + archive_file = f"{ARCHIVES_PATH}/{name}.tar" + if not os.path.exists(archive_file) and os.path.exists(archive_file + ".gz"): archive_file += ".gz" - info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) + info_file = f"{ARCHIVES_PATH}/{name}.info.json" files_to_delete = [archive_file, info_file] @@ -2697,5 +2693,5 @@ def binary_to_human(n, customary=False): for s in reversed(symbols): if n >= prefix[s]: value = float(n) / prefix[s] - return "%.1f%s" % (value, s) + return "{:.1f}{}".format(value, s) return "%s" % n diff --git a/src/yunohost/certificate.py b/src/certificate.py similarity index 80% rename from src/yunohost/certificate.py rename to src/certificate.py index 52d58777b..2a9fb4ce9 100644 --- a/src/yunohost/certificate.py +++ b/src/certificate.py @@ -54,14 +54,12 @@ WEBROOT_FOLDER = "/tmp/acme-challenge-public/" SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem" ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem" -SSL_DIR = "/usr/share/yunohost/yunohost-config/ssl/yunoCA" +SSL_DIR = "/usr/share/yunohost/ssl" KEY_SIZE = 3072 VALIDITY_LIMIT = 15 # days -# For tests -STAGING_CERTIFICATION_AUTHORITY = "https://acme-staging-v02.api.letsencrypt.org" # For prod PRODUCTION_CERTIFICATION_AUTHORITY = "https://acme-v02.api.letsencrypt.org" @@ -70,31 +68,28 @@ PRODUCTION_CERTIFICATION_AUTHORITY = "https://acme-v02.api.letsencrypt.org" # -def certificate_status(domain_list, full=False): +def certificate_status(domains, full=False): """ Print the status of certificate for given domains (all by default) Keyword argument: - domain_list -- Domains to be checked + domains -- Domains to be checked full -- Display more info about the certificates """ - import yunohost.domain + from yunohost.domain import domain_list, _assert_domain_exists # If no domains given, consider all yunohost domains - if domain_list == []: - domain_list = yunohost.domain.domain_list()["domains"] + if domains == []: + domains = domain_list()["domains"] # Else, validate that yunohost knows the domains given else: - yunohost_domains_list = yunohost.domain.domain_list()["domains"] - for domain in domain_list: - # Is it in Yunohost domain list? - if domain not in yunohost_domains_list: - raise YunohostValidationError("domain_name_unknown", domain=domain) + for domain in domains: + _assert_domain_exists(domain) certificates = {} - for domain in domain_list: + for domain in domains: status = _get_status(domain) if not full: @@ -116,9 +111,7 @@ def certificate_status(domain_list, full=False): return {"certificates": certificates} -def certificate_install( - domain_list, force=False, no_checks=False, self_signed=False, staging=False -): +def certificate_install(domain_list, force=False, no_checks=False, self_signed=False): """ Install a Let's Encrypt certificate for given domains (all by default) @@ -133,7 +126,7 @@ def certificate_install( if self_signed: _certificate_install_selfsigned(domain_list, force) else: - _certificate_install_letsencrypt(domain_list, force, no_checks, staging) + _certificate_install_letsencrypt(domain_list, force, no_checks) def _certificate_install_selfsigned(domain_list, force=False): @@ -146,11 +139,7 @@ def _certificate_install_selfsigned(domain_list, force=False): # Paths of files and folder we'll need date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") - new_cert_folder = "%s/%s-history/%s-selfsigned" % ( - CERT_FOLDER, - domain, - date_tag, - ) + new_cert_folder = f"{CERT_FOLDER}/{domain}-history/{date_tag}-selfsigned" conf_template = os.path.join(SSL_DIR, "openssl.cnf") @@ -184,10 +173,8 @@ def _certificate_install_selfsigned(domain_list, force=False): # Use OpenSSL command line to create a certificate signing request, # and self-sign the cert commands = [ - "openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" - % (conf_file, csr_file, key_file), - "openssl ca -config %s -days 3650 -in %s -out %s -batch" - % (conf_file, csr_file, crt_file), + f"openssl req -new -config {conf_file} -out {csr_file} -keyout {key_file} -nodes -batch", + f"openssl ca -config {conf_file} -days 3650 -in {csr_file} -out {crt_file} -batch", ] for command in commands: @@ -237,39 +224,32 @@ def _certificate_install_selfsigned(domain_list, force=False): ) operation_logger.success() else: - msg = ( - "Installation of self-signed certificate installation for %s failed !" - % (domain) - ) + msg = f"Installation of self-signed certificate installation for {domain} failed !" logger.error(msg) operation_logger.error(msg) -def _certificate_install_letsencrypt( - domain_list, force=False, no_checks=False, staging=False -): - import yunohost.domain +def _certificate_install_letsencrypt(domains, force=False, no_checks=False): + from yunohost.domain import domain_list, _assert_domain_exists if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() # If no domains given, consider all yunohost domains with self-signed # certificates - if domain_list == []: - for domain in yunohost.domain.domain_list()["domains"]: + if domains == []: + for domain in domain_list()["domains"]: status = _get_status(domain) if status["CA_type"]["code"] != "self-signed": continue - domain_list.append(domain) + domains.append(domain) # Else, validate that yunohost knows the domains given else: - for domain in domain_list: - yunohost_domains_list = yunohost.domain.domain_list()["domains"] - if domain not in yunohost_domains_list: - raise YunohostValidationError("domain_name_unknown", domain=domain) + for domain in domains: + _assert_domain_exists(domain) # Is it self-signed? status = _get_status(domain) @@ -278,13 +258,8 @@ def _certificate_install_letsencrypt( "certmanager_domain_cert_not_selfsigned", domain=domain ) - if staging: - logger.warning( - "Please note that you used the --staging option, and that no new certificate will actually be enabled !" - ) - # Actual install steps - for domain in domain_list: + for domain in domains: if not no_checks: try: @@ -298,23 +273,19 @@ def _certificate_install_letsencrypt( operation_logger = OperationLogger( "letsencrypt_cert_install", [("domain", domain)], - args={"force": force, "no_checks": no_checks, "staging": staging}, + args={"force": force, "no_checks": no_checks}, ) operation_logger.start() try: - _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) + _fetch_and_enable_new_certificate(domain, no_checks=no_checks) except Exception as e: - msg = "Certificate installation for %s failed !\nException: %s" % ( - domain, - e, - ) + msg = f"Certificate installation for {domain} failed !\nException: {e}" logger.error(msg) operation_logger.error(msg) if no_checks: logger.error( - "Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s." - % domain + f"Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain {domain}." ) else: logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) @@ -322,26 +293,24 @@ def _certificate_install_letsencrypt( operation_logger.success() -def certificate_renew( - domain_list, force=False, no_checks=False, email=False, staging=False -): +def certificate_renew(domains, force=False, no_checks=False, email=False): """ Renew Let's Encrypt certificate for given domains (all by default) Keyword argument: - domain_list -- Domains for which to renew the certificates + domains -- Domains for which to renew the certificates force -- Ignore the validity threshold (15 days) no-check -- Disable some checks about the reachability of web server before attempting the renewing email -- Emails root if some renewing failed """ - import yunohost.domain + from yunohost.domain import domain_list, _assert_domain_exists # If no domains given, consider all yunohost domains with Let's Encrypt # certificates - if domain_list == []: - for domain in yunohost.domain.domain_list()["domains"]: + if domains == []: + for domain in domain_list()["domains"]: # Does it have a Let's Encrypt cert? status = _get_status(domain) @@ -359,18 +328,17 @@ def certificate_renew( ) continue - domain_list.append(domain) + domains.append(domain) - if len(domain_list) == 0 and not email: + if len(domains) == 0 and not email: logger.info("No certificate needs to be renewed.") # Else, validate the domain list given else: - for domain in domain_list: + for domain in domains: - # Is it in Yunohost dmomain list? - if domain not in yunohost.domain.domain_list()["domains"]: - raise YunohostValidationError("domain_name_unknown", domain=domain) + # Is it in Yunohost domain list? + _assert_domain_exists(domain) status = _get_status(domain) @@ -392,13 +360,8 @@ def certificate_renew( "certmanager_acme_not_configured_for_domain", domain=domain ) - if staging: - logger.warning( - "Please note that you used the --staging option, and that no new certificate will actually be enabled !" - ) - # Actual renew steps - for domain in domain_list: + for domain in domains: if not no_checks: try: @@ -418,26 +381,22 @@ def certificate_renew( args={ "force": force, "no_checks": no_checks, - "staging": staging, "email": email, }, ) operation_logger.start() try: - _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) + _fetch_and_enable_new_certificate(domain, no_checks=no_checks) except Exception as e: import traceback from io import StringIO stack = StringIO() traceback.print_exc(file=stack) - msg = "Certificate renewing for %s failed!" % (domain) + msg = f"Certificate renewing for {domain} failed!" if no_checks: - msg += ( - "\nPlease consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s." - % domain - ) + msg += f"\nPlease consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain {domain}." logger.error(msg) operation_logger.error(msg) logger.error(stack.getvalue()) @@ -457,44 +416,30 @@ def certificate_renew( def _email_renewing_failed(domain, exception_message, stack=""): - from_ = "certmanager@%s (Certificate Manager)" % domain + from_ = f"certmanager@{domain} (Certificate Manager)" to_ = "root" - subject_ = "Certificate renewing attempt for %s failed!" % domain + subject_ = f"Certificate renewing attempt for {domain} failed!" logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") - text = """ -An attempt for renewing the certificate for domain %s failed with the following + message = f"""\ +From: {from_} +To: {to_} +Subject: {subject_} + + +An attempt for renewing the certificate for domain {domain} failed with the following error : -%s -%s +{exception_message} +{stack} Here's the tail of /var/log/yunohost/yunohost-cli.log, which might help to investigate : -%s +{logs} -- Certificate Manager - -""" % ( - domain, - exception_message, - stack, - logs, - ) - - message = """\ -From: %s -To: %s -Subject: %s - -%s -""" % ( - from_, - to_, - subject_, - text, - ) +""" import smtplib @@ -505,17 +450,11 @@ Subject: %s def _check_acme_challenge_configuration(domain): - domain_conf = "/etc/nginx/conf.d/%s.conf" % domain - if "include /etc/nginx/conf.d/acme-challenge.conf.inc" in read_file(domain_conf): - return True - else: - # This is for legacy setups which haven't updated their domain conf to - # the new conf that include the acme snippet... - legacy_acme_conf = "/etc/nginx/conf.d/%s.d/000-acmechallenge.conf" % domain - return os.path.exists(legacy_acme_conf) + domain_conf = f"/etc/nginx/conf.d/{domain}.conf" + return "include /etc/nginx/conf.d/acme-challenge.conf.inc" in read_file(domain_conf) -def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): +def _fetch_and_enable_new_certificate(domain, no_checks=False): if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() @@ -538,7 +477,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): # Prepare certificate signing request logger.debug("Prepare key and certificate signing request (CSR) for %s...", domain) - domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain) + domain_key_file = f"{TMP_FOLDER}/{domain}.pem" _generate_key(domain_key_file) _set_permissions(domain_key_file, "root", "ssl-cert", 0o640) @@ -547,12 +486,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): # Sign the certificate logger.debug("Now using ACME Tiny to sign the certificate...") - domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) - - if staging: - certification_authority = STAGING_CERTIFICATION_AUTHORITY - else: - certification_authority = PRODUCTION_CERTIFICATION_AUTHORITY + domain_csr_file = f"{TMP_FOLDER}/{domain}.csr" try: signed_certificate = sign_certificate( @@ -561,7 +495,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): WEBROOT_FOLDER, log=logger, disable_check=no_checks, - CA=certification_authority, + CA=PRODUCTION_CERTIFICATION_AUTHORITY, ) except ValueError as e: if "urn:acme:error:rateLimited" in str(e): @@ -581,17 +515,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): # Create corresponding directory date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") - if staging: - folder_flag = "staging" - else: - folder_flag = "letsencrypt" - - new_cert_folder = "%s/%s-history/%s-%s" % ( - CERT_FOLDER, - domain, - date_tag, - folder_flag, - ) + new_cert_folder = f"{CERT_FOLDER}/{domain}-history/{date_tag}-letsencrypt" os.makedirs(new_cert_folder) @@ -610,9 +534,6 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): _set_permissions(domain_cert_file, "root", "ssl-cert", 0o640) - if staging: - return - _enable_certificate(domain, new_cert_folder) # Check the status of the certificate is now good @@ -648,7 +569,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): csr.add_extensions( [ crypto.X509Extension( - "subjectAltName".encode("utf8"), + b"subjectAltName", False, ("DNS:" + subdomain).encode("utf8"), ) @@ -682,6 +603,8 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): + import yunohost.domain + cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): @@ -710,7 +633,7 @@ def _get_status(domain): ) days_remaining = (valid_up_to - datetime.utcnow()).days - if cert_issuer == "yunohost.org" or cert_issuer == _name_self_CA(): + if cert_issuer in ["yunohost.org"] + yunohost.domain.domain_list()["domains"]: CA_type = { "code": "self-signed", "verbose": "Self-signed", @@ -722,12 +645,6 @@ def _get_status(domain): "verbose": "Let's Encrypt", } - elif cert_issuer.startswith("Fake LE"): - CA_type = { - "code": "fake-lets-encrypt", - "verbose": "Fake Let's Encrypt", - } - else: CA_type = { "code": "other-unknown", @@ -830,6 +747,12 @@ def _enable_certificate(domain, new_cert_folder): logger.debug("Restarting services...") for service in ("postfix", "dovecot", "metronome"): + # Ugly trick to not restart metronome if it's not installed + if ( + service == "metronome" + and os.system("dpkg --list | grep -q 'ii *metronome'") != 0 + ): + continue _run_service_command("restart", service) if os.path.isfile("/etc/yunohost/installed"): @@ -850,21 +773,17 @@ def _backup_current_cert(domain): cert_folder_domain = os.path.join(CERT_FOLDER, domain) date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") - backup_folder = "%s-backups/%s" % (cert_folder_domain, date_tag) + backup_folder = f"{cert_folder_domain}-backups/{date_tag}" shutil.copytree(cert_folder_domain, backup_folder) 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 + from yunohost.utils.dns import is_yunohost_dyndns_domain + httpreachable = ( Diagnoser.get_cached_report( "web", item={"domain": domain}, warn_if_no_cache=False @@ -872,16 +791,56 @@ 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 "@" + ) + + # Stupid edge case for subdomains of ynh dyndns domains ... + # ... related to the fact that we don't actually check subdomains for + # dyndns domains because we assume that there's already the wildcard doing + # the job, hence no "A:foobar" ... Instead, just check that the parent domain + # is correctly configured. + if is_yunohost_dyndns_domain(parent_domain): + record_name = "@" + + 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("A:*") + if not AAAA_record_status: + AAAA_record_status = dnsrecords.get("data", {}).get("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/data/hooks/diagnosis/00-basesystem.py b/src/diagnosers/00-basesystem.py similarity index 83% rename from data/hooks/diagnosis/00-basesystem.py rename to src/diagnosers/00-basesystem.py index 3623c10e2..a36394ce8 100644 --- a/data/hooks/diagnosis/00-basesystem.py +++ b/src/diagnosers/00-basesystem.py @@ -3,18 +3,22 @@ import os import json import subprocess +from typing import List +from moulinette.utils import log from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_file, read_json, write_to_json from yunohost.diagnosis import Diagnoser from yunohost.utils.packages import ynh_packages_version +logger = log.getActionLogger("yunohost.diagnosis") -class BaseSystemDiagnoser(Diagnoser): + +class MyDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 - dependencies = [] + dependencies: List[str] = [] def run(self): @@ -42,10 +46,10 @@ class BaseSystemDiagnoser(Diagnoser): elif os.path.exists("/sys/devices/virtual/dmi/id/sys_vendor"): model = read_file("/sys/devices/virtual/dmi/id/sys_vendor").strip() if os.path.exists("/sys/devices/virtual/dmi/id/product_name"): - model = "%s %s" % ( - model, - read_file("/sys/devices/virtual/dmi/id/product_name").strip(), - ) + product_name = read_file( + "/sys/devices/virtual/dmi/id/product_name" + ).strip() + model = f"{model} {product_name}" hardware["data"]["model"] = model hardware["details"] = ["diagnosis_basesystem_hardware_model"] @@ -116,7 +120,7 @@ class BaseSystemDiagnoser(Diagnoser): bad_sury_packages = list(self.bad_sury_packages()) if bad_sury_packages: cmd_to_fix = "apt install --allow-downgrades " + " ".join( - ["%s=%s" % (package, version) for package, version in bad_sury_packages] + [f"{package}={version}" for package, version in bad_sury_packages] ) yield dict( meta={"test": "packages_from_sury"}, @@ -133,6 +137,13 @@ class BaseSystemDiagnoser(Diagnoser): summary="diagnosis_backports_in_sources_list", ) + if self.number_of_recent_auth_failure() > 750: + yield dict( + meta={"test": "high_number_auth_failure"}, + status="WARNING", + summary="diagnosis_high_number_auth_failures", + ) + def bad_sury_packages(self): packages_to_check = ["openssl", "libssl1.1", "libssl-dev"] @@ -154,6 +165,23 @@ class BaseSystemDiagnoser(Diagnoser): cmd = "grep -q -nr '^ *deb .*-backports' /etc/apt/sources.list*" return os.system(cmd) == 0 + def number_of_recent_auth_failure(self): + + # Those syslog facilities correspond to auth and authpriv + # c.f. https://unix.stackexchange.com/a/401398 + # and https://wiki.archlinux.org/title/Systemd/Journal#Facility + cmd = "journalctl -q SYSLOG_FACILITY=10 SYSLOG_FACILITY=4 --since '1day ago' | grep 'authentication failure' | wc -l" + + n_failures = check_output(cmd) + try: + return int(n_failures) + except Exception: + logger.warning( + "Failed to parse number of recent auth failures, expected an int, got '%s'" + % n_failures + ) + return -1 + def is_vulnerable_to_meltdown(self): # meltdown CVE: https://security-tracker.debian.org/tracker/CVE-2017-5754 @@ -172,20 +200,20 @@ class BaseSystemDiagnoser(Diagnoser): if not os.path.exists(dpkg_log) or os.path.getmtime( cache_file ) > os.path.getmtime(dpkg_log): - self.logger_debug( + logger.debug( "Using cached results for meltdown checker, from %s" % cache_file ) return read_json(cache_file)[0]["VULNERABLE"] # script taken from https://github.com/speed47/spectre-meltdown-checker # script commit id is store directly in the script - SCRIPT_PATH = "/usr/lib/moulinette/yunohost/vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh" + SCRIPT_PATH = "/usr/lib/python3/dist-packages/yunohost/vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh" # '--variant 3' corresponds to Meltdown # example output from the script: # [{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}] try: - self.logger_debug("Running meltdown vulnerability checker") + logger.debug("Running meltdown vulnerability checker") call = subprocess.Popen( "bash %s --batch json --variant 3" % SCRIPT_PATH, shell=True, @@ -207,7 +235,7 @@ class BaseSystemDiagnoser(Diagnoser): # stuff which should be the last line output = output.strip() if "\n" in output: - self.logger_debug("Original meltdown checker output : %s" % output) + logger.debug("Original meltdown checker output : %s" % output) output = output.split("\n")[-1] CVEs = json.loads(output) @@ -217,18 +245,14 @@ class BaseSystemDiagnoser(Diagnoser): import traceback traceback.print_exc() - self.logger_warning( + logger.warning( "Something wrong happened when trying to diagnose Meltdown vunerability, exception: %s" % e ) raise Exception("Command output for failed meltdown check: '%s'" % output) - self.logger_debug( + logger.debug( "Writing results from meltdown checker to cache file, %s" % cache_file ) write_to_json(cache_file, CVEs) return CVEs[0]["VULNERABLE"] - - -def main(args, env, loggers): - return BaseSystemDiagnoser(args, env, loggers).diagnose() diff --git a/data/hooks/diagnosis/10-ip.py b/src/diagnosers/10-ip.py similarity index 93% rename from data/hooks/diagnosis/10-ip.py rename to src/diagnosers/10-ip.py index 408019668..247c486fc 100644 --- a/data/hooks/diagnosis/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -3,7 +3,9 @@ import re import os import random +from typing import List +from moulinette.utils import log from moulinette.utils.network import download_text from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_file @@ -11,12 +13,14 @@ from moulinette.utils.filesystem import read_file from yunohost.diagnosis import Diagnoser from yunohost.utils.network import get_network_interfaces +logger = log.getActionLogger("yunohost.diagnosis") -class IPDiagnoser(Diagnoser): + +class MyDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 - dependencies = [] + dependencies: List[str] = [] def run(self): @@ -144,16 +148,14 @@ class IPDiagnoser(Diagnoser): ) if not any(is_default_route(r) for r in routes): - self.logger_debug( + logger.debug( "No default route for IPv%s, so assuming there's no IP address for that version" % protocol ) return None # We use the resolver file as a list of well-known, trustable (ie not google ;)) IPs that we can ping - resolver_file = ( - "/usr/share/yunohost/templates/dnsmasq/plain/resolv.dnsmasq.conf" - ) + resolver_file = "/usr/share/yunohost/conf/dnsmasq/plain/resolv.dnsmasq.conf" resolvers = [ r.split(" ")[1] for r in read_file(resolver_file).split("\n") @@ -167,10 +169,7 @@ class IPDiagnoser(Diagnoser): assert ( resolvers != [] - ), "Uhoh, need at least one IPv%s DNS resolver in %s ..." % ( - protocol, - resolver_file, - ) + ), f"Uhoh, need at least one IPv{protocol} DNS resolver in {resolver_file} ..." # So let's try to ping the first 4~5 resolvers (shuffled) # If we succesfully ping any of them, we conclude that we are indeed connected @@ -220,11 +219,7 @@ class IPDiagnoser(Diagnoser): try: return download_text(url, timeout=30).strip() except Exception as e: - self.logger_debug( - "Could not get public IPv%s : %s" % (str(protocol), str(e)) - ) + protocol = str(protocol) + e = str(e) + self.logger_debug(f"Could not get public IPv{protocol} : {e}") return None - - -def main(args, env, loggers): - return IPDiagnoser(args, env, loggers).diagnose() diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py similarity index 75% rename from data/hooks/diagnosis/12-dnsrecords.py rename to src/diagnosers/12-dnsrecords.py index 6110024f4..91fcf10fa 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -2,49 +2,50 @@ import os import re - +from typing import List from datetime import datetime, timedelta -from publicsuffix import PublicSuffixList +from publicsuffix2 import PublicSuffixList +from moulinette.utils import log from moulinette.utils.process import check_output -from yunohost.utils.network import dig +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, _build_dns_conf, _get_maindomain +from yunohost.domain import domain_list, _get_maindomain +from yunohost.dns import _build_dns_conf, _get_dns_zone_for_domain -YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] -SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"] +logger = log.getActionLogger("yunohost.diagnosis") -class DNSRecordsDiagnoser(Diagnoser): +class MyDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 - dependencies = ["ip"] + dependencies: List[str] = ["ip"] def run(self): main_domain = _get_maindomain() - all_domains = domain_list()["domains"] - for domain in all_domains: - self.logger_debug("Diagnosing DNS conf for %s" % domain) - is_subdomain = domain.split(".", 1)[1] in all_domains - is_specialusedomain = any( - domain.endswith("." + tld) for tld in SPECIAL_USE_TLDS - ) + major_domains = domain_list(exclude_subdomains=True)["domains"] + for domain in major_domains: + logger.debug("Diagnosing DNS conf for %s" % domain) + for report in self.check_domain( domain, domain == main_domain, - is_subdomain=is_subdomain, - is_specialusedomain=is_specialusedomain, ): yield report # 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 @@ -55,25 +56,25 @@ class DNSRecordsDiagnoser(Diagnoser): for report in self.check_expiration_date(domains_from_registrar): yield report - def check_domain(self, domain, is_main_domain, is_subdomain, is_specialusedomain): + def check_domain(self, domain, is_main_domain): - expected_configuration = _build_dns_conf( - domain, include_empty_AAAA_if_no_ipv6=True - ) - - categories = ["basic", "mail", "xmpp", "extra"] - # For subdomains, we only diagnosis A and AAAA records - if is_subdomain: - categories = ["basic"] - - if is_specialusedomain: - categories = [] + if is_special_use_tld(domain): yield dict( meta={"domain": domain}, data={}, status="INFO", summary="diagnosis_dns_specialusedomain", ) + return + + 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: @@ -82,10 +83,25 @@ class DNSRecordsDiagnoser(Diagnoser): results = {} for r in records: + id_ = r["type"] + ":" + r["name"] - r["current"] = self.get_current_record(domain, r["name"], r["type"]) + 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... + # Should find a cleaner solution in the suggested conf... + if r["type"] in ["MX", "TXT"] and fqdn not in [ + domain, + f"mail._domainkey.{domain}", + f"_dmarc.{domain}", + ]: + continue + + r["current"] = self.get_current_record(fqdn, r["type"]) if r["value"] == "@": r["value"] = domain + "." + elif r["type"] == "CNAME": + r["value"] = r["value"] + f".{base_dns_zone}." if self.current_record_match_expected(r): results[id_] = "OK" @@ -106,7 +122,10 @@ class DNSRecordsDiagnoser(Diagnoser): # A bad or missing A record is critical ... # And so is a wrong AAAA record # (However, a missing AAAA record is acceptable) - if results["A:@"] != "OK" or results["AAAA:@"] == "WRONG": + if ( + results[f"A:{basename}"] != "OK" + or results[f"AAAA:{basename}"] == "WRONG" + ): return True return False @@ -118,6 +137,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, @@ -127,10 +152,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: @@ -139,10 +161,9 @@ class DNSRecordsDiagnoser(Diagnoser): yield output - def get_current_record(self, domain, name, type_): + def get_current_record(self, fqdn, type_): - query = "%s.%s" % (name, domain) if name != "@" else domain - success, answers = dig(query, type_, resolvers="force_external") + success, answers = dig(fqdn, type_, resolvers="force_external") if success != "ok": return None @@ -170,7 +191,7 @@ class DNSRecordsDiagnoser(Diagnoser): ) # For SPF, ignore parts starting by ip4: or ip6: - if r["name"] == "@": + if "v=spf1" in r["value"]: current = { part for part in current @@ -189,7 +210,6 @@ class DNSRecordsDiagnoser(Diagnoser): """ Alert if expiration date of a domain is soon """ - details = {"not_found": [], "error": [], "warning": [], "success": []} for domain in domains: @@ -199,6 +219,7 @@ class DNSRecordsDiagnoser(Diagnoser): status_ns, _ = dig(domain, "NS", resolvers="force_external") status_a, _ = dig(domain, "A", resolvers="force_external") if "ok" not in [status_ns, status_a]: + # i18n: diagnosis_domain_not_found_details details["not_found"].append( ( "diagnosis_domain_%s_details" % (expire_date), @@ -206,7 +227,7 @@ class DNSRecordsDiagnoser(Diagnoser): ) ) else: - self.logger_debug("Dyndns domain: %s" % (domain)) + logger.debug("Dyndns domain: %s" % (domain)) continue expire_in = expire_date - datetime.now() @@ -233,6 +254,12 @@ class DNSRecordsDiagnoser(Diagnoser): # Allow to ignore specifically a single domain if len(details[alert_type]) == 1: meta["domain"] = details[alert_type][0][1]["domain"] + + # i18n: diagnosis_domain_expiration_not_found + # i18n: diagnosis_domain_expiration_error + # i18n: diagnosis_domain_expiration_warning + # i18n: diagnosis_domain_expiration_success + # i18n: diagnosis_domain_expiration_not_found_details yield dict( meta=meta, data={}, @@ -275,7 +302,3 @@ class DNSRecordsDiagnoser(Diagnoser): return datetime.strptime(match.group(1), "%d-%b-%Y") return "expiration_not_found" - - -def main(args, env, loggers): - return DNSRecordsDiagnoser(args, env, loggers).diagnose() diff --git a/data/hooks/diagnosis/14-ports.py b/src/diagnosers/14-ports.py similarity index 97% rename from data/hooks/diagnosis/14-ports.py rename to src/diagnosers/14-ports.py index 7581a1ac6..be172e524 100644 --- a/data/hooks/diagnosis/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -1,16 +1,17 @@ #!/usr/bin/env python import os +from typing import List from yunohost.diagnosis import Diagnoser from yunohost.service import _get_services -class PortsDiagnoser(Diagnoser): +class MyDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 - dependencies = ["ip"] + dependencies: List[str] = ["ip"] def run(self): @@ -146,7 +147,3 @@ class PortsDiagnoser(Diagnoser): "diagnosis_ports_forwarding_tip", ], ) - - -def main(args, env, loggers): - return PortsDiagnoser(args, env, loggers).diagnose() diff --git a/data/hooks/diagnosis/21-web.py b/src/diagnosers/21-web.py similarity index 95% rename from data/hooks/diagnosis/21-web.py rename to src/diagnosers/21-web.py index 40a6c26b4..584505ad1 100644 --- a/data/hooks/diagnosis/21-web.py +++ b/src/diagnosers/21-web.py @@ -3,20 +3,22 @@ import os import random import requests +from typing import List 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" -class WebDiagnoser(Diagnoser): +class MyDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 - dependencies = ["ip"] + dependencies: List[str] = ["ip"] def run(self): @@ -34,11 +36,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) @@ -121,6 +123,10 @@ class WebDiagnoser(Diagnoser): for domain in domains: + # i18n: diagnosis_http_bad_status_code + # i18n: diagnosis_http_connection_error + # i18n: diagnosis_http_timeout + # If both IPv4 and IPv6 (if applicable) are good if all( results[ipversion][domain]["status"] == "ok" for ipversion in ipversions @@ -193,7 +199,3 @@ class WebDiagnoser(Diagnoser): summary="diagnosis_http_partially_unreachable", details=[detail.replace("error_http_check", "diagnosis_http")], ) - - -def main(args, env, loggers): - return WebDiagnoser(args, env, loggers).diagnose() diff --git a/data/hooks/diagnosis/24-mail.py b/src/diagnosers/24-mail.py similarity index 90% rename from data/hooks/diagnosis/24-mail.py rename to src/diagnosers/24-mail.py index 63f685a26..7fe7a08db 100644 --- a/data/hooks/diagnosis/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -3,25 +3,29 @@ import os import dns.resolver import re +from typing import List from subprocess import CalledProcessError +from moulinette.utils import log from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_yaml from yunohost.diagnosis import Diagnoser from yunohost.domain import _get_maindomain, domain_list from yunohost.settings import settings_get -from yunohost.utils.network import dig +from yunohost.utils.dns import dig -DEFAULT_DNS_BLACKLIST = "/usr/share/yunohost/other/dnsbl_list.yml" +DEFAULT_DNS_BLACKLIST = "/usr/share/yunohost/dnsbl_list.yml" + +logger = log.getActionLogger("yunohost.diagnosis") -class MailDiagnoser(Diagnoser): +class MyDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 - dependencies = ["ip"] + dependencies: List[str] = ["ip"] def run(self): @@ -35,14 +39,14 @@ class MailDiagnoser(Diagnoser): # TODO check that the recent mail logs are not filled with thousand of email sending (unusual number of mail sent) # TODO check for unusual failed sending attempt being refused in the logs ? checks = [ - "check_outgoing_port_25", - "check_ehlo", - "check_fcrdns", - "check_blacklist", - "check_queue", + "check_outgoing_port_25", # i18n: diagnosis_mail_outgoing_port_25_ok + "check_ehlo", # i18n: diagnosis_mail_ehlo_ok + "check_fcrdns", # i18n: diagnosis_mail_fcrdns_ok + "check_blacklist", # i18n: diagnosis_mail_blacklist_ok + "check_queue", # i18n: diagnosis_mail_queue_ok ] for check in checks: - self.logger_debug("Running " + check) + logger.debug("Running " + check) reports = list(getattr(self, check)()) for report in reports: yield report @@ -102,6 +106,10 @@ class MailDiagnoser(Diagnoser): continue if r["status"] != "ok": + # i18n: diagnosis_mail_ehlo_bad_answer + # i18n: diagnosis_mail_ehlo_bad_answer_details + # i18n: diagnosis_mail_ehlo_unreachable + # i18n: diagnosis_mail_ehlo_unreachable_details summary = r["status"].replace("error_smtp_", "diagnosis_mail_ehlo_") yield dict( meta={"test": "mail_ehlo", "ipversion": ipversion}, @@ -205,8 +213,11 @@ class MailDiagnoser(Diagnoser): query = subdomain + "." + blacklist["dns_server"] # Do the DNS Query - status, _ = dig(query, "A") - if status != "ok": + status, answers = dig(query, "A") + if status != "ok" or ( + answers + and set(answers) <= set(blacklist["non_blacklisted_return_code"]) + ): continue # Try to get the reason @@ -288,7 +299,3 @@ class MailDiagnoser(Diagnoser): if global_ipv6: outgoing_ips.append(global_ipv6) return (outgoing_ipversions, outgoing_ips) - - -def main(args, env, loggers): - return MailDiagnoser(args, env, loggers).diagnose() diff --git a/data/hooks/diagnosis/30-services.py b/src/diagnosers/30-services.py similarity index 89% rename from data/hooks/diagnosis/30-services.py rename to src/diagnosers/30-services.py index adbcc73b9..f09688911 100644 --- a/data/hooks/diagnosis/30-services.py +++ b/src/diagnosers/30-services.py @@ -1,16 +1,17 @@ #!/usr/bin/env python import os +from typing import List from yunohost.diagnosis import Diagnoser from yunohost.service import service_status -class ServicesDiagnoser(Diagnoser): +class MyDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 - dependencies = [] + dependencies: List[str] = [] def run(self): @@ -41,7 +42,3 @@ class ServicesDiagnoser(Diagnoser): item["summary"] = "diagnosis_services_running" yield item - - -def main(args, env, loggers): - return ServicesDiagnoser(args, env, loggers).diagnose() diff --git a/data/hooks/diagnosis/50-systemresources.py b/src/diagnosers/50-systemresources.py similarity index 93% rename from data/hooks/diagnosis/50-systemresources.py rename to src/diagnosers/50-systemresources.py index a662e392e..6ac7f0ec4 100644 --- a/data/hooks/diagnosis/50-systemresources.py +++ b/src/diagnosers/50-systemresources.py @@ -3,21 +3,22 @@ import os import psutil import datetime import re +from typing import List from moulinette.utils.process import check_output from yunohost.diagnosis import Diagnoser -class SystemResourcesDiagnoser(Diagnoser): +class MyDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 - dependencies = [] + dependencies: List[str] = [] def run(self): - MB = 1024 ** 2 + MB = 1024**2 GB = MB * 1024 # @@ -132,7 +133,7 @@ class SystemResourcesDiagnoser(Diagnoser): d for d in disk_partitions if d.mountpoint in ["/", "/var"] ] main_space = sum( - [psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions] + psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions ) if main_space < 10 * GB: yield dict( @@ -156,7 +157,7 @@ class SystemResourcesDiagnoser(Diagnoser): kills_count = self.recent_kills_by_oom_reaper() if kills_count: kills_summary = "\n".join( - ["%s (x%s)" % (proc, count) for proc, count in kills_count] + [f"{proc} (x{count})" for proc, count in kills_count] ) yield dict( @@ -202,9 +203,11 @@ def human_size(bytes_): # Adapted from https://stackoverflow.com/a/1094933 for unit in ["", "ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: if abs(bytes_) < 1024.0: - return "%s %sB" % (round_(bytes_), unit) + bytes_ = round_(bytes_) + return f"{bytes_} {unit}B" bytes_ /= 1024.0 - return "%s %sB" % (round_(bytes_), "Yi") + bytes_ = round_(bytes_) + return f"{bytes_} YiB" def round_(n): @@ -214,7 +217,3 @@ def round_(n): if n > 10: n = int(round(n)) return n - - -def main(args, env, loggers): - return SystemResourcesDiagnoser(args, env, loggers).diagnose() diff --git a/data/hooks/diagnosis/70-regenconf.py b/src/diagnosers/70-regenconf.py similarity index 94% rename from data/hooks/diagnosis/70-regenconf.py rename to src/diagnosers/70-regenconf.py index 8ccbeed58..591f883a4 100644 --- a/data/hooks/diagnosis/70-regenconf.py +++ b/src/diagnosers/70-regenconf.py @@ -2,6 +2,7 @@ import os import re +from typing import List from yunohost.settings import settings_get from yunohost.diagnosis import Diagnoser @@ -9,11 +10,11 @@ from yunohost.regenconf import _get_regenconf_infos, _calculate_hash from moulinette.utils.filesystem import read_file -class RegenconfDiagnoser(Diagnoser): +class MyDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 - dependencies = [] + dependencies: List[str] = [] def run(self): @@ -70,7 +71,3 @@ class RegenconfDiagnoser(Diagnoser): for path, hash_ in infos["conffiles"].items(): if hash_ != _calculate_hash(path): yield {"path": path, "category": category} - - -def main(args, env, loggers): - return RegenconfDiagnoser(args, env, loggers).diagnose() diff --git a/data/hooks/diagnosis/80-apps.py b/src/diagnosers/80-apps.py similarity index 92% rename from data/hooks/diagnosis/80-apps.py rename to src/diagnosers/80-apps.py index 177ec590f..56e45f831 100644 --- a/data/hooks/diagnosis/80-apps.py +++ b/src/diagnosers/80-apps.py @@ -1,17 +1,18 @@ #!/usr/bin/env python import os +from typing import List from yunohost.app import app_list from yunohost.diagnosis import Diagnoser -class AppDiagnoser(Diagnoser): +class MyDiagnoser(Diagnoser): id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 - dependencies = [] + dependencies: List[str] = [] def run(self): @@ -76,7 +77,7 @@ class AppDiagnoser(Diagnoser): for deprecated_helper in deprecated_helpers: if ( os.system( - f"grep -nr -q '{deprecated_helper}' {app['setting_path']}/scripts/" + f"grep -hr '{deprecated_helper}' {app['setting_path']}/scripts/ | grep -v -q '^\\s*#'" ) == 0 ): @@ -90,7 +91,3 @@ class AppDiagnoser(Diagnoser): == 0 ): yield ("error", "diagnosis_apps_deprecated_practices") - - -def main(args, env, loggers): - return AppDiagnoser(args, env, loggers).diagnose() diff --git a/src/yunohost/data_migrations/__init__.py b/src/diagnosers/__init__.py similarity index 100% rename from src/yunohost/data_migrations/__init__.py rename to src/diagnosers/__init__.py diff --git a/src/yunohost/diagnosis.py b/src/diagnosis.py similarity index 88% rename from src/yunohost/diagnosis.py rename to src/diagnosis.py index 4ac5e2731..007719dfc 100644 --- a/src/yunohost/diagnosis.py +++ b/src/diagnosis.py @@ -27,6 +27,8 @@ import re import os import time +import glob +from importlib import import_module from moulinette import m18n, Moulinette from moulinette.utils import log @@ -38,7 +40,6 @@ from moulinette.utils.filesystem import ( ) from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.hook import hook_list, hook_exec logger = log.getActionLogger("yunohost.diagnosis") @@ -48,15 +49,13 @@ DIAGNOSIS_SERVER = "diagnosis.yunohost.org" def diagnosis_list(): - all_categories_names = [h for h, _ in _list_diagnosis_categories()] - return {"categories": all_categories_names} + return {"categories": _list_diagnosis_categories()} def diagnosis_get(category, item): # Get all the categories - all_categories = _list_diagnosis_categories() - all_categories_names = [c for c, _ in all_categories] + all_categories_names = _list_diagnosis_categories() if category not in all_categories_names: raise YunohostValidationError( @@ -84,8 +83,7 @@ def diagnosis_show( return # Get all the categories - all_categories = _list_diagnosis_categories() - all_categories_names = [category for category, _ in all_categories] + all_categories_names = _list_diagnosis_categories() # Check the requested category makes sense if categories == []: @@ -174,8 +172,7 @@ def diagnosis_run( return # Get all the categories - all_categories = _list_diagnosis_categories() - all_categories_names = [category for category, _ in all_categories] + all_categories_names = _list_diagnosis_categories() # Check the requested category makes sense if categories == []: @@ -191,11 +188,12 @@ def diagnosis_run( # Call the hook ... diagnosed_categories = [] for category in categories: - logger.debug("Running diagnosis for %s ..." % category) - path = [p for n, p in all_categories if n == category][0] + logger.debug(f"Running diagnosis for {category} ...") + + diagnoser = _load_diagnoser(category) try: - code, report = hook_exec(path, args={"force": force}, env=None) + code, report = diagnoser.diagnose(force=force) except Exception: import traceback @@ -275,8 +273,7 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): def validate_filter_criterias(filter_): # Get all the categories - all_categories = _list_diagnosis_categories() - all_categories_names = [category for category, _ in all_categories] + all_categories_names = _list_diagnosis_categories() # Sanity checks for the provided arguments if len(filter_) == 0: @@ -285,7 +282,7 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): ) category = filter_[0] if category not in all_categories_names: - raise YunohostValidationError("%s is not a diagnosis category" % category) + raise YunohostValidationError(f"{category} is not a diagnosis category") if any("=" not in criteria for criteria in filter_[1:]): raise YunohostValidationError( "Criterias should be of the form key=value (e.g. domain=yolo.test)" @@ -404,12 +401,8 @@ def add_ignore_flag_to_issues(report): class Diagnoser: - def __init__(self, args, env, loggers): + def __init__(self): - # FIXME ? That stuff with custom loggers is weird ... (mainly inherited from the bash hooks, idk) - self.logger_debug, self.logger_warning, self.logger_info = loggers - self.env = env - self.args = args or {} self.cache_file = Diagnoser.cache_file(self.id_) self.description = Diagnoser.get_description(self.id_) @@ -424,13 +417,10 @@ class Diagnoser: os.makedirs(DIAGNOSIS_CACHE) return write_to_json(self.cache_file, report) - def diagnose(self): + def diagnose(self, force=False): - if ( - not self.args.get("force", False) - and self.cached_time_ago() < self.cache_duration - ): - self.logger_debug("Cache still valid : %s" % self.cache_file) + if not force and self.cached_time_ago() < self.cache_duration: + logger.debug(f"Cache still valid : {self.cache_file}") logger.info( m18n.n("diagnosis_cache_still_valid", category=self.description) ) @@ -464,7 +454,7 @@ class Diagnoser: new_report = {"id": self.id_, "cached_for": self.cache_duration, "items": items} - self.logger_debug("Updating cache %s" % self.cache_file) + logger.debug(f"Updating cache {self.cache_file}") self.write_cache(new_report) Diagnoser.i18n(new_report) add_ignore_flag_to_issues(new_report) @@ -537,7 +527,7 @@ class Diagnoser: @staticmethod def cache_file(id_): - return os.path.join(DIAGNOSIS_CACHE, "%s.json" % id_) + return os.path.join(DIAGNOSIS_CACHE, f"{id_}.json") @staticmethod def get_cached_report(id_, item=None, warn_if_no_cache=True): @@ -640,7 +630,7 @@ class Diagnoser: elif ipversion == 6: socket.getaddrinfo = getaddrinfo_ipv6_only - url = "https://%s/%s" % (DIAGNOSIS_SERVER, uri) + url = f"https://{DIAGNOSIS_SERVER}/{uri}" try: r = requests.post(url, json=data, timeout=timeout) finally: @@ -648,40 +638,69 @@ class Diagnoser: if r.status_code not in [200, 400]: raise Exception( - "The remote diagnosis server failed miserably while trying to diagnose your server. This is most likely an error on Yunohost's infrastructure and not on your side. Please contact the YunoHost team an provide them with the following information.
URL: %s
Status code: %s" - % (url, r.status_code) + f"The remote diagnosis server failed miserably while trying to diagnose your server. This is most likely an error on Yunohost's infrastructure and not on your side. Please contact the YunoHost team an provide them with the following information.
URL: {url}
Status code: {r.status_code}" ) if r.status_code == 400: - raise Exception("Diagnosis request was refused: %s" % r.content) + raise Exception(f"Diagnosis request was refused: {r.content}") try: r = r.json() except Exception as e: raise Exception( - "Failed to parse json from diagnosis server response.\nError: %s\nOriginal content: %s" - % (e, r.content) + f"Failed to parse json from diagnosis server response.\nError: {e}\nOriginal content: {r.content}" ) return r def _list_diagnosis_categories(): - hooks_raw = hook_list("diagnosis", list_by="priority", show_info=True)["hooks"] - hooks = [] - for _, some_hooks in sorted(hooks_raw.items(), key=lambda h: int(h[0])): - for name, info in some_hooks.items(): - hooks.append((name, info["path"])) - return hooks + paths = glob.glob(os.path.dirname(__file__) + "/diagnosers/??-*.py") + names = [ + name.split("-")[-1] + for name in sorted([os.path.basename(path)[: -len(".py")] for path in paths]) + ] + + return names + + +def _load_diagnoser(diagnoser_name): + + logger.debug(f"Loading diagnoser {diagnoser_name}") + + paths = glob.glob(os.path.dirname(__file__) + f"/diagnosers/??-{diagnoser_name}.py") + + if len(paths) != 1: + raise YunohostError( + f"Uhoh, found several matches (or none?) for diagnoser {diagnoser_name} : {paths}", + raw_msg=True, + ) + + module_id = os.path.basename(paths[0][: -len(".py")]) + + try: + # this is python builtin method to import a module using a name, we + # use that to import the migration as a python object so we'll be + # able to run it in the next loop + module = import_module(f"yunohost.diagnosers.{module_id}") + return module.MyDiagnoser() + except Exception as e: + import traceback + + traceback.print_exc() + + raise YunohostError( + f"Failed to load diagnoser {diagnoser_name} : {e}", raw_msg=True + ) def _email_diagnosis_issues(): from yunohost.domain import _get_maindomain maindomain = _get_maindomain() - from_ = "diagnosis@%s (Automatic diagnosis on %s)" % (maindomain, maindomain) + from_ = f"diagnosis@{maindomain} (Automatic diagnosis on {maindomain})" to_ = "root" - subject_ = "Issues found by automatic diagnosis on %s" % maindomain + subject_ = f"Issues found by automatic diagnosis on {maindomain}" disclaimer = "The automatic diagnosis on your YunoHost server identified some issues on your server. You will find a description of the issues below. You can manage those issues in the 'Diagnosis' section in your webadmin." @@ -691,23 +710,17 @@ def _email_diagnosis_issues(): content = _dump_human_readable_reports(issues) - message = """\ -From: %s -To: %s -Subject: %s + message = f"""\ +From: {from_} +To: {to_} +Subject: {subject_} -%s +{disclaimer} --- -%s -""" % ( - from_, - to_, - subject_, - disclaimer, - content, - ) +{content} +""" import smtplib diff --git a/src/dns.py b/src/dns.py new file mode 100644 index 000000000..27b60561e --- /dev/null +++ b/src/dns.py @@ -0,0 +1,1015 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2013 YunoHost + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +""" yunohost_domain.py + + Manage domains +""" +import os +import re +import time + +from difflib import SequenceMatcher +from collections import OrderedDict + +from moulinette import m18n, Moulinette +from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_file, write_to_file, read_toml, mkdir + +from yunohost.domain import ( + domain_list, + _assert_domain_exists, + domain_config_get, + _get_domain_settings, + _set_domain_settings, + _list_subdomains_of, +) +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 +from yunohost.hook import hook_callback + +logger = getActionLogger("yunohost.domain") + +DOMAIN_REGISTRAR_LIST_PATH = "/usr/share/yunohost/registrar_list.toml" + + +def domain_dns_suggest(domain): + """ + Generate DNS configuration for a domain + + Keyword argument: + domain -- Domain name + + """ + + 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) + + result = "" + + if dns_conf["basic"]: + result += "; Basic ipv4/ipv6 records" + for record in dns_conf["basic"]: + result += "\n{name} {ttl} IN {type} {value}".format(**record) + + if dns_conf["mail"]: + result += "\n\n" + result += "; Mail" + for record in dns_conf["mail"]: + result += "\n{name} {ttl} IN {type} {value}".format(**record) + result += "\n\n" + + if dns_conf["xmpp"]: + result += "\n\n" + result += "; XMPP" + for record in dns_conf["xmpp"]: + result += "\n{name} {ttl} IN {type} {value}".format(**record) + + if dns_conf["extra"]: + result += "\n\n" + result += "; Extra" + for record in dns_conf["extra"]: + result += "\n{name} {ttl} IN {type} {value}".format(**record) + + for name, record_list in dns_conf.items(): + if name not in ("basic", "xmpp", "mail", "extra") and record_list: + result += "\n\n" + result += "; " + name + for record in record_list: + result += "\n{name} {ttl} IN {type} {value}".format(**record) + + if Moulinette.interface.type == "cli": + # FIXME Update this to point to our "dns push" doc + logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation")) + + return result + + +def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): + """ + Internal function that will returns a data structure containing the needed + information to generate/adapt the dns configuration + + Arguments: + domains -- List of a domain and its subdomains + + The returned datastructure will have the following form: + { + "basic": [ + # if ipv4 available + {"type": "A", "name": "@", "value": "123.123.123.123", "ttl": 3600}, + # if ipv6 available + {"type": "AAAA", "name": "@", "value": "valid-ipv6", "ttl": 3600}, + ], + "xmpp": [ + {"type": "SRV", "name": "_xmpp-client._tcp", "value": "0 5 5222 domain.tld.", "ttl": 3600}, + {"type": "SRV", "name": "_xmpp-server._tcp", "value": "0 5 5269 domain.tld.", "ttl": 3600}, + {"type": "CNAME", "name": "muc", "value": "@", "ttl": 3600}, + {"type": "CNAME", "name": "pubsub", "value": "@", "ttl": 3600}, + {"type": "CNAME", "name": "vjud", "value": "@", "ttl": 3600} + {"type": "CNAME", "name": "xmpp-upload", "value": "@", "ttl": 3600} + ], + "mail": [ + {"type": "MX", "name": "@", "value": "10 domain.tld.", "ttl": 3600}, + {"type": "TXT", "name": "@", "value": "\"v=spf1 a mx ip4:123.123.123.123 ipv6:valid-ipv6 -all\"", "ttl": 3600 }, + {"type": "TXT", "name": "mail._domainkey", "value": "\"v=DKIM1; k=rsa; p=some-super-long-key\"", "ttl": 3600}, + {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600} + ], + "extra": [ + # if ipv4 available + {"type": "A", "name": "*", "value": "123.123.123.123", "ttl": 3600}, + # if ipv6 available + {"type": "AAAA", "name": "*", "value": "valid-ipv6", "ttl": 3600}, + {"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600}, + ], + "example_of_a_custom_rule": [ + {"type": "SRV", "name": "_matrix", "value": "domain.tld.", "ttl": 3600} + ], + } + """ + + basic = [] + mail = [] + xmpp = [] + extra = [] + ipv4 = get_public_ip() + ipv6 = get_public_ip(6) + + # 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 is_yunohost_dyndns_domain(base_domain): + subdomains = [] + else: + subdomains = _list_subdomains_of(base_domain) + + domains_settings = { + domain: domain_config_get(domain, export=True) + for domain in [base_domain] + subdomains + } + + base_dns_zone = _get_dns_zone_for_domain(base_domain) + + for domain, settings in domains_settings.items(): + + # Domain # Base DNS zone # Basename # Suffix # + # ------------------ # ----------------- # --------- # -------- # + # domain.tld # domain.tld # @ # # + # sub.domain.tld # domain.tld # sub # .sub # + # foo.sub.domain.tld # domain.tld # foo.sub # .foo.sub # + # sub.domain.tld # sub.domain.tld # @ # # + # foo.sub.domain.tld # sub.domain.tld # foo # .foo # + + basename = domain.replace(base_dns_zone, "").rstrip(".") or "@" + suffix = f".{basename}" if basename != "@" else "" + + # ttl = settings["ttl"] + ttl = 3600 + + ########################### + # Basic ipv4/ipv6 records # + ########################### + if ipv4: + basic.append([basename, ttl, "A", ipv4]) + + if ipv6: + basic.append([basename, ttl, "AAAA", ipv6]) + elif include_empty_AAAA_if_no_ipv6: + basic.append([basename, ttl, "AAAA", None]) + + ######### + # Email # + ######### + if settings["mail_in"]: + mail.append([basename, ttl, "MX", f"10 {domain}."]) + + if settings["mail_out"]: + mail.append([basename, ttl, "TXT", '"v=spf1 a mx -all"']) + + # DKIM/DMARC record + dkim_host, dkim_publickey = _get_DKIM(domain) + + if dkim_host: + mail += [ + [f"{dkim_host}{suffix}", ttl, "TXT", dkim_publickey], + [f"_dmarc{suffix}", ttl, "TXT", '"v=DMARC1; p=none"'], + ] + + ######## + # XMPP # + ######## + if settings["xmpp"]: + xmpp += [ + [ + f"_xmpp-client._tcp{suffix}", + ttl, + "SRV", + f"0 5 5222 {domain}.", + ], + [ + f"_xmpp-server._tcp{suffix}", + ttl, + "SRV", + f"0 5 5269 {domain}.", + ], + [f"muc{suffix}", ttl, "CNAME", basename], + [f"pubsub{suffix}", ttl, "CNAME", basename], + [f"vjud{suffix}", ttl, "CNAME", basename], + [f"xmpp-upload{suffix}", ttl, "CNAME", basename], + ] + + ######### + # Extra # + ######### + + # Only recommend wildcard and CAA for the top level + if domain == base_domain: + if ipv4: + extra.append([f"*{suffix}", ttl, "A", ipv4]) + + if ipv6: + extra.append([f"*{suffix}", ttl, "AAAA", ipv6]) + elif include_empty_AAAA_if_no_ipv6: + extra.append([f"*{suffix}", ttl, "AAAA", None]) + + extra.append([basename, ttl, "CAA", '128 issue "letsencrypt.org"']) + + #################### + # Standard records # + #################### + + records = { + "basic": [ + {"name": name, "ttl": ttl_, "type": type_, "value": value} + for name, ttl_, type_, value in basic + ], + "xmpp": [ + {"name": name, "ttl": ttl_, "type": type_, "value": value} + for name, ttl_, type_, value in xmpp + ], + "mail": [ + {"name": name, "ttl": ttl_, "type": type_, "value": value} + for name, ttl_, type_, value in mail + ], + "extra": [ + {"name": name, "ttl": ttl_, "type": type_, "value": value} + for name, ttl_, type_, value in extra + ], + } + + ################## + # Custom records # + ################## + + # 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(): + # + # There can be multiple results per hook name, so results look like + # {'/some/path/to/hook1': + # { 'state': 'succeed', + # 'stdreturn': [{'type': 'SRV', + # 'name': 'stuff.foo.bar.', + # 'value': 'yoloswag', + # 'ttl': 3600}] + # }, + # '/some/path/to/hook2': + # { ... }, + # [...] + # + # Loop over the sub-results + custom_records = [ + v["stdreturn"] for v in results.values() if v and v["stdreturn"] + ] + + records[hook_name] = [] + for record_list in custom_records: + # Check that record_list is indeed a list of dict + # with the required keys + if ( + not isinstance(record_list, list) + or any(not isinstance(record, dict) for record in record_list) + or any( + key not in record + for record in record_list + for key in ["name", "ttl", "type", "value"] + ) + ): + # Display an error, mainly for app packagers trying to implement a hook + logger.warning( + "Ignored custom record from hook '%s' because the data is not a *list* of dict with keys name, ttl, type and value. Raw data : %s" + % (hook_name, record_list) + ) + continue + + records[hook_name].extend(record_list) + + return records + + +def _get_DKIM(domain): + DKIM_file = f"/etc/dkim/{domain}.mail.txt" + + if not os.path.isfile(DKIM_file): + return (None, None) + + with open(DKIM_file) as f: + dkim_content = f.read() + + # Gotta manage two formats : + # + # Legacy + # ----- + # + # mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " + # "p=" ) + # + # New + # ------ + # + # mail._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; " + # "p=" ) + + is_legacy_format = " h=sha256; " not in dkim_content + + # Legacy DKIM format + if is_legacy_format: + dkim = re.match( + ( + r"^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+" + r'[^"]*"v=(?P[^";]+);' + r'[\s"]*k=(?P[^";]+);' + r'[\s"]*p=(?P

[^";]+)' + ), + dkim_content, + re.M | re.S, + ) + else: + dkim = re.match( + ( + r"^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+" + r'[^"]*"v=(?P[^";]+);' + r'[\s"]*h=(?P[^";]+);' + r'[\s"]*k=(?P[^";]+);' + r'[\s"]*p=(?P

[^";]+)' + ), + dkim_content, + re.M | re.S, + ) + + if not dkim: + return (None, None) + + if is_legacy_format: + return ( + dkim.group("host"), + '"v={v}; k={k}; p={p}"'.format( + v=dkim.group("v"), k=dkim.group("k"), p=dkim.group("p") + ), + ) + else: + return ( + dkim.group("host"), + '"v={v}; h={h}; k={k}; p={p}"'.format( + v=dkim.group("v"), + h=dkim.group("h"), + k=dkim.group("k"), + p=dkim.group("p"), + ), + ) + + +def _get_dns_zone_for_domain(domain): + """ + Get the DNS zone of a domain + + Keyword arguments: + domain -- The domain name + + """ + + # 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 + 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" + cache_file = f"{cache_folder}/{domain}" + cache_duration = 3600 # one hour + if ( + os.path.exists(cache_file) + and abs(os.path.getctime(cache_file) - time.time()) < cache_duration + ): + dns_zone = read_file(cache_file).strip() + if dns_zone: + return dns_zone + + # Check cache for parent domain + # This is another strick to try to prevent this function from being + # a bottleneck on system with 1 main domain + 10ish subdomains + # when building the dns conf for the main domain (which will call domain_config_get, etc...) + parent_domain = domain.split(".", 1)[1] + if parent_domain in domain_list()["domains"]: + parent_cache_file = f"{cache_folder}/{parent_domain}" + if ( + os.path.exists(parent_cache_file) + and abs(os.path.getctime(parent_cache_file) - time.time()) < cache_duration + ): + dns_zone = read_file(parent_cache_file).strip() + if dns_zone: + return dns_zone + + # For foo.bar.baz.gni we want to scan all the parent domains + # (including the domain itself) + # foo.bar.baz.gni + # bar.baz.gni + # baz.gni + # gni + # Until we find the first one that has a NS record + parent_list = [domain.split(".", i)[-1] for i, _ in enumerate(domain.split("."))] + + for parent in parent_list: + + # 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, force=True) + write_to_file(cache_file, parent) + return parent + + if len(parent_list) >= 2: + zone = parent_list[-2] + else: + zone = parent_list[-1] + + logger.warning( + f"Could not identify the dns zone for domain {domain}, returning {zone}" + ) + return zone + + +def _get_registrar_config_section(domain): + + from lexicon.providers.auto import _relevant_provider_for_domain + + registrar_infos = {} + + dns_zone = _get_dns_zone_for_domain(domain) + + # If parent domain exists in yunohost + parent_domain = domain.split(".", 1)[1] + if parent_domain in domain_list()["domains"]: + + # Dirty hack to have a link on the webadmin + if Moulinette.interface.type == "api": + parent_domain_link = f"[{parent_domain}](#/domains/{parent_domain}/config)" + else: + parent_domain_link = parent_domain + + registrar_infos["registrar"] = OrderedDict( + { + "type": "alert", + "style": "info", + "ask": m18n.n( + "domain_dns_registrar_managed_in_parent_domain", + parent_domain=domain, + parent_domain_link=parent_domain_link, + ), + "value": "parent_domain", + } + ) + return OrderedDict(registrar_infos) + + # TODO big project, integrate yunohost's dynette as a registrar-like provider + # TODO big project, integrate other dyndns providers such as netlib.re, or cf the list of dyndns providers supported by cloudron... + if is_yunohost_dyndns_domain(dns_zone): + registrar_infos["registrar"] = OrderedDict( + { + "type": "alert", + "style": "success", + "ask": m18n.n("domain_dns_registrar_yunohost"), + "value": "yunohost", + } + ) + 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] + except ValueError: + registrar_infos["registrar"] = OrderedDict( + { + "type": "alert", + "style": "warning", + "ask": m18n.n("domain_dns_registrar_not_supported"), + "value": None, + } + ) + else: + + registrar_infos["registrar"] = OrderedDict( + { + "type": "alert", + "style": "info", + "ask": m18n.n("domain_dns_registrar_supported", registrar=registrar), + "value": registrar, + } + ) + + TESTED_REGISTRARS = ["ovh", "gandi"] + if registrar not in TESTED_REGISTRARS: + registrar_infos["experimental_disclaimer"] = OrderedDict( + { + "type": "alert", + "style": "danger", + "ask": m18n.n( + "domain_dns_registrar_experimental", registrar=registrar + ), + } + ) + + # TODO : add a help tip with the link to the registar's API doc (c.f. Lexicon's README) + registrar_list = read_toml(DOMAIN_REGISTRAR_LIST_PATH) + registrar_credentials = registrar_list[registrar] + for credential, infos in registrar_credentials.items(): + infos["default"] = infos.get("default", "") + infos["optional"] = infos.get("optional", "False") + registrar_infos.update(registrar_credentials) + + return OrderedDict(registrar_infos) + + +def _get_registar_settings(domain): + + _assert_domain_exists(domain) + + settings = domain_config_get(domain, key="dns.registrar", export=True) + + registrar = settings.pop("registrar") + + if "experimental_disclaimer" in settings: + settings.pop("experimental_disclaimer") + + return registrar, settings + + +@is_unit_operation() +def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge=False): + """ + Send DNS records to the previously-configured registrar of the domain. + """ + + from lexicon.client import Client as LexiconClient + from lexicon.config import ConfigResolver as LexiconConfigResolver + + registrar, registrar_credentials = _get_registar_settings(domain) + + _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) + + # FIXME: in the future, properly unify this with yunohost dyndns update + if registrar == "yunohost": + logger.info(m18n.n("domain_dns_registrar_yunohost")) + return {} + + if registrar == "parent_domain": + parent_domain = domain.split(".", 1)[1] + registar, registrar_credentials = _get_registar_settings(parent_domain) + if any(registrar_credentials.values()): + raise YunohostValidationError( + "domain_dns_push_managed_in_parent_domain", + domain=domain, + parent_domain=parent_domain, + ) + else: + raise YunohostValidationError( + "domain_registrar_is_not_configured", domain=parent_domain + ) + + if not all(registrar_credentials.values()): + raise YunohostValidationError( + "domain_registrar_is_not_configured", domain=domain + ) + + base_dns_zone = _get_dns_zone_for_domain(domain) + + # Convert the generated conf into a format that matches what we'll fetch using the API + # Makes it easier to compare "wanted records" with "current records on remote" + wanted_records = [] + for records in _build_dns_conf(domain).values(): + for record in records: + + # Make sure the name is a FQDN + name = ( + f"{record['name']}.{base_dns_zone}" + if record["name"] != "@" + else base_dns_zone + ) + type_ = record["type"] + content = record["value"] + + # Make sure the content is also a FQDN (with trailing . ?) + if content == "@" and record["type"] == "CNAME": + content = base_dns_zone + "." + + wanted_records.append( + {"name": name, "type": type_, "ttl": record["ttl"], "content": content} + ) + + # FIXME Lexicon does not support CAA records + # See https://github.com/AnalogJ/lexicon/issues/282 and https://github.com/AnalogJ/lexicon/pull/371 + # They say it's trivial to implement it! + # And yet, it is still not done/merged + # Update by Aleks: it works - at least with Gandi ?! + # wanted_records = [record for record in wanted_records if record["type"] != "CAA"] + + if purge: + wanted_records = [] + force = True + + # Construct the base data structure to use lexicon's API. + + base_config = { + "provider_name": registrar, + "domain": base_dns_zone, + registrar: registrar_credentials, + } + + # Ugly hack to be able to fetch all record types at once: + # we initialize a LexiconClient with a dummy type "all" + # (which lexicon doesnt actually understands) + # then trigger ourselves the authentication + list_records + # instead of calling .execute() + query = ( + LexiconConfigResolver() + .with_dict(dict_object=base_config) + .with_dict(dict_object={"action": "list", "type": "all"}) + ) + client = LexiconClient(query) + try: + client.provider.authenticate() + except Exception as e: + raise YunohostValidationError( + "domain_dns_push_failed_to_authenticate", domain=domain, error=str(e) + ) + + try: + current_records = client.provider.list_records() + except Exception as e: + raise YunohostError("domain_dns_push_failed_to_list", error=str(e)) + + managed_dns_records_hashes = _get_managed_dns_records_hashes(domain) + + # Keep only records for relevant types: A, AAAA, MX, TXT, CNAME, SRV + relevant_types = ["A", "AAAA", "MX", "TXT", "CNAME", "SRV", "CAA"] + current_records = [r for r in current_records if r["type"] in relevant_types] + + # Ignore records which are for a higher-level domain + # i.e. we don't care about the records for domain.tld when pushing yuno.domain.tld + current_records = [ + r + for r in current_records + if r["name"].endswith(f".{domain}") or r["name"] == domain + ] + + for record in current_records: + + # Try to get rid of weird stuff like ".domain.tld" or "@.domain.tld" + record["name"] = record["name"].strip("@").strip(".") + + # Some API return '@' in content and we shall convert it to absolute/fqdn + record["content"] = ( + record["content"] + .replace("@.", base_dns_zone + ".") + .replace("@", base_dns_zone + ".") + ) + + if record["type"] == "TXT": + if not record["content"].startswith('"'): + record["content"] = '"' + record["content"] + if not record["content"].endswith('"'): + record["content"] = record["content"] + '"' + + # Check if this record was previously set by YunoHost + record["managed_by_yunohost"] = ( + _hash_dns_record(record) in managed_dns_records_hashes + ) + + # Step 0 : Get the list of unique (type, name) + # And compare the current and wanted records + # + # i.e. we want this kind of stuff: + # wanted current + # (A, .domain.tld) 1.2.3.4 1.2.3.4 + # (A, www.domain.tld) 1.2.3.4 5.6.7.8 + # (A, foobar.domain.tld) 1.2.3.4 + # (AAAA, .domain.tld) 2001::abcd + # (MX, .domain.tld) 10 domain.tld [10 mx1.ovh.net, 20 mx2.ovh.net] + # (TXT, .domain.tld) "v=spf1 ..." ["v=spf1", "foobar"] + # (SRV, .domain.tld) 0 5 5269 domain.tld + changes = {"delete": [], "update": [], "create": [], "unchanged": []} + + type_and_names = sorted( + {(r["type"], r["name"]) for r in current_records + wanted_records} + ) + comparison = { + type_and_name: {"current": [], "wanted": []} for type_and_name in type_and_names + } + + for record in current_records: + comparison[(record["type"], record["name"])]["current"].append(record) + + for record in wanted_records: + comparison[(record["type"], record["name"])]["wanted"].append(record) + + for type_and_name, records in comparison.items(): + + # + # Step 1 : compute a first "diff" where we remove records which are the same on both sides + # + wanted_contents = [r["content"] for r in records["wanted"]] + current_contents = [r["content"] for r in records["current"]] + + current = [r for r in records["current"] if r["content"] not in wanted_contents] + wanted = [r for r in records["wanted"] if r["content"] not in current_contents] + + # + # Step 2 : simple case: 0 record on one side, 0 on the other + # -> either nothing do (0/0) or creations (0/N) or deletions (N/0) + # + if len(current) == 0 and len(wanted) == 0: + # No diff, nothing to do + changes["unchanged"].extend(records["current"]) + continue + + elif len(wanted) == 0: + changes["delete"].extend(current) + continue + + elif len(current) == 0: + changes["create"].extend(wanted) + continue + + # + # Step 3 : N record on one side, M on the other + # + # Fuzzy matching strategy: + # For each wanted record, try to find a current record which looks like the wanted one + # -> if found, trigger an update + # -> if no match found, trigger a create + # + for record in wanted: + + def likeliness(r): + # We compute this only on the first 100 chars, to have a high value even for completely different DKIM keys + return SequenceMatcher( + None, r["content"][:100], record["content"][:100] + ).ratio() + + matches = sorted(current, key=lambda r: likeliness(r), reverse=True) + if matches and likeliness(matches[0]) > 0.50: + match = matches[0] + # Remove the match from 'current' so that it's not added to the removed stuff later + current.remove(match) + match["old_content"] = match["content"] + match["content"] = record["content"] + changes["update"].append(match) + else: + changes["create"].append(record) + + # + # For all other remaining current records: + # -> trigger deletions + # + for record in current: + changes["delete"].append(record) + + def relative_name(name): + name = name.strip(".") + name = name.replace("." + base_dns_zone, "") + name = name.replace(base_dns_zone, "@") + return name + + def human_readable_record(action, record): + name = relative_name(record["name"]) + name = name[:20] + t = record["type"] + + if not force and action in ["update", "delete"]: + ignored = ( + "" + if record["managed_by_yunohost"] + else "(ignored, won't be changed by Yunohost unless forced)" + ) + else: + ignored = "" + + if action == "create": + new_content = record.get("content", "(None)")[:30] + return f"{name:>20} [{t:^5}] {new_content:^30} {ignored}" + elif action == "update": + old_content = record.get("old_content", "(None)")[:30] + new_content = record.get("content", "(None)")[:30] + return ( + f"{name:>20} [{t:^5}] {old_content:^30} -> {new_content:^30} {ignored}" + ) + elif action == "unchanged": + old_content = record.get("content", "(None)")[:30] + return f"{name:>20} [{t:^5}] {old_content:^30}" + else: + old_content = record.get("content", "(None)")[:30] + return f"{name:>20} [{t:^5}] {old_content:^30} {ignored}" + + if dry_run: + if Moulinette.interface.type == "api": + for records in changes.values(): + for record in records: + record["name"] = relative_name(record["name"]) + return changes + else: + out = {"delete": [], "create": [], "update": [], "unchanged": []} + for action in ["delete", "create", "update", "unchanged"]: + for record in changes[action]: + out[action].append(human_readable_record(action, record)) + + return out + + # If --force ain't used, we won't delete/update records not managed by yunohost + if not force: + for action in ["delete", "update"]: + changes[action] = [r for r in changes[action] if r["managed_by_yunohost"]] + + def progress(info=""): + progress.nb += 1 + width = 20 + bar = int(progress.nb * width / progress.total) + bar = "[" + "#" * bar + "." * (width - bar) + "]" + if info: + bar += " > " + info + if progress.old == bar: + return + progress.old = bar + logger.info(bar) + + progress.nb = 0 + progress.old = "" + progress.total = len(changes["delete"] + changes["create"] + changes["update"]) + + if progress.total == 0: + logger.success(m18n.n("domain_dns_push_already_up_to_date")) + return {} + + # + # Actually push the records + # + + operation_logger.start() + logger.info(m18n.n("domain_dns_pushing")) + + new_managed_dns_records_hashes = [_hash_dns_record(r) for r in changes["unchanged"]] + results = {"warnings": [], "errors": []} + + for action in ["delete", "create", "update"]: + + for record in changes[action]: + + relative_name = record["name"].replace(base_dns_zone, "").rstrip(".") or "@" + progress( + f"{action} {record['type']:^5} / {relative_name}" + ) # FIXME: i18n but meh + + # Apparently Lexicon yields us some 'id' during fetch + # But wants 'identifier' during push ... + if "id" in record: + record["identifier"] = record["id"] + del record["id"] + + if registrar == "godaddy": + if record["name"] == base_dns_zone: + record["name"] = "@." + record["name"] + if record["type"] in ["MX", "SRV", "CAA"]: + logger.warning( + f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy." + ) + results["warnings"].append( + f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy." + ) + continue + + record["action"] = action + query = ( + LexiconConfigResolver() + .with_dict(dict_object=base_config) + .with_dict(dict_object=record) + ) + + try: + result = LexiconClient(query).execute() + except Exception as e: + msg = m18n.n( + "domain_dns_push_record_failed", + action=action, + type=record["type"], + name=record["name"], + error=str(e), + ) + logger.error(msg) + results["errors"].append(msg) + else: + if result: + new_managed_dns_records_hashes.append(_hash_dns_record(record)) + else: + msg = m18n.n( + "domain_dns_push_record_failed", + action=action, + type=record["type"], + name=record["name"], + error="unkonwn error?", + ) + logger.error(msg) + results["errors"].append(msg) + + _set_managed_dns_records_hashes(domain, new_managed_dns_records_hashes) + + # Everything succeeded + if len(results["errors"]) + len(results["warnings"]) == 0: + logger.success(m18n.n("domain_dns_push_success")) + return {} + # Everything failed + elif len(results["errors"]) + len(results["warnings"]) == progress.total: + logger.error(m18n.n("domain_dns_push_failed")) + else: + logger.warning(m18n.n("domain_dns_push_partial_failure")) + + return results + + +def _get_managed_dns_records_hashes(domain: str) -> list: + return _get_domain_settings(domain).get("managed_dns_records_hashes", []) + + +def _set_managed_dns_records_hashes(domain: str, hashes: list) -> None: + settings = _get_domain_settings(domain) + settings["managed_dns_records_hashes"] = hashes or [] + _set_domain_settings(domain, settings) + + +def _hash_dns_record(record: dict) -> int: + + fields = ["name", "type", "content"] + record_ = {f: record.get(f) for f in fields} + + return hash(frozenset(record_.items())) diff --git a/src/domain.py b/src/domain.py new file mode 100644 index 000000000..e40b4f03c --- /dev/null +++ b/src/domain.py @@ -0,0 +1,570 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2013 YunoHost + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +""" yunohost_domain.py + + Manage domains +""" +import os +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, rm + +from yunohost.app import ( + app_ssowatconf, + _installed_apps, + _get_app_settings, + _get_conflicting_apps, +) +from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf +from yunohost.utils.config import ConfigPanel, Question +from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.log import is_unit_operation + +logger = getActionLogger("yunohost.domain") + +DOMAIN_CONFIG_PATH = "/usr/share/yunohost/config_domain.toml" +DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" + +# Lazy dev caching to avoid re-query ldap every time we need the domain list +domain_list_cache: Dict[str, Any] = {} + + +def domain_list(exclude_subdomains=False): + """ + List domains + + Keyword argument: + exclude_subdomains -- Filter out domains that are subdomains of other declared domains + + """ + global domain_list_cache + if not exclude_subdomains and domain_list_cache: + return domain_list_cache + + from yunohost.utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + result = [ + entry["virtualdomain"][0] + for entry in ldap.search("ou=domains", "virtualdomain=*", ["virtualdomain"]) + ] + + result_list = [] + for domain in result: + if exclude_subdomains: + parent_domain = domain.split(".", 1)[1] + if parent_domain in result: + continue + + result_list.append(domain) + + def cmp_domain(domain): + # Keep the main part of the domain and the extension together + # eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] + domain = domain.split(".") + domain[-1] = domain[-2] + domain.pop() + domain = list(reversed(domain)) + return domain + + result_list = sorted(result_list, key=cmp_domain) + + # Don't cache answer if using exclude_subdomains + if exclude_subdomains: + return {"domains": result_list, "main": _get_maindomain()} + + domain_list_cache = {"domains": result_list, "main": _get_maindomain()} + return domain_list_cache + + +def _assert_domain_exists(domain): + if domain not in domain_list()["domains"]: + raise YunohostValidationError("domain_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): + """ + Create a custom domain + + Keyword argument: + domain -- Domain name to add + dyndns -- Subscribe to DynDNS + + """ + from yunohost.hook import hook_callback + from yunohost.app import app_ssowatconf + from yunohost.utils.ldap import _get_ldap_interface + from yunohost.certificate import _certificate_install_selfsigned + + if domain.startswith("xmpp-upload."): + raise YunohostValidationError("domain_cannot_add_xmpp_upload") + + ldap = _get_ldap_interface() + + try: + ldap.validate_uniqueness({"virtualdomain": domain}) + except MoulinetteError: + raise YunohostValidationError("domain_exists") + + # Lower domain to avoid some edge cases issues + # See: https://forum.yunohost.org/t/invalid-domain-causes-diagnosis-web-to-fail-fr-on-demand/11765 + domain = domain.lower() + + # Non-latin characters (e.g. café.com => xn--caf-dma.com) + domain = domain.encode("idna").decode("utf-8") + + # DynDNS domain + if dyndns: + + from yunohost.utils.dns import is_yunohost_dyndns_domain + from yunohost.dyndns import _guess_current_dyndns_domain + + # Do not allow to subscribe to multiple dyndns domains... + if _guess_current_dyndns_domain() != (None, None): + raise YunohostValidationError("domain_dyndns_already_subscribed") + + # Check that this domain can effectively be provided by + # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st) + if not is_yunohost_dyndns_domain(domain): + raise YunohostValidationError("domain_dyndns_root_unknown") + + operation_logger.start() + + if dyndns: + from yunohost.dyndns import dyndns_subscribe + + # Actually subscribe + dyndns_subscribe(domain=domain) + + _certificate_install_selfsigned([domain], True) + + try: + attr_dict = { + "objectClass": ["mailDomain", "top"], + "virtualdomain": domain, + } + + try: + ldap.add(f"virtualdomain={domain},ou=domains", attr_dict) + except Exception as e: + raise YunohostError("domain_creation_failed", domain=domain, error=e) + finally: + global domain_list_cache + domain_list_cache = {} + + # Don't regen these conf if we're still in postinstall + if os.path.exists("/etc/yunohost/installed"): + # Sometime we have weird issues with the regenconf where some files + # appears as manually modified even though they weren't touched ... + # There are a few ideas why this happens (like backup/restore nginx + # conf ... which we shouldnt do ...). This in turns creates funky + # situation where the regenconf may refuse to re-create the conf + # (when re-creating a domain..) + # So here we force-clear the has out of the regenconf if it exists. + # This is a pretty ad hoc solution and only applied to nginx + # because it's one of the major service, but in the long term we + # should identify the root of this bug... + _force_clear_hashes([f"/etc/nginx/conf.d/{domain}.conf"]) + regen_conf( + names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"] + ) + app_ssowatconf() + + except Exception as e: + # Force domain removal silently + try: + domain_remove(domain, force=True) + except Exception: + pass + raise e + + hook_callback("post_domain_add", args=[domain]) + + logger.success(m18n.n("domain_created")) + + +@is_unit_operation() +def domain_remove(operation_logger, domain, remove_apps=False, force=False): + """ + Delete domains + + Keyword argument: + domain -- Domain to delete + remove_apps -- Remove applications installed on the domain + force -- Force the domain removal and don't not ask confirmation to + remove apps if remove_apps is specified + + """ + from yunohost.hook import hook_callback + from yunohost.app import app_ssowatconf, app_info, app_remove + from yunohost.utils.ldap import _get_ldap_interface + + # the 'force' here is related to the exception happening in domain_add ... + # we don't want to check the domain exists because the ldap add may have + # failed + if not force: + _assert_domain_exists(domain) + + # Check domain is not the main domain + if domain == _get_maindomain(): + other_domains = domain_list()["domains"] + other_domains.remove(domain) + + if other_domains: + raise YunohostValidationError( + "domain_cannot_remove_main", + domain=domain, + other_domains="\n * " + ("\n * ".join(other_domains)), + ) + else: + raise YunohostValidationError( + "domain_cannot_remove_main_add_new_one", domain=domain + ) + + # Check if apps are installed on the domain + apps_on_that_domain = [] + + for app in _installed_apps(): + settings = _get_app_settings(app) + label = app_info(app)["name"] + if settings.get("domain") == domain: + apps_on_that_domain.append( + ( + app, + f" - {app} \"{label}\" on https://{domain}{settings['path']}" + if "path" in settings + else app, + ) + ) + + if apps_on_that_domain: + if remove_apps: + if Moulinette.interface.type == "cli" and not force: + answer = Moulinette.prompt( + m18n.n( + "domain_remove_confirm_apps_removal", + apps="\n".join([x[1] for x in apps_on_that_domain]), + answers="y/N", + ), + color="yellow", + ) + if answer.upper() != "Y": + raise YunohostError("aborting") + + for app, _ in apps_on_that_domain: + app_remove(app) + else: + raise YunohostValidationError( + "domain_uninstall_app_first", + apps="\n".join([x[1] for x in apps_on_that_domain]), + ) + + operation_logger.start() + + ldap = _get_ldap_interface() + try: + ldap.remove("virtualdomain=" + domain + ",ou=domains") + except Exception as e: + raise YunohostError("domain_deletion_failed", domain=domain, error=e) + finally: + global domain_list_cache + domain_list_cache = {} + + stuff_to_delete = [ + f"/etc/yunohost/certs/{domain}", + f"/etc/yunohost/dyndns/K{domain}.+*", + f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", + ] + + for stuff in stuff_to_delete: + rm(stuff, force=True, recursive=True) + + # Sometime we have weird issues with the regenconf where some files + # appears as manually modified even though they weren't touched ... + # There are a few ideas why this happens (like backup/restore nginx + # conf ... which we shouldnt do ...). This in turns creates funky + # situation where the regenconf may refuse to re-create the conf + # (when re-creating a domain..) + # + # So here we force-clear the has out of the regenconf if it exists. + # This is a pretty ad hoc solution and only applied to nginx + # because it's one of the major service, but in the long term we + # should identify the root of this bug... + _force_clear_hashes([f"/etc/nginx/conf.d/{domain}.conf"]) + # And in addition we even force-delete the file Otherwise, if the file was + # manually modified, it may not get removed by the regenconf which leads to + # catastrophic consequences of nginx breaking because it can't load the + # cert file which disappeared etc.. + if os.path.exists(f"/etc/nginx/conf.d/{domain}.conf"): + _process_regen_conf( + f"/etc/nginx/conf.d/{domain}.conf", new_conf=None, save=True + ) + + regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"]) + app_ssowatconf() + + hook_callback("post_domain_remove", args=[domain]) + + logger.success(m18n.n("domain_deleted")) + + +@is_unit_operation() +def domain_main_domain(operation_logger, new_main_domain=None): + """ + Check the current main domain, or change it + + Keyword argument: + new_main_domain -- The new domain to be set as the main domain + + """ + from yunohost.tools import _set_hostname + + # If no new domain specified, we return the current main domain + if not new_main_domain: + return {"current_main_domain": _get_maindomain()} + + # Check domain exists + _assert_domain_exists(new_main_domain) + + operation_logger.related_to.append(("domain", new_main_domain)) + operation_logger.start() + + # Apply changes to ssl certs + try: + write_to_file("/etc/yunohost/current_host", new_main_domain) + global domain_list_cache + domain_list_cache = {} + _set_hostname(new_main_domain) + except Exception as e: + logger.warning(str(e), exc_info=1) + raise YunohostError("main_domain_change_failed") + + # Generate SSOwat configuration file + app_ssowatconf() + + # Regen configurations + if os.path.exists("/etc/yunohost/installed"): + regen_conf() + + logger.success(m18n.n("main_domain_changed")) + + +def domain_url_available(domain, path): + """ + Check availability of a web path + + Keyword argument: + domain -- The domain for the web path (e.g. your.domain.tld) + path -- The path to check (e.g. /coffee) + """ + + return len(_get_conflicting_apps(domain, path)) == 0 + + +def _get_maindomain(): + with open("/etc/yunohost/current_host", "r") as f: + maindomain = f.readline().rstrip() + return maindomain + + +def domain_config_get(domain, key="", full=False, export=False): + """ + Display a domain configuration + """ + + if full and export: + raise YunohostValidationError( + "You can't use --full and --export together.", raw_msg=True + ) + + if full: + mode = "full" + elif export: + mode = "export" + else: + mode = "classic" + + config = DomainConfigPanel(domain) + return config.get(key, mode) + + +@is_unit_operation() +def domain_config_set( + operation_logger, domain, key=None, value=None, args=None, args_file=None +): + """ + Apply a new domain configuration + """ + Question.operation_logger = operation_logger + config = DomainConfigPanel(domain) + return config.set(key, value, args, args_file, operation_logger=operation_logger) + + +class DomainConfigPanel(ConfigPanel): + entity_type = "domain" + save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml" + save_mode = "diff" + + def _apply(self): + if ( + "default_app" in self.future_values + and self.future_values["default_app"] != self.values["default_app"] + ): + from yunohost.app import app_ssowatconf, app_map + + if "/" in app_map(raw=True)[self.entity]: + raise YunohostValidationError( + "app_make_default_location_already_used", + app=self.future_values["default_app"], + domain=self.entity, + other_app=app_map(raw=True)[self.entity]["/"]["id"], + ) + + super()._apply() + + # Reload ssowat if default app changed + if ( + "default_app" in self.future_values + and self.future_values["default_app"] != self.values["default_app"] + ): + app_ssowatconf() + + def _get_toml(self): + + toml = super()._get_toml() + + toml["feature"]["xmpp"]["xmpp"]["default"] = ( + 1 if self.entity == _get_maindomain() else 0 + ) + + # Optimize wether or not to load the DNS section, + # e.g. we don't want to trigger the whole _get_registary_config_section + # when just getting the current value from the feature section + filter_key = self.filter_key.split(".") if self.filter_key != "" else [] + if not filter_key or filter_key[0] == "dns": + from yunohost.dns import _get_registrar_config_section + + toml["dns"]["registrar"] = _get_registrar_config_section(self.entity) + + # FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ... + self.registar_id = toml["dns"]["registrar"]["registrar"]["value"] + del toml["dns"]["registrar"]["registrar"]["value"] + + return toml + + def _load_current_values(self): + + # TODO add mechanism to share some settings with other domains on the same zone + super()._load_current_values() + + # FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ... + filter_key = self.filter_key.split(".") if self.filter_key != "" else [] + if not filter_key or filter_key[0] == "dns": + self.values["registrar"] = self.registar_id + + +def _get_domain_settings(domain: str) -> dict: + + _assert_domain_exists(domain) + + if os.path.exists(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml"): + return read_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml") or {} + else: + return {} + + +def _set_domain_settings(domain: str, settings: dict) -> None: + + _assert_domain_exists(domain) + + write_to_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", settings) + + +# +# +# Stuff managed in other files +# +# + + +def domain_cert_status(domain_list, full=False): + from yunohost.certificate import certificate_status + + return certificate_status(domain_list, full) + + +def domain_cert_install(domain_list, force=False, no_checks=False, self_signed=False): + from yunohost.certificate import certificate_install + + return certificate_install(domain_list, force, no_checks, self_signed) + + +def domain_cert_renew(domain_list, force=False, no_checks=False, email=False): + from yunohost.certificate import certificate_renew + + return certificate_renew(domain_list, force, no_checks, email) + + +def domain_dns_conf(domain): + return domain_dns_suggest(domain) + + +def domain_dns_suggest(domain): + from yunohost.dns import domain_dns_suggest + + return domain_dns_suggest(domain) + + +def domain_dns_push(domain, dry_run, force, purge): + from yunohost.dns import domain_dns_push + + return domain_dns_push(domain, dry_run, force, purge) diff --git a/src/yunohost/dyndns.py b/src/dyndns.py similarity index 54% rename from src/yunohost/dyndns.py rename to src/dyndns.py index c8249e439..34f3dd5dc 100644 --- a/src/yunohost/dyndns.py +++ b/src/dyndns.py @@ -33,97 +33,58 @@ 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, rm, chown, chmod from moulinette.utils.network import download_json from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.domain import _get_maindomain, _build_dns_conf -from yunohost.utils.network import get_public_ip, dig +from yunohost.domain import _get_maindomain +from yunohost.utils.network import get_public_ip +from yunohost.utils.dns import dig, is_yunohost_dyndns_domain from yunohost.log import is_unit_operation from yunohost.regenconf import regen_conf logger = getActionLogger("yunohost.dyndns") -DYNDNS_ZONE = "/etc/yunohost/dyndns/zone" - -RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile(r".*/K(?P[^\s\+]+)\.\+157.+\.private$") - -RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile( - r".*/K(?P[^\s\+]+)\.\+165.+\.private$" -) +DYNDNS_PROVIDER = "dyndns.yunohost.org" +DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"] -def _dyndns_provides(provider, domain): +def _dyndns_available(domain): """ - Checks if a provider provide/manage a given domain. + Checks if a domain is available on dyndns.yunohost.org Keyword arguments: - provider -- The url of the provider, e.g. "dyndns.yunohost.org" - domain -- The full domain that you'd like.. e.g. "foo.nohost.me" - - Returns: - True if the provider provide/manages the domain. False otherwise. - """ - - logger.debug("Checking if %s is managed by %s ..." % (domain, provider)) - - try: - # Dyndomains will be a list of domains supported by the provider - # e.g. [ "nohost.me", "noho.st" ] - dyndomains = download_json("https://%s/domains" % provider, timeout=30) - except MoulinetteError as e: - logger.error(str(e)) - raise YunohostError( - "dyndns_could_not_check_provide", domain=domain, provider=provider - ) - - # Extract 'dyndomain' from 'domain', e.g. 'nohost.me' from 'foo.nohost.me' - dyndomain = ".".join(domain.split(".")[1:]) - - return dyndomain in dyndomains - - -def _dyndns_available(provider, domain): - """ - Checks if a domain is available from a given provider. - - Keyword arguments: - provider -- The url of the provider, e.g. "dyndns.yunohost.org" domain -- The full domain that you'd like.. e.g. "foo.nohost.me" Returns: True if the domain is available, False otherwise. """ - logger.debug("Checking if domain %s is available on %s ..." % (domain, provider)) + logger.debug(f"Checking if domain {domain} is available on {DYNDNS_PROVIDER} ...") try: r = download_json( - "https://%s/test/%s" % (provider, domain), expected_status_code=None + f"https://{DYNDNS_PROVIDER}/test/{domain}", expected_status_code=None ) except MoulinetteError as e: logger.error(str(e)) raise YunohostError( - "dyndns_could_not_check_available", domain=domain, provider=provider + "dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER ) - return r == "Domain %s is available" % domain + return r == f"Domain {domain} is available" @is_unit_operation() -def dyndns_subscribe( - operation_logger, subscribe_host="dyndns.yunohost.org", domain=None, key=None -): +def dyndns_subscribe(operation_logger, domain=None, key=None): """ Subscribe to a DynDNS service Keyword argument: domain -- Full domain to subscribe with key -- Public DNS key - subscribe_host -- Dynette HTTP API to subscribe to - """ - if _guess_current_dyndns_domain(subscribe_host) != (None, None): + if _guess_current_dyndns_domain() != (None, None): raise YunohostValidationError("domain_dyndns_already_subscribed") if domain is None: @@ -131,17 +92,21 @@ def dyndns_subscribe( operation_logger.related_to.append(("domain", domain)) # Verify if domain is provided by subscribe_host - if not _dyndns_provides(subscribe_host, domain): + if not is_yunohost_dyndns_domain(domain): raise YunohostValidationError( - "dyndns_domain_not_provided", domain=domain, provider=subscribe_host + "dyndns_domain_not_provided", domain=domain, provider=DYNDNS_PROVIDER ) # Verify if domain is available - if not _dyndns_available(subscribe_host, domain): + if not _dyndns_available(domain): raise YunohostValidationError("dyndns_unavailable", domain=domain) operation_logger.start() + # '165' is the convention identifier for hmac-sha512 algorithm + # '1234' is idk? doesnt matter, but the old format contained a number here... + key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" + if key is None: if len(glob.glob("/etc/yunohost/dyndns/*.key")) == 0: if not os.path.exists("/etc/yunohost/dyndns"): @@ -149,44 +114,47 @@ def dyndns_subscribe( logger.debug(m18n.n("dyndns_key_generating")) - 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" - ) + # Here, we emulate the behavior of the old 'dnssec-keygen' utility + # which since bullseye was replaced by ddns-keygen which is now + # in the bind9 package ... but installing bind9 will conflict with dnsmasq + # and is just madness just to have access to a tsig keygen utility -.- - 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: - key = f.readline().strip().split(" ", 6)[-1] + # Use 512 // 8 = 64 bytes for hmac-sha512 (c.f. https://git.hactrn.net/sra/tsig-keygen/src/master/tsig-keygen.py) + secret = base64.b64encode(os.urandom(512 // 8)).decode("ascii") + + # Idk why but the secret is split in two parts, with the first one + # being 57-long char ... probably some DNS format + secret = f"{secret[:56]} {secret[56:]}" + + key_content = f"{domain}. IN KEY 0 3 165 {secret}" + write_to_file(key_file, key_content) + + chmod("/etc/yunohost/dyndns", 0o600, recursive=True) + chown("/etc/yunohost/dyndns", "root", recursive=True) import requests # lazy loading this module for performance reasons # Send subscription try: + # Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever... + b64encoded_key = base64.b64encode(secret.encode()).decode() r = requests.post( - "https://%s/key/%s?key_algo=hmac-sha512" - % (subscribe_host, base64.b64encode(key.encode()).decode()), + f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", data={"subdomain": domain}, timeout=30, ) except Exception as e: - os.system("rm -f %s" % private_file) - os.system("rm -f %s" % key_file) + 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(key_file, force=True) try: error = json.loads(r.text)["error"] except Exception: - error = 'Server error, code: %s. (Message: "%s")' % (r.status_code, r.text) + error = f'Server error, code: {r.status_code}. (Message: "{r.text}")' raise YunohostError("dyndns_registration_failed", error=error) - # Yunohost regen conf will add the dyndns cron job if a private key exists + # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns regen_conf(["yunohost"]) @@ -205,11 +173,7 @@ def dyndns_subscribe( @is_unit_operation() def dyndns_update( operation_logger, - dyn_host="dyndns.yunohost.org", domain=None, - key=None, - ipv4=None, - ipv6=None, force=False, dry_run=False, ): @@ -218,59 +182,76 @@ def dyndns_update( Keyword argument: domain -- Full domain to update - dyn_host -- Dynette DNS server to inform - key -- Public DNS key - ipv4 -- IP address to send - ipv6 -- IPv6 address to send - """ - # Get old ipv4/v6 - old_ipv4, old_ipv6 = (None, None) # (default values) + from yunohost.dns import _build_dns_conf + import dns.query + import dns.tsig + import dns.tsigkeyring + import dns.update # If domain is not given, try to guess it from keys available... + key = None if domain is None: - (domain, key) = _guess_current_dyndns_domain(dyn_host) + (domain, key) = _guess_current_dyndns_domain() if domain is None: raise YunohostValidationError("dyndns_no_domain_registered") # If key is not given, pick the first file we find with the domain given - else: - if key is None: - keys = glob.glob("/etc/yunohost/dyndns/K{0}.+*.private".format(domain)) + elif key is None: + keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") - if not keys: - raise YunohostValidationError("dyndns_key_not_found") + if not keys: + raise YunohostValidationError("dyndns_key_not_found") - key = keys[0] + key = keys[0] + + # Get current IPv4 and IPv6 + ipv4 = get_public_ip() + ipv6 = get_public_ip(6) + + if ipv4 is None and ipv6 is None: + logger.debug( + "No ipv4 nor ipv6 ?! Sounds like the server is not connected to the internet, or the ip.yunohost.org infrastructure is down somehow" + ) + return # Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me' - host = domain.split(".")[1:] - host = ".".join(host) + zone = domain.split(".")[1:] + zone = ".".join(zone) - logger.debug("Building zone update file ...") + logger.debug("Building zone update ...") - lines = [ - "server %s" % dyn_host, - "zone %s" % host, - ] + with open(key) as f: + key = f.readline().strip().split(" ", 6)[-1] + + keyring = dns.tsigkeyring.from_text({f"{domain}.": key}) + # Python's dns.update is similar to the old nsupdate cli tool + update = dns.update.Update(zone, keyring=keyring, keyalgorithm=dns.tsig.HMAC_SHA512) + + auth_resolvers = [] + + for dns_auth in DYNDNS_DNS_AUTH: + for type_ in ["A", "AAAA"]: + + ok, result = dig(dns_auth, type_) + if ok == "ok" and len(result) and result[0]: + auth_resolvers.append(result[0]) + + if not auth_resolvers: + raise YunohostError( + f"Failed to resolve IPv4/IPv6 for {DYNDNS_DNS_AUTH} ?", raw_msg=True + ) def resolve_domain(domain, rdtype): - # FIXME make this work for IPv6-only hosts too.. - ok, result = dig(dyn_host, "A") - dyn_host_ip = result[0] if ok == "ok" and len(result) else None - if not dyn_host_ip: - raise YunohostError("Failed to resolve %s" % dyn_host, raw_msg=True) - - ok, result = dig(domain, rdtype, resolvers=[dyn_host_ip]) + ok, result = dig(domain, rdtype, resolvers=auth_resolvers) if ok == "ok": return result[0] if len(result) else None elif result[0] == "Timeout": logger.debug( - "Timed-out while trying to resolve %s record for %s using %s" - % (rdtype, domain, dyn_host) + f"Timed-out while trying to resolve {rdtype} record for {domain}" ) else: return None @@ -287,25 +268,13 @@ def dyndns_update( else: return None - raise YunohostError( - "Failed to resolve %s for %s" % (rdtype, domain), raw_msg=True - ) + raise YunohostError(f"Failed to resolve {rdtype} for {domain}", raw_msg=True) old_ipv4 = resolve_domain(domain, "A") old_ipv6 = resolve_domain(domain, "AAAA") - # Get current IPv4 and IPv6 - ipv4_ = get_public_ip() - ipv6_ = get_public_ip(6) - - if ipv4 is None: - ipv4 = ipv4_ - - if ipv6 is None: - ipv6 = ipv6_ - - logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6)) - logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6)) + logger.debug(f"Old IPv4/v6 are ({old_ipv4}, {old_ipv6})") + logger.debug(f"Requested IPv4/v6 are ({ipv4}, {ipv6})") # no need to update if (not force and not dry_run) and (old_ipv4 == ipv4 and old_ipv6 == ipv6): @@ -330,9 +299,10 @@ def dyndns_update( # [{"name": "...", "ttl": "...", "type": "...", "value": "..."}] for records in dns_conf.values(): for record in records: - action = "update delete {name}.{domain}.".format(domain=domain, **record) - action = action.replace(" @.", " ") - lines.append(action) + name = ( + f"{record['name']}.{domain}." if record["name"] != "@" else f"{domain}." + ) + update.delete(name) # Add the new records for all domain/subdomains @@ -344,50 +314,34 @@ def dyndns_update( if record["value"] == "@": record["value"] = domain record["value"] = record["value"].replace(";", r"\;") - - action = "update add {name}.{domain}. {ttl} {type} {value}".format( - domain=domain, **record + name = ( + f"{record['name']}.{domain}." if record["name"] != "@" else f"{domain}." ) - action = action.replace(" @.", " ") - lines.append(action) - lines += ["show", "send"] - - # Write the actions to do to update to a file, to be able to pass it - # to nsupdate as argument - write_to_file(DYNDNS_ZONE, "\n".join(lines)) + update.add(name, record["ttl"], record["type"], record["value"]) logger.debug("Now pushing new conf to DynDNS host...") + logger.debug(update) if not dry_run: try: - command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE] - subprocess.check_call(command) - except subprocess.CalledProcessError: + r = dns.query.tcp(update, auth_resolvers[0]) + except Exception as e: + logger.error(e) + raise YunohostError("dyndns_ip_update_failed") + + if "rcode NOERROR" not in str(r): + logger.error(str(r)) raise YunohostError("dyndns_ip_update_failed") logger.success(m18n.n("dyndns_ip_updated")) else: - print(read_file(DYNDNS_ZONE)) - print("") print( "Warning: dry run, this is only the generated config, it won't be applied" ) -def dyndns_installcron(): - logger.warning( - "This command is deprecated. The dyndns cron job should automatically be added/removed by the regenconf depending if there's a private key in /etc/yunohost/dyndns. You can run the regenconf yourself with 'yunohost tools regen-conf yunohost'." - ) - - -def dyndns_removecron(): - logger.warning( - "This command is deprecated. The dyndns cron job should automatically be added/removed by the regenconf depending if there's a private key in /etc/yunohost/dyndns. You can run the regenconf yourself with 'yunohost tools regen-conf yunohost'." - ) - - -def _guess_current_dyndns_domain(dyn_host): +def _guess_current_dyndns_domain(): """ This function tries to guess which domain should be updated by "dyndns_update()" because there's not proper management of the current @@ -396,21 +350,21 @@ def _guess_current_dyndns_domain(dyn_host): dynette...) """ + DYNDNS_KEY_REGEX = re.compile(r".*/K(?P[^\s\+]+)\.\+165.+\.key$") + # Retrieve the first registered domain - paths = list(glob.iglob("/etc/yunohost/dyndns/K*.private")) + paths = list(glob.iglob("/etc/yunohost/dyndns/K*.key")) for path in paths: - match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path) + match = DYNDNS_KEY_REGEX.match(path) if not match: - match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path) - if not match: - continue + continue _domain = match.group("domain") # Verify if domain is registered (i.e., if it's available, skip # current domain beause that's not the one we want to update..) # If there's only 1 such key found, then avoid doing the request # for nothing (that's very probably the one we want to find ...) - if len(paths) > 1 and _dyndns_available(dyn_host, _domain): + if len(paths) > 1 and _dyndns_available(_domain): continue else: return (_domain, path) diff --git a/src/yunohost/firewall.py b/src/firewall.py similarity index 98% rename from src/yunohost/firewall.py rename to src/firewall.py index 4be6810ec..a1c0b187f 100644 --- a/src/yunohost/firewall.py +++ b/src/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 diff --git a/src/yunohost/hook.py b/src/hook.py similarity index 87% rename from src/yunohost/hook.py rename to src/hook.py index 0594a27ae..70d3b281b 100644 --- a/src/yunohost/hook.py +++ b/src/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_json +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} @@ -96,7 +95,7 @@ def hook_info(action, name): priorities = set() # Search in custom folder first - for h in iglob("{:s}{:s}/*-{:s}".format(CUSTOM_HOOK_FOLDER, action, name)): + for h in iglob(f"{CUSTOM_HOOK_FOLDER}{action}/*-{name}"): priority, _ = _extract_filename_parts(os.path.basename(h)) priorities.add(priority) hooks.append( @@ -106,7 +105,7 @@ def hook_info(action, name): } ) # Append non-overwritten system hooks - for h in iglob("{:s}{:s}/*-{:s}".format(HOOK_FOLDER, action, name)): + for h in iglob(f"{HOOK_FOLDER}{action}/*-{name}"): priority, _ = _extract_filename_parts(os.path.basename(h)) if priority not in priorities: hooks.append( @@ -157,7 +156,7 @@ def hook_list(action, list_by="name", show_info=False): try: d[priority].add(name) except KeyError: - d[priority] = set([name]) + d[priority] = {name} elif list_by == "name" or list_by == "folder": if show_info: @@ -198,7 +197,7 @@ def hook_list(action, list_by="name", show_info=False): or (f.startswith("__") and f.endswith("__")) ): continue - path = "%s%s/%s" % (folder, action, f) + path = f"{folder}{action}/{f}" priority, name = _extract_filename_parts(f) _append_hook(d, priority, name, path) @@ -326,7 +325,7 @@ def hook_exec( chdir=None, env=None, user="root", - return_format="json", + return_format="yaml", ): """ Execute hook from a file with arguments @@ -360,6 +359,7 @@ def hook_exec( r"Created symlink /etc/systemd", r"dpkg: warning: while removing .* not empty so not removed", r"apt-key output should not be parsed", + r"update-rc.d: ", ] return all(not re.search(w, msg) for w in irrelevant_warnings) @@ -407,7 +407,7 @@ def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers): if not chdir: # use the script directory as current one chdir, cmd_script = os.path.split(path) - cmd_script = "./{0}".format(cmd_script) + cmd_script = f"./{cmd_script}" else: cmd_script = path @@ -431,14 +431,19 @@ def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers): # use xtrace on fd 7 which is redirected to stdout env["BASH_XTRACEFD"] = "7" - cmd = '/bin/bash -x "{script}" {args} 7>&1' - command.append(cmd.format(script=cmd_script, args=cmd_args)) + command.append(f'/bin/bash -x "{cmd_script}" {cmd_args} 7>&1') logger.debug("Executing command '%s'" % command) _env = os.environ.copy() _env.update(env) + # Remove the 'HOME' var which is causing some inconsistencies between + # cli and webapi (HOME ain't defined in yunohost-api because ran from systemd) + # Apps that need the HOME var should define it in the app scripts + if "HOME" in _env: + del _env["HOME"] + returncode = call_async_output(command, loggers, shell=False, cwd=chdir, env=_env) raw_content = None @@ -447,10 +452,10 @@ def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers): raw_content = f.read() returncontent = {} - if return_format == "json": + if return_format == "yaml": if raw_content != "": try: - returncontent = read_json(stdreturn) + returncontent = read_yaml(stdreturn) except Exception as e: raise YunohostError( "hook_json_return_error", @@ -498,6 +503,40 @@ def _hook_exec_python(path, args, env, loggers): return ret +def hook_exec_with_script_debug_if_failure(*args, **kwargs): + + operation_logger = kwargs.pop("operation_logger") + error_message_if_failed = kwargs.pop("error_message_if_failed") + error_message_if_script_failed = kwargs.pop("error_message_if_script_failed") + + failed = True + failure_message_with_debug_instructions = None + try: + retcode, retpayload = hook_exec(*args, **kwargs) + failed = True if retcode != 0 else False + if failed: + error = error_message_if_script_failed + logger.error(error_message_if_failed(error)) + failure_message_with_debug_instructions = operation_logger.error(error) + if Moulinette.interface.type != "api": + operation_logger.dump_script_log_extract_for_debugging() + # Script got manually interrupted ... + # N.B. : KeyboardInterrupt does not inherit from Exception + except (KeyboardInterrupt, EOFError): + error = m18n.n("operation_interrupted") + logger.error(error_message_if_failed(error)) + failure_message_with_debug_instructions = operation_logger.error(error) + # Something wrong happened in Yunohost's code (most probably hook_exec) + except Exception: + import traceback + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error(error_message_if_failed(error)) + failure_message_with_debug_instructions = operation_logger.error(error) + + return failed, failure_message_with_debug_instructions + + def _extract_filename_parts(filename): """Extract hook parts from filename""" if "-" in filename: diff --git a/src/yunohost/log.py b/src/log.py similarity index 86% rename from src/yunohost/log.py rename to src/log.py index 3f6382af2..9f9e0b753 100644 --- a/src/yunohost/log.py +++ b/src/log.py @@ -29,6 +29,7 @@ import re import yaml import glob import psutil +from typing import List from datetime import datetime, timedelta from logging import FileHandler, getLogger, Formatter @@ -41,12 +42,36 @@ from yunohost.utils.packages import get_ynh_package_version from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, read_yaml +logger = getActionLogger("yunohost.log") + CATEGORIES_PATH = "/var/log/yunohost/categories/" OPERATIONS_PATH = "/var/log/yunohost/categories/operation/" METADATA_FILE_EXT = ".yml" LOG_FILE_EXT = ".log" -logger = getActionLogger("yunohost.log") +BORING_LOG_LINES = [ + r"set [+-]x$", + r"set [+-]o xtrace$", + r"set [+-]o errexit$", + r"set [+-]o nounset$", + r"trap '' EXIT", + r"local \w+$", + r"local exit_code=(1|0)$", + r"local legacy_args=.*$", + r"local -A args_array$", + r"args_array=.*$", + r"ret_code=1", + r".*Helper used in legacy mode.*", + r"ynh_handle_getopts_args", + r"ynh_script_progression", + r"sleep 0.5", + r"'\[' (1|0) -eq (1|0) '\]'$", + r"\[?\['? -n '' '?\]\]?$", + r"rm -rf /var/cache/yunohost/download/$", + r"type -t ynh_clean_setup$", + r"DEBUG - \+ echo '", + r"DEBUG - \+ exit (1|0)$", +] def log_list(limit=None, with_details=False, with_suboperations=False): @@ -69,7 +94,13 @@ def log_list(limit=None, with_details=False, with_suboperations=False): logs = list(reversed(sorted(logs))) if limit is not None: - logs = logs[:limit] + if with_suboperations: + logs = logs[:limit] + else: + # If we displaying only parent, we are still gonna load up to limit * 5 logs + # because many of them are suboperations which are not gonna be kept + # Yet we still want to obtain ~limit number of logs + logs = logs[: limit * 5] for log in logs: @@ -122,6 +153,9 @@ def log_list(limit=None, with_details=False, with_suboperations=False): else: operations = [o for o in operations.values()] + if limit: + operations = operations[:limit] + operations = list(reversed(sorted(operations, key=lambda o: o["name"]))) # Reverse the order of log when in cli, more comfortable to read (avoid # unecessary scrolling) @@ -151,26 +185,19 @@ def log_show( filter_irrelevant = True if filter_irrelevant: - filters = [ - r"set [+-]x$", - r"set [+-]o xtrace$", - r"local \w+$", - r"local legacy_args=.*$", - r".*Helper used in legacy mode.*", - r"args_array=.*$", - r"local -A args_array$", - r"ynh_handle_getopts_args", - r"ynh_script_progression", - ] + + def _filter(lines): + filters = [re.compile(f) for f in BORING_LOG_LINES] + return [ + line + for line in lines + if not any(f.search(line.strip()) for f in filters) + ] + else: - filters = [] - def _filter_lines(lines, filters=[]): - - filters = [re.compile(f) for f in filters] - return [ - line for line in lines if not any(f.search(line.strip()) for f in filters) - ] + def _filter(lines): + return lines # Normalize log/metadata paths and filenames abs_path = path @@ -209,7 +236,7 @@ def log_show( content += "\n============\n\n" if os.path.exists(log_path): actual_log = read_file(log_path) - content += "\n".join(_filter_lines(actual_log.split("\n"), filters)) + content += "\n".join(_filter(actual_log.split("\n"))) url = yunopaste(content) @@ -282,13 +309,13 @@ def log_show( if os.path.exists(log_path): from yunohost.service import _tail - if number and filters: + if number and filter_irrelevant: logs = _tail(log_path, int(number * 4)) elif number: logs = _tail(log_path, int(number)) else: logs = read_file(log_path) - logs = _filter_lines(logs, filters) + logs = list(_filter(logs)) if number: logs = logs[-number:] infos["log_path"] = log_path @@ -381,7 +408,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) @@ -427,7 +454,7 @@ class RedactingFormatter(Formatter): # (the secret part being at least 3 chars to avoid catching some lines like just "db_pwd=") # Some names like "key" or "manifest_key" are ignored, used in helpers like ynh_app_setting_set or ynh_read_manifest match = re.search( - r"(pwd|pass|password|passphrase|secret\w*|\w+key|token|PASSPHRASE)=(\S{3,})$", + r"(pwd|pass|passwd|password|passphrase|secret\w*|\w+key|token|PASSPHRASE)=(\S{3,})$", record.strip(), ) if ( @@ -442,7 +469,7 @@ class RedactingFormatter(Formatter): ) -class OperationLogger(object): +class OperationLogger: """ Instances of this class represents unit operation done on the ynh instance. @@ -453,7 +480,7 @@ class OperationLogger(object): This class record logs and metadata like context or start time/end time. """ - _instances = [] + _instances: List[object] = [] def __init__(self, operation, related_to=None, **kwargs): # TODO add a way to not save password on app installation @@ -517,7 +544,7 @@ class OperationLogger(object): # We use proc.open_files() to list files opened / actively used by this proc # We only keep files matching a recent yunohost operation log active_logs = sorted( - [f.path for f in proc.open_files() if f.path in recent_operation_logs], + (f.path for f in proc.open_files() if f.path in recent_operation_logs), key=os.path.getctime, reverse=True, ) @@ -632,6 +659,11 @@ class OperationLogger(object): data["error"] = self._error # TODO: detect if 'extra' erase some key of 'data' data.update(self.extra) + # Remove the 'args' arg from args (yodawg). It corresponds to url-encoded args for app install, config panel set, etc + # Because the data are url encoded, it's hell to properly redact secrets inside it, + # and the useful info is usually already available in `env` too + if "args" in data and isinstance(data["args"], dict) and "args" in data["args"]: + data["args"].pop("args") return data def success(self): @@ -707,6 +739,47 @@ class OperationLogger(object): else: self.error(m18n.n("log_operation_unit_unclosed_properly")) + def dump_script_log_extract_for_debugging(self): + + with open(self.log_path, "r") as f: + lines = f.readlines() + + # A line typically looks like + # 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo + # And we just want the part starting by "DEBUG - " + lines = [line for line in lines if ":" in line.strip()] + lines = [line.strip().split(": ", 1)[1] for line in lines] + # And we ignore boring/irrelevant lines + # Annnnnnd we also ignore lines matching [number] + such as + # 72971 DEBUG 29739 + ynh_exit_properly + # which are lines from backup-before-upgrade or restore-after-failed-upgrade ... + filters = [re.compile(f_) for f_ in BORING_LOG_LINES] + filters.append(re.compile(r"\d+ \+ ")) + lines = [ + line + for line in lines + if not any(filter_.search(line) for filter_ in filters) + ] + + lines_to_display = [] + + # Get the 20 lines before the last 'ynh_exit_properly' + rev_lines = list(reversed(lines)) + for i, line in enumerate(rev_lines): + if line.endswith("+ ynh_exit_properly"): + lines_to_display = reversed(rev_lines[i : i + 20]) + break + + # If didnt find anything, just get the last 20 lines + if not lines_to_display: + lines_to_display = lines[-20:] + + logger.warning( + "Here's an extract of the logs before the crash. It might help debugging the error:" + ) + for line in lines_to_display: + logger.info(line) + def _get_datetime_from_name(name): diff --git a/src/migrations/0021_migrate_to_bullseye.py b/src/migrations/0021_migrate_to_bullseye.py new file mode 100644 index 000000000..551a6f64b --- /dev/null +++ b/src/migrations/0021_migrate_to_bullseye.py @@ -0,0 +1,442 @@ +import glob +import os + +from moulinette import m18n +from yunohost.utils.error import YunohostError +from moulinette.utils.log import getActionLogger +from moulinette.utils.process import check_output, call_async_output +from moulinette.utils.filesystem import read_file, rm, write_to_file + +from yunohost.tools import ( + Migration, + tools_update, + tools_upgrade, + _apt_log_line_is_relevant, +) +from yunohost.app import unstable_apps +from yunohost.regenconf import manually_modified_files, _force_clear_hashes +from yunohost.utils.filesystem import free_space_in_directory +from yunohost.utils.packages import ( + get_ynh_package_version, + _list_upgradable_apt_packages, +) +from yunohost.service import _get_services, _save_services + +logger = getActionLogger("yunohost.migration") + +N_CURRENT_DEBIAN = 10 +N_CURRENT_YUNOHOST = 4 + +N_NEXT_DEBAN = 11 +N_NEXT_YUNOHOST = 11 + + +class MyMigration(Migration): + + "Upgrade the system to Debian Bullseye and Yunohost 11.x" + + mode = "manual" + + def run(self): + + self.check_assertions() + + logger.info(m18n.n("migration_0021_start")) + + # + # Add new apt .deb signing key + # + + new_apt_key = "https://forge.yunohost.org/yunohost_bullseye.asc" + check_output(f"wget -O- {new_apt_key} -q | apt-key add -qq -") + + # + # Patch sources.list + # + logger.info(m18n.n("migration_0021_patching_sources_list")) + self.patch_apt_sources_list() + + # Force add sury if it's not there yet + # This is to solve some weird issue with php-common breaking php7.3-common, + # hence breaking many php7.3-deps + # hence triggering some dependency conflict (or foobar-ynh-deps uninstall) + # Adding it there shouldnt be a big deal - Yunohost 11.x does add it + # through its regen conf anyway. + if not os.path.exists("/etc/apt/sources.list.d/extra_php_version.list"): + open("/etc/apt/sources.list.d/extra_php_version.list", "w").write( + "deb https://packages.sury.org/php/ bullseye main" + ) + os.system( + 'wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/extra_php_version.gpg"' + ) + + # + # Run apt update + # + + tools_update(target="system") + + # Tell libc6 it's okay to restart system stuff during the upgrade + os.system( + "echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections" + ) + + # Do not restart nginx during the upgrade of nginx-common and nginx-extras ... + # c.f. https://manpages.debian.org/bullseye/init-system-helpers/deb-systemd-invoke.1p.en.html + # and zcat /usr/share/doc/init-system-helpers/README.policy-rc.d.gz + # and the code inside /usr/bin/deb-systemd-invoke to see how it calls /usr/sbin/policy-rc.d ... + # and also invoke-rc.d ... + write_to_file( + "/usr/sbin/policy-rc.d", + '#!/bin/bash\n[[ "$1" =~ "nginx" ]] && [[ "$2" == "restart" ]] && exit 101 || exit 0', + ) + os.system("chmod +x /usr/sbin/policy-rc.d") + + # Don't send an email to root about the postgresql migration. It should be handled automatically after. + os.system( + "echo 'postgresql-common postgresql-common/obsolete-major seen true' | debconf-set-selections" + ) + + # + # Patch yunohost conflicts + # + logger.info(m18n.n("migration_0021_patch_yunohost_conflicts")) + + self.patch_yunohost_conflicts() + + # + # Specific tweaking to get rid of custom my.cnf and use debian's default one + # (my.cnf is actually a symlink to mariadb.cnf) + # + + _force_clear_hashes(["/etc/mysql/my.cnf"]) + rm("/etc/mysql/mariadb.cnf", force=True) + rm("/etc/mysql/my.cnf", force=True) + ret = self.apt_install( + "mariadb-common --reinstall -o Dpkg::Options::='--force-confmiss'" + ) + if ret != 0: + raise YunohostError("Failed to reinstall mariadb-common ?", raw_msg=True) + + # + # /usr/share/yunohost/yunohost-config/ssl/yunoCA -> /usr/share/yunohost/ssl + # + if os.path.exists("/usr/share/yunohost/yunohost-config/ssl/yunoCA"): + os.system( + "mv /usr/share/yunohost/yunohost-config/ssl/yunoCA /usr/share/yunohost/ssl" + ) + rm("/usr/share/yunohost/yunohost-config", recursive=True, force=True) + + # + # /home/yunohost.conf -> /var/cache/yunohost/regenconf + # + if os.path.exists("/home/yunohost.conf"): + os.system("mv /home/yunohost.conf /var/cache/yunohost/regenconf") + rm("/home/yunohost.conf", recursive=True, force=True) + + # Remove legacy postgresql service record added by helpers, + # will now be dynamically handled by the core in bullseye + services = _get_services() + if "postgresql" in services: + del services["postgresql"] + _save_services(services) + + # + # Main upgrade + # + logger.info(m18n.n("migration_0021_main_upgrade")) + + apps_packages = self.get_apps_equivs_packages() + self.hold(apps_packages) + tools_upgrade(target="system", allow_yunohost_upgrade=False) + + if self.debian_major_version() == N_CURRENT_DEBIAN: + raise YunohostError("migration_0021_still_on_buster_after_main_upgrade") + + # Force explicit install of php7.4-fpm and other old 'default' dependencies + # that are now only in Recommends + # + # Also, we need to install php7.4 equivalents of other php7.3 dependencies. + # For example, Nextcloud may depend on php7.3-zip, and after the php pool migration + # to autoupgrade Nextcloud to 7.4, it will need the php7.4-zip to work. + # The following list is based on an ad-hoc analysis of php deps found in the + # app ecosystem, with a known equivalent on php7.4. + # + # This is kinda a dirty hack as it doesnt properly update the *-ynh-deps virtual packages + # with the proper list of dependencies, and the dependencies install this way + # will get flagged as 'manually installed'. + # + # We'll probably want to do something during the Bullseye->Bookworm migration to re-flag + # these as 'auto' so they get autoremoved if not needed anymore. + # Also hopefully by then we'll have manifestv2 (maybe) and will be able to use + # the apt resource mecanism to regenerate the *-ynh-deps virtual packages ;) + + php73packages_suffixes = [ + "apcu", + "bcmath", + "bz2", + "dom", + "gmp", + "igbinary", + "imagick", + "imap", + "mbstring", + "memcached", + "mysqli", + "mysqlnd", + "pgsql", + "redis", + "simplexml", + "soap", + "sqlite3", + "ssh2", + "tidy", + "xml", + "xmlrpc", + "xsl", + "zip", + ] + + cmd = ( + "apt show '*-ynh-deps' 2>/dev/null" + " | grep Depends" + f" | grep -o -E \"php7.3-({'|'.join(php73packages_suffixes)})\"" + " | sort | uniq" + " | sed 's/php7.3/php7.4/g'" + " || true" + ) + + basephp74packages_to_install = [ + "php7.4-fpm", + "php7.4-common", + "php7.4-ldap", + "php7.4-intl", + "php7.4-mysql", + "php7.4-gd", + "php7.4-curl", + "php-php-gettext", + ] + + php74packages_to_install = basephp74packages_to_install + [ + f.strip() for f in check_output(cmd).split("\n") if f.strip() + ] + + ret = self.apt_install( + f"{' '.join(php74packages_to_install)} " + "$(dpkg --list | grep ynh-deps | awk '{print $2}') " + "-o Dpkg::Options::='--force-confmiss'" + ) + if ret != 0: + raise YunohostError( + "Failed to force the install of php dependencies ?", raw_msg=True + ) + + # Clean the mess + logger.info(m18n.n("migration_0021_cleaning_up")) + os.system("apt autoremove --assume-yes") + os.system("apt clean --assume-yes") + + # + # Yunohost upgrade + # + logger.info(m18n.n("migration_0021_yunohost_upgrade")) + + self.unhold(apps_packages) + + cmd = "LC_ALL=C" + cmd += " DEBIAN_FRONTEND=noninteractive" + cmd += " APT_LISTCHANGES_FRONTEND=none" + cmd += " apt dist-upgrade " + cmd += " --quiet -o=Dpkg::Use-Pty=0 --fix-broken --dry-run" + cmd += " | grep -q 'ynh-deps'" + + logger.info("Simulating upgrade...") + if os.system(cmd) == 0: + raise YunohostError( + "The upgrade cannot be completed, because some app dependencies would need to be removed?", + raw_msg=True, + ) + + postupgradecmds = f"apt-mark auto {' '.join(basephp74packages_to_install)}\n" + postupgradecmds += "rm -f /usr/sbin/policy-rc.d\n" + postupgradecmds += "echo 'Restarting nginx...' >&2\n" + postupgradecmds += "systemctl restart nginx\n" + + tools_upgrade(target="system", postupgradecmds=postupgradecmds) + + def debian_major_version(self): + # The python module "platform" and lsb_release are not reliable because + # on some setup, they may still return Release=9 even after upgrading to + # buster ... (Apparently this is related to OVH overriding some stuff + # with /etc/lsb-release for instance -_-) + # Instead, we rely on /etc/os-release which should be the raw info from + # the distribution... + return int( + check_output( + "grep VERSION_ID /etc/os-release | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2" + ) + ) + + def yunohost_major_version(self): + return int(get_ynh_package_version("yunohost")["version"].split(".")[0]) + + def check_assertions(self): + + # Be on buster (10.x) and yunohost 4.x + # NB : we do both check to cover situations where the upgrade crashed + # in the middle and debian version could be > 9.x but yunohost package + # would still be in 3.x... + if ( + not self.debian_major_version() == N_CURRENT_DEBIAN + and not self.yunohost_major_version() == N_CURRENT_YUNOHOST + ): + raise YunohostError("migration_0021_not_buster") + + # Have > 1 Go free space on /var/ ? + if free_space_in_directory("/var/") / (1024**3) < 1.0: + raise YunohostError("migration_0021_not_enough_free_space") + + # Check system is up to date + # (but we don't if 'bullseye' is already in the sources.list ... + # which means maybe a previous upgrade crashed and we're re-running it) + if " bullseye " not in read_file("/etc/apt/sources.list"): + tools_update(target="system") + upgradable_system_packages = list(_list_upgradable_apt_packages()) + if upgradable_system_packages: + raise YunohostError("migration_0021_system_not_fully_up_to_date") + + @property + def disclaimer(self): + + # Avoid having a super long disclaimer + uncessary check if we ain't + # on buster / yunohost 4.x anymore + # NB : we do both check to cover situations where the upgrade crashed + # in the middle and debian version could be >= 10.x but yunohost package + # would still be in 4.x... + if ( + not self.debian_major_version() == N_CURRENT_DEBIAN + and not self.yunohost_major_version() == N_CURRENT_YUNOHOST + ): + return None + + # Get list of problematic apps ? I.e. not official or community+working + problematic_apps = unstable_apps() + problematic_apps = "".join(["\n - " + app for app in problematic_apps]) + + # Manually modified files ? (c.f. yunohost service regen-conf) + modified_files = manually_modified_files() + modified_files = "".join(["\n - " + f for f in modified_files]) + + message = m18n.n("migration_0021_general_warning") + + # FIXME: update this message with updated topic link once we release the migration as stable + message = ( + "N.B.: **THIS MIGRATION IS STILL IN BETA-STAGE** ! If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read and share feedbacks on this forum thread: https://forum.yunohost.org/t/18531\n\n" + + message + ) + # message = ( + # "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/12195\n\n" + # + message + # ) + + if problematic_apps: + message += "\n\n" + m18n.n( + "migration_0021_problematic_apps_warning", + problematic_apps=problematic_apps, + ) + + if modified_files: + message += "\n\n" + m18n.n( + "migration_0021_modified_files", manually_modified_files=modified_files + ) + + return message + + def patch_apt_sources_list(self): + + sources_list = glob.glob("/etc/apt/sources.list.d/*.list") + sources_list.append("/etc/apt/sources.list") + + # This : + # - replace single 'buster' occurence by 'bulleye' + # - comments lines containing "backports" + # - replace 'buster/updates' by 'bullseye/updates' (or same with -) + # Special note about the security suite: + # https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html#security-archive + for f in sources_list: + command = ( + f"sed -i {f} " + "-e 's@ buster @ bullseye @g' " + "-e '/backports/ s@^#*@#@' " + "-e 's@ buster/updates @ bullseye-security @g' " + "-e 's@ buster-@ bullseye-@g' " + ) + os.system(command) + + def get_apps_equivs_packages(self): + + command = ( + "dpkg --get-selections" + " | grep -v deinstall" + " | awk '{print $1}'" + " | { grep 'ynh-deps$' || true; }" + ) + + output = check_output(command) + + return output.split("\n") if output else [] + + def hold(self, packages): + for package in packages: + os.system(f"apt-mark hold {package}") + + def unhold(self, packages): + for package in packages: + os.system(f"apt-mark unhold {package}") + + def apt_install(self, cmd): + def is_relevant(line): + return "Reading database ..." not in line.rstrip() + + callbacks = ( + lambda l: logger.info("+ " + l.rstrip() + "\r") + if _apt_log_line_is_relevant(l) + else logger.debug(l.rstrip() + "\r"), + lambda l: logger.warning(l.rstrip()) + if _apt_log_line_is_relevant(l) + else logger.debug(l.rstrip()), + ) + + cmd = ( + "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt install --quiet -o=Dpkg::Use-Pty=0 --fix-broken --assume-yes " + + cmd + ) + + logger.debug("Running: %s" % cmd) + + return call_async_output(cmd, callbacks, shell=True) + + def patch_yunohost_conflicts(self): + # + # This is a super dirty hack to remove the conflicts from yunohost's debian/control file + # Those conflicts are there to prevent mistakenly upgrading critical packages + # such as dovecot, postfix, nginx, openssl, etc... usually related to mistakenly + # using backports etc. + # + # The hack consists in savagely removing the conflicts directly in /var/lib/dpkg/status + # + + # We only patch the conflict if we're on yunohost 4.x + if self.yunohost_major_version() != N_CURRENT_YUNOHOST: + return + + conflicts = check_output("dpkg-query -s yunohost | grep '^Conflicts:'").strip() + if conflicts: + # We want to keep conflicting with apache/bind9 tho + new_conflicts = "Conflicts: apache2, bind9" + + command = ( + f"sed -i /var/lib/dpkg/status -e 's@{conflicts}@{new_conflicts}@g'" + ) + logger.debug(f"Running: {command}") + os.system(command) diff --git a/src/migrations/0022_php73_to_php74_pools.py b/src/migrations/0022_php73_to_php74_pools.py new file mode 100644 index 000000000..a2e5eae54 --- /dev/null +++ b/src/migrations/0022_php73_to_php74_pools.py @@ -0,0 +1,96 @@ +import os +import glob +from shutil import copy2 + +from moulinette.utils.log import getActionLogger + +from yunohost.app import _is_installed +from yunohost.utils.legacy import _patch_legacy_php_versions_in_settings +from yunohost.tools import Migration +from yunohost.service import _run_service_command + +logger = getActionLogger("yunohost.migration") + +OLDPHP_POOLS = "/etc/php/7.3/fpm/pool.d" +NEWPHP_POOLS = "/etc/php/7.4/fpm/pool.d" + +OLDPHP_SOCKETS_PREFIX = "/run/php/php7.3-fpm" +NEWPHP_SOCKETS_PREFIX = "/run/php/php7.4-fpm" + +# Because of synapse é_è +OLDPHP_SOCKETS_PREFIX2 = "/run/php7.3-fpm" +NEWPHP_SOCKETS_PREFIX2 = "/run/php7.4-fpm" + +MIGRATION_COMMENT = ( + "; YunoHost note : this file was automatically moved from {}".format(OLDPHP_POOLS) +) + + +class MyMigration(Migration): + + "Migrate php7.3-fpm 'pool' conf files to php7.4" + + dependencies = ["migrate_to_bullseye"] + + def run(self): + # Get list of php7.3 pool files + oldphp_pool_files = glob.glob("{}/*.conf".format(OLDPHP_POOLS)) + + # Keep only basenames + oldphp_pool_files = [os.path.basename(f) for f in oldphp_pool_files] + + # Ignore the "www.conf" (default stuff, probably don't want to touch it ?) + oldphp_pool_files = [f for f in oldphp_pool_files if f != "www.conf"] + + for pf in oldphp_pool_files: + + # Copy the files to the php7.3 pool + src = "{}/{}".format(OLDPHP_POOLS, pf) + dest = "{}/{}".format(NEWPHP_POOLS, pf) + copy2(src, dest) + + # Replace the socket prefix if it's found + c = "sed -i -e 's@{}@{}@g' {}".format( + OLDPHP_SOCKETS_PREFIX, NEWPHP_SOCKETS_PREFIX, dest + ) + os.system(c) + c = "sed -i -e 's@{}@{}@g' {}".format( + OLDPHP_SOCKETS_PREFIX2, NEWPHP_SOCKETS_PREFIX2, dest + ) + os.system(c) + + # Also add a comment that it was automatically moved from php7.3 + # (for human traceability and backward migration) + c = "sed -i '1i {}' {}".format(MIGRATION_COMMENT, dest) + os.system(c) + + app_id = os.path.basename(pf)[: -len(".conf")] + if _is_installed(app_id): + _patch_legacy_php_versions_in_settings( + "/etc/yunohost/apps/%s/" % app_id + ) + + nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/%s.conf" % app_id) + for nf in nginx_conf_files: + # Replace the socket prefix if it's found + c = "sed -i -e 's@{}@{}@g' {}".format( + OLDPHP_SOCKETS_PREFIX, NEWPHP_SOCKETS_PREFIX, nf + ) + os.system(c) + c = "sed -i -e 's@{}@{}@g' {}".format( + OLDPHP_SOCKETS_PREFIX2, NEWPHP_SOCKETS_PREFIX2, nf + ) + os.system(c) + + os.system( + "rm /etc/logrotate.d/php7.3-fpm" + ) # We remove this otherwise the logrotate cron will be unhappy + + # Reload/restart the php pools + os.system("systemctl stop php7.3-fpm") + os.system("systemctl disable php7.3-fpm") + _run_service_command("restart", "php7.4-fpm") + _run_service_command("enable", "php7.4-fpm") + + # Reload nginx + _run_service_command("reload", "nginx") diff --git a/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py b/src/migrations/0023_postgresql_11_to_13.py similarity index 67% rename from src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py rename to src/migrations/0023_postgresql_11_to_13.py index 1ccf5ccc9..8f03f8c5f 100644 --- a/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py +++ b/src/migrations/0023_postgresql_11_to_13.py @@ -1,4 +1,5 @@ import subprocess +import time from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError @@ -12,41 +13,43 @@ logger = getActionLogger("yunohost.migration") class MyMigration(Migration): - "Migrate DBs from Postgresql 9.6 to 11 after migrating to Buster" + "Migrate DBs from Postgresql 11 to 13 after migrating to Bullseye" - dependencies = ["migrate_to_buster"] + dependencies = ["migrate_to_bullseye"] def run(self): - if not self.package_is_installed("postgresql-9.6"): - logger.warning(m18n.n("migration_0017_postgresql_96_not_installed")) + if not self.package_is_installed("postgresql-11"): + logger.warning(m18n.n("migration_0023_postgresql_11_not_installed")) return - if not self.package_is_installed("postgresql-11"): - raise YunohostValidationError("migration_0017_postgresql_11_not_installed") + if not self.package_is_installed("postgresql-13"): + raise YunohostValidationError("migration_0023_postgresql_13_not_installed") - # Make sure there's a 9.6 cluster + # Make sure there's a 11 cluster try: - self.runcmd("pg_lsclusters | grep -q '^9.6 '") + self.runcmd("pg_lsclusters | grep -q '^11 '") except Exception: logger.warning( - "It looks like there's not active 9.6 cluster, so probably don't need to run this migration" + "It looks like there's not active 11 cluster, so probably don't need to run this migration" ) return if not space_used_by_directory( - "/var/lib/postgresql/9.6" + "/var/lib/postgresql/11" ) > free_space_in_directory("/var/lib/postgresql"): raise YunohostValidationError( - "migration_0017_not_enough_space", path="/var/lib/postgresql/" + "migration_0023_not_enough_space", path="/var/lib/postgresql/" ) self.runcmd("systemctl stop postgresql") + time.sleep(3) self.runcmd( - "LC_ALL=C pg_dropcluster --stop 11 main || true" - ) # We do not trigger an exception if the command fails because that probably means cluster 11 doesn't exists, which is fine because it's created during the pg_upgradecluster) - self.runcmd("LC_ALL=C pg_upgradecluster -m upgrade 9.6 main") - self.runcmd("LC_ALL=C pg_dropcluster --stop 9.6 main") + "LC_ALL=C pg_dropcluster --stop 13 main || true" + ) # We do not trigger an exception if the command fails because that probably means cluster 13 doesn't exists, which is fine because it's created during the pg_upgradecluster) + time.sleep(3) + self.runcmd("LC_ALL=C pg_upgradecluster -m upgrade 11 main") + self.runcmd("LC_ALL=C pg_dropcluster --stop 11 main") self.runcmd("systemctl start postgresql") def package_is_installed(self, package_name): diff --git a/src/yunohost/tests/__init__.py b/src/migrations/__init__.py similarity index 100% rename from src/yunohost/tests/__init__.py rename to src/migrations/__init__.py diff --git a/src/yunohost/permission.py b/src/permission.py similarity index 94% rename from src/yunohost/permission.py rename to src/permission.py index 01330ad7f..2a6f6d954 100644 --- a/src/yunohost/permission.py +++ b/src/permission.py @@ -58,7 +58,7 @@ def user_permission_list( ldap = _get_ldap_interface() permissions_infos = ldap.search( - "ou=permission,dc=yunohost,dc=org", + "ou=permission", "(objectclass=permissionYnh)", [ "cn", @@ -133,13 +133,13 @@ def user_permission_list( main_perm_name = name.split(".")[0] + ".main" if main_perm_name not in permissions: logger.debug( - "Uhoh, unknown permission %s ? (Maybe we're in the process or deleting the perm for this app...)" - % main_perm_name + f"Uhoh, unknown permission {main_perm_name} ? (Maybe we're in the process or deleting the perm for this app...)" ) continue main_perm_label = permissions[main_perm_name]["label"] infos["sublabel"] = infos["label"] - infos["label"] = "%s (%s)" % (main_perm_label, infos["label"]) + label_ = infos["label"] + infos["label"] = f"{main_perm_label} ({label_})" if short: permissions = list(permissions.keys()) @@ -406,9 +406,7 @@ def permission_create( permission = permission + ".main" # Validate uniqueness of permission in LDAP - if ldap.get_conflict( - {"cn": permission}, base_dn="ou=permission,dc=yunohost,dc=org" - ): + if ldap.get_conflict({"cn": permission}, base_dn="ou=permission"): raise YunohostValidationError("permission_already_exist", permission=permission) # Get random GID @@ -451,28 +449,32 @@ def permission_create( operation_logger.start() try: - ldap.add("cn=%s,ou=permission" % permission, attr_dict) + ldap.add(f"cn={permission},ou=permission", attr_dict) except Exception as e: raise YunohostError( "permission_creation_failed", permission=permission, error=e ) - permission_url( - permission, - url=url, - add_url=additional_urls, - auth_header=auth_header, - sync_perm=False, - ) + try: + permission_url( + permission, + url=url, + add_url=additional_urls, + auth_header=auth_header, + sync_perm=False, + ) - new_permission = _update_ldap_group_permission( - permission=permission, - allowed=allowed, - label=label, - show_tile=show_tile, - protected=protected, - sync_perm=sync_perm, - ) + new_permission = _update_ldap_group_permission( + permission=permission, + allowed=allowed, + label=label, + show_tile=show_tile, + protected=protected, + sync_perm=sync_perm, + ) + except Exception: + permission_delete(permission, force=True) + raise logger.debug(m18n.n("permission_created", permission=permission)) return new_permission @@ -580,7 +582,7 @@ def permission_url( try: ldap.update( - "cn=%s,ou=permission" % permission, + f"cn={permission},ou=permission", { "URL": [url] if url is not None else [], "additionalUrls": new_additional_urls, @@ -628,7 +630,7 @@ def permission_delete(operation_logger, permission, force=False, sync_perm=True) operation_logger.start() try: - ldap.remove("cn=%s,ou=permission" % permission) + ldap.remove(f"cn={permission},ou=permission") except Exception as e: raise YunohostError( "permission_deletion_failed", permission=permission, error=e @@ -660,13 +662,11 @@ def permission_sync_to_user(): currently_allowed_users = set(permission_infos["corresponding_users"]) # These are the users that should be allowed because they are member of a group that is allowed for this permission ... - should_be_allowed_users = set( - [ - user - for group in permission_infos["allowed"] - for user in groups[group]["members"] - ] - ) + should_be_allowed_users = { + user + for group in permission_infos["allowed"] + for user in groups[group]["members"] + } # Note that a LDAP operation with the same value that is in LDAP crash SLAP. # So we need to check before each ldap operation that we really change something in LDAP @@ -676,15 +676,14 @@ def permission_sync_to_user(): new_inherited_perms = { "inheritPermission": [ - "uid=%s,ou=users,dc=yunohost,dc=org" % u - for u in should_be_allowed_users + f"uid={u},ou=users,dc=yunohost,dc=org" for u in should_be_allowed_users ], "memberUid": should_be_allowed_users, } # Commit the change with the new inherited stuff try: - ldap.update("cn=%s,ou=permission" % permission_name, new_inherited_perms) + ldap.update(f"cn={permission_name},ou=permission", new_inherited_perms) except Exception as e: raise YunohostError( "permission_update_failed", permission=permission_name, error=e @@ -762,7 +761,7 @@ def _update_ldap_group_permission( update["showTile"] = [str(show_tile).upper()] try: - ldap.update("cn=%s,ou=permission" % permission, update) + ldap.update(f"cn={permission},ou=permission", update) except Exception as e: raise YunohostError("permission_update_failed", permission=permission, error=e) @@ -860,11 +859,9 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): re:^/api/.*|/scripts/api.js$ """ - from yunohost.domain import domain_list + from yunohost.domain import _assert_domain_exists from yunohost.app import _assert_no_conflicting_apps - domains = domain_list()["domains"] - # # Regexes # @@ -896,8 +893,8 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): domain, path = url[3:].split("/", 1) path = "/" + path - if domain.replace("%", "").replace("\\", "") not in domains: - raise YunohostValidationError("domain_name_unknown", domain=domain) + domain_with_no_regex = domain.replace("%", "").replace("\\", "") + _assert_domain_exists(domain_with_no_regex) validate_regex(path) @@ -931,8 +928,7 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): domain, path = split_domain_path(url) sanitized_url = domain + path - if domain not in domains: - raise YunohostValidationError("domain_name_unknown", domain=domain) + _assert_domain_exists(domain) _assert_no_conflicting_apps(domain, path, ignore_app=app) diff --git a/src/yunohost/regenconf.py b/src/regenconf.py similarity index 93% rename from src/yunohost/regenconf.py rename to src/regenconf.py index 0608bcf8c..e513a1506 100644 --- a/src/yunohost/regenconf.py +++ b/src/regenconf.py @@ -35,7 +35,7 @@ from yunohost.utils.error import YunohostError from yunohost.log import is_unit_operation from yunohost.hook import hook_callback, hook_list -BASE_CONF_PATH = "/home/yunohost.conf" +BASE_CONF_PATH = "/var/cache/yunohost/regenconf" BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, "backup") PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, "pending") REGEN_CONF_FILE = "/etc/yunohost/regenconf.yml" @@ -48,7 +48,7 @@ logger = log.getActionLogger("yunohost.regenconf") @is_unit_operation([("names", "configuration")]) def regen_conf( operation_logger, - names=[], + names=None, with_diff=False, force=False, dry_run=False, @@ -66,6 +66,9 @@ def regen_conf( """ + if names is None: + names = [] + result = {} # Return the list of pending conf @@ -105,13 +108,9 @@ def regen_conf( else: filesystem.mkdir(PENDING_CONF_DIR, 0o755, True) - # Format common hooks arguments - common_args = [1 if force else 0, 1 if dry_run else 0] - # Execute hooks for pre-regen - pre_args = [ - "pre", - ] + common_args + # element 2 and 3 with empty string is because of legacy... + pre_args = ["pre", "", ""] def _pre_call(name, priority, path, args): # create the pending conf directory for the category @@ -129,16 +128,6 @@ def regen_conf( if not names: names = hook_list("conf_regen", list_by="name", show_info=False)["hooks"] - # Dirty hack for legacy code : avoid attempting to regen the conf for - # glances because it got removed ... This is only needed *once* - # during the upgrade from 3.7 to 3.8 because Yunohost will attempt to - # regen glance's conf *before* it gets automatically removed from - # services.yml (which will happens only during the regen-conf of - # 'yunohost', so at the very end of the regen-conf cycle) Anyway, - # this can be safely removed once we're in >= 4.0 - if "glances" in names: - names.remove("glances") - # [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) @@ -151,6 +140,9 @@ def regen_conf( # though kinda tight-coupled to the postinstall logic :s if os.path.exists("/etc/yunohost/installed"): env["YNH_DOMAINS"] = " ".join(domain_list()["domains"]) + env["YNH_MAIN_DOMAINS"] = " ".join( + domain_list(exclude_subdomains=True)["domains"] + ) pre_result = hook_callback("conf_regen", names, pre_callback=_pre_call, env=env) @@ -417,9 +409,8 @@ def regen_conf( return result # Execute hooks for post-regen - post_args = [ - "post", - ] + common_args + # element 2 and 3 with empty string is because of legacy... + post_args = ["post", "", ""] def _pre_call(name, priority, path, args): # append coma-separated applied changes for the category @@ -456,16 +447,12 @@ def _save_regenconf_infos(infos): categories -- A dict containing the regenconf infos """ - # Ugly hack to get rid of legacy glances stuff - if "glances" in infos: - del infos["glances"] - try: with open(REGEN_CONF_FILE, "w") as f: yaml.safe_dump(infos, f, default_flow_style=False) except Exception as e: logger.warning( - "Error while saving regenconf infos, exception: %s", e, exc_info=1 + f"Error while saving regenconf infos, exception: {e}", exc_info=1 ) raise @@ -521,9 +508,7 @@ def _calculate_hash(path): return hasher.hexdigest() except IOError as e: - logger.warning( - "Error while calculating file '%s' hash: %s", path, e, exc_info=1 - ) + logger.warning(f"Error while calculating file '{path}' hash: {e}", exc_info=1) return None @@ -575,11 +560,11 @@ def _get_conf_hashes(category): categories = _get_regenconf_infos() if category not in categories: - logger.debug("category %s is not in categories.yml yet.", category) + logger.debug(f"category {category} is not in categories.yml yet.") return {} elif categories[category] is None or "conffiles" not in categories[category]: - logger.debug("No configuration files for category %s.", category) + logger.debug(f"No configuration files for category {category}.") return {} else: @@ -588,7 +573,7 @@ def _get_conf_hashes(category): def _update_conf_hashes(category, hashes): """Update the registered conf hashes for a category""" - logger.debug("updating conf hashes for '%s' with: %s", category, hashes) + logger.debug(f"updating conf hashes for '{category}' with: {hashes}") categories = _get_regenconf_infos() category_conf = categories.get(category, {}) @@ -619,8 +604,7 @@ def _force_clear_hashes(paths): for category in categories.keys(): if path in categories[category]["conffiles"]: logger.debug( - "force-clearing old conf hash for %s in category %s" - % (path, category) + f"force-clearing old conf hash for {path} in category {category}" ) del categories[category]["conffiles"][path] @@ -636,12 +620,9 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): """ if save: - backup_path = os.path.join( - BACKUP_CONF_DIR, - "{0}-{1}".format( - system_conf.lstrip("/"), datetime.utcnow().strftime("%Y%m%d.%H%M%S") - ), - ) + system_conf_ = system_conf.lstrip("/") + now_ = datetime.utcnow().strftime("%Y%m%d.%H%M%S") + backup_path = os.path.join(BACKUP_CONF_DIR, f"{system_conf_}-{now_}") backup_dir = os.path.dirname(backup_path) if not os.path.isdir(backup_dir): @@ -666,9 +647,7 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): logger.debug(m18n.n("regenconf_file_updated", conf=system_conf)) except Exception as e: logger.warning( - "Exception while trying to regenerate conf '%s': %s", - system_conf, - e, + f"Exception while trying to regenerate conf '{system_conf}': {e}", exc_info=1, ) if not new_conf and os.path.exists(system_conf): diff --git a/src/yunohost/service.py b/src/service.py similarity index 87% rename from src/yunohost/service.py rename to src/service.py index fb12e9053..506d3223e 100644 --- a/src/yunohost/service.py +++ b/src/service.py @@ -48,7 +48,7 @@ from moulinette.utils.filesystem import ( MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" SERVICES_CONF = "/etc/yunohost/services.yml" -SERVICES_CONF_BASE = "/usr/share/yunohost/templates/yunohost/services.yml" +SERVICES_CONF_BASE = "/usr/share/yunohost/conf/yunohost/services.yml" logger = getActionLogger("yunohost.service") @@ -57,12 +57,10 @@ def service_add( name, description=None, log=None, - log_type=None, test_status=None, test_conf=None, needs_exposed_ports=None, need_lock=False, - status=None, ): """ Add a custom service @@ -71,12 +69,10 @@ def service_add( name -- Service name to add description -- description of the service log -- Absolute path to log file to display - log_type -- (deprecated) Specify if the corresponding log is a file or a systemd log test_status -- Specify a custom bash command to check the status of the service. N.B. : it only makes sense to specify this if the corresponding systemd service does not return the proper information. test_conf -- Specify a custom bash command to check if the configuration of the service is valid or broken, similar to nginx -t. needs_exposed_ports -- A list of ports that needs to be publicly exposed for the service to work as intended. need_lock -- Use this option to prevent deadlocks if the service does invoke yunohost commands. - status -- Deprecated, doesn't do anything anymore. Use test_status instead. """ services = _get_services() @@ -86,15 +82,6 @@ def service_add( if not isinstance(log, list): log = [log] - # Deprecated log_type stuff - if log_type is not None: - logger.warning( - "/!\\ Packagers! --log_type is deprecated. You do not need to specify --log_type systemd anymore ... Yunohost now automatically fetch the journalctl of the systemd service by default." - ) - # Usually when adding such a service, the service name will be provided so we remove it as it's not a log file path - if name in log: - log.remove(name) - service["log"] = log if not description: @@ -123,7 +110,7 @@ def service_add( # Try to get the description from systemd service _, systemd_info = _get_service_information_from_systemd(name) type_ = systemd_info.get("Type") if systemd_info is not None else "" - if type_ == "oneshot" and name != "postgresql": + if type_ == "oneshot": logger.warning( "/!\\ Packagers! Please provide a --test_status when adding oneshot-type services in Yunohost, such that it has a reliable way to check if the service is running or not." ) @@ -256,7 +243,7 @@ def service_restart(names): ) -def service_reload_or_restart(names): +def service_reload_or_restart(names, test_conf=True): """ Reload one or more services if they support it. If not, restart them instead. If the services are not running yet, they will be started. @@ -266,7 +253,36 @@ def service_reload_or_restart(names): """ if isinstance(names, str): names = [names] + + services = _get_services() + for name in names: + + logger.debug(f"Reloading service {name}") + + test_conf_cmd = services.get(name, {}).get("test_conf") + if test_conf and test_conf_cmd: + + p = subprocess.Popen( + test_conf_cmd, + shell=True, + executable="/bin/bash", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + out, _ = p.communicate() + if p.returncode != 0: + errors = out.decode().strip().split("\n") + logger.error( + m18n.n( + "service_not_reloading_because_conf_broken", + name=name, + errors=errors, + ) + ) + continue + if _run_service_command("reload-or-restart", name): logger.success(m18n.n("service_reloaded_or_restarted", service=name)) else: @@ -391,8 +407,7 @@ def _get_and_format_service_status(service, infos): if raw_status is None: logger.error( - "Failed to get status information via dbus for service %s, systemctl didn't recognize this service ('NoSuchUnit')." - % systemd_service + f"Failed to get status information via dbus for service {systemd_service}, systemctl didn't recognize this service ('NoSuchUnit')." ) return { "status": "unknown", @@ -408,7 +423,7 @@ def _get_and_format_service_status(service, infos): # If no description was there, try to get it from the .json locales if not description: - translation_key = "service_description_%s" % service + translation_key = f"service_description_{service}" if m18n.key_exists(translation_key): description = m18n.n(translation_key) else: @@ -429,7 +444,7 @@ def _get_and_format_service_status(service, infos): "enabled" if glob("/etc/rc[S5].d/S??" + service) else "disabled" ) elif os.path.exists( - "/etc/systemd/system/multi-user.target.wants/%s.service" % service + f"/etc/systemd/system/multi-user.target.wants/{service}.service" ): output["start_on_boot"] = "enabled" @@ -545,29 +560,6 @@ def service_log(name, number=50): return result -def service_regen_conf( - names=[], with_diff=False, force=False, dry_run=False, list_pending=False -): - - services = _get_services() - - if isinstance(names, str): - names = [names] - - for name in names: - if name not in services.keys(): - raise YunohostValidationError("service_unknown", service=name) - - if names is []: - names = list(services.keys()) - - logger.warning(m18n.n("service_regen_conf_is_deprecated")) - - from yunohost.regenconf import regen_conf - - return regen_conf(names, with_diff, force, dry_run, list_pending) - - def _run_service_command(action, service): """ Run services management command (start, stop, enable, disable, restart, reload) @@ -592,11 +584,10 @@ def _run_service_command(action, service): ] if action not in possible_actions: raise ValueError( - "Unknown action '%s', available actions are: %s" - % (action, ", ".join(possible_actions)) + f"Unknown action '{action}', available actions are: {', '.join(possible_actions)}" ) - cmd = "systemctl %s %s" % (action, service) + cmd = f"systemctl {action} {service}" need_lock = services[service].get("need_lock", False) and action in [ "start", @@ -611,7 +602,7 @@ def _run_service_command(action, service): try: # Launch the command - logger.debug("Running '%s'" % cmd) + logger.debug(f"Running '{cmd}'") p = subprocess.Popen(cmd.split(), stderr=subprocess.STDOUT) # If this command needs a lock (because the service uses yunohost # commands inside), find the PID and add a lock for it @@ -644,7 +635,7 @@ def _give_lock(action, service, p): else: systemctl_PID_name = "ControlPID" - cmd_get_son_PID = "systemctl show %s -p %s" % (service, systemctl_PID_name) + cmd_get_son_PID = f"systemctl show {service} -p {systemctl_PID_name}" son_PID = 0 # As long as we did not found the PID and that the command is still running while son_PID == 0 and p.poll() is None: @@ -657,10 +648,8 @@ def _give_lock(action, service, p): # If we found a PID if son_PID != 0: # Append the PID to the lock file - logger.debug( - "Giving a lock to PID %s for service %s !" % (str(son_PID), service) - ) - append_to_file(MOULINETTE_LOCK, "\n%s" % str(son_PID)) + logger.debug(f"Giving a lock to PID {son_PID} for service {service} !") + append_to_file(MOULINETTE_LOCK, f"\n{son_PID}") return son_PID @@ -703,19 +692,29 @@ def _get_services(): # Dirty hack to check the status of ynh-vpnclient if "ynh-vpnclient" in services: - status_check = "systemctl is-active openvpn@client.service" - if "test_status" not in services["ynh-vpnclient"]: - services["ynh-vpnclient"]["test_status"] = status_check if "log" not in services["ynh-vpnclient"]: services["ynh-vpnclient"]["log"] = ["/var/log/ynh-vpnclient.log"] - # Stupid hack for postgresql which ain't an official service ... Can't - # really inject that info otherwise. Real service we want to check for - # status and log is in fact postgresql@x.y-main (x.y being the version) - if "postgresql" in services: - if "description" in services["postgresql"]: - del services["postgresql"]["description"] - services["postgresql"]["actual_systemd_service"] = "postgresql@11-main" + services_with_package_condition = [ + name + for name, infos in services.items() + if infos.get("ignore_if_package_is_not_installed") + ] + for name in services_with_package_condition: + package = services[name]["ignore_if_package_is_not_installed"] + if os.system(f"dpkg --list | grep -q 'ii *{package}'") != 0: + del services[name] + + php_fpm_versions = check_output( + r"dpkg --list | grep -P 'ii php\d.\d-fpm' | awk '{print $2}' | grep -o -P '\d.\d' || true" + ) + php_fpm_versions = [v for v in php_fpm_versions.split("\n") if v.strip()] + for version in php_fpm_versions: + services[f"php{version}-fpm"] = { + "log": f"/var/log/php{version}-fpm.log", + "test_conf": f"php-fpm{version} --test", # ofc the service is phpx.y-fpm but the program is php-fpmx.y because why not ... + "category": "web", + } # Remove legacy /var/log/daemon.log and /var/log/syslog from log entries # because they are too general. Instead, now the journalctl log is @@ -745,14 +744,22 @@ def _save_services(services): diff = {} for service_name, service_infos in services.items(): - service_conf_base = conf_base.get(service_name, {}) + + # Ignore php-fpm services, they are to be added dynamically by the core, + # but not actually saved + if service_name.startswith("php") and service_name.endswith("-fpm"): + continue + + service_conf_base = conf_base.get(service_name, {}) or {} diff[service_name] = {} for key, value in service_infos.items(): if service_conf_base.get(key) != value: diff[service_name][key] = value - diff = {name: infos for name, infos in diff.items() if infos} + diff = { + name: infos for name, infos in diff.items() if infos or name not in conf_base + } write_to_yaml(SERVICES_CONF, diff) @@ -820,7 +827,7 @@ def _find_previous_log_file(file): i = int(i[0]) + 1 if len(i) > 0 else 1 previous_file = file if i == 1 else splitext[0] - previous_file = previous_file + ".%d" % (i) + previous_file = previous_file + f".{i}" if os.path.exists(previous_file): return previous_file @@ -836,14 +843,10 @@ def _get_journalctl_logs(service, number="all"): systemd_service = services.get(service, {}).get("actual_systemd_service", service) try: return check_output( - "journalctl --no-hostname --no-pager -u {0} -n{1}".format( - systemd_service, number - ) + f"journalctl --no-hostname --no-pager -u {systemd_service} -n{number}" ) except Exception: import traceback - return ( - "error while get services logs from journalctl:\n%s" - % traceback.format_exc() - ) + trace_ = traceback.format_exc() + return f"error while get services logs from journalctl:\n{trace_}" diff --git a/src/yunohost/settings.py b/src/settings.py similarity index 91% rename from src/yunohost/settings.py rename to src/settings.py index fe072cddb..cec416550 100644 --- a/src/yunohost/settings.py +++ b/src/settings.py @@ -18,6 +18,9 @@ SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json" def is_boolean(value): + TRUE = ["true", "on", "yes", "y", "1"] + FALSE = ["false", "off", "no", "n", "0"] + """ Ensure a string value is intended as a boolean @@ -30,9 +33,11 @@ def is_boolean(value): """ if isinstance(value, bool): return True, value + if value in [0, 1]: + return True, bool(value) elif isinstance(value, str): - if str(value).lower() in ["true", "on", "yes", "false", "off", "no"]: - return True, str(value).lower() in ["true", "on", "yes"] + if str(value).lower() in TRUE + FALSE: + return True, str(value).lower() in TRUE else: return False, None else: @@ -76,6 +81,17 @@ DEFAULTS = OrderedDict( "security.ssh.port", {"type": "int", "default": 22}, ), + ( + "security.ssh.password_authentication", + {"type": "bool", "default": True}, + ), + ( + "security.nginx.redirect_to_https", + { + "type": "bool", + "default": True, + }, + ), ( "security.nginx.compatibility", { @@ -212,7 +228,7 @@ def settings_set(key, value): try: trigger_post_change_hook(key, old_value, value) except Exception as e: - logger.error("Post-change hook for setting %s failed : %s" % (key, e)) + logger.error(f"Post-change hook for setting {key} failed : {e}") raise @@ -269,7 +285,7 @@ def settings_reset_all(): def _get_setting_description(key): - return m18n.n("global_settings_setting_%s" % key.replace(".", "_")) + return m18n.n(f"global_settings_setting_{key}".replace(".", "_")) def _get_settings(): @@ -299,7 +315,7 @@ def _get_settings(): try: unknown_settings = json.load(open(unknown_settings_path, "r")) except Exception as e: - logger.warning("Error while loading unknown settings %s" % e) + logger.warning(f"Error while loading unknown settings {e}") try: with open(SETTINGS_PATH) as settings_fd: @@ -325,9 +341,7 @@ def _get_settings(): _save_settings(unknown_settings, location=unknown_settings_path) _save_settings(settings) except Exception as e: - logger.warning( - "Failed to save unknown settings (because %s), aborting." % e - ) + logger.warning(f"Failed to save unknown settings (because {e}), aborting.") return settings @@ -357,13 +371,12 @@ post_change_hooks = {} def post_change_hook(setting_name): def decorator(func): - assert setting_name in DEFAULTS.keys(), ( - "The setting %s does not exists" % setting_name - ) - assert setting_name not in post_change_hooks, ( - "You can only register one post change hook per setting (in particular for %s)" - % setting_name - ) + assert ( + setting_name in DEFAULTS.keys() + ), f"The setting {setting_name} does not exists" + assert ( + setting_name not in post_change_hooks + ), f"You can only register one post change hook per setting (in particular for {setting_name})" post_change_hooks[setting_name] = func return func @@ -372,7 +385,7 @@ def post_change_hook(setting_name): def trigger_post_change_hook(setting_name, old_value, new_value): if setting_name not in post_change_hooks: - logger.debug("Nothing to do after changing setting %s" % setting_name) + logger.debug(f"Nothing to do after changing setting {setting_name}") return f = post_change_hooks[setting_name] @@ -392,6 +405,7 @@ def trigger_post_change_hook(setting_name, old_value, new_value): @post_change_hook("ssowat.panel_overlay.enabled") +@post_change_hook("security.nginx.redirect_to_https") @post_change_hook("security.nginx.compatibility") @post_change_hook("security.webadmin.allowlist.enabled") @post_change_hook("security.webadmin.allowlist") @@ -407,6 +421,7 @@ def reconfigure_nginx_and_yunohost(setting_name, old_value, new_value): @post_change_hook("security.ssh.compatibility") +@post_change_hook("security.ssh.password_authentication") def reconfigure_ssh(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["ssh"]) diff --git a/src/yunohost/ssh.py b/src/ssh.py similarity index 96% rename from src/yunohost/ssh.py rename to src/ssh.py index ecee39f4a..b89dc6c8e 100644 --- a/src/yunohost/ssh.py +++ b/src/ssh.py @@ -99,7 +99,7 @@ def user_ssh_remove_key(username, key): if not os.path.exists(authorized_keys_file): raise YunohostValidationError( - "this key doesn't exists ({} dosesn't exists)".format(authorized_keys_file), + f"this key doesn't exists ({authorized_keys_file} dosesn't exists)", raw_msg=True, ) @@ -107,7 +107,7 @@ def user_ssh_remove_key(username, key): if key not in authorized_keys_content: raise YunohostValidationError( - "Key '{}' is not present in authorized_keys".format(key), raw_msg=True + f"Key '{key}' is not present in authorized_keys", raw_msg=True ) # don't delete the previous comment because we can't verify if it's legit @@ -172,7 +172,7 @@ def _get_user_for_ssh(username, attrs=None): ldap = _get_ldap_interface() user = ldap.search( - "ou=users,dc=yunohost,dc=org", + "ou=users", "(&(objectclass=person)(uid=%s))" % username, attrs, ) diff --git a/src/yunohost/utils/__init__.py b/src/tests/__init__.py similarity index 100% rename from src/yunohost/utils/__init__.py rename to src/tests/__init__.py diff --git a/src/yunohost/tests/conftest.py b/src/tests/conftest.py similarity index 87% rename from src/yunohost/tests/conftest.py rename to src/tests/conftest.py index 6b4e2c3fd..cd5cb307e 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/tests/conftest.py @@ -1,14 +1,11 @@ import os import pytest -import sys import moulinette from moulinette import m18n, Moulinette from yunohost.utils.error import YunohostError from contextlib import contextmanager -sys.path.append("..") - @pytest.fixture(scope="session", autouse=True) def clone_test_app(request): @@ -64,12 +61,6 @@ def new_translate(self, key, *args, **kwargs): moulinette.core.Translator.translate = new_translate -def new_m18nn(self, key, *args, **kwargs): - return self._namespaces[self._current_namespace].translate(key, *args, **kwargs) - - -moulinette.core.Moulinette18n.n = new_m18nn - # # Init the moulinette to have the cli loggers stuff # # @@ -77,6 +68,8 @@ moulinette.core.Moulinette18n.n = new_m18nn def pytest_cmdline_main(config): + import sys + sys.path.insert(0, "/usr/lib/moulinette/") import yunohost @@ -84,9 +77,12 @@ def pytest_cmdline_main(config): class DummyInterface: - type = "test" + type = "cli" - def prompt(*args, **kwargs): + def prompt(self, *args, **kwargs): raise NotImplementedError + def display(self, message, *args, **kwargs): + print(message) + Moulinette._interface = DummyInterface() diff --git a/src/yunohost/tests/test_appscatalog.py b/src/tests/test_app_catalog.py similarity index 99% rename from src/yunohost/tests/test_appscatalog.py rename to src/tests/test_app_catalog.py index a2619a660..e9ecb1c12 100644 --- a/src/yunohost/tests/test_appscatalog.py +++ b/src/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, @@ -132,7 +132,7 @@ def test_apps_catalog_update_nominal(mocker): catalog = app_catalog(with_categories=True) assert "apps" in catalog - assert set(catalog["apps"].keys()) == set(["foo", "bar"]) + assert set(catalog["apps"].keys()) == {"foo", "bar"} assert "categories" in catalog assert [c["id"] for c in catalog["categories"]] == ["yolo", "swag"] diff --git a/src/tests/test_app_config.py b/src/tests/test_app_config.py new file mode 100644 index 000000000..d6cf8045d --- /dev/null +++ b/src/tests/test_app_config.py @@ -0,0 +1,211 @@ +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 +from yunohost.app import ( + app_setting, + app_install, + app_remove, + _is_installed, + app_config_get, + app_config_set, + app_ssowatconf, +) +from yunohost.user import user_create, user_delete + +from yunohost.utils.error import YunohostError, YunohostValidationError + + +def setup_function(function): + + clean() + + +def teardown_function(function): + + clean() + + +def clean(): + + # Make sure we have a ssowat + os.system("mkdir -p /etc/ssowat/") + app_ssowatconf() + + test_apps = ["config_app", "legacy_app"] + + for test_app in test_apps: + + if _is_installed(test_app): + app_remove(test_app) + + for filepath in glob.glob("/etc/nginx/conf.d/*.d/*%s*" % test_app): + os.remove(filepath) + for folderpath in glob.glob("/etc/yunohost/apps/*%s*" % test_app): + shutil.rmtree(folderpath, ignore_errors=True) + for folderpath in glob.glob("/var/www/*%s*" % test_app): + shutil.rmtree(folderpath, ignore_errors=True) + + os.system("bash -c \"mysql -B 2>/dev/null <<< 'DROP DATABASE %s' \"" % test_app) + os.system( + "bash -c \"mysql -B 2>/dev/null <<< 'DROP USER %s@localhost'\"" % test_app + ) + + # Reset failed quota for service to avoid running into start-limit rate ? + os.system("systemctl reset-failed nginx") + os.system("systemctl start nginx") + + +@pytest.fixture() +def legacy_app(request): + + main_domain = _get_maindomain() + + app_install( + os.path.join(get_test_apps_dir(), "legacy_app_ynh"), + args="domain={}&path={}&is_public={}".format(main_domain, "/", 1), + force=True, + ) + + def remove_app(): + app_remove("legacy_app") + + request.addfinalizer(remove_app) + + return "legacy_app" + + +@pytest.fixture() +def config_app(request): + + app_install( + os.path.join(get_test_apps_dir(), "config_app_ynh"), + args="", + force=True, + ) + + def remove_app(): + app_remove("config_app") + + request.addfinalizer(remove_app) + + return "config_app" + + +def test_app_config_get(config_app): + + user_create("alice", "Alice", "White", _get_maindomain(), "test123Ynh") + + assert isinstance(app_config_get(config_app), dict) + assert isinstance(app_config_get(config_app, full=True), dict) + assert isinstance(app_config_get(config_app, export=True), dict) + assert isinstance(app_config_get(config_app, "main"), dict) + assert isinstance(app_config_get(config_app, "main.components"), dict) + assert app_config_get(config_app, "main.components.boolean") == "0" + + user_delete("alice") + + +def test_app_config_nopanel(legacy_app): + + with pytest.raises(YunohostValidationError): + app_config_get(legacy_app) + + +def test_app_config_get_nonexistentstuff(config_app): + + with pytest.raises(YunohostValidationError): + app_config_get("nonexistent") + + with pytest.raises(YunohostValidationError): + app_config_get(config_app, "nonexistent") + + with pytest.raises(YunohostValidationError): + app_config_get(config_app, "main.nonexistent") + + with pytest.raises(YunohostValidationError): + app_config_get(config_app, "main.components.nonexistent") + + app_setting(config_app, "boolean", delete=True) + with pytest.raises(YunohostError): + app_config_get(config_app, "main.components.boolean") + + +def test_app_config_regular_setting(config_app): + + assert app_config_get(config_app, "main.components.boolean") == "0" + + app_config_set(config_app, "main.components.boolean", "no") + + assert app_config_get(config_app, "main.components.boolean") == "0" + assert app_setting(config_app, "boolean") == "0" + + app_config_set(config_app, "main.components.boolean", "yes") + + assert app_config_get(config_app, "main.components.boolean") == "1" + assert app_setting(config_app, "boolean") == "1" + + 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") + + +def test_app_config_bind_on_file(config_app): + + # c.f. conf/test.php in the config app + assert '$arg5= "Arg5 value";' in read_file("/var/www/config_app/test.php") + assert app_config_get(config_app, "bind.variable.arg5") == "Arg5 value" + assert app_setting(config_app, "arg5") is None + + app_config_set(config_app, "bind.variable.arg5", "Foo Bar") + + assert '$arg5= "Foo Bar";' in read_file("/var/www/config_app/test.php") + assert app_config_get(config_app, "bind.variable.arg5") == "Foo Bar" + assert app_setting(config_app, "arg5") == "Foo Bar" + + +def test_app_config_custom_get(config_app): + + assert app_setting(config_app, "arg9") is None + assert ( + "Files in /var/www" + in app_config_get(config_app, "bind.function.arg9")["ask"]["en"] + ) + assert app_setting(config_app, "arg9") is None + + +def test_app_config_custom_validator(config_app): + + # c.f. the config script + # arg8 is a password that must be at least 8 chars + assert not os.path.exists("/var/www/config_app/password") + assert app_setting(config_app, "arg8") is None + + with pytest.raises(YunohostValidationError): + app_config_set(config_app, "bind.function.arg8", "pZo6i7u91h") + + assert not os.path.exists("/var/www/config_app/password") + assert app_setting(config_app, "arg8") is None + + +def test_app_config_custom_set(config_app): + + assert not os.path.exists("/var/www/config_app/password") + assert app_setting(config_app, "arg8") is None + + app_config_set(config_app, "bind.function.arg8", "OneSuperStrongPassword") + + assert os.path.exists("/var/www/config_app/password") + content = read_file("/var/www/config_app/password") + assert "OneSuperStrongPassword" not in content + assert content.startswith("$6$saltsalt$") + assert app_setting(config_app, "arg8") is None diff --git a/src/yunohost/tests/test_apps.py b/src/tests/test_apps.py similarity index 89% rename from src/yunohost/tests/test_apps.py rename to src/tests/test_apps.py index eba5a5916..2a808b5bd 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/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"] + test_apps = [ + "break_yo_system", + "legacy_app", + "legacy_app__2", + "full_domain_app", + "my_webapp", + ] for test_app in test_apps: @@ -105,7 +111,7 @@ def secondary_domain(request): def app_expected_files(domain, app): - yield "/etc/nginx/conf.d/%s.d/%s.conf" % (domain, app) + yield "/etc/nginx/conf.d/{}.d/{}.conf".format(domain, app) if app.startswith("legacy_app"): yield "/var/www/%s/index.html" % app yield "/etc/yunohost/apps/%s/settings.yml" % app @@ -132,7 +138,7 @@ def app_is_exposed_on_http(domain, path, message_in_page): try: r = requests.get( - "http://127.0.0.1" + path + "/", + "https://127.0.0.1" + path + "/", headers={"Host": domain}, timeout=10, verify=False, @@ -146,7 +152,7 @@ def install_legacy_app(domain, path, public=True): app_install( os.path.join(get_test_apps_dir(), "legacy_app_ynh"), - args="domain=%s&path=%s&is_public=%s" % (domain, path, 1 if public else 0), + args="domain={}&path={}&is_public={}".format(domain, path, 1 if public else 0), force=True, ) @@ -164,7 +170,7 @@ def install_break_yo_system(domain, breakwhat): app_install( os.path.join(get_test_apps_dir(), "break_yo_system_ynh"), - args="domain=%s&breakwhat=%s" % (domain, breakwhat), + args="domain={}&breakwhat={}".format(domain, breakwhat), force=True, ) @@ -189,6 +195,32 @@ 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=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] + 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") @@ -306,7 +338,7 @@ def test_legacy_app_failed_remove(mocker, secondary_domain): # The remove script runs with set -eu and attempt to remove this # file without -f, so will fail if it's not there ;) - os.remove("/etc/nginx/conf.d/%s.d/%s.conf" % (secondary_domain, "legacy_app")) + os.remove("/etc/nginx/conf.d/{}.d/{}.conf".format(secondary_domain, "legacy_app")) # TODO / FIXME : can't easily validate that 'app_not_properly_removed' # is triggered for weird reasons ... diff --git a/src/yunohost/tests/test_appurl.py b/src/tests/test_appurl.py similarity index 61% rename from src/yunohost/tests/test_appurl.py rename to src/tests/test_appurl.py index f15ed391f..c036ae28a 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/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, _normalize_domain_path +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,20 +33,56 @@ def teardown_function(function): pass -def test_normalize_domain_path(): +def test_parse_app_instance_name(): - assert _normalize_domain_path("https://yolo.swag/", "macnuggets") == ( - "yolo.swag", - "/macnuggets", + 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/") + 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 _normalize_domain_path("http://yolo.swag", "/macnuggets/") == ( - "yolo.swag", - "/macnuggets", + assert _is_app_repo_url( + "https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing/" ) - assert _normalize_domain_path("yolo.swag/", "macnuggets/") == ( - "yolo.swag", - "/macnuggets", + 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("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 _is_app_repo_url("https://git.super.host/~max/foobar_ynh") + + 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(): @@ -58,7 +99,7 @@ def test_registerurl(): app_install( os.path.join(get_test_apps_dir(), "register_url_app_ynh"), - args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"), + args="domain={}&path={}".format(maindomain, "/urlregisterapp"), force=True, ) @@ -68,7 +109,7 @@ def test_registerurl(): with pytest.raises(YunohostError): app_install( os.path.join(get_test_apps_dir(), "register_url_app_ynh"), - args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"), + args="domain={}&path={}".format(maindomain, "/urlregisterapp"), force=True, ) @@ -78,7 +119,7 @@ def test_registerurl_baddomain(): with pytest.raises(YunohostError): app_install( os.path.join(get_test_apps_dir(), "register_url_app_ynh"), - args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp"), + args="domain={}&path={}".format("yolo.swag", "/urlregisterapp"), force=True, ) @@ -193,7 +234,7 @@ def test_normalize_permission_path_with_unknown_domain(): def test_normalize_permission_path_conflicting_path(): app_install( os.path.join(get_test_apps_dir(), "register_url_app_ynh"), - args="domain=%s&path=%s" % (maindomain, "/url/registerapp"), + args="domain={}&path={}".format(maindomain, "/url/registerapp"), force=True, ) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/tests/test_backuprestore.py similarity index 92% rename from src/yunohost/tests/test_backuprestore.py rename to src/tests/test_backuprestore.py index 30204fa86..03c3aa0c7 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -2,6 +2,7 @@ import pytest import os import shutil import subprocess +from mock import patch from .conftest import message, raiseYunohostError, get_test_apps_dir @@ -47,8 +48,8 @@ def setup_function(function): for m in function.__dict__.get("pytestmark", []) } - if "with_wordpress_archive_from_3p8" in markers: - add_archive_wordpress_from_3p8() + if "with_wordpress_archive_from_4p2" in markers: + add_archive_wordpress_from_4p2() assert len(backup_list()["archives"]) == 1 if "with_legacy_app_installed" in markers: @@ -70,14 +71,15 @@ def setup_function(function): ) assert app_is_installed("backup_recommended_app") - if "with_system_archive_from_3p8" in markers: - add_archive_system_from_3p8() + if "with_system_archive_from_4p2" in markers: + add_archive_system_from_4p2() assert len(backup_list()["archives"]) == 1 if "with_permission_app_installed" in markers: assert not app_is_installed("permissions_app") user_create("alice", "Alice", "White", maindomain, "test123Ynh") - install_app("permissions_app_ynh", "/urlpermissionapp" "&admin=alice") + with patch.object(os, "isatty", return_value=False): + install_app("permissions_app_ynh", "/urlpermissionapp" "&admin=alice") assert app_is_installed("permissions_app") if "with_custom_domain" in markers: @@ -137,7 +139,7 @@ def app_is_installed(app): # These are files we know should be installed by the app app_files = [] - app_files.append("/etc/nginx/conf.d/%s.d/%s.conf" % (maindomain, app)) + app_files.append("/etc/nginx/conf.d/{}.d/{}.conf".format(maindomain, app)) app_files.append("/var/www/%s/index.html" % app) app_files.append("/etc/importantfile") @@ -148,7 +150,7 @@ def backup_test_dependencies_are_met(): # Dummy test apps (or backup archives) assert os.path.exists( - os.path.join(get_test_apps_dir(), "backup_wordpress_from_3p8") + os.path.join(get_test_apps_dir(), "backup_wordpress_from_4p2") ) assert os.path.exists(os.path.join(get_test_apps_dir(), "legacy_app_ynh")) assert os.path.exists( @@ -212,30 +214,30 @@ def install_app(app, path, additionnal_args=""): app_install( os.path.join(get_test_apps_dir(), app), - args="domain=%s&path=%s%s" % (maindomain, path, additionnal_args), + args="domain={}&path={}{}".format(maindomain, path, additionnal_args), force=True, ) -def add_archive_wordpress_from_3p8(): +def add_archive_wordpress_from_4p2(): os.system("mkdir -p /home/yunohost.backup/archives") os.system( "cp " - + os.path.join(get_test_apps_dir(), "backup_wordpress_from_3p8/backup.tar.gz") - + " /home/yunohost.backup/archives/backup_wordpress_from_3p8.tar.gz" + + os.path.join(get_test_apps_dir(), "backup_wordpress_from_4p2/backup.tar") + + " /home/yunohost.backup/archives/backup_wordpress_from_4p2.tar" ) -def add_archive_system_from_3p8(): +def add_archive_system_from_4p2(): os.system("mkdir -p /home/yunohost.backup/archives") os.system( "cp " - + os.path.join(get_test_apps_dir(), "backup_system_from_3p8/backup.tar.gz") - + " /home/yunohost.backup/archives/backup_system_from_3p8.tar.gz" + + os.path.join(get_test_apps_dir(), "backup_system_from_4p2/backup.tar") + + " /home/yunohost.backup/archives/backup_system_from_4p2.tar" ) @@ -305,8 +307,8 @@ def test_backup_and_restore_all_sys(mocker): # -@pytest.mark.with_system_archive_from_3p8 -def test_restore_system_from_Ynh3p8(monkeypatch, mocker): +@pytest.mark.with_system_archive_from_4p2 +def test_restore_system_from_Ynh4p2(monkeypatch, mocker): # Backup current system with message(mocker, "backup_created"): @@ -451,9 +453,9 @@ def test_backup_using_copy_method(mocker): # -@pytest.mark.with_wordpress_archive_from_3p8 +@pytest.mark.with_wordpress_archive_from_4p2 @pytest.mark.with_custom_domain("yolo.test") -def test_restore_app_wordpress_from_Ynh3p8(mocker): +def test_restore_app_wordpress_from_Ynh4p2(mocker): with message(mocker, "restore_complete"): backup_restore( @@ -461,7 +463,7 @@ def test_restore_app_wordpress_from_Ynh3p8(mocker): ) -@pytest.mark.with_wordpress_archive_from_3p8 +@pytest.mark.with_wordpress_archive_from_4p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_script_failure_handling(monkeypatch, mocker): def custom_hook_exec(name, *args, **kwargs): @@ -469,7 +471,7 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker): monkeypatch.undo() return (1, None) - monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec) + monkeypatch.setattr("yunohost.hook.hook_exec", custom_hook_exec) assert not _is_installed("wordpress") @@ -482,7 +484,7 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker): assert not _is_installed("wordpress") -@pytest.mark.with_wordpress_archive_from_3p8 +@pytest.mark.with_wordpress_archive_from_4p2 def test_restore_app_not_enough_free_space(monkeypatch, mocker): def custom_free_space_in_directory(dirpath): return 0 @@ -501,7 +503,7 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker): assert not _is_installed("wordpress") -@pytest.mark.with_wordpress_archive_from_3p8 +@pytest.mark.with_wordpress_archive_from_4p2 def test_restore_app_not_in_backup(mocker): assert not _is_installed("wordpress") @@ -517,7 +519,7 @@ def test_restore_app_not_in_backup(mocker): assert not _is_installed("yoloswag") -@pytest.mark.with_wordpress_archive_from_3p8 +@pytest.mark.with_wordpress_archive_from_4p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_already_installed(mocker): @@ -627,7 +629,7 @@ def test_restore_archive_with_no_json(mocker): # Create a backup with no info.json associated os.system("touch /tmp/afile") - os.system("tar -czvf /home/yunohost.backup/archives/badbackup.tar.gz /tmp/afile") + os.system("tar -cvf /home/yunohost.backup/archives/badbackup.tar /tmp/afile") assert "badbackup" in backup_list()["archives"] @@ -635,18 +637,18 @@ def test_restore_archive_with_no_json(mocker): backup_restore(name="badbackup", force=True) -@pytest.mark.with_wordpress_archive_from_3p8 +@pytest.mark.with_wordpress_archive_from_4p2 def test_restore_archive_with_bad_archive(mocker): # Break the archive os.system( - "head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_3p8.tar.gz > /home/yunohost.backup/archives/backup_wordpress_from_3p8_bad.tar.gz" + "head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_4p2.tar > /home/yunohost.backup/archives/backup_wordpress_from_4p2_bad.tar" ) - assert "backup_wordpress_from_3p8_bad" in backup_list()["archives"] + assert "backup_wordpress_from_4p2_bad" in backup_list()["archives"] with raiseYunohostError(mocker, "backup_archive_corrupted"): - backup_restore(name="backup_wordpress_from_3p8_bad", force=True) + backup_restore(name="backup_wordpress_from_4p2_bad", force=True) clean_tmp_backup_directory() diff --git a/src/yunohost/tests/test_changeurl.py b/src/tests/test_changeurl.py similarity index 96% rename from src/yunohost/tests/test_changeurl.py rename to src/tests/test_changeurl.py index e375bd9f0..04cb4a1a9 100644 --- a/src/yunohost/tests/test_changeurl.py +++ b/src/tests/test_changeurl.py @@ -26,7 +26,7 @@ def teardown_function(function): def install_changeurl_app(path): app_install( os.path.join(get_test_apps_dir(), "change_url_app_ynh"), - args="domain=%s&path=%s" % (maindomain, path), + args="domain={}&path={}".format(maindomain, path), force=True, ) diff --git a/src/tests/test_dns.py b/src/tests/test_dns.py new file mode 100644 index 000000000..a23ac7982 --- /dev/null +++ b/src/tests/test_dns.py @@ -0,0 +1,85 @@ +import pytest + +from moulinette.utils.filesystem import read_toml + +from yunohost.domain import domain_add, domain_remove +from yunohost.dns import ( + DOMAIN_REGISTRAR_LIST_PATH, + _get_dns_zone_for_domain, + _get_registrar_config_section, + _build_dns_conf, +) + + +def setup_function(function): + + clean() + + +def teardown_function(function): + + clean() + + +def clean(): + pass + + +# DNS utils testing +def test_get_dns_zone_from_domain_existing(): + assert _get_dns_zone_for_domain("yunohost.org") == "yunohost.org" + assert _get_dns_zone_for_domain("donate.yunohost.org") == "yunohost.org" + assert _get_dns_zone_for_domain("fr.wikipedia.org") == "wikipedia.org" + assert _get_dns_zone_for_domain("www.fr.wikipedia.org") == "wikipedia.org" + assert ( + _get_dns_zone_for_domain("non-existing-domain.yunohost.org") == "yunohost.org" + ) + 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.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" + + +# Domain registrar testing +def test_registrar_list_integrity(): + assert read_toml(DOMAIN_REGISTRAR_LIST_PATH) + + +def test_magic_guess_registrar_weird_domain(): + assert _get_registrar_config_section("yolo.tld")["registrar"]["value"] is None + + +def test_magic_guess_registrar_ovh(): + assert ( + _get_registrar_config_section("yolo.yunohost.org")["registrar"]["value"] + == "ovh" + ) + + +def test_magic_guess_registrar_yunodyndns(): + assert ( + _get_registrar_config_section("yolo.nohost.me")["registrar"]["value"] + == "yunohost" + ) + + +@pytest.fixture +def example_domain(): + domain_add("example.tld") + yield "example.tld" + domain_remove("example.tld") + + +def test_domain_dns_suggest(example_domain): + + assert _build_dns_conf(example_domain) + + +# def domain_dns_push(domain, dry_run): +# import yunohost.dns +# return yunohost.dns.domain_registrar_push(domain, dry_run) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py new file mode 100644 index 000000000..95a33e0ba --- /dev/null +++ b/src/tests/test_domains.py @@ -0,0 +1,118 @@ +import pytest +import os + +from moulinette.core import MoulinetteError + +from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.domain import ( + DOMAIN_SETTINGS_DIR, + _get_maindomain, + domain_add, + domain_remove, + domain_list, + domain_main_domain, + domain_config_get, + domain_config_set, +) + +TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"] + + +def setup_function(function): + + # Save domain list in variable to avoid multiple calls to domain_list() + domains = domain_list()["domains"] + + # First domain is main domain + if not TEST_DOMAINS[0] in domains: + domain_add(TEST_DOMAINS[0]) + else: + # Reset settings if any + os.system(f"rm -rf {DOMAIN_SETTINGS_DIR}/{TEST_DOMAINS[0]}.yml") + + if not _get_maindomain() == TEST_DOMAINS[0]: + domain_main_domain(TEST_DOMAINS[0]) + + # Clear other domains + for domain in domains: + if domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]: + # Clean domains not used for testing + domain_remove(domain) + elif domain in TEST_DOMAINS: + # Reset settings if any + os.system(f"rm -rf {DOMAIN_SETTINGS_DIR}/{domain}.yml") + + # Create classical second domain of not exist + if TEST_DOMAINS[1] not in domains: + domain_add(TEST_DOMAINS[1]) + + # Third domain is not created + + clean() + + +def teardown_function(function): + + clean() + + +def clean(): + pass + + +# Domains management testing +def test_domain_add(): + assert TEST_DOMAINS[2] not in domain_list()["domains"] + domain_add(TEST_DOMAINS[2]) + assert TEST_DOMAINS[2] in domain_list()["domains"] + + +def test_domain_add_existing_domain(): + with pytest.raises(MoulinetteError): + assert TEST_DOMAINS[1] in domain_list()["domains"] + domain_add(TEST_DOMAINS[1]) + + +def test_domain_remove(): + assert TEST_DOMAINS[1] in domain_list()["domains"] + domain_remove(TEST_DOMAINS[1]) + assert TEST_DOMAINS[1] not in domain_list()["domains"] + + +def test_main_domain(): + current_main_domain = _get_maindomain() + assert domain_main_domain()["current_main_domain"] == current_main_domain + + +def test_main_domain_change_unknown(): + with pytest.raises(YunohostValidationError): + domain_main_domain(TEST_DOMAINS[2]) + + +def test_change_main_domain(): + assert _get_maindomain() != TEST_DOMAINS[1] + domain_main_domain(TEST_DOMAINS[1]) + assert _get_maindomain() == TEST_DOMAINS[1] + + +# Domain settings testing +def test_domain_config_get_default(): + assert domain_config_get(TEST_DOMAINS[0], "feature.xmpp.xmpp") == 1 + assert domain_config_get(TEST_DOMAINS[1], "feature.xmpp.xmpp") == 0 + + +def test_domain_config_get_export(): + + assert domain_config_get(TEST_DOMAINS[0], export=True)["xmpp"] == 1 + assert domain_config_get(TEST_DOMAINS[1], export=True)["xmpp"] == 0 + + +def test_domain_config_set(): + assert domain_config_get(TEST_DOMAINS[1], "feature.xmpp.xmpp") == 0 + domain_config_set(TEST_DOMAINS[1], "feature.xmpp.xmpp", "yes") + assert domain_config_get(TEST_DOMAINS[1], "feature.xmpp.xmpp") == 1 + + +def test_domain_configs_unknown(): + with pytest.raises(YunohostError): + domain_config_get(TEST_DOMAINS[2], "feature.xmpp.xmpp.xmpp") diff --git a/src/yunohost/tests/test_ldapauth.py b/src/tests/test_ldapauth.py similarity index 100% rename from src/yunohost/tests/test_ldapauth.py rename to src/tests/test_ldapauth.py diff --git a/src/yunohost/tests/test_permission.py b/src/tests/test_permission.py similarity index 97% rename from src/yunohost/tests/test_permission.py rename to src/tests/test_permission.py index b33c2f213..4e7f9f53d 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/tests/test_permission.py @@ -236,17 +236,17 @@ def check_LDAP_db_integrity(): ldap = _get_ldap_interface() user_search = ldap.search( - "ou=users,dc=yunohost,dc=org", + "ou=users", "(&(objectclass=person)(!(uid=root))(!(uid=nobody)))", ["uid", "memberOf", "permission"], ) group_search = ldap.search( - "ou=groups,dc=yunohost,dc=org", + "ou=groups", "(objectclass=groupOfNamesYnh)", ["cn", "member", "memberUid", "permission"], ) permission_search = ldap.search( - "ou=permission,dc=yunohost,dc=org", + "ou=permission", "(objectclass=permissionYnh)", ["cn", "groupPermission", "inheritPermission", "memberUid"], ) @@ -347,7 +347,7 @@ def check_permission_for_apps(): # {"bar", "foo"} # and compare this to the list of installed apps ... - app_perms_prefix = set(p.split(".")[0] for p in app_perms) + app_perms_prefix = {p.split(".")[0] for p in app_perms} assert set(_installed_apps()) == app_perms_prefix @@ -398,7 +398,7 @@ def test_permission_list(): assert res["wiki.main"]["allowed"] == ["all_users"] assert res["blog.main"]["allowed"] == ["alice"] assert res["blog.api"]["allowed"] == ["visitors"] - assert set(res["wiki.main"]["corresponding_users"]) == set(["alice", "bob"]) + assert set(res["wiki.main"]["corresponding_users"]) == {"alice", "bob"} assert res["blog.main"]["corresponding_users"] == ["alice"] assert res["blog.api"]["corresponding_users"] == [] assert res["wiki.main"]["url"] == "/" @@ -442,7 +442,7 @@ def test_permission_create_main(mocker): res = user_permission_list(full=True)["permissions"] assert "site.main" in res assert res["site.main"]["allowed"] == ["all_users"] - assert set(res["site.main"]["corresponding_users"]) == set(["alice", "bob"]) + assert set(res["site.main"]["corresponding_users"]) == {"alice", "bob"} assert res["site.main"]["protected"] is False @@ -630,8 +630,8 @@ def test_permission_add_group(mocker): user_permission_update("wiki.main", add="alice") res = user_permission_list(full=True)["permissions"] - assert set(res["wiki.main"]["allowed"]) == set(["all_users", "alice"]) - assert set(res["wiki.main"]["corresponding_users"]) == set(["alice", "bob"]) + assert set(res["wiki.main"]["allowed"]) == {"all_users", "alice"} + assert set(res["wiki.main"]["corresponding_users"]) == {"alice", "bob"} def test_permission_remove_group(mocker): @@ -680,7 +680,7 @@ def test_permission_reset(mocker): res = user_permission_list(full=True)["permissions"] assert res["blog.main"]["allowed"] == ["all_users"] - assert set(res["blog.main"]["corresponding_users"]) == set(["alice", "bob"]) + assert set(res["blog.main"]["corresponding_users"]) == {"alice", "bob"} def test_permission_reset_idempotency(): @@ -690,7 +690,7 @@ def test_permission_reset_idempotency(): res = user_permission_list(full=True)["permissions"] assert res["blog.main"]["allowed"] == ["all_users"] - assert set(res["blog.main"]["corresponding_users"]) == set(["alice", "bob"]) + assert set(res["blog.main"]["corresponding_users"]) == {"alice", "bob"} def test_permission_change_label(mocker): @@ -1013,9 +1013,7 @@ def test_permission_app_install(): assert res["permissions_app.dev"]["url"] == "/dev" assert res["permissions_app.main"]["allowed"] == ["all_users"] - assert set(res["permissions_app.main"]["corresponding_users"]) == set( - ["alice", "bob"] - ) + assert set(res["permissions_app.main"]["corresponding_users"]) == {"alice", "bob"} assert res["permissions_app.admin"]["allowed"] == ["alice"] assert res["permissions_app.admin"]["corresponding_users"] == ["alice"] @@ -1049,7 +1047,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 +1070,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 +1133,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, ) diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py new file mode 100644 index 000000000..5917d32d4 --- /dev/null +++ b/src/tests/test_questions.py @@ -0,0 +1,2189 @@ +import sys +import pytest +import os + +from mock import patch +from io import StringIO + +from moulinette import Moulinette + +from yunohost import domain, user +from yunohost.utils.config import ( + ask_questions_and_parse_answers, + PasswordQuestion, + DomainQuestion, + PathQuestion, + BooleanQuestion, + FileQuestion, + evaluate_simple_js_expression, +) +from yunohost.utils.error import YunohostError, YunohostValidationError + + +""" +Argument default format: +{ + "name": "the_name", + "type": "one_of_the_available_type", // "sting" is not specified + "ask": { + "en": "the question in english", + "fr": "the question in french" + }, + "help": { + "en": "some help text in english", + "fr": "some help text in french" + }, + "example": "an example value", // optional + "default", "some stuff", // optional, not available for all types + "optional": true // optional, will skip if not answered +} + +User answers: +{"name": "value", ...} +""" + + +def test_question_empty(): + ask_questions_and_parse_answers([], {}) == [] + + +def test_question_string(): + questions = [ + { + "name": "some_string", + "type": "string", + } + ] + answers = {"some_string": "some_value"} + + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "some_value" + + +def test_question_string_from_query_string(): + + questions = [ + { + "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 = [ + { + "name": "some_string", + } + ] + answers = {"some_string": "some_value"} + + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "some_value" + + +def test_question_string_no_input(): + questions = [ + { + "name": "some_string", + } + ] + answers = {} + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_string_input(): + questions = [ + { + "name": "some_string", + "ask": "some question", + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "some_value" + + +def test_question_string_input_no_ask(): + questions = [ + { + "name": "some_string", + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "some_value" + + +def test_question_string_no_input_optional(): + questions = [ + { + "name": "some_string", + "optional": True, + } + ] + answers = {} + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "" + + +def test_question_string_optional_with_input(): + questions = [ + { + "name": "some_string", + "ask": "some question", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "some_value" + + +def test_question_string_optional_with_empty_input(): + questions = [ + { + "name": "some_string", + "ask": "some question", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value=""), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "" + + +def test_question_string_optional_with_input_without_ask(): + questions = [ + { + "name": "some_string", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "some_value" + + +def test_question_string_no_input_default(): + questions = [ + { + "name": "some_string", + "ask": "some question", + "default": "some_value", + } + ] + answers = {} + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "some_value" + + +def test_question_string_input_test_ask(): + ask_text = "some question" + questions = [ + { + "name": "some_string", + "ask": ask_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + prompt.assert_called_with( + message=ask_text, + is_password=False, + confirm=False, + prefill="", + is_multiline=False, + autocomplete=[], + help=None, + ) + + +def test_question_string_input_test_ask_with_default(): + ask_text = "some question" + default_text = "some example" + questions = [ + { + "name": "some_string", + "ask": ask_text, + "default": default_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + prompt.assert_called_with( + message=ask_text, + is_password=False, + confirm=False, + prefill=default_text, + is_multiline=False, + autocomplete=[], + help=None, + ) + + +@pytest.mark.skip # we should do something with this example +def test_question_string_input_test_ask_with_example(): + ask_text = "some question" + example_text = "some example" + questions = [ + { + "name": "some_string", + "ask": ask_text, + "example": example_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + assert ask_text in prompt.call_args[1]["message"] + assert example_text in prompt.call_args[1]["message"] + + +@pytest.mark.skip # we should do something with this help +def test_question_string_input_test_ask_with_help(): + ask_text = "some question" + help_text = "some_help" + questions = [ + { + "name": "some_string", + "ask": ask_text, + "help": help_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + assert ask_text in prompt.call_args[1]["message"] + assert help_text in prompt.call_args[1]["message"] + + +def test_question_string_with_choice(): + questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] + answers = {"some_string": "fr"} + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "fr" + + +def test_question_string_with_choice_prompt(): + questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] + answers = {"some_string": "fr"} + with patch.object(Moulinette, "prompt", return_value="fr"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "fr" + + +def test_question_string_with_choice_bad(): + questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] + answers = {"some_string": "bad"} + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_string_with_choice_ask(): + ask_text = "some question" + choices = ["fr", "en", "es", "it", "ru"] + questions = [ + { + "name": "some_string", + "ask": ask_text, + "choices": choices, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="ru") as prompt, patch.object( + os, "isatty", return_value=True + ): + ask_questions_and_parse_answers(questions, answers) + assert ask_text in prompt.call_args[1]["message"] + + for choice in choices: + assert choice in prompt.call_args[1]["message"] + + +def test_question_string_with_choice_default(): + questions = [ + { + "name": "some_string", + "type": "string", + "choices": ["fr", "en"], + "default": "en", + } + ] + answers = {} + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_string" + assert out.type == "string" + assert out.value == "en" + + +def test_question_password(): + questions = [ + { + "name": "some_password", + "type": "password", + } + ] + answers = {"some_password": "some_value"} + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_password" + assert out.type == "password" + assert out.value == "some_value" + + +def test_question_password_no_input(): + questions = [ + { + "name": "some_password", + "type": "password", + } + ] + answers = {} + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_password_input(): + questions = [ + { + "name": "some_password", + "type": "password", + "ask": "some question", + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_password" + assert out.type == "password" + assert out.value == "some_value" + + +def test_question_password_input_no_ask(): + questions = [ + { + "name": "some_password", + "type": "password", + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_password" + assert out.type == "password" + assert out.value == "some_value" + + +def test_question_password_no_input_optional(): + questions = [ + { + "name": "some_password", + "type": "password", + "optional": True, + } + ] + answers = {} + + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_password" + assert out.type == "password" + assert out.value == "" + + questions = [ + {"name": "some_password", "type": "password", "optional": True, "default": ""} + ] + + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_password" + assert out.type == "password" + assert out.value == "" + + +def test_question_password_optional_with_input(): + questions = [ + { + "name": "some_password", + "ask": "some question", + "type": "password", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_password" + assert out.type == "password" + assert out.value == "some_value" + + +def test_question_password_optional_with_empty_input(): + questions = [ + { + "name": "some_password", + "ask": "some question", + "type": "password", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value=""), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_password" + assert out.type == "password" + assert out.value == "" + + +def test_question_password_optional_with_input_without_ask(): + questions = [ + { + "name": "some_password", + "type": "password", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_password" + assert out.type == "password" + assert out.value == "some_value" + + +def test_question_password_no_input_default(): + questions = [ + { + "name": "some_password", + "type": "password", + "ask": "some question", + "default": "some_value", + } + ] + answers = {} + + # no default for password! + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + +@pytest.mark.skip # this should raises +def test_question_password_no_input_example(): + questions = [ + { + "name": "some_password", + "type": "password", + "ask": "some question", + "example": "some_value", + } + ] + answers = {"some_password": "some_value"} + + # no example for password! + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_password_input_test_ask(): + ask_text = "some question" + questions = [ + { + "name": "some_password", + "type": "password", + "ask": ask_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + prompt.assert_called_with( + message=ask_text, + is_password=True, + confirm=False, + prefill="", + is_multiline=False, + autocomplete=[], + help=None, + ) + + +@pytest.mark.skip # we should do something with this example +def test_question_password_input_test_ask_with_example(): + ask_text = "some question" + example_text = "some example" + questions = [ + { + "name": "some_password", + "type": "password", + "ask": ask_text, + "example": example_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + assert ask_text in prompt.call_args[1]["message"] + assert example_text in prompt.call_args[1]["message"] + + +@pytest.mark.skip # we should do something with this help +def test_question_password_input_test_ask_with_help(): + ask_text = "some question" + help_text = "some_help" + questions = [ + { + "name": "some_password", + "type": "password", + "ask": ask_text, + "help": help_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + assert ask_text in prompt.call_args[1]["message"] + assert help_text in prompt.call_args[1]["message"] + + +def test_question_password_bad_chars(): + questions = [ + { + "name": "some_password", + "type": "password", + "ask": "some question", + "example": "some_value", + } + ] + + for i in PasswordQuestion.forbidden_chars: + with pytest.raises(YunohostError), patch.object( + os, "isatty", return_value=False + ): + ask_questions_and_parse_answers(questions, {"some_password": i * 8}) + + +def test_question_password_strong_enough(): + questions = [ + { + "name": "some_password", + "type": "password", + "ask": "some question", + "example": "some_value", + } + ] + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + # too short + ask_questions_and_parse_answers(questions, {"some_password": "a"}) + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, {"some_password": "password"}) + + +def test_question_password_optional_strong_enough(): + questions = [ + { + "name": "some_password", + "ask": "some question", + "type": "password", + "optional": True, + } + ] + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + # too short + ask_questions_and_parse_answers(questions, {"some_password": "a"}) + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, {"some_password": "password"}) + + +def test_question_path(): + questions = [ + { + "name": "some_path", + "type": "path", + } + ] + answers = {"some_path": "/some_value"} + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_path" + assert out.type == "path" + assert out.value == "/some_value" + + +def test_question_path_no_input(): + questions = [ + { + "name": "some_path", + "type": "path", + } + ] + answers = {} + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_path_input(): + questions = [ + { + "name": "some_path", + "type": "path", + "ask": "some question", + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_path" + assert out.type == "path" + assert out.value == "/some_value" + + +def test_question_path_input_no_ask(): + questions = [ + { + "name": "some_path", + "type": "path", + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_path" + assert out.type == "path" + assert out.value == "/some_value" + + +def test_question_path_no_input_optional(): + questions = [ + { + "name": "some_path", + "type": "path", + "optional": True, + } + ] + answers = {} + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_path" + assert out.type == "path" + assert out.value == "" + + +def test_question_path_optional_with_input(): + questions = [ + { + "name": "some_path", + "ask": "some question", + "type": "path", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_path" + assert out.type == "path" + assert out.value == "/some_value" + + +def test_question_path_optional_with_empty_input(): + questions = [ + { + "name": "some_path", + "ask": "some question", + "type": "path", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value=""), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_path" + assert out.type == "path" + assert out.value == "" + + +def test_question_path_optional_with_input_without_ask(): + questions = [ + { + "name": "some_path", + "type": "path", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_path" + assert out.type == "path" + assert out.value == "/some_value" + + +def test_question_path_no_input_default(): + questions = [ + { + "name": "some_path", + "ask": "some question", + "type": "path", + "default": "some_value", + } + ] + answers = {} + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_path" + assert out.type == "path" + assert out.value == "/some_value" + + +def test_question_path_input_test_ask(): + ask_text = "some question" + questions = [ + { + "name": "some_path", + "type": "path", + "ask": ask_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + prompt.assert_called_with( + message=ask_text, + is_password=False, + confirm=False, + prefill="", + is_multiline=False, + autocomplete=[], + help=None, + ) + + +def test_question_path_input_test_ask_with_default(): + ask_text = "some question" + default_text = "someexample" + questions = [ + { + "name": "some_path", + "type": "path", + "ask": ask_text, + "default": default_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + prompt.assert_called_with( + message=ask_text, + is_password=False, + confirm=False, + prefill=default_text, + is_multiline=False, + autocomplete=[], + help=None, + ) + + +@pytest.mark.skip # we should do something with this example +def test_question_path_input_test_ask_with_example(): + ask_text = "some question" + example_text = "some example" + questions = [ + { + "name": "some_path", + "type": "path", + "ask": ask_text, + "example": example_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + assert ask_text in prompt.call_args[1]["message"] + assert example_text in prompt.call_args[1]["message"] + + +@pytest.mark.skip # we should do something with this help +def test_question_path_input_test_ask_with_help(): + ask_text = "some question" + help_text = "some_help" + questions = [ + { + "name": "some_path", + "type": "path", + "ask": ask_text, + "help": help_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="some_value" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + assert ask_text in prompt.call_args[1]["message"] + assert help_text in prompt.call_args[1]["message"] + + +def test_question_boolean(): + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] + answers = {"some_boolean": "y"} + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_boolean" + assert out.type == "boolean" + assert out.value == 1 + + +def test_question_boolean_all_yes(): + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] + + for value in ["Y", "yes", "Yes", "YES", "1", 1, True, "True", "TRUE", "true"]: + out = ask_questions_and_parse_answers(questions, {"some_boolean": value})[0] + assert out.name == "some_boolean" + assert out.type == "boolean" + assert out.value == 1 + + +def test_question_boolean_all_no(): + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] + + for value in ["n", "N", "no", "No", "No", "0", 0, False, "False", "FALSE", "false"]: + out = ask_questions_and_parse_answers(questions, {"some_boolean": value})[0] + assert out.name == "some_boolean" + assert out.type == "boolean" + assert out.value == 0 + + +# XXX apparently boolean are always False (0) by default, I'm not sure what to think about that +def test_question_boolean_no_input(): + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] + answers = {} + + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.value == 0 + + +def test_question_boolean_bad_input(): + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] + answers = {"some_boolean": "stuff"} + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_boolean_input(): + questions = [ + { + "name": "some_boolean", + "type": "boolean", + "ask": "some question", + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="y"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + assert out.value == 1 + + with patch.object(Moulinette, "prompt", return_value="n"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + assert out.value == 0 + + +def test_question_boolean_input_no_ask(): + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="y"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + assert out.value == 1 + + +def test_question_boolean_no_input_optional(): + questions = [ + { + "name": "some_boolean", + "type": "boolean", + "optional": True, + } + ] + answers = {} + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + assert out.value == 0 + + +def test_question_boolean_optional_with_input(): + questions = [ + { + "name": "some_boolean", + "ask": "some question", + "type": "boolean", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="y"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + assert out.value == 1 + + +def test_question_boolean_optional_with_empty_input(): + questions = [ + { + "name": "some_boolean", + "ask": "some question", + "type": "boolean", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value=""), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.value == 0 + + +def test_question_boolean_optional_with_input_without_ask(): + questions = [ + { + "name": "some_boolean", + "type": "boolean", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="n"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.value == 0 + + +def test_question_boolean_no_input_default(): + questions = [ + { + "name": "some_boolean", + "ask": "some question", + "type": "boolean", + "default": 0, + } + ] + answers = {} + + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.value == 0 + + +def test_question_boolean_bad_default(): + questions = [ + { + "name": "some_boolean", + "ask": "some question", + "type": "boolean", + "default": "bad default", + } + ] + answers = {} + with pytest.raises(YunohostError): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_boolean_input_test_ask(): + ask_text = "some question" + questions = [ + { + "name": "some_boolean", + "type": "boolean", + "ask": ask_text, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value=0) as prompt, patch.object( + os, "isatty", return_value=True + ): + ask_questions_and_parse_answers(questions, answers) + prompt.assert_called_with( + message=ask_text + " [yes | no]", + is_password=False, + confirm=False, + prefill="no", + is_multiline=False, + autocomplete=[], + help=None, + ) + + +def test_question_boolean_input_test_ask_with_default(): + ask_text = "some question" + default_text = 1 + questions = [ + { + "name": "some_boolean", + "type": "boolean", + "ask": ask_text, + "default": default_text, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value=1) as prompt, patch.object( + os, "isatty", return_value=True + ): + ask_questions_and_parse_answers(questions, answers) + prompt.assert_called_with( + message=ask_text + " [yes | no]", + is_password=False, + confirm=False, + prefill="yes", + is_multiline=False, + autocomplete=[], + help=None, + ) + + +def test_question_domain_empty(): + questions = [ + { + "name": "some_domain", + "type": "domain", + } + ] + main_domain = "my_main_domain.com" + answers = {} + + with patch.object( + domain, "_get_maindomain", return_value="my_main_domain.com" + ), patch.object( + domain, "domain_list", return_value={"domains": [main_domain]} + ), patch.object( + os, "isatty", return_value=False + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_domain" + assert out.type == "domain" + assert out.value == main_domain + + +def test_question_domain(): + main_domain = "my_main_domain.com" + domains = [main_domain] + questions = [ + { + "name": "some_domain", + "type": "domain", + } + ] + + answers = {"some_domain": main_domain} + + with patch.object( + domain, "_get_maindomain", return_value=main_domain + ), patch.object(domain, "domain_list", return_value={"domains": domains}): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_domain" + assert out.type == "domain" + assert out.value == main_domain + + +def test_question_domain_two_domains(): + main_domain = "my_main_domain.com" + other_domain = "some_other_domain.tld" + domains = [main_domain, other_domain] + + questions = [ + { + "name": "some_domain", + "type": "domain", + } + ] + answers = {"some_domain": other_domain} + + with patch.object( + domain, "_get_maindomain", return_value=main_domain + ), patch.object(domain, "domain_list", return_value={"domains": domains}): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_domain" + assert out.type == "domain" + assert out.value == other_domain + + answers = {"some_domain": main_domain} + + with patch.object( + domain, "_get_maindomain", return_value=main_domain + ), patch.object(domain, "domain_list", return_value={"domains": domains}): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_domain" + assert out.type == "domain" + assert out.value == main_domain + + +def test_question_domain_two_domains_wrong_answer(): + main_domain = "my_main_domain.com" + other_domain = "some_other_domain.tld" + domains = [main_domain, other_domain] + + questions = [ + { + "name": "some_domain", + "type": "domain", + } + ] + answers = {"some_domain": "doesnt_exist.pouet"} + + with patch.object( + domain, "_get_maindomain", return_value=main_domain + ), patch.object(domain, "domain_list", return_value={"domains": domains}): + with pytest.raises(YunohostError), patch.object( + os, "isatty", return_value=False + ): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_domain_two_domains_default_no_ask(): + main_domain = "my_main_domain.com" + other_domain = "some_other_domain.tld" + domains = [main_domain, other_domain] + + questions = [ + { + "name": "some_domain", + "type": "domain", + } + ] + answers = {} + + with patch.object( + domain, "_get_maindomain", return_value=main_domain + ), patch.object( + domain, "domain_list", return_value={"domains": domains} + ), patch.object( + os, "isatty", return_value=False + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_domain" + assert out.type == "domain" + assert out.value == main_domain + + +def test_question_domain_two_domains_default(): + main_domain = "my_main_domain.com" + other_domain = "some_other_domain.tld" + domains = [main_domain, other_domain] + + questions = [{"name": "some_domain", "type": "domain", "ask": "choose a domain"}] + answers = {} + + with patch.object( + domain, "_get_maindomain", return_value=main_domain + ), patch.object( + domain, "domain_list", return_value={"domains": domains} + ), patch.object( + os, "isatty", return_value=False + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_domain" + assert out.type == "domain" + assert out.value == main_domain + + +def test_question_domain_two_domains_default_input(): + main_domain = "my_main_domain.com" + other_domain = "some_other_domain.tld" + domains = [main_domain, other_domain] + + questions = [{"name": "some_domain", "type": "domain", "ask": "choose a domain"}] + answers = {} + + with patch.object( + domain, "_get_maindomain", return_value=main_domain + ), patch.object( + domain, "domain_list", return_value={"domains": domains} + ), patch.object( + os, "isatty", return_value=True + ): + with patch.object(Moulinette, "prompt", return_value=main_domain): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_domain" + assert out.type == "domain" + assert out.value == main_domain + + with patch.object(Moulinette, "prompt", return_value=other_domain): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_domain" + assert out.type == "domain" + assert out.value == other_domain + + +def test_question_user_empty(): + users = { + "some_user": { + "ssh_allowed": False, + "username": "some_user", + "mailbox-quota": "0", + "mail": "p@ynh.local", + "fullname": "the first name the last name", + } + } + + questions = [ + { + "name": "some_user", + "type": "user", + } + ] + answers = {} + + with patch.object(user, "user_list", return_value={"users": users}): + with pytest.raises(YunohostError), patch.object( + os, "isatty", return_value=False + ): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_user(): + username = "some_user" + users = { + username: { + "ssh_allowed": False, + "username": "some_user", + "mailbox-quota": "0", + "mail": "p@ynh.local", + "fullname": "the first name the last name", + } + } + + questions = [ + { + "name": "some_user", + "type": "user", + } + ] + answers = {"some_user": username} + + with patch.object(user, "user_list", return_value={"users": users}), patch.object( + user, "user_info", return_value={} + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_user" + assert out.type == "user" + assert out.value == username + + +def test_question_user_two_users(): + username = "some_user" + other_user = "some_other_user" + users = { + username: { + "ssh_allowed": False, + "username": "some_user", + "mailbox-quota": "0", + "mail": "p@ynh.local", + "fullname": "the first name the last name", + }, + other_user: { + "ssh_allowed": False, + "username": "some_user", + "mailbox-quota": "0", + "mail": "z@ynh.local", + "fullname": "john doe", + }, + } + + questions = [ + { + "name": "some_user", + "type": "user", + } + ] + answers = {"some_user": other_user} + + with patch.object(user, "user_list", return_value={"users": users}), patch.object( + user, "user_info", return_value={} + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_user" + assert out.type == "user" + assert out.value == other_user + + answers = {"some_user": username} + + with patch.object(user, "user_list", return_value={"users": users}), patch.object( + user, "user_info", return_value={} + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_user" + assert out.type == "user" + assert out.value == username + + +def test_question_user_two_users_wrong_answer(): + username = "my_username.com" + other_user = "some_other_user" + users = { + username: { + "ssh_allowed": False, + "username": "some_user", + "mailbox-quota": "0", + "mail": "p@ynh.local", + "fullname": "the first name the last name", + }, + other_user: { + "ssh_allowed": False, + "username": "some_user", + "mailbox-quota": "0", + "mail": "z@ynh.local", + "fullname": "john doe", + }, + } + + questions = [ + { + "name": "some_user", + "type": "user", + } + ] + answers = {"some_user": "doesnt_exist.pouet"} + + with patch.object(user, "user_list", return_value={"users": users}): + with pytest.raises(YunohostError), patch.object( + os, "isatty", return_value=False + ): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_user_two_users_no_default(): + username = "my_username.com" + other_user = "some_other_user.tld" + users = { + username: { + "ssh_allowed": False, + "username": "some_user", + "mailbox-quota": "0", + "mail": "p@ynh.local", + "fullname": "the first name the last name", + }, + other_user: { + "ssh_allowed": False, + "username": "some_user", + "mailbox-quota": "0", + "mail": "z@ynh.local", + "fullname": "john doe", + }, + } + + questions = [{"name": "some_user", "type": "user", "ask": "choose a user"}] + answers = {} + + with patch.object(user, "user_list", return_value={"users": users}): + with pytest.raises(YunohostError), patch.object( + os, "isatty", return_value=False + ): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_user_two_users_default_input(): + username = "my_username.com" + other_user = "some_other_user.tld" + users = { + username: { + "ssh_allowed": False, + "username": "some_user", + "mailbox-quota": "0", + "mail": "p@ynh.local", + "fullname": "the first name the last name", + }, + other_user: { + "ssh_allowed": False, + "username": "some_user", + "mailbox-quota": "0", + "mail": "z@ynh.local", + "fullname": "john doe", + }, + } + + questions = [{"name": "some_user", "type": "user", "ask": "choose a user"}] + answers = {} + + with patch.object(user, "user_list", return_value={"users": users}), patch.object( + os, "isatty", return_value=True + ): + with patch.object(user, "user_info", return_value={}): + + with patch.object(Moulinette, "prompt", return_value=username): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_user" + assert out.type == "user" + assert out.value == username + + with patch.object(Moulinette, "prompt", return_value=other_user): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_user" + assert out.type == "user" + assert out.value == other_user + + +def test_question_number(): + questions = [ + { + "name": "some_number", + "type": "number", + } + ] + answers = {"some_number": 1337} + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value == 1337 + + +def test_question_number_no_input(): + questions = [ + { + "name": "some_number", + "type": "number", + } + ] + answers = {} + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_number_bad_input(): + questions = [ + { + "name": "some_number", + "type": "number", + } + ] + answers = {"some_number": "stuff"} + + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + answers = {"some_number": 1.5} + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_number_input(): + questions = [ + { + "name": "some_number", + "type": "number", + "ask": "some question", + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value == 1337 + + with patch.object(Moulinette, "prompt", return_value=1337), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value == 1337 + + with patch.object(Moulinette, "prompt", return_value="0"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value == 0 + + +def test_question_number_input_no_ask(): + questions = [ + { + "name": "some_number", + "type": "number", + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value == 1337 + + +def test_question_number_no_input_optional(): + questions = [ + { + "name": "some_number", + "type": "number", + "optional": True, + } + ] + answers = {} + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value is None + + +def test_question_number_optional_with_input(): + questions = [ + { + "name": "some_number", + "ask": "some question", + "type": "number", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="1337"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value == 1337 + + +def test_question_number_optional_with_input_without_ask(): + questions = [ + { + "name": "some_number", + "type": "number", + "optional": True, + } + ] + answers = {} + + with patch.object(Moulinette, "prompt", return_value="0"), patch.object( + os, "isatty", return_value=True + ): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value == 0 + + +def test_question_number_no_input_default(): + questions = [ + { + "name": "some_number", + "ask": "some question", + "type": "number", + "default": 1337, + } + ] + answers = {} + with patch.object(os, "isatty", return_value=False): + out = ask_questions_and_parse_answers(questions, answers)[0] + + assert out.name == "some_number" + assert out.type == "number" + assert out.value == 1337 + + +def test_question_number_bad_default(): + questions = [ + { + "name": "some_number", + "ask": "some question", + "type": "number", + "default": "bad default", + } + ] + answers = {} + with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): + ask_questions_and_parse_answers(questions, answers) + + +def test_question_number_input_test_ask(): + ask_text = "some question" + questions = [ + { + "name": "some_number", + "type": "number", + "ask": ask_text, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="1111" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + prompt.assert_called_with( + message=ask_text, + is_password=False, + confirm=False, + prefill="", + is_multiline=False, + autocomplete=[], + help=None, + ) + + +def test_question_number_input_test_ask_with_default(): + ask_text = "some question" + default_value = 1337 + questions = [ + { + "name": "some_number", + "type": "number", + "ask": ask_text, + "default": default_value, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="1111" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + prompt.assert_called_with( + message=ask_text, + is_password=False, + confirm=False, + prefill=str(default_value), + is_multiline=False, + autocomplete=[], + help=None, + ) + + +@pytest.mark.skip # we should do something with this example +def test_question_number_input_test_ask_with_example(): + ask_text = "some question" + example_value = 1337 + questions = [ + { + "name": "some_number", + "type": "number", + "ask": ask_text, + "example": example_value, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="1111" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + assert ask_text in prompt.call_args[1]["message"] + assert example_value in prompt.call_args[1]["message"] + + +@pytest.mark.skip # we should do something with this help +def test_question_number_input_test_ask_with_help(): + ask_text = "some question" + help_value = 1337 + questions = [ + { + "name": "some_number", + "type": "number", + "ask": ask_text, + "help": help_value, + } + ] + answers = {} + + with patch.object( + Moulinette, "prompt", return_value="1111" + ) as prompt, patch.object(os, "isatty", return_value=True): + ask_questions_and_parse_answers(questions, answers) + assert ask_text in prompt.call_args[1]["message"] + assert help_value in prompt.call_args[1]["message"] + + +def test_question_display_text(): + questions = [{"name": "some_app", "type": "display_text", "ask": "foobar"}] + answers = {} + + with patch.object(sys, "stdout", new_callable=StringIO) as stdout, patch.object( + os, "isatty", return_value=True + ): + ask_questions_and_parse_answers(questions, answers) + assert "foobar" in stdout.getvalue() + + +def test_question_file_from_cli(): + + FileQuestion.clean_upload_dirs() + + 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(b"helloworld") + 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 + 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.normalize("https://yolo.swag/") == "yolo.swag" + assert DomainQuestion.normalize("http://yolo.swag") == "yolo.swag" + assert DomainQuestion.normalize("yolo.swag/") == "yolo.swag" + + +def test_normalize_path(): + + assert PathQuestion.normalize("") == "/" + assert PathQuestion.normalize("") == "/" + assert PathQuestion.normalize("macnuggets") == "/macnuggets" + 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" + + +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/tests/test_regenconf.py b/src/tests/test_regenconf.py similarity index 100% rename from src/yunohost/tests/test_regenconf.py rename to src/tests/test_regenconf.py diff --git a/src/yunohost/tests/test_service.py b/src/tests/test_service.py similarity index 80% rename from src/yunohost/tests/test_service.py rename to src/tests/test_service.py index 1f82dc8fd..88013a3fe 100644 --- a/src/yunohost/tests/test_service.py +++ b/src/tests/test_service.py @@ -9,6 +9,7 @@ from yunohost.service import ( service_add, service_remove, service_log, + service_reload_or_restart, ) @@ -38,6 +39,10 @@ def clean(): _save_services(services) + if os.path.exists("/etc/nginx/conf.d/broken.conf"): + os.remove("/etc/nginx/conf.d/broken.conf") + os.system("systemctl reload-or-restart nginx") + def test_service_status_all(): @@ -118,3 +123,20 @@ def test_service_update_to_remove_properties(): assert _get_services()["dummyservice"].get("test_status") == "false" service_add("dummyservice", description="dummy", test_status="") assert not _get_services()["dummyservice"].get("test_status") + + +def test_service_conf_broken(): + + os.system("echo pwet > /etc/nginx/conf.d/broken.conf") + + status = service_status("nginx") + assert status["status"] == "running" + assert status["configuration"] == "broken" + assert "broken.conf" in status["configuration-details"][0] + + # Service reload-or-restart should check that the conf ain't valid + # before reload-or-restart, hence the service should still be running + service_reload_or_restart("nginx") + assert status["status"] == "running" + + os.remove("/etc/nginx/conf.d/broken.conf") diff --git a/src/yunohost/tests/test_settings.py b/src/tests/test_settings.py similarity index 100% rename from src/yunohost/tests/test_settings.py rename to src/tests/test_settings.py diff --git a/src/yunohost/tests/test_user-group.py b/src/tests/test_user-group.py similarity index 98% rename from src/yunohost/tests/test_user-group.py rename to src/tests/test_user-group.py index 60e748108..e561118e0 100644 --- a/src/yunohost/tests/test_user-group.py +++ b/src/tests/test_user-group.py @@ -221,7 +221,7 @@ def test_create_user_already_exists(mocker): def test_create_user_with_domain_that_doesnt_exists(mocker): - with raiseYunohostError(mocker, "domain_name_unknown"): + with raiseYunohostError(mocker, "domain_unknown"): user_create("alice", "Alice", "White", "doesnt.exists", "test123Ynh") @@ -281,7 +281,7 @@ def test_update_group_add_user(mocker): user_group_update("dev", add=["bob"]) group_res = user_group_list()["groups"] - assert set(group_res["dev"]["members"]) == set(["alice", "bob"]) + assert set(group_res["dev"]["members"]) == {"alice", "bob"} def test_update_group_add_user_already_in(mocker): diff --git a/src/yunohost/tools.py b/src/tools.py similarity index 69% rename from src/yunohost/tools.py rename to src/tools.py index 4190e7614..bb7ded03a 100644 --- a/src/yunohost/tools.py +++ b/src/tools.py @@ -29,20 +29,19 @@ import subprocess import time from importlib import import_module from packaging import version +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.process import call_async_output +from moulinette.utils.filesystem import read_yaml, write_to_yaml, cp, mkdir, rm -from yunohost.app import ( - _update_apps_catalog, - app_info, - app_upgrade, +from yunohost.app import app_upgrade, app_list +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 yunohost.firewall import firewall_upnp from yunohost.service import service_start, service_enable from yunohost.regenconf import regen_conf @@ -54,8 +53,6 @@ from yunohost.utils.packages import ( from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation, OperationLogger -# FIXME this is a duplicate from apps.py -APPS_SETTING_PATH = "/etc/yunohost/apps/" MIGRATIONS_STATE_PATH = "/etc/yunohost/migrations.yaml" logger = getActionLogger("yunohost.tools") @@ -97,7 +94,7 @@ def tools_adminpw(new_password, check_strength=True): {"userPassword": [new_hash]}, ) except Exception as e: - logger.error("unable to change admin password : %s" % e) + logger.error(f"unable to change admin password : {e}") raise YunohostError("admin_password_change_failed") else: # Write as root password @@ -144,7 +141,7 @@ def _set_hostname(hostname, pretty_hostname=None): """ if not pretty_hostname: - pretty_hostname = "(YunoHost/%s)" % hostname + pretty_hostname = f"(YunoHost/{hostname})" # First clear nsswitch cache for hosts to make sure hostname is resolved... subprocess.call(["nscd", "-i", "hosts"]) @@ -202,12 +199,12 @@ def tools_postinstall( password -- YunoHost admin password """ + from yunohost.dyndns import _dyndns_available + from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.utils.password import assert_password_is_strong_enough from yunohost.domain import domain_main_domain import psutil - dyndns_provider = "dyndns.yunohost.org" - # Do some checks at first if os.path.isfile("/etc/yunohost/installed"): raise YunohostValidationError("yunohost_already_installed") @@ -222,9 +219,9 @@ def tools_postinstall( disk_partitions = sorted(psutil.disk_partitions(), key=lambda k: k.mountpoint) main_disk_partitions = [d for d in disk_partitions if d.mountpoint in ["/", "/var"]] main_space = sum( - [psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions] + psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions ) - GB = 1024 ** 3 + GB = 1024**3 if not force_diskspace and main_space < 10 * GB: raise YunohostValidationError("postinstall_low_rootfsspace") @@ -232,33 +229,24 @@ def tools_postinstall( if not force_password: assert_password_is_strong_enough("admin", password) - if not ignore_dyndns: - # Check if yunohost dyndns can handle the given domain - # (i.e. is it a .nohost.me ? a .noho.st ?) + # If this is a nohost.me/noho.st, actually check for availability + if not ignore_dyndns and is_yunohost_dyndns_domain(domain): + # Check if the domain is available... try: - is_nohostme_or_nohost = _dyndns_provides(dyndns_provider, domain) + available = _dyndns_available(domain) # If an exception is thrown, most likely we don't have internet # connectivity or something. Assume that this domain isn't manageable # and inform the user that we could not contact the dyndns host server. except Exception: logger.warning( - m18n.n("dyndns_provider_unreachable", provider=dyndns_provider) + m18n.n("dyndns_provider_unreachable", provider="dyndns.yunohost.org") ) - is_nohostme_or_nohost = False - # If this is a nohost.me/noho.st, actually check for availability - if is_nohostme_or_nohost: - # (Except if the user explicitly said he/she doesn't care about dyndns) - if ignore_dyndns: - dyndns = False - # Check if the domain is available... - elif _dyndns_available(dyndns_provider, domain): - dyndns = True - # If not, abort the postinstall - else: - raise YunohostValidationError("dyndns_unavailable", domain=domain) + if available: + dyndns = True + # If not, abort the postinstall else: - dyndns = False + raise YunohostValidationError("dyndns_unavailable", domain=domain) else: dyndns = False @@ -329,29 +317,17 @@ def tools_regen_conf( return regen_conf(names, with_diff, force, dry_run, list_pending) -def tools_update(target=None, apps=False, system=False): +def tools_update(target=None): """ Update apps & system package cache """ - # Legacy options (--system, --apps) - if apps or system: - logger.warning( - "Using 'yunohost tools update' with --apps / --system is deprecated, just write 'yunohost tools update apps system' (no -- prefix anymore)" - ) - if apps and system: - target = "all" - elif apps: - target = "apps" - else: - target = "system" - - elif not target: + if not target: target = "all" if target not in ["system", "apps", "all"]: raise YunohostError( - "Unknown target %s, should be 'system', 'apps' or 'all'" % target, + f"Unknown target {target}, should be 'system', 'apps' or 'all'", raw_msg=True, ) @@ -410,7 +386,7 @@ def tools_update(target=None, apps=False, system=False): except YunohostError as e: logger.error(str(e)) - upgradable_apps = list(_list_upgradable_apps()) + upgradable_apps = list(app_list(upgradable=True)["apps"]) if len(upgradable_apps) == 0 and len(upgradable_system_packages) == 0: logger.info(m18n.n("already_up_to_date")) @@ -418,45 +394,8 @@ def tools_update(target=None, apps=False, system=False): return {"system": upgradable_system_packages, "apps": upgradable_apps} -def _list_upgradable_apps(): - - app_list_installed = os.listdir(APPS_SETTING_PATH) - for app_id in app_list_installed: - - app_dict = app_info(app_id, full=True) - - if app_dict["upgradable"] == "yes": - - # FIXME : would make more sense for these infos to be computed - # directly in app_info and used to check the upgradability of - # the app... - current_version = app_dict.get("manifest", {}).get("version", "?") - current_commit = app_dict.get("settings", {}).get("current_revision", "?")[ - :7 - ] - new_version = ( - app_dict.get("from_catalog", {}).get("manifest", {}).get("version", "?") - ) - new_commit = ( - app_dict.get("from_catalog", {}).get("git", {}).get("revision", "?")[:7] - ) - - if current_version == new_version: - current_version += " (" + current_commit + ")" - new_version += " (" + new_commit + ")" - - yield { - "id": app_id, - "label": app_dict["label"], - "current_version": current_version, - "new_version": new_version, - } - - @is_unit_operation() -def tools_upgrade( - operation_logger, target=None, apps=False, system=False, allow_yunohost_upgrade=True -): +def tools_upgrade(operation_logger, target=None): """ Update apps & package cache, then display changelog @@ -473,23 +412,8 @@ def tools_upgrade( if not packages.dpkg_lock_available(): raise YunohostValidationError("dpkg_lock_not_available") - # Legacy options management (--system, --apps) - if target is None: - - logger.warning( - "Using 'yunohost tools upgrade' with --apps / --system is deprecated, just write 'yunohost tools upgrade apps' or 'system' (no -- prefix anymore)" - ) - - if (system, apps) == (True, True): - raise YunohostValidationError("tools_upgrade_cant_both") - - if (system, apps) == (False, False): - raise YunohostValidationError("tools_upgrade_at_least_one") - - target = "apps" if apps else "system" - if target not in ["apps", "system"]: - raise Exception( + raise YunohostValidationError( "Uhoh ?! tools_upgrade should have 'apps' or 'system' value for argument target" ) @@ -502,7 +426,7 @@ def tools_upgrade( # Make sure there's actually something to upgrade - upgradable_apps = [app["id"] for app in _list_upgradable_apps()] + upgradable_apps = [app["id"] for app in app_list(upgradable=True)["apps"]] if not upgradable_apps: logger.info(m18n.n("apps_already_up_to_date")) @@ -513,7 +437,7 @@ def tools_upgrade( try: app_upgrade(app=upgradable_apps) except Exception as e: - logger.warning("unable to upgrade apps: %s" % str(e)) + logger.warning(f"unable to upgrade apps: {e}") logger.error(m18n.n("app_upgrade_some_app_failed")) return @@ -532,19 +456,10 @@ def tools_upgrade( logger.info(m18n.n("upgrading_packages")) operation_logger.start() - # Critical packages are packages that we can't just upgrade - # randomly from yunohost itself... upgrading them is likely to - critical_packages = ["moulinette", "yunohost", "yunohost-admin", "ssowat"] - - critical_packages_upgradable = [ - p["name"] for p in upgradables if p["name"] in critical_packages - ] - noncritical_packages_upgradable = [ - p["name"] for p in upgradables if p["name"] not in critical_packages - ] - # Prepare dist-upgrade command dist_upgrade = "DEBIAN_FRONTEND=noninteractive" + if Moulinette.interface.type == "api": + dist_upgrade += " YUNOHOST_API_RESTART_WILL_BE_HANDLED_BY_YUNOHOST=yes" dist_upgrade += " APT_LISTCHANGES_FRONTEND=none" dist_upgrade += " apt-get" dist_upgrade += ( @@ -554,136 +469,72 @@ def tools_upgrade( dist_upgrade += ' -o Dpkg::Options::="--force-conf{}"'.format(conf_flag) dist_upgrade += " dist-upgrade" - # - # "Regular" packages upgrade - # - if noncritical_packages_upgradable: + logger.info(m18n.n("tools_upgrade")) - logger.info(m18n.n("tools_upgrade_regular_packages")) + logger.debug("Running apt command :\n{}".format(dist_upgrade)) - # Mark all critical packages as held - for package in critical_packages: - check_output("apt-mark hold %s" % package) + def is_relevant(line): + irrelevants = [ + "service sudo-ldap already provided", + "Reading database ...", + ] + return all(i not in line.rstrip() for i in irrelevants) - # Doublecheck with apt-mark showhold that packages are indeed held ... - held_packages = check_output("apt-mark showhold").split("\n") - if any(p not in held_packages for p in critical_packages): - logger.warning(m18n.n("tools_upgrade_cant_hold_critical_packages")) - operation_logger.error(m18n.n("packages_upgrade_failed")) - raise YunohostError(m18n.n("packages_upgrade_failed")) + callbacks = ( + lambda l: logger.info("+ " + l.rstrip() + "\r") + if _apt_log_line_is_relevant(l) + else logger.debug(l.rstrip() + "\r"), + lambda l: logger.warning(l.rstrip()) + if _apt_log_line_is_relevant(l) + else logger.debug(l.rstrip()), + ) + returncode = call_async_output(dist_upgrade, callbacks, shell=True) - logger.debug("Running apt command :\n{}".format(dist_upgrade)) + # If yunohost is being upgraded from the webadmin + if "yunohost" in upgradables and Moulinette.interface.type == "api": - def is_relevant(line): - irrelevants = [ - "service sudo-ldap already provided", - "Reading database ...", - ] - return all(i not in line.rstrip() for i in irrelevants) + # Restart the API after 10 sec (at now doesn't support sub-minute times...) + # We do this so that the API / webadmin still gets the proper HTTP response + # It's then up to the webadmin to implement a proper UX process to wait 10 sec and then auto-fresh the webadmin + cmd = 'at -M now >/dev/null 2>&1 <<< "sleep 10; systemctl restart yunohost-api"' + # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... + subprocess.check_call(["bash", "-c", cmd]) - callbacks = ( - lambda l: logger.info("+ " + l.rstrip() + "\r") - if is_relevant(l) - else logger.debug(l.rstrip() + "\r"), - lambda l: logger.warning(l.rstrip()) - if is_relevant(l) - else logger.debug(l.rstrip()), - ) - returncode = call_async_output(dist_upgrade, callbacks, shell=True) - if returncode != 0: - upgradables = list(_list_upgradable_apt_packages()) - noncritical_packages_upgradable = [ - p["name"] for p in upgradables if p["name"] not in critical_packages - ] - logger.warning( - m18n.n( - "tools_upgrade_regular_packages_failed", - packages_list=", ".join(noncritical_packages_upgradable), - ) - ) - operation_logger.error(m18n.n("packages_upgrade_failed")) - raise YunohostError(m18n.n("packages_upgrade_failed")) - - # - # Critical packages upgrade - # - if critical_packages_upgradable and allow_yunohost_upgrade: - - logger.info(m18n.n("tools_upgrade_special_packages")) - - # Mark all critical packages as unheld - for package in critical_packages: - check_output("apt-mark unhold %s" % package) - - # Doublecheck with apt-mark showhold that packages are indeed unheld ... - held_packages = check_output("apt-mark showhold").split("\n") - if any(p in held_packages for p in critical_packages): - logger.warning(m18n.n("tools_upgrade_cant_unhold_critical_packages")) - operation_logger.error(m18n.n("packages_upgrade_failed")) - raise YunohostError(m18n.n("packages_upgrade_failed")) - - # - # Here we use a dirty hack to run a command after the current - # "yunohost tools upgrade", because the upgrade of yunohost - # will also trigger other yunohost commands (e.g. "yunohost tools migrations run") - # (also the upgrade of the package, if executed from the webadmin, is - # likely to kill/restart the api which is in turn likely to kill this - # command before it ends...) - # - logfile = operation_logger.log_path - dist_upgrade = dist_upgrade + " 2>&1 | tee -a {}".format(logfile) - - MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" - wait_until_end_of_yunohost_command = ( - "(while [ -f {} ]; do sleep 2; done)".format(MOULINETTE_LOCK) - ) - mark_success = ( - "(echo 'Done!' | tee -a {} && echo 'success: true' >> {})".format( - logfile, operation_logger.md_path + if returncode != 0: + upgradables = list(_list_upgradable_apt_packages()) + logger.warning( + m18n.n( + "tools_upgrade_failed", + packages_list=", ".join(upgradables), ) ) - mark_failure = ( - "(echo 'Failed :(' | tee -a {} && echo 'success: false' >> {})".format( - logfile, operation_logger.md_path - ) - ) - update_log_metadata = "sed -i \"s/ended_at: .*$/ended_at: $(date -u +'%Y-%m-%d %H:%M:%S.%N')/\" {}" - update_log_metadata = update_log_metadata.format(operation_logger.md_path) - # Dirty hack such that the operation_logger does not add ended_at - # and success keys in the log metadata. (c.f. the code of the - # is_unit_operation + operation_logger.close()) We take care of - # this ourselves (c.f. the mark_success and updated_log_metadata in - # the huge command launched by os.system) - operation_logger.ended_at = "notyet" + logger.success(m18n.n("system_upgraded")) + operation_logger.success() - upgrade_completed = "\n" + m18n.n( - "tools_upgrade_special_packages_completed" - ) - command = "({wait} && {dist_upgrade}) && {mark_success} || {mark_failure}; {update_metadata}; echo '{done}'".format( - wait=wait_until_end_of_yunohost_command, - dist_upgrade=dist_upgrade, - mark_success=mark_success, - mark_failure=mark_failure, - update_metadata=update_log_metadata, - done=upgrade_completed, - ) - logger.warning(m18n.n("tools_upgrade_special_packages_explanation")) - logger.debug("Running command :\n{}".format(command)) - open("/tmp/yunohost-selfupgrade", "w").write( - "rm /tmp/yunohost-selfupgrade; " + command - ) - # Using systemd-run --scope is like nohup/disown and &, but more robust somehow - # (despite using nohup/disown and &, the self-upgrade process was still getting killed...) - # ref: https://unix.stackexchange.com/questions/420594/why-process-killed-with-nohup - # (though I still don't understand it 100%...) - os.system("systemd-run --scope bash /tmp/yunohost-selfupgrade &") - return - - else: - logger.success(m18n.n("system_upgraded")) - operation_logger.success() +def _apt_log_line_is_relevant(line): + irrelevants = [ + "service sudo-ldap already provided", + "Reading database ...", + "Preparing to unpack", + "Selecting previously unselected package", + "Created symlink /etc/systemd", + "Replacing config file", + "Creating config file", + "Installing new version of config file", + "Installing new config file as you requested", + ", does not exist on system.", + "unable to delete old directory", + "update-alternatives:", + "Configuration file '/etc", + "==> Modified (by you or by a script) since installation.", + "==> Package distributor has shipped an updated version.", + "==> Keeping old config file as default.", + "is a disabled or a static unit", + " update-rc.d: warning: start and stop actions are no longer supported; falling back to defaults", + ] + return line.rstrip() and all(i not in line.rstrip() for i in irrelevants) @is_unit_operation() @@ -958,19 +809,6 @@ def _write_migration_state(migration_id, state): def _get_migrations_list(): - migrations = [] - - try: - from . import data_migrations - except ImportError: - # not data migrations present, return empty list - return migrations - - migrations_path = data_migrations.__path__[0] - - if not os.path.exists(migrations_path): - logger.warn(m18n.n("migrations_cant_reach_migration_file", migrations_path)) - return migrations # states is a datastructure that represents the last run migration # it has this form: @@ -983,9 +821,11 @@ def _get_migrations_list(): # (in particular, pending migrations / not already ran are not listed states = tools_migrations_state()["migrations"] + migrations = [] + migrations_folder = os.path.dirname(__file__) + "/migrations/" for migration_file in [ x - for x in os.listdir(migrations_path) + for x in os.listdir(migrations_folder) if re.match(r"^\d+_[a-zA-Z0-9_]+\.py$", x) ]: m = _load_migration(migration_file) @@ -1001,20 +841,20 @@ def _get_migration_by_name(migration_name): """ try: - from . import data_migrations + from . import migrations except ImportError: - raise AssertionError("Unable to find migration with name %s" % migration_name) + raise AssertionError(f"Unable to find migration with name {migration_name}") - migrations_path = data_migrations.__path__[0] + migrations_path = migrations.__path__[0] migrations_found = [ x for x in os.listdir(migrations_path) if re.match(r"^\d+_%s\.py$" % migration_name, x) ] - assert len(migrations_found) == 1, ( - "Unable to find migration with name %s" % migration_name - ) + assert ( + len(migrations_found) == 1 + ), f"Unable to find migration with name {migration_name}" return _load_migration(migrations_found[0]) @@ -1029,7 +869,7 @@ def _load_migration(migration_file): # this is python builtin method to import a module using a name, we # use that to import the migration as a python object so we'll be # able to run it in the next loop - module = import_module("yunohost.data_migrations.{}".format(migration_id)) + module = import_module("yunohost.migrations.{}".format(migration_id)) return module.MyMigration(migration_id) except Exception as e: import traceback @@ -1108,12 +948,14 @@ def _tools_migrations_run_before_app_restore(backup_version, app_id): raise -class Migration(object): +class Migration: # Those are to be implemented by daughter classes mode = "auto" - dependencies = [] # List of migration ids required before running this migration + dependencies: List[ + str + ] = [] # List of migration ids required before running this migration @property def disclaimer(self): @@ -1131,9 +973,9 @@ class Migration(object): @property def description(self): - return m18n.n("migration_description_%s" % self.id) + return m18n.n(f"migration_description_{self.id}") - def ldap_migration(run): + def ldap_migration(self, run): def func(self): # Backup LDAP before the migration @@ -1142,12 +984,14 @@ 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( @@ -1164,17 +1008,19 @@ 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 diff --git a/src/yunohost/user.py b/src/user.py similarity index 93% rename from src/yunohost/user.py rename to src/user.py index c89f9a05f..7d023fd83 100644 --- a/src/yunohost/user.py +++ b/src/user.py @@ -97,7 +97,7 @@ def user_list(fields=None): and values[0].strip() == "/bin/false", } - attrs = set(["uid"]) + attrs = {"uid"} users = {} if not fields: @@ -111,7 +111,7 @@ def user_list(fields=None): ldap = _get_ldap_interface() result = ldap.search( - "ou=users,dc=yunohost,dc=org", + "ou=users", "(&(objectclass=person)(!(uid=root))(!(uid=nobody)))", attrs, ) @@ -138,11 +138,10 @@ def user_create( domain, password, mailbox_quota="0", - mail=None, from_import=False, ): - from yunohost.domain import domain_list, _get_maindomain + from yunohost.domain import domain_list, _get_maindomain, _assert_domain_exists from yunohost.hook import hook_callback from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.ldap import _get_ldap_interface @@ -150,12 +149,6 @@ def user_create( # Ensure sufficiently complex password assert_password_is_strong_enough("user", password) - if mail is not None: - logger.warning( - "Packagers ! Using --mail in 'yunohost user create' is deprecated ... please use --domain instead." - ) - domain = mail.split("@")[-1] - # Validate domain used for email address/xmpp account if domain is None: if Moulinette.interface.type == "api": @@ -166,18 +159,17 @@ def user_create( # On affiche les differents domaines possibles Moulinette.display(m18n.n("domains_available")) for domain in domain_list()["domains"]: - Moulinette.display("- {}".format(domain)) + Moulinette.display(f"- {domain}") maindomain = _get_maindomain() domain = Moulinette.prompt( - m18n.n("ask_user_domain") + " (default: %s)" % maindomain + m18n.n("ask_user_domain") + f" (default: {maindomain})" ) if not domain: domain = maindomain # Check that the domain exists - if domain not in domain_list()["domains"]: - raise YunohostValidationError("domain_name_unknown", domain=domain) + _assert_domain_exists(domain) mail = username + "@" + domain ldap = _get_ldap_interface() @@ -216,7 +208,7 @@ def user_create( uid_guid_found = uid not in all_uid and uid not in all_gid # Adapt values for LDAP - fullname = "%s %s" % (firstname, lastname) + fullname = f"{firstname} {lastname}" attr_dict = { "objectClass": [ @@ -241,11 +233,11 @@ def user_create( } # If it is the first user, add some aliases - if not ldap.search(base="ou=users,dc=yunohost,dc=org", filter="uid=*"): + if not ldap.search(base="ou=users", filter="uid=*"): attr_dict["mail"] = [attr_dict["mail"]] + aliases try: - ldap.add("uid=%s,ou=users" % username, attr_dict) + ldap.add(f"uid={username},ou=users", attr_dict) except Exception as e: raise YunohostError("user_creation_failed", user=username, error=e) @@ -262,11 +254,9 @@ def user_create( logger.warning(m18n.n("user_home_creation_failed", home=home), exc_info=1) try: - subprocess.check_call( - ["setfacl", "-m", "g:all_users:---", "/home/%s" % username] - ) + subprocess.check_call(["setfacl", "-m", "g:all_users:---", f"/home/{username}"]) except subprocess.CalledProcessError: - logger.warning("Failed to protect /home/%s" % username, exc_info=1) + logger.warning(f"Failed to protect /home/{username}", exc_info=1) # Create group for user and add to group 'all_users' user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) @@ -326,7 +316,7 @@ def user_delete(operation_logger, username, purge=False, from_import=False): ldap = _get_ldap_interface() try: - ldap.remove("uid=%s,ou=users" % username) + ldap.remove(f"uid={username},ou=users") except Exception as e: raise YunohostError("user_deletion_failed", user=username, error=e) @@ -334,8 +324,8 @@ def user_delete(operation_logger, username, purge=False, from_import=False): subprocess.call(["nscd", "-i", "passwd"]) if purge: - subprocess.call(["rm", "-rf", "/home/{0}".format(username)]) - subprocess.call(["rm", "-rf", "/var/mail/{0}".format(username)]) + subprocess.call(["rm", "-rf", f"/home/{username}"]) + subprocess.call(["rm", "-rf", f"/var/mail/{username}"]) hook_callback("post_user_delete", args=[username, purge]) @@ -385,7 +375,7 @@ def user_update( ldap = _get_ldap_interface() attrs_to_fetch = ["givenName", "sn", "mail", "maildrop"] result = ldap.search( - base="ou=users,dc=yunohost,dc=org", + base="ou=users", filter="uid=" + username, attrs=attrs_to_fetch, ) @@ -421,7 +411,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"), 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) @@ -512,7 +504,7 @@ def user_update( operation_logger.start() try: - ldap.update("uid=%s,ou=users" % username, new_attr_dict) + ldap.update(f"uid={username},ou=users", new_attr_dict) except Exception as e: raise YunohostError("user_update_failed", user=username, error=e) @@ -544,7 +536,7 @@ def user_info(username): else: filter = "uid=" + username - result = ldap.search("ou=users,dc=yunohost,dc=org", filter, user_attrs) + result = ldap.search("ou=users", filter, user_attrs) if result: user = result[0] @@ -583,11 +575,11 @@ def user_info(username): logger.warning(m18n.n("mailbox_disabled", user=username)) else: try: - cmd = "doveadm -f flow quota get -u %s" % user["uid"][0] - cmd_result = check_output(cmd) + uid_ = user["uid"][0] + cmd_result = check_output(f"doveadm -f flow quota get -u {uid_}") except Exception as e: cmd_result = "" - logger.warning("Failed to fetch quota info ... : %s " % str(e)) + logger.warning(f"Failed to fetch quota info ... : {e}") # Exemple of return value for cmd: # """Quota name=User quota Type=STORAGE Value=0 Limit=- %=0 @@ -676,7 +668,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"] @@ -713,8 +705,7 @@ def user_import(operation_logger, csvfile, update=False, delete=False): unknown_groups = [g for g in user["groups"] if g not in existing_groups] if unknown_groups: format_errors.append( - f"username '{user['username']}': unknown groups %s" - % ", ".join(unknown_groups) + f"username '{user['username']}': unknown groups {', '.join(unknown_groups)}" ) # Validate that domains exist @@ -735,8 +726,7 @@ def user_import(operation_logger, csvfile, update=False, delete=False): if unknown_domains: format_errors.append( - f"username '{user['username']}': unknown domains %s" - % ", ".join(unknown_domains) + f"username '{user['username']}': unknown domains {', '.join(unknown_domains)}" ) if format_errors: @@ -944,7 +934,7 @@ def user_group_list(short=False, full=False, include_primary_groups=True): ldap = _get_ldap_interface() groups_infos = ldap.search( - "ou=groups,dc=yunohost,dc=org", + "ou=groups", "(objectclass=groupOfNamesYnh)", ["cn", "member", "permission"], ) @@ -994,9 +984,7 @@ def user_group_create( ldap = _get_ldap_interface() # Validate uniqueness of groupname in LDAP - conflict = ldap.get_conflict( - {"cn": groupname}, base_dn="ou=groups,dc=yunohost,dc=org" - ) + conflict = ldap.get_conflict({"cn": groupname}, base_dn="ou=groups") if conflict: raise YunohostValidationError("group_already_exist", group=groupname) @@ -1008,7 +996,7 @@ def user_group_create( m18n.n("group_already_exist_on_system_but_removing_it", group=groupname) ) subprocess.check_call( - "sed --in-place '/^%s:/d' /etc/group" % groupname, shell=True + f"sed --in-place '/^{groupname}:/d' /etc/group", shell=True ) else: raise YunohostValidationError( @@ -1038,7 +1026,7 @@ def user_group_create( operation_logger.start() try: - ldap.add("cn=%s,ou=groups" % groupname, attr_dict) + ldap.add(f"cn={groupname},ou=groups", attr_dict) except Exception as e: raise YunohostError("group_creation_failed", group=groupname, error=e) @@ -1081,7 +1069,7 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): operation_logger.start() ldap = _get_ldap_interface() try: - ldap.remove("cn=%s,ou=groups" % groupname) + ldap.remove(f"cn={groupname},ou=groups") except Exception as e: raise YunohostError("group_deletion_failed", group=groupname, error=e) @@ -1177,7 +1165,7 @@ def user_group_update( ldap = _get_ldap_interface() try: ldap.update( - "cn=%s,ou=groups" % groupname, + f"cn={groupname},ou=groups", {"member": set(new_group_dns), "memberUid": set(new_group)}, ) except Exception as e: @@ -1210,7 +1198,7 @@ def user_group_info(groupname): # Fetch info for this group result = ldap.search( - "ou=groups,dc=yunohost,dc=org", + "ou=groups", "cn=" + groupname, ["cn", "member", "permission"], ) @@ -1262,25 +1250,23 @@ def user_group_remove(groupname, usernames, force=False, sync_perm=True): def user_permission_list(short=False, full=False, apps=[]): - import yunohost.permission + from yunohost.permission import user_permission_list - return yunohost.permission.user_permission_list( - short, full, absolute_urls=True, apps=apps - ) + return user_permission_list(short, full, absolute_urls=True, apps=apps) def user_permission_update(permission, label=None, show_tile=None, sync_perm=True): - import yunohost.permission + from yunohost.permission import user_permission_update - return yunohost.permission.user_permission_update( + return user_permission_update( permission, label=label, show_tile=show_tile, sync_perm=sync_perm ) def user_permission_add(permission, names, protected=None, force=False, sync_perm=True): - import yunohost.permission + from yunohost.permission import user_permission_update - return yunohost.permission.user_permission_update( + return user_permission_update( permission, add=names, protected=protected, force=force, sync_perm=sync_perm ) @@ -1288,23 +1274,23 @@ def user_permission_add(permission, names, protected=None, force=False, sync_per def user_permission_remove( permission, names, protected=None, force=False, sync_perm=True ): - import yunohost.permission + from yunohost.permission import user_permission_update - return yunohost.permission.user_permission_update( + return user_permission_update( permission, remove=names, protected=protected, force=force, sync_perm=sync_perm ) def user_permission_reset(permission, sync_perm=True): - import yunohost.permission + from yunohost.permission import user_permission_reset - return yunohost.permission.user_permission_reset(permission, sync_perm=sync_perm) + return user_permission_reset(permission, sync_perm=sync_perm) def user_permission_info(permission): - import yunohost.permission + from yunohost.permission import user_permission_info - return yunohost.permission.user_permission_info(permission) + return user_permission_info(permission) # @@ -1333,9 +1319,9 @@ def user_ssh_remove_key(username, key): def _convertSize(num, suffix=""): for unit in ["K", "M", "G", "T", "P", "E", "Z"]: if abs(num) < 1024.0: - return "%3.1f%s%s" % (num, unit, suffix) + return "{:3.1f}{}{}".format(num, unit, suffix) num /= 1024.0 - return "%.1f%s%s" % (num, "Yi", suffix) + return "{:.1f}{}{}".format(num, "Yi", suffix) def _hash_user_password(password): diff --git a/src/yunohost/vendor/__init__.py b/src/utils/__init__.py similarity index 100% rename from src/yunohost/vendor/__init__.py rename to src/utils/__init__.py diff --git a/src/utils/config.py b/src/utils/config.py new file mode 100644 index 000000000..56f632b09 --- /dev/null +++ b/src/utils/config.py @@ -0,0 +1,1420 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2018 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +import glob +import os +import re +import urllib.parse +import tempfile +import shutil +import ast +import operator as op +from collections import OrderedDict +from typing import Optional, Dict, List, Union, Any, Mapping, Callable + +from moulinette.interfaces.cli import colorize +from moulinette import Moulinette, m18n +from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import ( + read_file, + write_to_file, + read_toml, + read_yaml, + write_to_yaml, + mkdir, +) + +from yunohost.utils.i18n import _value_for_locale +from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.log import OperationLogger + +logger = getActionLogger("yunohost.config") +CONFIG_PANEL_VERSION_SUPPORTED = 1.0 + +# Those js-like evaluate functions are used to eval safely visible attributes +# The goal is to evaluate in the same way than js simple-evaluate +# https://github.com/shepherdwind/simple-evaluate +def evaluate_simple_ast(node, context=None): + if context is None: + context = {} + + operators = { + ast.Not: op.not_, + ast.Mult: op.mul, + ast.Div: op.truediv, # number + ast.Mod: op.mod, # number + ast.Add: op.add, # str + ast.Sub: op.sub, # number + ast.USub: op.neg, # Negative number + ast.Gt: op.gt, + ast.Lt: op.lt, + ast.GtE: op.ge, + ast.LtE: op.le, + ast.Eq: op.eq, + ast.NotEq: op.ne, + } + context["true"] = True + context["false"] = False + context["null"] = None + + # Variable + if isinstance(node, ast.Name): # Variable + return context[node.id] + + # Python <=3.7 String + elif isinstance(node, ast.Str): + return node.s + + # Python <=3.7 Number + elif isinstance(node, ast.Num): + return node.n + + # Boolean, None and Python 3.8 for Number, Boolean, String and None + elif isinstance(node, (ast.Constant, ast.NameConstant)): + return node.value + + # + - * / % + elif ( + isinstance(node, ast.BinOp) and type(node.op) in operators + ): # + left = evaluate_simple_ast(node.left, context) + right = evaluate_simple_ast(node.right, context) + if type(node.op) == ast.Add: + if isinstance(left, str) or isinstance(right, str): # support 'I am ' + 42 + left = str(left) + right = str(right) + elif type(left) != type(right): # support "111" - "1" -> 110 + left = float(left) + right = float(right) + + return operators[type(node.op)](left, right) + + # Comparison + # JS and Python don't give the same result for multi operators + # like True == 10 > 2. + elif ( + isinstance(node, ast.Compare) and len(node.comparators) == 1 + ): # + left = evaluate_simple_ast(node.left, context) + right = evaluate_simple_ast(node.comparators[0], context) + operator = node.ops[0] + if isinstance(left, (int, float)) or isinstance(right, (int, float)): + try: + left = float(left) + right = float(right) + except ValueError: + return type(operator) == ast.NotEq + try: + return operators[type(operator)](left, right) + except TypeError: # support "e" > 1 -> False like in JS + return False + + # and / or + elif isinstance(node, ast.BoolOp): # + for value in node.values: + value = evaluate_simple_ast(value, context) + if isinstance(node.op, ast.And) and not value: + return False + elif isinstance(node.op, ast.Or) and value: + return True + return isinstance(node.op, ast.And) + + # not / USub (it's negation number -\d) + elif isinstance(node, ast.UnaryOp): # e.g., -1 + return operators[type(node.op)](evaluate_simple_ast(node.operand, context)) + + # match function call + elif isinstance(node, ast.Call) and node.func.__dict__.get("id") == "match": + return re.match( + evaluate_simple_ast(node.args[1], context), context[node.args[0].id] + ) + + # Unauthorized opcode + else: + opcode = str(type(node)) + raise YunohostError( + f"Unauthorize opcode '{opcode}' in visible attribute", raw_msg=True + ) + + +def js_to_python(expr): + in_string = None + py_expr = "" + i = 0 + escaped = False + for char in expr: + if char in r"\"'": + # Start a string + if not in_string: + in_string = char + + # Finish a string + elif in_string == char and not escaped: + in_string = None + + # If we are not in a string, replace operators + elif not in_string: + if char == "!" and expr[i + 1] != "=": + char = "not " + elif char in "|&" and py_expr[-1:] == char: + py_expr = py_expr[:-1] + char = " and " if char == "&" else " or " + + # Determine if next loop will be in escaped mode + escaped = char == "\\" and not escaped + py_expr += char + i += 1 + return py_expr + + +def evaluate_simple_js_expression(expr, context={}): + if not expr.strip(): + return False + node = ast.parse(js_to_python(expr), mode="eval").body + return evaluate_simple_ast(node, context) + + +class ConfigPanel: + entity_type = "config" + save_path_tpl: Union[str, None] = None + config_path_tpl = "/usr/share/yunohost/config_{entity_type}.toml" + save_mode = "full" + + @classmethod + def list(cls): + """ + List available config panel + """ + try: + entities = [ + re.match( + "^" + cls.save_path_tpl.format(entity="(?p)") + "$", f + ).group("entity") + for f in glob.glob(cls.save_path_tpl.format(entity="*")) + if os.path.isfile(f) + ] + except FileNotFoundError: + entities = [] + return entities + + def __init__(self, entity, config_path=None, save_path=None, creation=False): + self.entity = entity + self.config_path = config_path + if not config_path: + self.config_path = self.config_path_tpl.format( + entity=entity, entity_type=self.entity_type + ) + self.save_path = save_path + if not save_path and self.save_path_tpl: + self.save_path = self.save_path_tpl.format(entity=entity) + self.config = {} + self.values = {} + self.new_values = {} + + if ( + self.save_path + and self.save_mode != "diff" + and not creation + and not os.path.exists(self.save_path) + ): + raise YunohostValidationError( + f"{self.entity_type}_unknown", **{self.entity_type: entity} + ) + if self.save_path and creation and os.path.exists(self.save_path): + raise YunohostValidationError( + f"{self.entity_type}_exists", **{self.entity_type: entity} + ) + + # Search for hooks in the config panel + self.hooks = { + func: getattr(self, func) + for func in dir(self) + if callable(getattr(self, func)) + and re.match("^(validate|post_ask)__", func) + } + + def get(self, key="", mode="classic"): + self.filter_key = key or "" + + # Read config panel toml + self._get_config_panel() + + if not self.config: + raise YunohostValidationError("config_no_panel") + + # Read or get values and hydrate the config + self._load_current_values() + self._hydrate() + + # In 'classic' mode, we display the current value if key refer to an option + if self.filter_key.count(".") == 2 and mode == "classic": + option = self.filter_key.split(".")[-1] + return self.values.get(option, None) + + # Format result in 'classic' or 'export' mode + logger.debug(f"Formating result in '{mode}' mode") + result = {} + for panel, section, option in self._iterate(): + key = f"{panel['id']}.{section['id']}.{option['id']}" + if mode == "export": + result[option["id"]] = option.get("current_value") + continue + + ask = None + if "ask" in option: + ask = _value_for_locale(option["ask"]) + elif "i18n" in self.config: + ask = m18n.n(self.config["i18n"] + "_" + option["id"]) + + if mode == "full": + option["ask"] = ask + question_class = ARGUMENTS_TYPE_PARSERS[option.get("type", "string")] + # FIXME : maybe other properties should be taken from the question, not just choices ?. + option["choices"] = question_class(option).choices + option["default"] = question_class(option).default + option["pattern"] = question_class(option).pattern + else: + result[key] = {"ask": ask} + if "current_value" in option: + question_class = ARGUMENTS_TYPE_PARSERS[ + option.get("type", "string") + ] + result[key]["value"] = question_class.humanize( + option["current_value"], option + ) + # FIXME: semantics, technically here this is not about a prompt... + if question_class.hide_user_input_in_prompt: + result[key][ + "value" + ] = "**************" # Prevent displaying password in `config get` + + if mode == "full": + return self.config + else: + return result + + def set( + self, key=None, value=None, args=None, args_file=None, operation_logger=None + ): + self.filter_key = key or "" + + # Read config panel toml + self._get_config_panel() + + if not self.config: + raise YunohostValidationError("config_no_panel") + + if (args is not None or args_file is not None) and value is not None: + raise YunohostValidationError( + "You should either provide a value, or a serie of args/args_file, but not both at the same time", + raw_msg=True, + ) + + if self.filter_key.count(".") != 2 and value is not None: + raise YunohostValidationError("config_cant_set_value_on_section") + + # Import and parse pre-answered options + logger.debug("Import and parse pre-answered options") + self._parse_pre_answered(args, value, args_file) + + # Read or get values and hydrate the config + self._load_current_values() + self._hydrate() + Question.operation_logger = operation_logger + self._ask() + + if operation_logger: + operation_logger.start() + + try: + self._apply() + except YunohostError: + raise + # Script got manually interrupted ... + # N.B. : KeyboardInterrupt does not inherit from Exception + except (KeyboardInterrupt, EOFError): + error = m18n.n("operation_interrupted") + logger.error(m18n.n("config_apply_failed", error=error)) + raise + # Something wrong happened in Yunohost's code (most probably hook_exec) + except Exception: + import traceback + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error(m18n.n("config_apply_failed", error=error)) + raise + finally: + # Delete files uploaded from API + # FIXME : this is currently done in the context of config panels, + # but could also happen in the context of app install ... (or anywhere else + # where we may parse args etc...) + FileQuestion.clean_upload_dirs() + + self._reload_services() + + logger.success("Config updated as expected") + operation_logger.success() + + def _get_toml(self): + return read_toml(self.config_path) + + def _get_config_panel(self): + + # Split filter_key + filter_key = self.filter_key.split(".") if self.filter_key != "" else [] + if len(filter_key) > 3: + raise YunohostError( + f"The filter key {filter_key} has too many sub-levels, the max is 3.", + raw_msg=True, + ) + + if not os.path.exists(self.config_path): + logger.debug(f"Config panel {self.config_path} doesn't exists") + return None + + toml_config_panel = self._get_toml() + + # Check TOML config panel is in a supported version + if float(toml_config_panel["version"]) < CONFIG_PANEL_VERSION_SUPPORTED: + raise YunohostError( + "config_version_not_supported", version=toml_config_panel["version"] + ) + + # Transform toml format into internal format + format_description = { + "root": { + "properties": ["version", "i18n"], + "defaults": {"version": 1.0}, + }, + "panels": { + "properties": ["name", "services", "actions", "help"], + "defaults": { + "services": [], + "actions": {"apply": {"en": "Apply"}}, + }, + }, + "sections": { + "properties": ["name", "services", "optional", "help", "visible"], + "defaults": { + "name": "", + "services": [], + "optional": True, + }, + }, + "options": { + "properties": [ + "ask", + "type", + "bind", + "help", + "example", + "default", + "style", + "icon", + "placeholder", + "visible", + "optional", + "choices", + "yes", + "no", + "pattern", + "limit", + "min", + "max", + "step", + "accept", + "redact", + "filter", + ], + "defaults": {}, + }, + } + + def _build_internal_config_panel(raw_infos, level): + """Convert TOML in internal format ('full' mode used by webadmin) + Here are some properties of 1.0 config panel in toml: + - node properties and node children are mixed, + - text are in english only + - some properties have default values + This function detects all children nodes and put them in a list + """ + + defaults = format_description[level]["defaults"] + properties = format_description[level]["properties"] + + # Start building the ouput (merging the raw infos + defaults) + out = {key: raw_infos.get(key, value) for key, value in defaults.items()} + + # Now fill the sublevels (+ apply filter_key) + i = list(format_description).index(level) + sublevel = list(format_description)[i + 1] if level != "options" else None + search_key = filter_key[i] if len(filter_key) > i else False + + for key, value in raw_infos.items(): + # Key/value are a child node + if ( + isinstance(value, OrderedDict) + and key not in properties + and sublevel + ): + # We exclude all nodes not referenced by the filter_key + if search_key and key != search_key: + continue + subnode = _build_internal_config_panel(value, sublevel) + subnode["id"] = key + if level == "root": + subnode.setdefault("name", {"en": key.capitalize()}) + elif level == "sections": + subnode["name"] = key # legacy + subnode.setdefault("optional", raw_infos.get("optional", True)) + out.setdefault(sublevel, []).append(subnode) + # Key/value are a property + else: + if key not in properties: + logger.warning(f"Unknown key '{key}' found in config panel") + # Todo search all i18n keys + out[key] = ( + value if key not in ["ask", "help", "name"] else {"en": value} + ) + return out + + self.config = _build_internal_config_panel(toml_config_panel, "root") + + try: + self.config["panels"][0]["sections"][0]["options"][0] + except (KeyError, IndexError): + raise YunohostValidationError( + "config_unknown_filter_key", filter_key=self.filter_key + ) + + # List forbidden keywords from helpers and sections toml (to avoid conflict) + forbidden_keywords = [ + "old", + "app", + "changed", + "file_hash", + "binds", + "types", + "formats", + "getter", + "setter", + "short_setting", + "type", + "bind", + "nothing_changed", + "changes_validated", + "result", + "max_progression", + ] + forbidden_keywords += format_description["sections"] + + for _, _, option in self._iterate(): + if option["id"] in forbidden_keywords: + raise YunohostError("config_forbidden_keyword", keyword=option["id"]) + return self.config + + def _hydrate(self): + # Hydrating config panel with current value + logger.debug("Hydrating config with current values") + for _, _, option in self._iterate(): + if option["id"] not in self.values: + allowed_empty_types = ["alert", "display_text", "markdown", "file"] + if ( + option["type"] in allowed_empty_types + or option.get("bind") == "null" + ): + continue + else: + raise YunohostError( + f"Config panel question '{option['id']}' should be initialized with a value during install or upgrade.", + raw_msg=True, + ) + value = self.values[option["name"]] + # In general, the value is just a simple value. + # Sometimes it could be a dict used to overwrite the option itself + value = value if isinstance(value, dict) else {"current_value": value} + option.update(value) + + return self.values + + def _ask(self): + logger.debug("Ask unanswered question and prevalidate data") + + if "i18n" in self.config: + for panel, section, option in self._iterate(): + if "ask" not in option: + option["ask"] = m18n.n(self.config["i18n"] + "_" + option["id"]) + + def display_header(message): + """CLI panel/section header display""" + if Moulinette.interface.type == "cli" and self.filter_key.count(".") < 2: + Moulinette.display(colorize(message, "purple")) + + for panel, section, obj in self._iterate(["panel", "section"]): + if panel == obj: + name = _value_for_locale(panel["name"]) + display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}") + continue + name = _value_for_locale(section["name"]) + if name: + display_header(f"\n# {name}") + + # Check and ask unanswered questions + prefilled_answers = self.args.copy() + prefilled_answers.update(self.new_values) + + questions = ask_questions_and_parse_answers( + section["options"], + prefilled_answers=prefilled_answers, + current_values=self.values, + hooks=self.hooks, + ) + self.new_values.update( + { + question.name: question.value + for question in questions + if question.value is not None + } + ) + + self.errors = None + + def _get_default_values(self): + return { + option["id"]: option["default"] + for _, _, option in self._iterate() + if "default" in option + } + + @property + def future_values(self): + return {**self.values, **self.new_values} + + def __getattr__(self, name): + if "new_values" in self.__dict__ and name in self.new_values: + return self.new_values[name] + + if "values" in self.__dict__ and name in self.values: + return self.values[name] + + return self.__dict__[name] + + def _load_current_values(self): + """ + Retrieve entries in YAML file + And set default values if needed + """ + + # Inject defaults if needed (using the magic .update() ;)) + self.values = self._get_default_values() + + # Retrieve entries in the YAML + if os.path.exists(self.save_path) and os.path.isfile(self.save_path): + self.values.update(read_yaml(self.save_path) or {}) + + def _parse_pre_answered(self, args, value, args_file): + args = urllib.parse.parse_qs(args or "", keep_blank_values=True) + self.args = {key: ",".join(value_) for key, value_ in args.items()} + + if args_file: + # Import YAML / JSON file but keep --args values + self.args = {**read_yaml(args_file), **self.args} + + if value is not None: + self.args = {self.filter_key.split(".")[-1]: value} + + def _apply(self): + logger.info("Saving the new configuration...") + dir_path = os.path.dirname(os.path.realpath(self.save_path)) + if not os.path.exists(dir_path): + mkdir(dir_path, mode=0o700) + + values_to_save = self.future_values + if self.save_mode == "diff": + defaults = self._get_default_values() + values_to_save = { + k: v for k, v in values_to_save.items() if defaults.get(k) != v + } + + # Save the settings to the .yaml file + write_to_yaml(self.save_path, values_to_save) + + def _reload_services(self): + + from yunohost.service import service_reload_or_restart + + services_to_reload = set() + for panel, section, obj in self._iterate(["panel", "section", "option"]): + services_to_reload |= set(obj.get("services", [])) + + services_to_reload = list(services_to_reload) + services_to_reload.sort(key="nginx".__eq__) + if services_to_reload: + logger.info("Reloading services...") + for service in services_to_reload: + if hasattr(self, "entity"): + service = service.replace("__APP__", self.entity) + service_reload_or_restart(service) + + def _iterate(self, trigger=["option"]): + for panel in self.config.get("panels", []): + if "panel" in trigger: + yield (panel, None, panel) + for section in panel.get("sections", []): + if "section" in trigger: + yield (panel, section, section) + if "option" in trigger: + for option in section.get("options", []): + yield (panel, section, option) + + +class Question: + hide_user_input_in_prompt = False + pattern: Optional[Dict] = None + + def __init__( + self, + question: Dict[str, Any], + context: Mapping[str, Any] = {}, + hooks: Dict[str, Callable] = {}, + ): + self.name = question["name"] + self.context = context + self.hooks = hooks + self.type = question.get("type", "string") + self.default = question.get("default", None) + self.optional = question.get("optional", False) + self.visible = question.get("visible", None) + self.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.redact = question.get("redact", False) + self.filter = question.get("filter", None) + # .current_value is the currently stored value + self.current_value = question.get("current_value") + # .value is the "proposed" value which we got from the user + self.value = question.get("value") + # Use to return several values in case answer is in mutipart + self.values: Dict[str, Any] = {} + + # Empty value is parsed as empty string + if self.default == "": + self.default = None + + @staticmethod + def humanize(value, option={}): + return str(value) + + @staticmethod + def normalize(value, option={}): + if isinstance(value, str): + value = value.strip() + return value + + def _prompt(self, text): + prefill = "" + if self.current_value is not None: + prefill = self.humanize(self.current_value, self) + elif self.default is not None: + prefill = self.humanize(self.default, self) + self.value = Moulinette.prompt( + message=text, + is_password=self.hide_user_input_in_prompt, + confirm=False, + prefill=prefill, + is_multiline=(self.type == "text"), + autocomplete=self.choices, + help=_value_for_locale(self.help), + ) + + def ask_if_needed(self): + + if self.visible and not evaluate_simple_js_expression( + self.visible, context=self.context + ): + # FIXME There could be several use case if the question is not displayed: + # - we doesn't want to give a specific value + # - we want to keep the previous value + # - we want the default value + self.value = self.values[self.name] = None + return self.values + + for i in range(5): + # Display question if no value filled or if it's a readonly message + if Moulinette.interface.type == "cli" and os.isatty(1): + text_for_user_input_in_cli = self._format_text_for_user_input_in_cli() + if getattr(self, "readonly", False): + Moulinette.display(text_for_user_input_in_cli) + elif self.value is None: + self._prompt(text_for_user_input_in_cli) + + # Apply default value + class_default = getattr(self, "default_value", None) + if self.value in [None, ""] and ( + self.default is not None or class_default is not None + ): + self.value = class_default if self.default is None else self.default + + try: + # Normalize and validate + self.value = self.normalize(self.value, self) + self._prevalidate() + except YunohostValidationError as e: + # If in interactive cli, re-ask the current question + if i < 4 and Moulinette.interface.type == "cli" and os.isatty(1): + logger.error(str(e)) + self.value = None + continue + + # Otherwise raise the ValidationError + raise + + break + + self.value = self.values[self.name] = self._post_parse_value() + + # Search for post actions in hooks + post_hook = f"post_ask__{self.name}" + if post_hook in self.hooks: + self.values.update(self.hooks[post_hook](self)) + + return self.values + + def _prevalidate(self): + if self.value in [None, ""] and not self.optional: + raise YunohostValidationError("app_argument_required", name=self.name) + + # we have an answer, do some post checks + if self.value not in [None, ""]: + if self.choices and self.value not in self.choices: + raise YunohostValidationError( + "app_argument_choice_invalid", + name=self.name, + value=self.value, + choices=", ".join(self.choices), + ) + if self.pattern and not re.match(self.pattern["regexp"], str(self.value)): + raise YunohostValidationError( + self.pattern["error"], + name=self.name, + value=self.value, + ) + + def _format_text_for_user_input_in_cli(self): + + text_for_user_input_in_cli = _value_for_locale(self.ask) + + if self.choices: + + # Prevent displaying a shitload of choices + # (e.g. 100+ available users when choosing an app admin...) + choices = ( + list(self.choices.keys()) + if isinstance(self.choices, dict) + else self.choices + ) + choices_to_display = choices[:20] + remaining_choices = len(choices[20:]) + + if remaining_choices > 0: + choices_to_display += [ + m18n.n("other_available_options", n=remaining_choices) + ] + + choices_to_display = " | ".join(choices_to_display) + + text_for_user_input_in_cli += f" [{choices_to_display}]" + + return text_for_user_input_in_cli + + def _post_parse_value(self): + if not self.redact: + return self.value + + # Tell the operation_logger to redact all password-type / secret args + # Also redact the % escaped version of the password that might appear in + # the 'args' section of metadata (relevant for password with non-alphanumeric char) + data_to_redact = [] + if self.value and isinstance(self.value, str): + data_to_redact.append(self.value) + if self.current_value and isinstance(self.current_value, str): + data_to_redact.append(self.current_value) + data_to_redact += [ + urllib.parse.quote(data) + for data in data_to_redact + if urllib.parse.quote(data) != data + ] + + for operation_logger in OperationLogger._instances: + operation_logger.data_to_redact.extend(data_to_redact) + + return self.value + + +class StringQuestion(Question): + argument_type = "string" + default_value = "" + + +class EmailQuestion(StringQuestion): + pattern = { + "regexp": r"^.+@.+", + "error": "config_validate_email", # i18n: config_validate_email + } + + +class URLQuestion(StringQuestion): + pattern = { + "regexp": r"^https?://.*$", + "error": "config_validate_url", # i18n: config_validate_url + } + + +class DateQuestion(StringQuestion): + pattern = { + "regexp": r"^\d{4}-\d\d-\d\d$", + "error": "config_validate_date", # i18n: config_validate_date + } + + def _prevalidate(self): + from datetime import datetime + + super()._prevalidate() + + if self.value not in [None, ""]: + try: + datetime.strptime(self.value, "%Y-%m-%d") + except ValueError: + raise YunohostValidationError("config_validate_date") + + +class TimeQuestion(StringQuestion): + pattern = { + "regexp": r"^(1[12]|0?\d):[0-5]\d$", + "error": "config_validate_time", # i18n: config_validate_time + } + + +class ColorQuestion(StringQuestion): + pattern = { + "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", + "error": "config_validate_color", # i18n: config_validate_color + } + + +class TagsQuestion(Question): + argument_type = "tags" + + @staticmethod + def humanize(value, option={}): + if isinstance(value, list): + return ",".join(value) + return value + + @staticmethod + def normalize(value, option={}): + if isinstance(value, list): + return ",".join(value) + if isinstance(value, str): + value = value.strip() + return value + + def _prevalidate(self): + values = self.value + if isinstance(values, str): + values = values.split(",") + elif values is None: + values = [] + for value in values: + self.value = value + super()._prevalidate() + self.value = values + + def _post_parse_value(self): + if isinstance(self.value, list): + self.value = ",".join(self.value) + return super()._post_parse_value() + + +class PasswordQuestion(Question): + hide_user_input_in_prompt = True + argument_type = "password" + default_value = "" + forbidden_chars = "{}" + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + self.redact = True + if self.default is not None: + raise YunohostValidationError( + "app_argument_password_no_default", name=self.name + ) + + def _prevalidate(self): + super()._prevalidate() + + if self.value not in [None, ""]: + if any(char in self.value for char in self.forbidden_chars): + raise YunohostValidationError( + "pattern_password_app", forbidden_chars=self.forbidden_chars + ) + + # If it's an optional argument the value should be empty or strong enough + from yunohost.utils.password import assert_password_is_strong_enough + + assert_password_is_strong_enough("user", self.value) + + +class PathQuestion(Question): + argument_type = "path" + default_value = "" + + @staticmethod + def normalize(value, option={}): + + option = option.__dict__ if isinstance(option, Question) else option + + if not value.strip(): + if option.get("optional"): + return "" + # Hmpf here we could just have a "else" case + # but we also want PathQuestion.normalize("") to return "/" + # (i.e. if no option is provided, hence .get("optional") is None + elif option.get("optional") is False: + raise YunohostValidationError( + "app_argument_invalid", + name=option.get("name"), + error="Question is mandatory", + ) + + return "/" + value.strip().strip(" /") + + +class BooleanQuestion(Question): + argument_type = "boolean" + default_value = 0 + yes_answers = ["1", "yes", "y", "true", "t", "on"] + no_answers = ["0", "no", "n", "false", "f", "off"] + + @staticmethod + def humanize(value, option={}): + + option = option.__dict__ if isinstance(option, Question) else option + + yes = option.get("yes", 1) + no = option.get("no", 0) + + value = BooleanQuestion.normalize(value, option) + + if value == yes: + return "yes" + if value == no: + return "no" + if value is None: + return "" + + raise YunohostValidationError( + "app_argument_choice_invalid", + name=option.get("name"), + value=value, + choices="yes/no", + ) + + @staticmethod + def normalize(value, option={}): + + option = option.__dict__ if isinstance(option, Question) else option + + if isinstance(value, str): + value = value.strip() + + technical_yes = option.get("yes", 1) + technical_no = option.get("no", 0) + + no_answers = BooleanQuestion.no_answers + yes_answers = BooleanQuestion.yes_answers + + assert ( + str(technical_yes).lower() not in no_answers + ), f"'yes' value can't be in {no_answers}" + assert ( + str(technical_no).lower() not in yes_answers + ), f"'no' value can't be in {yes_answers}" + + no_answers += [str(technical_no).lower()] + yes_answers += [str(technical_yes).lower()] + + strvalue = str(value).lower() + + if strvalue in yes_answers: + return technical_yes + if strvalue in no_answers: + return technical_no + + if strvalue in ["none", ""]: + return None + + raise YunohostValidationError( + "app_argument_choice_invalid", + name=option.get("name"), + value=strvalue, + choices="yes/no", + ) + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + self.yes = question.get("yes", 1) + self.no = question.get("no", 0) + if self.default is None: + self.default = self.no + + def _format_text_for_user_input_in_cli(self): + text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() + + text_for_user_input_in_cli += " [yes | no]" + + return text_for_user_input_in_cli + + def get(self, key, default=None): + return getattr(self, key, default) + + +class DomainQuestion(Question): + argument_type = "domain" + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + from yunohost.domain import domain_list, _get_maindomain + + super().__init__(question, context, hooks) + + if self.default is None: + self.default = _get_maindomain() + + self.choices = { + domain: domain + " ★" if domain == self.default else domain + for domain in domain_list()["domains"] + } + + @staticmethod + def normalize(value, option={}): + if value.startswith("https://"): + value = value[len("https://") :] + elif value.startswith("http://"): + value = value[len("http://") :] + + # Remove trailing slashes + value = value.rstrip("/").lower() + + return value + + +class AppQuestion(Question): + argument_type = "app" + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + from yunohost.app import app_list + + super().__init__(question, context, hooks) + + apps = app_list(full=True)["apps"] + + if self.filter: + apps = [ + app + for app in apps + if evaluate_simple_js_expression(self.filter, context=app) + ] + + def _app_display(app): + domain_path_or_id = f" ({app.get('domain_path', app['id'])})" + return app["label"] + domain_path_or_id + + self.choices = {"_none": "---"} + self.choices.update({app["id"]: _app_display(app) for app in apps}) + + +class UserQuestion(Question): + argument_type = "user" + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + from yunohost.user import user_list, user_info + from yunohost.domain import _get_maindomain + + super().__init__(question, context, hooks) + + self.choices = { + username: f"{infos['fullname']} ({infos['mail']})" + for username, infos in user_list()["users"].items() + } + + if not self.choices: + raise YunohostValidationError( + "app_argument_invalid", + name=self.name, + error="You should create a YunoHost user first.", + ) + + if self.default is None: + root_mail = "root@%s" % _get_maindomain() + for user in self.choices.keys(): + if root_mail in user_info(user).get("mail-aliases", []): + self.default = user + break + + +class NumberQuestion(Question): + argument_type = "number" + default_value = None + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + self.min = question.get("min", None) + self.max = question.get("max", None) + self.step = question.get("step", None) + + @staticmethod + def normalize(value, option={}): + + if isinstance(value, int): + return value + + if isinstance(value, str): + value = value.strip() + + if isinstance(value, str) and value.isdigit(): + return int(value) + + if value in [None, ""]: + return value + + option = option.__dict__ if isinstance(option, Question) else option + raise YunohostValidationError( + "app_argument_invalid", + name=option.get("name"), + error=m18n.n("invalid_number"), + ) + + def _prevalidate(self): + super()._prevalidate() + if self.value in [None, ""]: + return + + if self.min is not None and int(self.value) < self.min: + raise YunohostValidationError( + "app_argument_invalid", + name=self.name, + error=m18n.n("invalid_number_min", min=self.min), + ) + + if self.max is not None and int(self.value) > self.max: + raise YunohostValidationError( + "app_argument_invalid", + name=self.name, + error=m18n.n("invalid_number_max", max=self.max), + ) + + +class DisplayTextQuestion(Question): + argument_type = "display_text" + readonly = True + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + + self.optional = True + self.style = question.get( + "style", "info" if question["type"] == "alert" else "" + ) + + def _format_text_for_user_input_in_cli(self): + text = _value_for_locale(self.ask) + + if self.style in ["success", "info", "warning", "danger"]: + color = { + "success": "green", + "info": "cyan", + "warning": "yellow", + "danger": "red", + } + prompt = m18n.g(self.style) if self.style != "danger" else m18n.n("danger") + return colorize(prompt, color[self.style]) + f" {text}" + else: + return text + + +class FileQuestion(Question): + argument_type = "file" + upload_dirs: List[str] = [] + + @classmethod + def clean_upload_dirs(cls): + # Delete files uploaded from API + for upload_dir in cls.upload_dirs: + if os.path.exists(upload_dir): + shutil.rmtree(upload_dir) + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + self.accept = question.get("accept", "") + + def _prevalidate(self): + if self.value is None: + self.value = self.current_value + + super()._prevalidate() + + 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 + + if not self.value: + return self.value + + upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_") + _, file_path = tempfile.mkstemp(dir=upload_dir) + + FileQuestion.upload_dirs += [upload_dir] + + logger.debug(f"Saving file {self.name} for file question into {file_path}") + + def is_file_path(s): + return isinstance(s, str) and s.startswith("/") and os.path.exists(s) + + if Moulinette.interface.type != "api" or is_file_path(self.value): + content = read_file(str(self.value), file_mode="rb") + else: + content = b64decode(self.value) + + write_to_file(file_path, content, file_mode="wb") + + self.value = file_path + + return self.value + + +ARGUMENTS_TYPE_PARSERS = { + "string": StringQuestion, + "text": StringQuestion, + "select": StringQuestion, + "tags": TagsQuestion, + "email": EmailQuestion, + "url": URLQuestion, + "date": DateQuestion, + "time": TimeQuestion, + "color": ColorQuestion, + "password": PasswordQuestion, + "path": PathQuestion, + "boolean": BooleanQuestion, + "domain": DomainQuestion, + "user": UserQuestion, + "number": NumberQuestion, + "range": NumberQuestion, + "display_text": DisplayTextQuestion, + "alert": DisplayTextQuestion, + "markdown": DisplayTextQuestion, + "file": FileQuestion, + "app": AppQuestion, +} + + +def ask_questions_and_parse_answers( + raw_questions: Dict, + prefilled_answers: Union[str, Mapping[str, Any]] = {}, + current_values: Mapping[str, Any] = {}, + hooks: Dict[str, Callable[[], None]] = {}, +) -> List[Question]: + """Parse arguments store in either manifest.json or actions.json or from a + config panel against the user answers when they are present. + + Keyword arguments: + raw_questions -- the arguments description store in yunohost + format from actions.json/toml, manifest.json/toml + or config_panel.json/toml + prefilled_answers -- a url "query-string" such as "domain=yolo.test&path=/foobar&admin=sam" + or a dict such as {"domain": "yolo.test", "path": "/foobar", "admin": "sam"} + """ + + if isinstance(prefilled_answers, str): + # FIXME FIXME : this is not uniform with config_set() which uses parse.qs (no l) + # parse_qsl parse single values + # whereas parse.qs return list of values (which is useful for tags, etc) + # For now, let's not migrate this piece of code to parse_qs + # Because Aleks believes some bits of the app CI rely on overriding values (e.g. foo=foo&...&foo=bar) + answers = dict( + urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True) + ) + elif isinstance(prefilled_answers, Mapping): + answers = {**prefilled_answers} + else: + answers = {} + + context = {**current_values, **answers} + out = [] + + for 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=context, hooks=hooks) + new_values = question.ask_if_needed() + answers.update(new_values) + context.update(new_values) + out.append(question) + + return out + + +def hydrate_questions_with_choices(raw_questions: List) -> List: + out = [] + + for raw_question in raw_questions: + question = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]( + raw_question + ) + if question.choices: + raw_question["choices"] = question.choices + raw_question["default"] = question.default + out.append(raw_question) + + return out diff --git a/src/utils/dns.py b/src/utils/dns.py new file mode 100644 index 000000000..ccb6c5406 --- /dev/null +++ b/src/utils/dns.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2018 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" +import dns.resolver +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 +# dig() often during same yunohost operation +external_resolvers_: List[str] = [] + + +def is_yunohost_dyndns_domain(domain): + + return any( + domain.endswith(f".{dyndns_domain}") for dyndns_domain in YNH_DYNDNS_DOMAINS + ) + + +def is_special_use_tld(domain): + + return any(domain.endswith(f".{tld}") for tld in SPECIAL_USE_TLDS) + + +def external_resolvers(): + + global external_resolvers_ + + if not external_resolvers_: + resolv_dnsmasq_conf = read_file("/etc/resolv.dnsmasq.conf").split("\n") + external_resolvers_ = [ + r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver") + ] + # We keep only ipv4 resolvers, otherwise on IPv4-only instances, IPv6 + # will be tried anyway resulting in super-slow dig requests that'll wait + # until timeout... + external_resolvers_ = [r for r in external_resolvers_ if ":" not in r] + + return external_resolvers_ + + +def dig( + qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False +): + """ + Do a quick DNS request and avoid the "search" trap inside /etc/resolv.conf + """ + + # It's very important to do the request with a qname ended by . + # If we don't and the domain fail, dns resolver try a second request + # by concatenate the qname with the end of the "hostname" + if not qname.endswith("."): + qname += "." + + if resolvers == "local": + resolvers = ["127.0.0.1"] + elif resolvers == "force_external": + resolvers = external_resolvers() + else: + assert isinstance(resolvers, list) + + resolver = dns.resolver.Resolver(configure=False) + resolver.use_edns(0, 0, edns_size) + resolver.nameservers = resolvers + # resolver.timeout is used to trigger the next DNS query on resolvers list. + # In python-dns 1.16, this value is set to 2.0. However, this means that if + # the 3 first dns resolvers in list are down, we wait 6 seconds before to + # run the DNS query to a DNS resolvers up... + # In diagnosis dnsrecords, with 10 domains this means at least 12min, too long. + resolver.timeout = 1.0 + # resolver.lifetime is the timeout for resolver.query() + # By default set it to 5 seconds to allow 4 resolvers to be unreachable. + resolver.lifetime = timeout + try: + answers = resolver.query(qname, rdtype) + except ( + dns.resolver.NXDOMAIN, + dns.resolver.NoNameservers, + dns.resolver.NoAnswer, + dns.exception.Timeout, + ) as e: + return ("nok", (e.__class__.__name__, e)) + + if not full_answers: + answers = [answer.to_text() for answer in answers] + + return ("ok", answers) diff --git a/src/yunohost/utils/error.py b/src/utils/error.py similarity index 88% rename from src/yunohost/utils/error.py rename to src/utils/error.py index f9b4ac61a..a92f3bd5a 100644 --- a/src/yunohost/utils/error.py +++ b/src/utils/error.py @@ -19,7 +19,7 @@ """ -from moulinette.core import MoulinetteError +from moulinette.core import MoulinetteError, MoulinetteAuthenticationError from moulinette import m18n @@ -59,4 +59,9 @@ class YunohostValidationError(YunohostError): def content(self): - return {"error": self.strerror, "error_key": self.key} + return {"error": self.strerror, "error_key": self.key, **self.kwargs} + + +class YunohostAuthenticationError(MoulinetteAuthenticationError): + + pass diff --git a/src/yunohost/utils/filesystem.py b/src/utils/filesystem.py similarity index 100% rename from src/yunohost/utils/filesystem.py rename to src/utils/filesystem.py diff --git a/src/utils/i18n.py b/src/utils/i18n.py new file mode 100644 index 000000000..a0daf8181 --- /dev/null +++ b/src/utils/i18n.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2018 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" +from moulinette import m18n + + +def _value_for_locale(values): + """ + Return proper value for current locale + + Keyword arguments: + values -- A dict of values associated to their locale + + Returns: + An utf-8 encoded string + + """ + if not isinstance(values, dict): + return values + + for lang in [m18n.locale, m18n.default_locale]: + try: + return values[lang] + except KeyError: + continue + + # Fallback to first value + return list(values.values())[0] diff --git a/src/yunohost/utils/ldap.py b/src/utils/ldap.py similarity index 97% rename from src/yunohost/utils/ldap.py rename to src/utils/ldap.py index 4f571ce6f..98c0fecf7 100644 --- a/src/yunohost/utils/ldap.py +++ b/src/utils/ldap.py @@ -101,7 +101,8 @@ class LDAPInterface: except ldap.SERVER_DOWN: raise YunohostError( "Service slapd is not running but is required to perform this action ... " - "You can try to investigate what's happening with 'systemctl status slapd'" + "You can try to investigate what's happening with 'systemctl status slapd'", + raw_msg=True, ) # Check that we are indeed logged in with the right identity @@ -139,6 +140,8 @@ class LDAPInterface: """ if not base: base = self.basedn + else: + base = base + "," + self.basedn try: result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs) @@ -240,7 +243,7 @@ class LDAPInterface: """ dn = rdn + "," + self.basedn - actual_entry = self.search(base=dn, attrs=None) + actual_entry = self.search(rdn, attrs=None) ldif = modlist.modifyModlist(actual_entry[0], attr_dict, ignore_oldexistent=1) if ldif == []: @@ -289,7 +292,7 @@ class LDAPInterface: attr_found[0], attr_found[1], ) - raise MoulinetteError( + raise YunohostError( "ldap_attribute_already_exists", attribute=attr_found[0], value=attr_found[1], diff --git a/src/utils/legacy.py b/src/utils/legacy.py new file mode 100644 index 000000000..85898f28d --- /dev/null +++ b/src/utils/legacy.py @@ -0,0 +1,306 @@ +import os +import re +import glob +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import ( + read_file, + write_to_file, + write_to_yaml, + write_to_json, + read_yaml, +) + +from yunohost.utils.error import YunohostValidationError + + +logger = getActionLogger("yunohost.legacy") + +LEGACY_PERMISSION_LABEL = { + ("nextcloud", "skipped"): "api", # .well-known + ("libreto", "skipped"): "pad access", # /[^/]+ + ("leed", "skipped"): "api", # /action.php, for cron task ... + ("mailman", "protected"): "admin", # /admin + ("prettynoemiecms", "protected"): "admin", # /admin + ("etherpad_mypads", "skipped"): "admin", # /admin + ("baikal", "protected"): "admin", # /admin/ + ("couchpotato", "unprotected"): "api", # /api + ("freshrss", "skipped"): "api", # /api/, + ("portainer", "skipped"): "api", # /api/webhooks/ + ("jeedom", "unprotected"): "api", # /core/api/jeeApi.php + ("bozon", "protected"): "user interface", # /index.php + ( + "limesurvey", + "protected", + ): "admin", # /index.php?r=admin,/index.php?r=plugins,/scripts + ("kanboard", "unprotected"): "api", # /jsonrpc.php + ("seafile", "unprotected"): "medias", # /media + ("ttrss", "skipped"): "api", # /public.php,/api,/opml.php?op=publish + ("libreerp", "protected"): "admin", # /web/database/manager + ("z-push", "skipped"): "api", # $domain/[Aa]uto[Dd]iscover/.* + ("radicale", "skipped"): "?", # $domain$path_url + ( + "jirafeau", + "protected", + ): "user interface", # $domain$path_url/$","$domain$path_url/admin.php.*$ + ("opensondage", "protected"): "admin", # $domain$path_url/admin/ + ( + "lstu", + "protected", + ): "user interface", # $domain$path_url/login$","$domain$path_url/logout$","$domain$path_url/api$","$domain$path_url/extensions$","$domain$path_url/stats$","$domain$path_url/d/.*$","$domain$path_url/a$","$domain$path_url/$ + ( + "lutim", + "protected", + ): "user interface", # $domain$path_url/stats/?$","$domain$path_url/manifest.webapp/?$","$domain$path_url/?$","$domain$path_url/[d-m]/.*$ + ( + "lufi", + "protected", + ): "user interface", # $domain$path_url/stats$","$domain$path_url/manifest.webapp$","$domain$path_url/$","$domain$path_url/d/.*$","$domain$path_url/m/.*$ + ( + "gogs", + "skipped", + ): "api", # $excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-receive%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-upload%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/info/refs +} + + +def legacy_permission_label(app, permission_type): + return LEGACY_PERMISSION_LABEL.get( + (app, permission_type), "Legacy %s urls" % permission_type + ) + + +def translate_legacy_default_app_in_ssowant_conf_json_persistent(): + from yunohost.app import app_list + from yunohost.domain import domain_config_set + + persistent_file_name = "/etc/ssowat/conf.json.persistent" + if not os.path.exists(persistent_file_name): + return + + # Ugly hack because for some reason so many people have tabs in their conf.json.persistent ... + os.system(r"sed -i 's/\t/ /g' /etc/ssowat/conf.json.persistent") + + # Ugly hack to try not to misarably fail migration + persistent = read_yaml(persistent_file_name) + + if "redirected_urls" not in persistent: + return + + redirected_urls = persistent["redirected_urls"] + + if not any( + from_url.count("/") == 1 and from_url.endswith("/") + for from_url in redirected_urls + ): + return + + apps = app_list()["apps"] + + if not any(app.get("domain_path") in redirected_urls.values() for app in apps): + return + + for from_url, dest_url in redirected_urls.copy().items(): + # Not a root domain, skip + if from_url.count("/") != 1 or not from_url.endswith("/"): + continue + for app in apps: + if app.get("domain_path") != dest_url: + continue + domain_config_set(from_url.strip("/"), "feature.app.default_app", app["id"]) + del redirected_urls[from_url] + + persistent["redirected_urls"] = redirected_urls + + write_to_json(persistent_file_name, persistent, sort_keys=True, indent=4) + + logger.warning( + "YunoHost automatically translated some legacy redirections in /etc/ssowat/conf.json.persistent to match the new default application using domain configuration" + ) + + +LEGACY_PHP_VERSION_REPLACEMENTS = [ + ("/etc/php5", "/etc/php/7.4"), + ("/etc/php/7.0", "/etc/php/7.4"), + ("/etc/php/7.3", "/etc/php/7.4"), + ("/var/run/php5-fpm", "/var/run/php/php7.4-fpm"), + ("/var/run/php/php7.0-fpm", "/var/run/php/php7.4-fpm"), + ("/var/run/php/php7.3-fpm", "/var/run/php/php7.4-fpm"), + ("php5", "php7.4"), + ("php7.0", "php7.4"), + ("php7.3", "php7.4"), + ('YNH_PHP_VERSION="7.3"', 'YNH_PHP_VERSION="7.4"'), + ( + 'phpversion="${phpversion:-7.0}"', + 'phpversion="${phpversion:-7.4}"', + ), # Many helpers like the composer ones use 7.0 by default ... + ( + 'phpversion="${phpversion:-7.3}"', + 'phpversion="${phpversion:-7.4}"', + ), # Many helpers like the composer ones use 7.0 by default ... + ( + '"$phpversion" == "7.0"', + '$(bc <<< "$phpversion >= 7.4") -eq 1', + ), # patch ynh_install_php to refuse installing/removing php <= 7.3 + ( + '"$phpversion" == "7.3"', + '$(bc <<< "$phpversion >= 7.4") -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(f"-e 's@{p}@{r}@g' " 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") in ["/etc/php/7.0/fpm", "/etc/php/7.3/fpm"]: + settings["fpm_config_dir"] = "/etc/php/7.4/fpm" + if settings.get("fpm_service") in ["php7.0-fpm", "php7.3-fpm"]: + settings["fpm_service"] = "php7.4-fpm" + if settings.get("phpversion") in ["7.0", "7.3"]: + settings["phpversion"] = "7.4" + + # We delete these checksums otherwise the file will appear as manually modified + list_to_remove = [ + "checksum__etc_php_7.3_fpm_pool", + "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 = { + "yunohost app initdb": {"important": True}, + "yunohost app checkport": {"important": True}, + "yunohost tools port-available": {"important": True}, + "yunohost app checkurl": {"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"], + "important": True, + }, + # Old $1, $2 in backup/restore scripts... + "backup_dir=$1": { + "only_for": ["scripts/backup", "scripts/restore"], + "important": True, + }, + # Old $1, $2 in backup/restore scripts... + "restore_dir=$1": {"only_for": ["scripts/restore"], "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/utils/network.py b/src/utils/network.py similarity index 66% rename from src/yunohost/utils/network.py rename to src/utils/network.py index e332a5a25..28dcb204c 100644 --- a/src/yunohost/utils/network.py +++ b/src/utils/network.py @@ -22,7 +22,6 @@ import os import re import logging import time -import dns.resolver from moulinette.utils.filesystem import read_file, write_to_file from moulinette.utils.network import download_text @@ -45,7 +44,7 @@ def get_public_ip(protocol=4): ): ip = read_file(cache_file).strip() ip = ip if ip else None # Empty file (empty string) means there's no IP - logger.debug("Reusing IPv%s from cache: %s" % (protocol, ip)) + logger.debug(f"Reusing IPv{protocol} from cache: {ip}") else: ip = get_public_ip_from_remote_server(protocol) logger.debug("IP fetched: %s" % ip) @@ -88,7 +87,7 @@ def get_public_ip_from_remote_server(protocol=4): try: return download_text(url, timeout=30).strip() except Exception as e: - logger.debug("Could not get public IPv%s : %s" % (str(protocol), str(e))) + logger.debug(f"Could not get public IPv{protocol} : {e}") return None @@ -124,76 +123,6 @@ def get_gateway(): return addr.popitem()[1] if len(addr) == 1 else None -# Lazy dev caching to avoid re-reading the file multiple time when calling -# dig() often during same yunohost operation -external_resolvers_ = [] - - -def external_resolvers(): - - global external_resolvers_ - - if not external_resolvers_: - resolv_dnsmasq_conf = read_file("/etc/resolv.dnsmasq.conf").split("\n") - external_resolvers_ = [ - r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver") - ] - # We keep only ipv4 resolvers, otherwise on IPv4-only instances, IPv6 - # will be tried anyway resulting in super-slow dig requests that'll wait - # until timeout... - external_resolvers_ = [r for r in external_resolvers_ if ":" not in r] - - return external_resolvers_ - - -def dig( - qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False -): - """ - Do a quick DNS request and avoid the "search" trap inside /etc/resolv.conf - """ - - # It's very important to do the request with a qname ended by . - # If we don't and the domain fail, dns resolver try a second request - # by concatenate the qname with the end of the "hostname" - if not qname.endswith("."): - qname += "." - - if resolvers == "local": - resolvers = ["127.0.0.1"] - elif resolvers == "force_external": - resolvers = external_resolvers() - else: - assert isinstance(resolvers, list) - - resolver = dns.resolver.Resolver(configure=False) - resolver.use_edns(0, 0, edns_size) - resolver.nameservers = resolvers - # resolver.timeout is used to trigger the next DNS query on resolvers list. - # In python-dns 1.16, this value is set to 2.0. However, this means that if - # the 3 first dns resolvers in list are down, we wait 6 seconds before to - # run the DNS query to a DNS resolvers up... - # In diagnosis dnsrecords, with 10 domains this means at least 12min, too long. - resolver.timeout = 1.0 - # resolver.lifetime is the timeout for resolver.query() - # By default set it to 5 seconds to allow 4 resolvers to be unreachable. - resolver.lifetime = timeout - try: - answers = resolver.query(qname, rdtype) - except ( - dns.resolver.NXDOMAIN, - dns.resolver.NoNameservers, - dns.resolver.NoAnswer, - dns.exception.Timeout, - ) as e: - return ("nok", (e.__class__.__name__, e)) - - if not full_answers: - answers = [answer.to_text() for answer in answers] - - return ("ok", answers) - - def _extract_inet(string, skip_netmask=False, skip_loopback=True): """ Extract IP addresses (v4 and/or v6) from a string limited to one diff --git a/src/yunohost/utils/packages.py b/src/utils/packages.py similarity index 100% rename from src/yunohost/utils/packages.py rename to src/utils/packages.py diff --git a/src/yunohost/utils/password.py b/src/utils/password.py similarity index 95% rename from src/yunohost/utils/password.py rename to src/utils/password.py index 9e693d8cd..5b8372962 100644 --- a/src/yunohost/utils/password.py +++ b/src/utils/password.py @@ -36,7 +36,7 @@ SMALL_PWD_LIST = [ "rpi", ] -MOST_USED_PASSWORDS = "/usr/share/yunohost/other/password/100000-most-used.txt" +MOST_USED_PASSWORDS = "/usr/share/yunohost/100000-most-used-passwords.txt" # Length, digits, lowers, uppers, others STRENGTH_LEVELS = [ @@ -51,7 +51,7 @@ def assert_password_is_strong_enough(profile, password): PasswordValidator(profile).validate(password) -class PasswordValidator(object): +class PasswordValidator: def __init__(self, profile): """ Initialize a password validator. @@ -111,8 +111,13 @@ class PasswordValidator(object): listed = password in SMALL_PWD_LIST or self.is_in_most_used_list(password) strength_level = self.strength_level(password) if listed: + # i18n: password_listed return ("error", "password_listed") if strength_level < self.validation_strength: + # i18n: password_too_simple_1 + # i18n: password_too_simple_2 + # i18n: password_too_simple_3 + # i18n: password_too_simple_4 return ("error", "password_too_simple_%s" % self.validation_strength) return ("success", "") diff --git a/src/yunohost/utils/yunopaste.py b/src/utils/yunopaste.py similarity index 98% rename from src/yunohost/utils/yunopaste.py rename to src/utils/yunopaste.py index 0c3e3c998..35e829991 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/utils/yunopaste.py @@ -49,7 +49,7 @@ def yunopaste(data): raw_msg=True, ) - return "%s/raw/%s" % (paste_server, url) + return "{}/raw/{}".format(paste_server, url) def anonymize(data): diff --git a/src/yunohost/vendor/acme_tiny/__init__.py b/src/vendor/__init__.py similarity index 100% rename from src/yunohost/vendor/acme_tiny/__init__.py rename to src/vendor/__init__.py diff --git a/src/vendor/acme_tiny/__init__.py b/src/vendor/acme_tiny/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/yunohost/vendor/acme_tiny/acme_tiny.py b/src/vendor/acme_tiny/acme_tiny.py similarity index 91% rename from src/yunohost/vendor/acme_tiny/acme_tiny.py rename to src/vendor/acme_tiny/acme_tiny.py index 3c13d13ec..0d2534df9 100644 --- a/src/yunohost/vendor/acme_tiny/acme_tiny.py +++ b/src/vendor/acme_tiny/acme_tiny.py @@ -38,7 +38,7 @@ def get_crt( ) out, err = proc.communicate(cmd_input) if proc.returncode != 0: - raise IOError("{0}\n{1}".format(err_msg, err)) + raise IOError("{}\n{}".format(err_msg, err)) return out # helper function - make request and automatically parse json response @@ -74,7 +74,7 @@ def get_crt( raise IndexError(resp_data) # allow 100 retrys for bad nonces if code not in [200, 201, 204]: raise ValueError( - "{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}\nResponse: {4}".format( + "{}:\nUrl: {}\nData: {}\nResponse Code: {}\nResponse: {}".format( err_msg, url, data, code, resp_data ) ) @@ -89,7 +89,7 @@ def get_crt( {"jwk": jwk} if acct_headers is None else {"kid": acct_headers["Location"]} ) protected64 = _b64(json.dumps(protected).encode("utf8")) - protected_input = "{0}.{1}".format(protected64, payload64).encode("utf8") + protected_input = "{}.{}".format(protected64, payload64).encode("utf8") out = _cmd( ["openssl", "dgst", "-sha256", "-sign", account_key], stdin=subprocess.PIPE, @@ -125,8 +125,8 @@ def get_crt( pub_hex, pub_exp = re.search( pub_pattern, out.decode("utf8"), re.MULTILINE | re.DOTALL ).groups() - pub_exp = "{0:x}".format(int(pub_exp)) - pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp + pub_exp = "{:x}".format(int(pub_exp)) + pub_exp = "0{}".format(pub_exp) if len(pub_exp) % 2 else pub_exp alg = "RS256" jwk = { "e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))), @@ -140,9 +140,9 @@ def get_crt( log.info("Parsing CSR...") out = _cmd( ["openssl", "req", "-in", csr, "-noout", "-text"], - err_msg="Error loading {0}".format(csr), + err_msg="Error loading {}".format(csr), ) - domains = set([]) + domains = set() common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode("utf8")) if common_name is not None: domains.add(common_name.group(1)) @@ -155,7 +155,7 @@ def get_crt( for san in subject_alt_names.group(1).split(", "): if san.startswith("DNS:"): domains.add(san[4:]) - log.info("Found domains: {0}".format(", ".join(domains))) + log.info("Found domains: {}".format(", ".join(domains))) # get the ACME directory of urls log.info("Getting directory...") @@ -178,7 +178,7 @@ def get_crt( {"contact": contact}, "Error updating contact details", ) - log.info("Updated contact details:\n{0}".format("\n".join(account["contact"]))) + log.info("Updated contact details:\n{}".format("\n".join(account["contact"]))) # create a new order log.info("Creating new order...") @@ -194,46 +194,46 @@ def get_crt( auth_url, None, "Error getting challenges" ) domain = authorization["identifier"]["value"] - log.info("Verifying {0}...".format(domain)) + log.info("Verifying {}...".format(domain)) # find the http-01 challenge and write the challenge file challenge = [c for c in authorization["challenges"] if c["type"] == "http-01"][ 0 ] token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge["token"]) - keyauthorization = "{0}.{1}".format(token, thumbprint) + keyauthorization = "{}.{}".format(token, thumbprint) wellknown_path = os.path.join(acme_dir, token) with open(wellknown_path, "w") as wellknown_file: wellknown_file.write(keyauthorization) # check that the file is in place try: - wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format( + wellknown_url = "http://{}/.well-known/acme-challenge/{}".format( domain, token ) assert disable_check or _do_request(wellknown_url)[0] == keyauthorization except (AssertionError, ValueError) as e: raise ValueError( - "Wrote file to {0}, but couldn't download {1}: {2}".format( + "Wrote file to {}, but couldn't download {}: {}".format( wellknown_path, wellknown_url, e ) ) # say the challenge is done _send_signed_request( - challenge["url"], {}, "Error submitting challenges: {0}".format(domain) + challenge["url"], {}, "Error submitting challenges: {}".format(domain) ) authorization = _poll_until_not( auth_url, ["pending"], - "Error checking challenge status for {0}".format(domain), + "Error checking challenge status for {}".format(domain), ) if authorization["status"] != "valid": raise ValueError( - "Challenge did not pass for {0}: {1}".format(domain, authorization) + "Challenge did not pass for {}: {}".format(domain, authorization) ) os.remove(wellknown_path) - log.info("{0} verified!".format(domain)) + log.info("{} verified!".format(domain)) # finalize the order with the csr log.info("Signing certificate...") @@ -251,7 +251,7 @@ def get_crt( "Error checking order status", ) if order["status"] != "valid": - raise ValueError("Order failed: {0}".format(order)) + raise ValueError("Order failed: {}".format(order)) # download the certificate certificate_pem, _, _ = _send_signed_request( diff --git a/src/yunohost/vendor/spectre-meltdown-checker/Dockerfile b/src/vendor/spectre-meltdown-checker/Dockerfile similarity index 100% rename from src/yunohost/vendor/spectre-meltdown-checker/Dockerfile rename to src/vendor/spectre-meltdown-checker/Dockerfile diff --git a/src/yunohost/vendor/spectre-meltdown-checker/LICENSE b/src/vendor/spectre-meltdown-checker/LICENSE similarity index 100% rename from src/yunohost/vendor/spectre-meltdown-checker/LICENSE rename to src/vendor/spectre-meltdown-checker/LICENSE diff --git a/src/yunohost/vendor/spectre-meltdown-checker/README.md b/src/vendor/spectre-meltdown-checker/README.md similarity index 100% rename from src/yunohost/vendor/spectre-meltdown-checker/README.md rename to src/vendor/spectre-meltdown-checker/README.md diff --git a/src/yunohost/vendor/spectre-meltdown-checker/docker-compose.yml b/src/vendor/spectre-meltdown-checker/docker-compose.yml similarity index 100% rename from src/yunohost/vendor/spectre-meltdown-checker/docker-compose.yml rename to src/vendor/spectre-meltdown-checker/docker-compose.yml diff --git a/src/yunohost/vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh b/src/vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh similarity index 100% rename from src/yunohost/vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh rename to src/vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh diff --git a/src/yunohost/data_migrations/0015_migrate_to_buster.py b/src/yunohost/data_migrations/0015_migrate_to_buster.py deleted file mode 100644 index 4f2d4caf8..000000000 --- a/src/yunohost/data_migrations/0015_migrate_to_buster.py +++ /dev/null @@ -1,291 +0,0 @@ -import glob -import os - -from moulinette import m18n -from yunohost.utils.error import YunohostError -from moulinette.utils.log import getActionLogger -from moulinette.utils.process import check_output, call_async_output -from moulinette.utils.filesystem import read_file - -from yunohost.tools import Migration, tools_update, tools_upgrade -from yunohost.app import unstable_apps -from yunohost.regenconf import manually_modified_files -from yunohost.utils.filesystem import free_space_in_directory -from yunohost.utils.packages import ( - get_ynh_package_version, - _list_upgradable_apt_packages, -) - -logger = getActionLogger("yunohost.migration") - - -class MyMigration(Migration): - - "Upgrade the system to Debian Buster and Yunohost 4.x" - - mode = "manual" - - def run(self): - - self.check_assertions() - - logger.info(m18n.n("migration_0015_start")) - - # - # Make sure certificates do not use weak signature hash algorithms (md5, sha1) - # otherwise nginx will later refuse to start which result in - # catastrophic situation - # - self.validate_and_upgrade_cert_if_necessary() - - # - # Patch sources.list - # - logger.info(m18n.n("migration_0015_patching_sources_list")) - self.patch_apt_sources_list() - tools_update(target="system") - - # Tell libc6 it's okay to restart system stuff during the upgrade - os.system( - "echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections" - ) - - # Don't send an email to root about the postgresql migration. It should be handled automatically after. - os.system( - "echo 'postgresql-common postgresql-common/obsolete-major seen true' | debconf-set-selections" - ) - - # - # Specific packages upgrades - # - logger.info(m18n.n("migration_0015_specific_upgrade")) - - # Update unscd independently, was 0.53-1+yunohost on stretch (custom build of ours) but now it's 0.53-1+b1 on vanilla buster, - # which for apt appears as a lower version (hence the --allow-downgrades and the hardcoded version number) - unscd_version = check_output( - 'dpkg -s unscd | grep "^Version: " | cut -d " " -f 2' - ) - if "yunohost" in unscd_version: - new_version = check_output( - "LC_ALL=C apt policy unscd 2>/dev/null | grep -v '\\*\\*\\*' | grep http -B1 | head -n 1 | awk '{print $1}'" - ).strip() - if new_version: - self.apt_install("unscd=%s --allow-downgrades" % new_version) - else: - logger.warning("Could not identify which version of unscd to install") - - # Upgrade libpam-modules independently, small issue related to willing to overwrite a file previously provided by Yunohost - libpammodules_version = check_output( - 'dpkg -s libpam-modules | grep "^Version: " | cut -d " " -f 2' - ) - if not libpammodules_version.startswith("1.3"): - self.apt_install('libpam-modules -o Dpkg::Options::="--force-overwrite"') - - # - # Main upgrade - # - logger.info(m18n.n("migration_0015_main_upgrade")) - - apps_packages = self.get_apps_equivs_packages() - self.hold(apps_packages) - tools_upgrade(target="system", allow_yunohost_upgrade=False) - - if self.debian_major_version() == 9: - raise YunohostError("migration_0015_still_on_stretch_after_main_upgrade") - - # Clean the mess - logger.info(m18n.n("migration_0015_cleaning_up")) - os.system("apt autoremove --assume-yes") - os.system("apt clean --assume-yes") - - # - # Yunohost upgrade - # - logger.info(m18n.n("migration_0015_yunohost_upgrade")) - self.unhold(apps_packages) - tools_upgrade(target="system") - - def debian_major_version(self): - # The python module "platform" and lsb_release are not reliable because - # on some setup, they may still return Release=9 even after upgrading to - # buster ... (Apparently this is related to OVH overriding some stuff - # with /etc/lsb-release for instance -_-) - # Instead, we rely on /etc/os-release which should be the raw info from - # the distribution... - return int( - check_output( - "grep VERSION_ID /etc/os-release | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2" - ) - ) - - def yunohost_major_version(self): - return int(get_ynh_package_version("yunohost")["version"].split(".")[0]) - - def check_assertions(self): - - # Be on stretch (9.x) and yunohost 3.x - # NB : we do both check to cover situations where the upgrade crashed - # in the middle and debian version could be > 9.x but yunohost package - # would still be in 3.x... - if ( - not self.debian_major_version() == 9 - and not self.yunohost_major_version() == 3 - ): - raise YunohostError("migration_0015_not_stretch") - - # Have > 1 Go free space on /var/ ? - if free_space_in_directory("/var/") / (1024 ** 3) < 1.0: - raise YunohostError("migration_0015_not_enough_free_space") - - # Check system is up to date - # (but we don't if 'stretch' is already in the sources.list ... - # which means maybe a previous upgrade crashed and we're re-running it) - if " buster " not in read_file("/etc/apt/sources.list"): - tools_update(target="system") - upgradable_system_packages = list(_list_upgradable_apt_packages()) - if upgradable_system_packages: - raise YunohostError("migration_0015_system_not_fully_up_to_date") - - @property - def disclaimer(self): - - # Avoid having a super long disclaimer + uncessary check if we ain't - # on stretch / yunohost 3.x anymore - # NB : we do both check to cover situations where the upgrade crashed - # in the middle and debian version could be >= 10.x but yunohost package - # would still be in 3.x... - if ( - not self.debian_major_version() == 9 - and not self.yunohost_major_version() == 3 - ): - return None - - # Get list of problematic apps ? I.e. not official or community+working - problematic_apps = unstable_apps() - problematic_apps = "".join(["\n - " + app for app in problematic_apps]) - - # Manually modified files ? (c.f. yunohost service regen-conf) - modified_files = manually_modified_files() - modified_files = "".join(["\n - " + f for f in modified_files]) - - message = m18n.n("migration_0015_general_warning") - - message = ( - "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/12195\n\n" - + message - ) - - if problematic_apps: - message += "\n\n" + m18n.n( - "migration_0015_problematic_apps_warning", - problematic_apps=problematic_apps, - ) - - if modified_files: - message += "\n\n" + m18n.n( - "migration_0015_modified_files", manually_modified_files=modified_files - ) - - return message - - def patch_apt_sources_list(self): - - sources_list = glob.glob("/etc/apt/sources.list.d/*.list") - sources_list.append("/etc/apt/sources.list") - - # This : - # - replace single 'stretch' occurence by 'buster' - # - comments lines containing "backports" - # - replace 'stretch/updates' by 'strech/updates' (or same with -) - for f in sources_list: - command = ( - "sed -i -e 's@ stretch @ buster @g' " - "-e '/backports/ s@^#*@#@' " - "-e 's@ stretch/updates @ buster/updates @g' " - "-e 's@ stretch-@ buster-@g' " - "{}".format(f) - ) - os.system(command) - - def get_apps_equivs_packages(self): - - command = ( - "dpkg --get-selections" - " | grep -v deinstall" - " | awk '{print $1}'" - " | { grep 'ynh-deps$' || true; }" - ) - - output = check_output(command) - - return output.split("\n") if output else [] - - def hold(self, packages): - for package in packages: - os.system("apt-mark hold {}".format(package)) - - def unhold(self, packages): - for package in packages: - os.system("apt-mark unhold {}".format(package)) - - def apt_install(self, cmd): - def is_relevant(line): - return "Reading database ..." not in line.rstrip() - - callbacks = ( - lambda l: logger.info("+ " + l.rstrip() + "\r") - if is_relevant(l) - else logger.debug(l.rstrip() + "\r"), - lambda l: logger.warning(l.rstrip()), - ) - - cmd = ( - "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt install --quiet -o=Dpkg::Use-Pty=0 --fix-broken --assume-yes " - + cmd - ) - - logger.debug("Running: %s" % cmd) - - call_async_output(cmd, callbacks, shell=True) - - def validate_and_upgrade_cert_if_necessary(self): - - active_certs = set( - check_output("grep -roh '/.*crt.pem' /etc/nginx/").split("\n") - ) - - cmd = "LC_ALL=C openssl x509 -in %s -text -noout | grep -i 'Signature Algorithm:' | awk '{print $3}' | uniq" - - default_crt = "/etc/yunohost/certs/yunohost.org/crt.pem" - default_key = "/etc/yunohost/certs/yunohost.org/key.pem" - default_signature = ( - check_output(cmd % default_crt) if default_crt in active_certs else None - ) - if default_signature is not None and ( - default_signature.startswith("md5") or default_signature.startswith("sha1") - ): - logger.warning( - "%s is using a pretty old certificate incompatible with newer versions of nginx ... attempting to regenerate a fresh one" - % default_crt - ) - - os.system("mv %s %s.old" % (default_crt, default_crt)) - os.system("mv %s %s.old" % (default_key, default_key)) - ret = os.system("/usr/share/yunohost/hooks/conf_regen/02-ssl init") - - if ret != 0 or not os.path.exists(default_crt): - logger.error("Upgrading the certificate failed ... reverting") - os.system("mv %s.old %s" % (default_crt, default_crt)) - os.system("mv %s.old %s" % (default_key, default_key)) - - signatures = {cert: check_output(cmd % cert) for cert in active_certs} - - def cert_is_weak(cert): - sig = signatures[cert] - return sig.startswith("md5") or sig.startswith("sha1") - - weak_certs = [cert for cert in signatures.keys() if cert_is_weak(cert)] - if weak_certs: - raise YunohostError( - "migration_0015_weak_certs", certs=", ".join(weak_certs) - ) diff --git a/src/yunohost/data_migrations/0016_php70_to_php73_pools.py b/src/yunohost/data_migrations/0016_php70_to_php73_pools.py deleted file mode 100644 index 6b424f211..000000000 --- a/src/yunohost/data_migrations/0016_php70_to_php73_pools.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -import glob -from shutil import copy2 - -from moulinette.utils.log import getActionLogger - -from yunohost.app import _is_installed, _patch_legacy_php_versions_in_settings -from yunohost.tools import Migration -from yunohost.service import _run_service_command - -logger = getActionLogger("yunohost.migration") - -PHP70_POOLS = "/etc/php/7.0/fpm/pool.d" -PHP73_POOLS = "/etc/php/7.3/fpm/pool.d" - -PHP70_SOCKETS_PREFIX = "/run/php/php7.0-fpm" -PHP73_SOCKETS_PREFIX = "/run/php/php7.3-fpm" - -MIGRATION_COMMENT = ( - "; YunoHost note : this file was automatically moved from {}".format(PHP70_POOLS) -) - - -class MyMigration(Migration): - - "Migrate php7.0-fpm 'pool' conf files to php7.3" - - dependencies = ["migrate_to_buster"] - - def run(self): - # Get list of php7.0 pool files - php70_pool_files = glob.glob("{}/*.conf".format(PHP70_POOLS)) - - # Keep only basenames - php70_pool_files = [os.path.basename(f) for f in php70_pool_files] - - # Ignore the "www.conf" (default stuff, probably don't want to touch it ?) - php70_pool_files = [f for f in php70_pool_files if f != "www.conf"] - - for f in php70_pool_files: - - # Copy the files to the php7.3 pool - src = "{}/{}".format(PHP70_POOLS, f) - dest = "{}/{}".format(PHP73_POOLS, f) - copy2(src, dest) - - # Replace the socket prefix if it's found - c = "sed -i -e 's@{}@{}@g' {}".format( - PHP70_SOCKETS_PREFIX, PHP73_SOCKETS_PREFIX, dest - ) - os.system(c) - - # Also add a comment that it was automatically moved from php7.0 - # (for human traceability and backward migration) - c = "sed -i '1i {}' {}".format(MIGRATION_COMMENT, dest) - os.system(c) - - app_id = os.path.basename(f)[: -len(".conf")] - if _is_installed(app_id): - _patch_legacy_php_versions_in_settings( - "/etc/yunohost/apps/%s/" % app_id - ) - - nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/%s.conf" % app_id) - for f in nginx_conf_files: - # Replace the socket prefix if it's found - c = "sed -i -e 's@{}@{}@g' {}".format( - PHP70_SOCKETS_PREFIX, PHP73_SOCKETS_PREFIX, f - ) - os.system(c) - - os.system( - "rm /etc/logrotate.d/php7.0-fpm" - ) # We remove this otherwise the logrotate cron will be unhappy - - # Reload/restart the php pools - _run_service_command("restart", "php7.3-fpm") - _run_service_command("enable", "php7.3-fpm") - os.system("systemctl stop php7.0-fpm") - os.system("systemctl disable php7.0-fpm") - - # Reload nginx - _run_service_command("reload", "nginx") diff --git a/src/yunohost/data_migrations/0018_xtable_to_nftable.py b/src/yunohost/data_migrations/0018_xtable_to_nftable.py deleted file mode 100644 index 94b47d944..000000000 --- a/src/yunohost/data_migrations/0018_xtable_to_nftable.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -import subprocess - -from moulinette import m18n -from yunohost.utils.error import YunohostError -from moulinette.utils.log import getActionLogger - -from yunohost.firewall import firewall_reload -from yunohost.service import service_restart -from yunohost.tools import Migration - -logger = getActionLogger("yunohost.migration") - - -class MyMigration(Migration): - - "Migrate legacy iptables rules from stretch that relied on xtable and should now rely on nftable" - - dependencies = ["migrate_to_buster"] - - def run(self): - - self.do_ipv4 = os.system("iptables -w -L >/dev/null") == 0 - self.do_ipv6 = os.system("ip6tables -w -L >/dev/null") == 0 - - if not self.do_ipv4: - logger.warning(m18n.n("iptables_unavailable")) - if not self.do_ipv6: - logger.warning(m18n.n("ip6tables_unavailable")) - - backup_folder = "/home/yunohost.backup/premigration/xtable_to_nftable/" - if not os.path.exists(backup_folder): - os.makedirs(backup_folder, 0o750) - self.backup_rules_ipv4 = os.path.join(backup_folder, "legacy_rules_ipv4") - self.backup_rules_ipv6 = os.path.join(backup_folder, "legacy_rules_ipv6") - - # Backup existing legacy rules to be able to rollback - if self.do_ipv4 and not os.path.exists(self.backup_rules_ipv4): - self.runcmd( - "iptables-legacy -L >/dev/null" - ) # For some reason if we don't do this, iptables-legacy-save is empty ? - self.runcmd("iptables-legacy-save > %s" % self.backup_rules_ipv4) - assert ( - open(self.backup_rules_ipv4).read().strip() - ), "Uhoh backup of legacy ipv4 rules is empty !?" - if self.do_ipv6 and not os.path.exists(self.backup_rules_ipv6): - self.runcmd( - "ip6tables-legacy -L >/dev/null" - ) # For some reason if we don't do this, iptables-legacy-save is empty ? - self.runcmd("ip6tables-legacy-save > %s" % self.backup_rules_ipv6) - assert ( - open(self.backup_rules_ipv6).read().strip() - ), "Uhoh backup of legacy ipv6 rules is empty !?" - - # We inject the legacy rules (iptables-legacy) into the new iptable (just "iptables") - try: - if self.do_ipv4: - self.runcmd("iptables-legacy-save | iptables-restore") - if self.do_ipv6: - self.runcmd("ip6tables-legacy-save | ip6tables-restore") - except Exception as e: - self.rollback() - raise YunohostError( - "migration_0018_failed_to_migrate_iptables_rules", error=e - ) - - # Reset everything in iptables-legacy - # Stolen from https://serverfault.com/a/200642 - try: - if self.do_ipv4: - self.runcmd( - "iptables-legacy-save | awk '/^[*]/ { print $1 }" # Keep lines like *raw, *filter and *nat - ' /^:[A-Z]+ [^-]/ { print $1 " ACCEPT" ; }' # Turn all policies to accept - " /COMMIT/ { print $0; }'" # Keep the line COMMIT - " | iptables-legacy-restore" - ) - if self.do_ipv6: - self.runcmd( - "ip6tables-legacy-save | awk '/^[*]/ { print $1 }" # Keep lines like *raw, *filter and *nat - ' /^:[A-Z]+ [^-]/ { print $1 " ACCEPT" ; }' # Turn all policies to accept - " /COMMIT/ { print $0; }'" # Keep the line COMMIT - " | ip6tables-legacy-restore" - ) - except Exception as e: - self.rollback() - raise YunohostError("migration_0018_failed_to_reset_legacy_rules", error=e) - - # You might be wondering "uh but is it really useful to - # iptables-legacy-save | iptables-restore considering firewall_reload() - # flush/resets everything anyway ?" - # But the answer is : firewall_reload() only resets the *filter table. - # On more complex setups (e.g. internet cube or docker) you will also - # have rules in the *nat (or maybe *raw?) sections of iptables. - firewall_reload() - service_restart("fail2ban") - - def rollback(self): - - if self.do_ipv4: - self.runcmd("iptables-legacy-restore < %s" % self.backup_rules_ipv4) - if self.do_ipv6: - self.runcmd("iptables-legacy-restore < %s" % self.backup_rules_ipv6) - - def runcmd(self, cmd, raise_on_errors=True): - - logger.debug("Running command: " + cmd) - - p = subprocess.Popen( - cmd, - shell=True, - executable="/bin/bash", - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - out, err = p.communicate() - returncode = p.returncode - if raise_on_errors and returncode != 0: - raise YunohostError( - "Failed to run command '{}'.\nreturncode: {}\nstdout:\n{}\nstderr:\n{}\n".format( - cmd, returncode, out, err - ) - ) - - out = out.strip().split(b"\n") - return (returncode, out, err) diff --git a/src/yunohost/data_migrations/0019_extend_permissions_features.py b/src/yunohost/data_migrations/0019_extend_permissions_features.py deleted file mode 100644 index 5d4343deb..000000000 --- a/src/yunohost/data_migrations/0019_extend_permissions_features.py +++ /dev/null @@ -1,107 +0,0 @@ -from moulinette import m18n -from moulinette.utils.log import getActionLogger - -from yunohost.tools import Migration -from yunohost.permission import user_permission_list -from yunohost.utils.legacy import migrate_legacy_permission_settings - -logger = getActionLogger("yunohost.migration") - - -class MyMigration(Migration): - """ - Add protected attribute in LDAP permission - """ - - @Migration.ldap_migration - def run(self, backup_folder): - - # Update LDAP database - self.add_new_ldap_attributes() - - # Migrate old settings - migrate_legacy_permission_settings() - - def add_new_ldap_attributes(self): - - from yunohost.utils.ldap import _get_ldap_interface - from yunohost.regenconf import regen_conf, BACKUP_CONF_DIR - - # Check if the migration can be processed - ldap_regen_conf_status = regen_conf(names=["slapd"], dry_run=True) - # By this we check if the have been customized - if ldap_regen_conf_status and ldap_regen_conf_status["slapd"]["pending"]: - logger.warning( - m18n.n( - "migration_0019_slapd_config_will_be_overwritten", - conf_backup_folder=BACKUP_CONF_DIR, - ) - ) - - # Update LDAP schema restart slapd - logger.info(m18n.n("migration_update_LDAP_schema")) - regen_conf(names=["slapd"], force=True) - - logger.info(m18n.n("migration_0019_add_new_attributes_in_ldap")) - ldap = _get_ldap_interface() - permission_list = user_permission_list(full=True)["permissions"] - - for permission in permission_list: - system_perms = { - "mail": "E-mail", - "xmpp": "XMPP", - "ssh": "SSH", - "sftp": "STFP", - } - if permission.split(".")[0] in system_perms: - update = { - "authHeader": ["FALSE"], - "label": [system_perms[permission.split(".")[0]]], - "showTile": ["FALSE"], - "isProtected": ["TRUE"], - } - else: - app, subperm_name = permission.split(".") - if permission.endswith(".main"): - update = { - "authHeader": ["TRUE"], - "label": [ - app - ], # Note that this is later re-changed during the call to migrate_legacy_permission_settings() if a 'label' setting exists - "showTile": ["TRUE"], - "isProtected": ["FALSE"], - } - else: - update = { - "authHeader": ["TRUE"], - "label": [subperm_name.title()], - "showTile": ["FALSE"], - "isProtected": ["TRUE"], - } - - ldap.update("cn=%s,ou=permission" % permission, update) - - introduced_in_version = "4.1" - - def run_after_system_restore(self): - # Update LDAP database - self.add_new_ldap_attributes() - - def run_before_app_restore(self, app_id): - from yunohost.app import app_setting - from yunohost.utils.legacy import migrate_legacy_permission_settings - - # Migrate old settings - legacy_permission_settings = [ - "skipped_uris", - "unprotected_uris", - "protected_uris", - "skipped_regex", - "unprotected_regex", - "protected_regex", - ] - if any( - app_setting(app_id, setting) is not None - for setting in legacy_permission_settings - ): - migrate_legacy_permission_settings(app=app_id) diff --git a/src/yunohost/data_migrations/0020_ssh_sftp_permissions.py b/src/yunohost/data_migrations/0020_ssh_sftp_permissions.py deleted file mode 100644 index f1dbcd1e7..000000000 --- a/src/yunohost/data_migrations/0020_ssh_sftp_permissions.py +++ /dev/null @@ -1,100 +0,0 @@ -import subprocess -import os - -from moulinette import m18n -from moulinette.utils.log import getActionLogger - -from yunohost.tools import Migration -from yunohost.permission import user_permission_update, permission_sync_to_user -from yunohost.regenconf import manually_modified_files - -logger = getActionLogger("yunohost.migration") - -################################################### -# Tools used also for restoration -################################################### - - -class MyMigration(Migration): - """ - Add new permissions around SSH/SFTP features - """ - - introduced_in_version = "4.2.2" - dependencies = ["extend_permissions_features"] - - @Migration.ldap_migration - def run(self, *args): - - from yunohost.utils.ldap import _get_ldap_interface - - ldap = _get_ldap_interface() - - existing_perms_raw = ldap.search( - "ou=permission,dc=yunohost,dc=org", "(objectclass=permissionYnh)", ["cn"] - ) - existing_perms = [perm["cn"][0] for perm in existing_perms_raw] - - # Add SSH and SFTP permissions - if "sftp.main" not in existing_perms: - ldap.add( - "cn=sftp.main,ou=permission", - { - "cn": "sftp.main", - "gidNumber": "5004", - "objectClass": ["posixGroup", "permissionYnh"], - "groupPermission": [], - "authHeader": "FALSE", - "label": "SFTP", - "showTile": "FALSE", - "isProtected": "TRUE", - }, - ) - - if "ssh.main" not in existing_perms: - ldap.add( - "cn=ssh.main,ou=permission", - { - "cn": "ssh.main", - "gidNumber": "5003", - "objectClass": ["posixGroup", "permissionYnh"], - "groupPermission": [], - "authHeader": "FALSE", - "label": "SSH", - "showTile": "FALSE", - "isProtected": "TRUE", - }, - ) - - # Add a bash terminal to each users - users = ldap.search( - "ou=users,dc=yunohost,dc=org", - filter="(loginShell=*)", - attrs=["dn", "uid", "loginShell"], - ) - for user in users: - if user["loginShell"][0] == "/bin/false": - dn = user["dn"][0].replace(",dc=yunohost,dc=org", "") - ldap.update(dn, {"loginShell": ["/bin/bash"]}) - else: - user_permission_update( - "ssh.main", add=user["uid"][0], sync_perm=False - ) - - permission_sync_to_user() - - # Somehow this is needed otherwise the PAM thing doesn't forget about the - # old loginShell value ? - subprocess.call(["nscd", "-i", "passwd"]) - - if ( - "/etc/ssh/sshd_config" in manually_modified_files() - and os.system( - "grep -q '^ *AllowGroups\\|^ *AllowUsers' /etc/ssh/sshd_config" - ) - != 0 - ): - logger.error(m18n.n("diagnosis_sshd_config_insecure")) - - def run_after_system_restore(self): - self.run() diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py deleted file mode 100644 index 3bc70c424..000000000 --- a/src/yunohost/domain.py +++ /dev/null @@ -1,688 +0,0 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_domain.py - - Manage domains -""" -import os -import re - -from moulinette import m18n, Moulinette -from moulinette.core import MoulinetteError -from yunohost.utils.error import YunohostError, YunohostValidationError -from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import write_to_file - -from yunohost.app import ( - app_ssowatconf, - _installed_apps, - _get_app_settings, - _get_conflicting_apps, -) -from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf -from yunohost.utils.network import get_public_ip -from yunohost.log import is_unit_operation -from yunohost.hook import hook_callback - -logger = getActionLogger("yunohost.domain") - - -def domain_list(exclude_subdomains=False): - """ - List domains - - Keyword argument: - exclude_subdomains -- Filter out domains that are subdomains of other declared domains - - """ - from yunohost.utils.ldap import _get_ldap_interface - - ldap = _get_ldap_interface() - result = [ - entry["virtualdomain"][0] - for entry in ldap.search( - "ou=domains,dc=yunohost,dc=org", "virtualdomain=*", ["virtualdomain"] - ) - ] - - result_list = [] - for domain in result: - if exclude_subdomains: - parent_domain = domain.split(".", 1)[1] - if parent_domain in result: - continue - - result_list.append(domain) - - def cmp_domain(domain): - # Keep the main part of the domain and the extension together - # eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] - domain = domain.split(".") - domain[-1] = domain[-2] + domain.pop() - domain = list(reversed(domain)) - return domain - - result_list = sorted(result_list, key=cmp_domain) - - return {"domains": result_list, "main": _get_maindomain()} - - -@is_unit_operation() -def domain_add(operation_logger, domain, dyndns=False): - """ - Create a custom domain - - Keyword argument: - domain -- Domain name to add - dyndns -- Subscribe to DynDNS - - """ - from yunohost.hook import hook_callback - from yunohost.app import app_ssowatconf - from yunohost.utils.ldap import _get_ldap_interface - from yunohost.certificate import _certificate_install_selfsigned - - if domain.startswith("xmpp-upload."): - raise YunohostValidationError("domain_cannot_add_xmpp_upload") - - ldap = _get_ldap_interface() - - try: - ldap.validate_uniqueness({"virtualdomain": domain}) - except MoulinetteError: - raise YunohostValidationError("domain_exists") - - # Lower domain to avoid some edge cases issues - # See: https://forum.yunohost.org/t/invalid-domain-causes-diagnosis-web-to-fail-fr-on-demand/11765 - domain = domain.lower() - - # Non-latin characters (e.g. café.com => xn--caf-dma.com) - domain = domain.encode("idna").decode("utf-8") - - # DynDNS domain - if dyndns: - - from yunohost.dyndns import _dyndns_provides, _guess_current_dyndns_domain - - # Do not allow to subscribe to multiple dyndns domains... - if _guess_current_dyndns_domain("dyndns.yunohost.org") != (None, None): - raise YunohostValidationError("domain_dyndns_already_subscribed") - - # Check that this domain can effectively be provided by - # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st) - if not _dyndns_provides("dyndns.yunohost.org", domain): - raise YunohostValidationError("domain_dyndns_root_unknown") - - operation_logger.start() - - if dyndns: - from yunohost.dyndns import dyndns_subscribe - - # Actually subscribe - dyndns_subscribe(domain=domain) - - _certificate_install_selfsigned([domain], False) - - try: - attr_dict = { - "objectClass": ["mailDomain", "top"], - "virtualdomain": domain, - } - - try: - ldap.add("virtualdomain=%s,ou=domains" % domain, attr_dict) - except Exception as e: - raise YunohostError("domain_creation_failed", domain=domain, error=e) - - # Don't regen these conf if we're still in postinstall - if os.path.exists("/etc/yunohost/installed"): - # Sometime we have weird issues with the regenconf where some files - # appears as manually modified even though they weren't touched ... - # There are a few ideas why this happens (like backup/restore nginx - # conf ... which we shouldnt do ...). This in turns creates funky - # situation where the regenconf may refuse to re-create the conf - # (when re-creating a domain..) - # So here we force-clear the has out of the regenconf if it exists. - # This is a pretty ad hoc solution and only applied to nginx - # because it's one of the major service, but in the long term we - # should identify the root of this bug... - _force_clear_hashes(["/etc/nginx/conf.d/%s.conf" % domain]) - regen_conf( - names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"] - ) - app_ssowatconf() - - except Exception as e: - # Force domain removal silently - try: - domain_remove(domain, force=True) - except Exception: - pass - raise e - - hook_callback("post_domain_add", args=[domain]) - - logger.success(m18n.n("domain_created")) - - -@is_unit_operation() -def domain_remove(operation_logger, domain, remove_apps=False, force=False): - """ - Delete domains - - Keyword argument: - domain -- Domain to delete - remove_apps -- Remove applications installed on the domain - force -- Force the domain removal and don't not ask confirmation to - remove apps if remove_apps is specified - - """ - from yunohost.hook import hook_callback - from yunohost.app import app_ssowatconf, app_info, app_remove - from yunohost.utils.ldap import _get_ldap_interface - - # the 'force' here is related to the exception happening in domain_add ... - # we don't want to check the domain exists because the ldap add may have - # failed - if not force and domain not in domain_list()["domains"]: - raise YunohostValidationError("domain_name_unknown", domain=domain) - - # Check domain is not the main domain - if domain == _get_maindomain(): - other_domains = domain_list()["domains"] - other_domains.remove(domain) - - if other_domains: - raise YunohostValidationError( - "domain_cannot_remove_main", - domain=domain, - other_domains="\n * " + ("\n * ".join(other_domains)), - ) - else: - raise YunohostValidationError( - "domain_cannot_remove_main_add_new_one", domain=domain - ) - - # Check if apps are installed on the domain - apps_on_that_domain = [] - - for app in _installed_apps(): - settings = _get_app_settings(app) - label = app_info(app)["name"] - if settings.get("domain") == domain: - apps_on_that_domain.append( - ( - app, - ' - %s "%s" on https://%s%s' - % (app, label, domain, settings["path"]) - if "path" in settings - else app, - ) - ) - - if apps_on_that_domain: - if remove_apps: - if Moulinette.interface.type == "cli" and not force: - answer = Moulinette.prompt( - m18n.n( - "domain_remove_confirm_apps_removal", - apps="\n".join([x[1] for x in apps_on_that_domain]), - answers="y/N", - ), - color="yellow", - ) - if answer.upper() != "Y": - raise YunohostError("aborting") - - for app, _ in apps_on_that_domain: - app_remove(app) - else: - raise YunohostValidationError( - "domain_uninstall_app_first", - apps="\n".join([x[1] for x in apps_on_that_domain]), - ) - - operation_logger.start() - - ldap = _get_ldap_interface() - try: - ldap.remove("virtualdomain=" + domain + ",ou=domains") - except Exception as e: - raise YunohostError("domain_deletion_failed", domain=domain, error=e) - - os.system("rm -rf /etc/yunohost/certs/%s" % domain) - - # Delete dyndns keys for this domain (if any) - os.system("rm -rf /etc/yunohost/dyndns/K%s.+*" % domain) - - # Sometime we have weird issues with the regenconf where some files - # appears as manually modified even though they weren't touched ... - # There are a few ideas why this happens (like backup/restore nginx - # conf ... which we shouldnt do ...). This in turns creates funky - # situation where the regenconf may refuse to re-create the conf - # (when re-creating a domain..) - # - # So here we force-clear the has out of the regenconf if it exists. - # This is a pretty ad hoc solution and only applied to nginx - # because it's one of the major service, but in the long term we - # should identify the root of this bug... - _force_clear_hashes(["/etc/nginx/conf.d/%s.conf" % domain]) - # And in addition we even force-delete the file Otherwise, if the file was - # manually modified, it may not get removed by the regenconf which leads to - # catastrophic consequences of nginx breaking because it can't load the - # cert file which disappeared etc.. - if os.path.exists("/etc/nginx/conf.d/%s.conf" % domain): - _process_regen_conf( - "/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True - ) - - regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"]) - app_ssowatconf() - - hook_callback("post_domain_remove", args=[domain]) - - logger.success(m18n.n("domain_deleted")) - - -def domain_dns_conf(domain, ttl=None): - """ - Generate DNS configuration for a domain - - Keyword argument: - domain -- Domain name - ttl -- Time to live - - """ - - if domain not in domain_list()["domains"]: - raise YunohostValidationError("domain_name_unknown", domain=domain) - - ttl = 3600 if ttl is None else ttl - - dns_conf = _build_dns_conf(domain, ttl) - - result = "" - - result += "; Basic ipv4/ipv6 records" - for record in dns_conf["basic"]: - result += "\n{name} {ttl} IN {type} {value}".format(**record) - - result += "\n\n" - result += "; XMPP" - for record in dns_conf["xmpp"]: - result += "\n{name} {ttl} IN {type} {value}".format(**record) - - result += "\n\n" - result += "; Mail" - for record in dns_conf["mail"]: - result += "\n{name} {ttl} IN {type} {value}".format(**record) - result += "\n\n" - - result += "; Extra" - for record in dns_conf["extra"]: - result += "\n{name} {ttl} IN {type} {value}".format(**record) - - for name, record_list in dns_conf.items(): - if name not in ("basic", "xmpp", "mail", "extra") and record_list: - result += "\n\n" - result += "; " + name - for record in record_list: - result += "\n{name} {ttl} IN {type} {value}".format(**record) - - if Moulinette.interface.type == "cli": - logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation")) - - return result - - -@is_unit_operation() -def domain_main_domain(operation_logger, new_main_domain=None): - """ - Check the current main domain, or change it - - Keyword argument: - new_main_domain -- The new domain to be set as the main domain - - """ - from yunohost.tools import _set_hostname - - # If no new domain specified, we return the current main domain - if not new_main_domain: - return {"current_main_domain": _get_maindomain()} - - # Check domain exists - if new_main_domain not in domain_list()["domains"]: - raise YunohostValidationError("domain_name_unknown", domain=new_main_domain) - - operation_logger.related_to.append(("domain", new_main_domain)) - operation_logger.start() - - # Apply changes to ssl certs - try: - write_to_file("/etc/yunohost/current_host", new_main_domain) - - _set_hostname(new_main_domain) - except Exception as e: - logger.warning("%s" % e, exc_info=1) - raise YunohostError("main_domain_change_failed") - - # Generate SSOwat configuration file - app_ssowatconf() - - # Regen configurations - if os.path.exists("/etc/yunohost/installed"): - regen_conf() - - logger.success(m18n.n("main_domain_changed")) - - -def domain_cert_status(domain_list, full=False): - import yunohost.certificate - - return yunohost.certificate.certificate_status(domain_list, full) - - -def domain_cert_install( - domain_list, force=False, no_checks=False, self_signed=False, staging=False -): - import yunohost.certificate - - return yunohost.certificate.certificate_install( - domain_list, force, no_checks, self_signed, staging - ) - - -def domain_cert_renew( - domain_list, force=False, no_checks=False, email=False, staging=False -): - import yunohost.certificate - - return yunohost.certificate.certificate_renew( - domain_list, force, no_checks, email, staging - ) - - -def domain_url_available(domain, path): - """ - Check availability of a web path - - Keyword argument: - domain -- The domain for the web path (e.g. your.domain.tld) - path -- The path to check (e.g. /coffee) - """ - - return len(_get_conflicting_apps(domain, path)) == 0 - - -def _get_maindomain(): - with open("/etc/yunohost/current_host", "r") as f: - maindomain = f.readline().rstrip() - return maindomain - - -def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False): - """ - Internal function that will returns a data structure containing the needed - information to generate/adapt the dns configuration - - The returned datastructure will have the following form: - { - "basic": [ - # if ipv4 available - {"type": "A", "name": "@", "value": "123.123.123.123", "ttl": 3600}, - # if ipv6 available - {"type": "AAAA", "name": "@", "value": "valid-ipv6", "ttl": 3600}, - ], - "xmpp": [ - {"type": "SRV", "name": "_xmpp-client._tcp", "value": "0 5 5222 domain.tld.", "ttl": 3600}, - {"type": "SRV", "name": "_xmpp-server._tcp", "value": "0 5 5269 domain.tld.", "ttl": 3600}, - {"type": "CNAME", "name": "muc", "value": "@", "ttl": 3600}, - {"type": "CNAME", "name": "pubsub", "value": "@", "ttl": 3600}, - {"type": "CNAME", "name": "vjud", "value": "@", "ttl": 3600} - {"type": "CNAME", "name": "xmpp-upload", "value": "@", "ttl": 3600} - ], - "mail": [ - {"type": "MX", "name": "@", "value": "10 domain.tld.", "ttl": 3600}, - {"type": "TXT", "name": "@", "value": "\"v=spf1 a mx ip4:123.123.123.123 ipv6:valid-ipv6 -all\"", "ttl": 3600 }, - {"type": "TXT", "name": "mail._domainkey", "value": "\"v=DKIM1; k=rsa; p=some-super-long-key\"", "ttl": 3600}, - {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600} - ], - "extra": [ - # if ipv4 available - {"type": "A", "name": "*", "value": "123.123.123.123", "ttl": 3600}, - # if ipv6 available - {"type": "AAAA", "name": "*", "value": "valid-ipv6", "ttl": 3600}, - {"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600}, - ], - "example_of_a_custom_rule": [ - {"type": "SRV", "name": "_matrix", "value": "domain.tld.", "ttl": 3600} - ], - } - """ - - ipv4 = get_public_ip() - ipv6 = get_public_ip(6) - - ########################### - # Basic ipv4/ipv6 records # - ########################### - - basic = [] - if ipv4: - basic.append(["@", ttl, "A", ipv4]) - - if ipv6: - basic.append(["@", ttl, "AAAA", ipv6]) - elif include_empty_AAAA_if_no_ipv6: - basic.append(["@", ttl, "AAAA", None]) - - ######### - # Email # - ######### - - mail = [ - ["@", ttl, "MX", "10 %s." % domain], - ["@", ttl, "TXT", '"v=spf1 a mx -all"'], - ] - - # DKIM/DMARC record - dkim_host, dkim_publickey = _get_DKIM(domain) - - if dkim_host: - mail += [ - [dkim_host, ttl, "TXT", dkim_publickey], - ["_dmarc", ttl, "TXT", '"v=DMARC1; p=none"'], - ] - - ######## - # XMPP # - ######## - - xmpp = [ - ["_xmpp-client._tcp", ttl, "SRV", "0 5 5222 %s." % domain], - ["_xmpp-server._tcp", ttl, "SRV", "0 5 5269 %s." % domain], - ["muc", ttl, "CNAME", "@"], - ["pubsub", ttl, "CNAME", "@"], - ["vjud", ttl, "CNAME", "@"], - ["xmpp-upload", ttl, "CNAME", "@"], - ] - - ######### - # Extra # - ######### - - extra = [] - - if ipv4: - extra.append(["*", ttl, "A", ipv4]) - - if ipv6: - extra.append(["*", ttl, "AAAA", ipv6]) - elif include_empty_AAAA_if_no_ipv6: - extra.append(["*", ttl, "AAAA", None]) - - extra.append(["@", ttl, "CAA", '128 issue "letsencrypt.org"']) - - #################### - # Standard records # - #################### - - records = { - "basic": [ - {"name": name, "ttl": ttl_, "type": type_, "value": value} - for name, ttl_, type_, value in basic - ], - "xmpp": [ - {"name": name, "ttl": ttl_, "type": type_, "value": value} - for name, ttl_, type_, value in xmpp - ], - "mail": [ - {"name": name, "ttl": ttl_, "type": type_, "value": value} - for name, ttl_, type_, value in mail - ], - "extra": [ - {"name": name, "ttl": ttl_, "type": type_, "value": value} - for name, ttl_, type_, value in extra - ], - } - - ################## - # Custom records # - ################## - - # Defined by custom hooks ships in apps for example ... - - hook_results = hook_callback("custom_dns_rules", args=[domain]) - for hook_name, results in hook_results.items(): - # - # There can be multiple results per hook name, so results look like - # {'/some/path/to/hook1': - # { 'state': 'succeed', - # 'stdreturn': [{'type': 'SRV', - # 'name': 'stuff.foo.bar.', - # 'value': 'yoloswag', - # 'ttl': 3600}] - # }, - # '/some/path/to/hook2': - # { ... }, - # [...] - # - # Loop over the sub-results - custom_records = [ - v["stdreturn"] for v in results.values() if v and v["stdreturn"] - ] - - records[hook_name] = [] - for record_list in custom_records: - # Check that record_list is indeed a list of dict - # with the required keys - if ( - not isinstance(record_list, list) - or any(not isinstance(record, dict) for record in record_list) - or any( - key not in record - for record in record_list - for key in ["name", "ttl", "type", "value"] - ) - ): - # Display an error, mainly for app packagers trying to implement a hook - logger.warning( - "Ignored custom record from hook '%s' because the data is not a *list* of dict with keys name, ttl, type and value. Raw data : %s" - % (hook_name, record_list) - ) - continue - - records[hook_name].extend(record_list) - - return records - - -def _get_DKIM(domain): - DKIM_file = "/etc/dkim/{domain}.mail.txt".format(domain=domain) - - if not os.path.isfile(DKIM_file): - return (None, None) - - with open(DKIM_file) as f: - dkim_content = f.read() - - # Gotta manage two formats : - # - # Legacy - # ----- - # - # mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " - # "p=" ) - # - # New - # ------ - # - # mail._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; " - # "p=" ) - - is_legacy_format = " h=sha256; " not in dkim_content - - # Legacy DKIM format - if is_legacy_format: - dkim = re.match( - ( - r"^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+" - r'[^"]*"v=(?P[^";]+);' - r'[\s"]*k=(?P[^";]+);' - r'[\s"]*p=(?P

[^";]+)' - ), - dkim_content, - re.M | re.S, - ) - else: - dkim = re.match( - ( - r"^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+" - r'[^"]*"v=(?P[^";]+);' - r'[\s"]*h=(?P[^";]+);' - r'[\s"]*k=(?P[^";]+);' - r'[\s"]*p=(?P

[^";]+)' - ), - dkim_content, - re.M | re.S, - ) - - if not dkim: - return (None, None) - - if is_legacy_format: - return ( - dkim.group("host"), - '"v={v}; k={k}; p={p}"'.format( - v=dkim.group("v"), k=dkim.group("k"), p=dkim.group("p") - ), - ) - else: - return ( - dkim.group("host"), - '"v={v}; h={h}; k={k}; p={p}"'.format( - v=dkim.group("v"), - h=dkim.group("h"), - k=dkim.group("k"), - p=dkim.group("p"), - ), - ) diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py deleted file mode 100644 index fe5c5f8cd..000000000 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ /dev/null @@ -1,1649 +0,0 @@ -import sys -import pytest - -from mock import patch -from io import StringIO -from collections import OrderedDict - -from moulinette import Moulinette - -from yunohost import domain, user -from yunohost.app import _parse_args_in_yunohost_format, PasswordArgumentParser -from yunohost.utils.error import YunohostError - - -""" -Argument default format: -{ - "name": "the_name", - "type": "one_of_the_available_type", // "sting" is not specified - "ask": { - "en": "the question in english", - "fr": "the question in french" - }, - "help": { - "en": "some help text in english", - "fr": "some help text in french" - }, - "example": "an example value", // optional - "default", "some stuff", // optional, not available for all types - "optional": true // optional, will skip if not answered -} - -User answers: -{"name": "value", ...} -""" - - -def test_parse_args_in_yunohost_format_empty(): - assert _parse_args_in_yunohost_format({}, []) == {} - - -def test_parse_args_in_yunohost_format_string(): - questions = [ - { - "name": "some_string", - "type": "string", - } - ] - answers = {"some_string": "some_value"} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_string_default_type(): - questions = [ - { - "name": "some_string", - } - ] - answers = {"some_string": "some_value"} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_string_no_input(): - questions = [ - { - "name": "some_string", - } - ] - answers = {} - - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_string_input(): - questions = [ - { - "name": "some_string", - "ask": "some question", - } - ] - answers = {} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_string_input_no_ask(): - questions = [ - { - "name": "some_string", - } - ] - answers = {} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_string_no_input_optional(): - questions = [ - { - "name": "some_string", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_string": ("", "string")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_string_optional_with_input(): - questions = [ - { - "name": "some_string", - "ask": "some question", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_string_optional_with_empty_input(): - questions = [ - { - "name": "some_string", - "ask": "some question", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_string": ("", "string")}) - - with patch.object(Moulinette.interface, "prompt", return_value=""): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_string_optional_with_input_without_ask(): - questions = [ - { - "name": "some_string", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_string_no_input_default(): - questions = [ - { - "name": "some_string", - "ask": "some question", - "default": "some_value", - } - ] - answers = {} - expected_result = OrderedDict({"some_string": ("some_value", "string")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_string_input_test_ask(): - ask_text = "some question" - questions = [ - { - "name": "some_string", - "ask": ask_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - prompt.assert_called_with(ask_text, False) - - -def test_parse_args_in_yunohost_format_string_input_test_ask_with_default(): - ask_text = "some question" - default_text = "some example" - questions = [ - { - "name": "some_string", - "ask": ask_text, - "default": default_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - prompt.assert_called_with("%s (default: %s)" % (ask_text, default_text), False) - - -@pytest.mark.skip # we should do something with this example -def test_parse_args_in_yunohost_format_string_input_test_ask_with_example(): - ask_text = "some question" - example_text = "some example" - questions = [ - { - "name": "some_string", - "ask": ask_text, - "example": example_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - assert ask_text in prompt.call_args[0][0] - assert example_text in prompt.call_args[0][0] - - -@pytest.mark.skip # we should do something with this help -def test_parse_args_in_yunohost_format_string_input_test_ask_with_help(): - ask_text = "some question" - help_text = "some_help" - questions = [ - { - "name": "some_string", - "ask": ask_text, - "help": help_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - assert ask_text in prompt.call_args[0][0] - assert help_text in prompt.call_args[0][0] - - -def test_parse_args_in_yunohost_format_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 - - -def test_parse_args_in_yunohost_format_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.interface, "prompt", return_value="fr"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_string_with_choice_bad(): - questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] - answers = {"some_string": "bad"} - - with pytest.raises(YunohostError): - assert _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_string_with_choice_ask(): - ask_text = "some question" - choices = ["fr", "en", "es", "it", "ru"] - questions = [ - { - "name": "some_string", - "ask": ask_text, - "choices": choices, - } - ] - answers = {} - - with patch.object(Moulinette.interface, "prompt", return_value="ru") as prompt: - _parse_args_in_yunohost_format(answers, questions) - assert ask_text in prompt.call_args[0][0] - - for choice in choices: - assert choice in prompt.call_args[0][0] - - -def test_parse_args_in_yunohost_format_string_with_choice_default(): - questions = [ - { - "name": "some_string", - "type": "string", - "choices": ["fr", "en"], - "default": "en", - } - ] - answers = {} - expected_result = OrderedDict({"some_string": ("en", "string")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_password(): - questions = [ - { - "name": "some_password", - "type": "password", - } - ] - answers = {"some_password": "some_value"} - expected_result = OrderedDict({"some_password": ("some_value", "password")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_password_no_input(): - questions = [ - { - "name": "some_password", - "type": "password", - } - ] - answers = {} - - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_password_input(): - questions = [ - { - "name": "some_password", - "type": "password", - "ask": "some question", - } - ] - answers = {} - expected_result = OrderedDict({"some_password": ("some_value", "password")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_password_input_no_ask(): - questions = [ - { - "name": "some_password", - "type": "password", - } - ] - answers = {} - expected_result = OrderedDict({"some_password": ("some_value", "password")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_password_no_input_optional(): - questions = [ - { - "name": "some_password", - "type": "password", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_password": ("", "password")}) - - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - questions = [ - {"name": "some_password", "type": "password", "optional": True, "default": ""} - ] - - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_password_optional_with_input(): - questions = [ - { - "name": "some_password", - "ask": "some question", - "type": "password", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_password": ("some_value", "password")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_password_optional_with_empty_input(): - questions = [ - { - "name": "some_password", - "ask": "some question", - "type": "password", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_password": ("", "password")}) - - with patch.object(Moulinette.interface, "prompt", return_value=""): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_password_optional_with_input_without_ask(): - questions = [ - { - "name": "some_password", - "type": "password", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_password": ("some_value", "password")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_password_no_input_default(): - questions = [ - { - "name": "some_password", - "type": "password", - "ask": "some question", - "default": "some_value", - } - ] - answers = {} - - # no default for password! - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -@pytest.mark.skip # this should raises -def test_parse_args_in_yunohost_format_password_no_input_example(): - questions = [ - { - "name": "some_password", - "type": "password", - "ask": "some question", - "example": "some_value", - } - ] - answers = {"some_password": "some_value"} - - # no example for password! - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_password_input_test_ask(): - ask_text = "some question" - questions = [ - { - "name": "some_password", - "type": "password", - "ask": ask_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - prompt.assert_called_with(ask_text, True) - - -@pytest.mark.skip # we should do something with this example -def test_parse_args_in_yunohost_format_password_input_test_ask_with_example(): - ask_text = "some question" - example_text = "some example" - questions = [ - { - "name": "some_password", - "type": "password", - "ask": ask_text, - "example": example_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - assert ask_text in prompt.call_args[0][0] - assert example_text in prompt.call_args[0][0] - - -@pytest.mark.skip # we should do something with this help -def test_parse_args_in_yunohost_format_password_input_test_ask_with_help(): - ask_text = "some question" - help_text = "some_help" - questions = [ - { - "name": "some_password", - "type": "password", - "ask": ask_text, - "help": help_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - assert ask_text in prompt.call_args[0][0] - assert help_text in prompt.call_args[0][0] - - -def test_parse_args_in_yunohost_format_password_bad_chars(): - questions = [ - { - "name": "some_password", - "type": "password", - "ask": "some question", - "example": "some_value", - } - ] - - for i in PasswordArgumentParser.forbidden_chars: - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format({"some_password": i * 8}, questions) - - -def test_parse_args_in_yunohost_format_password_strong_enough(): - questions = [ - { - "name": "some_password", - "type": "password", - "ask": "some question", - "example": "some_value", - } - ] - - with pytest.raises(YunohostError): - # too short - _parse_args_in_yunohost_format({"some_password": "a"}, questions) - - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format({"some_password": "password"}, questions) - - -def test_parse_args_in_yunohost_format_password_optional_strong_enough(): - questions = [ - { - "name": "some_password", - "ask": "some question", - "type": "password", - "optional": True, - } - ] - - with pytest.raises(YunohostError): - # too short - _parse_args_in_yunohost_format({"some_password": "a"}, questions) - - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format({"some_password": "password"}, questions) - - -def test_parse_args_in_yunohost_format_path(): - questions = [ - { - "name": "some_path", - "type": "path", - } - ] - answers = {"some_path": "some_value"} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_path_no_input(): - questions = [ - { - "name": "some_path", - "type": "path", - } - ] - answers = {} - - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_path_input(): - questions = [ - { - "name": "some_path", - "type": "path", - "ask": "some question", - } - ] - answers = {} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_path_input_no_ask(): - questions = [ - { - "name": "some_path", - "type": "path", - } - ] - answers = {} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_path_no_input_optional(): - questions = [ - { - "name": "some_path", - "type": "path", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_path": ("", "path")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_path_optional_with_input(): - questions = [ - { - "name": "some_path", - "ask": "some question", - "type": "path", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_path_optional_with_empty_input(): - questions = [ - { - "name": "some_path", - "ask": "some question", - "type": "path", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_path": ("", "path")}) - - with patch.object(Moulinette.interface, "prompt", return_value=""): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_path_optional_with_input_without_ask(): - questions = [ - { - "name": "some_path", - "type": "path", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) - - with patch.object(Moulinette.interface, "prompt", return_value="some_value"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_path_no_input_default(): - questions = [ - { - "name": "some_path", - "ask": "some question", - "type": "path", - "default": "some_value", - } - ] - answers = {} - expected_result = OrderedDict({"some_path": ("some_value", "path")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_path_input_test_ask(): - ask_text = "some question" - questions = [ - { - "name": "some_path", - "type": "path", - "ask": ask_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - prompt.assert_called_with(ask_text, False) - - -def test_parse_args_in_yunohost_format_path_input_test_ask_with_default(): - ask_text = "some question" - default_text = "some example" - questions = [ - { - "name": "some_path", - "type": "path", - "ask": ask_text, - "default": default_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - prompt.assert_called_with("%s (default: %s)" % (ask_text, default_text), False) - - -@pytest.mark.skip # we should do something with this example -def test_parse_args_in_yunohost_format_path_input_test_ask_with_example(): - ask_text = "some question" - example_text = "some example" - questions = [ - { - "name": "some_path", - "type": "path", - "ask": ask_text, - "example": example_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - assert ask_text in prompt.call_args[0][0] - assert example_text in prompt.call_args[0][0] - - -@pytest.mark.skip # we should do something with this help -def test_parse_args_in_yunohost_format_path_input_test_ask_with_help(): - ask_text = "some question" - help_text = "some_help" - questions = [ - { - "name": "some_path", - "type": "path", - "ask": ask_text, - "help": help_text, - } - ] - answers = {} - - with patch.object( - Moulinette.interface, "prompt", return_value="some_value" - ) as prompt: - _parse_args_in_yunohost_format(answers, questions) - assert ask_text in prompt.call_args[0][0] - assert help_text in prompt.call_args[0][0] - - -def test_parse_args_in_yunohost_format_boolean(): - questions = [ - { - "name": "some_boolean", - "type": "boolean", - } - ] - answers = {"some_boolean": "y"} - expected_result = OrderedDict({"some_boolean": (1, "boolean")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_boolean_all_yes(): - questions = [ - { - "name": "some_boolean", - "type": "boolean", - } - ] - expected_result = OrderedDict({"some_boolean": (1, "boolean")}) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "y"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "Y"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "yes"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "Yes"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "YES"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "1"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": 1}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": True}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "True"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "TRUE"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "true"}, questions) - == expected_result - ) - - -def test_parse_args_in_yunohost_format_boolean_all_no(): - questions = [ - { - "name": "some_boolean", - "type": "boolean", - } - ] - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "n"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "N"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "no"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "No"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "No"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "0"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": 0}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": False}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "False"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "FALSE"}, questions) - == expected_result - ) - assert ( - _parse_args_in_yunohost_format({"some_boolean": "false"}, questions) - == expected_result - ) - - -# XXX apparently boolean are always False (0) by default, I'm not sure what to think about that -def test_parse_args_in_yunohost_format_boolean_no_input(): - questions = [ - { - "name": "some_boolean", - "type": "boolean", - } - ] - answers = {} - - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_boolean_bad_input(): - questions = [ - { - "name": "some_boolean", - "type": "boolean", - } - ] - answers = {"some_boolean": "stuff"} - - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_boolean_input(): - questions = [ - { - "name": "some_boolean", - "type": "boolean", - "ask": "some question", - } - ] - answers = {} - - expected_result = OrderedDict({"some_boolean": (1, "boolean")}) - with patch.object(Moulinette.interface, "prompt", return_value="y"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) - with patch.object(Moulinette.interface, "prompt", return_value="n"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_boolean_input_no_ask(): - questions = [ - { - "name": "some_boolean", - "type": "boolean", - } - ] - answers = {} - expected_result = OrderedDict({"some_boolean": (1, "boolean")}) - - with patch.object(Moulinette.interface, "prompt", return_value="y"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_boolean_no_input_optional(): - questions = [ - { - "name": "some_boolean", - "type": "boolean", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_boolean_optional_with_input(): - questions = [ - { - "name": "some_boolean", - "ask": "some question", - "type": "boolean", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_boolean": (1, "boolean")}) - - with patch.object(Moulinette.interface, "prompt", return_value="y"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_boolean_optional_with_empty_input(): - questions = [ - { - "name": "some_boolean", - "ask": "some question", - "type": "boolean", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false - - with patch.object(Moulinette.interface, "prompt", return_value=""): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_boolean_optional_with_input_without_ask(): - questions = [ - { - "name": "some_boolean", - "type": "boolean", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) - - with patch.object(Moulinette.interface, "prompt", return_value="n"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_boolean_no_input_default(): - questions = [ - { - "name": "some_boolean", - "ask": "some question", - "type": "boolean", - "default": 0, - } - ] - answers = {} - expected_result = OrderedDict({"some_boolean": (0, "boolean")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_boolean_bad_default(): - questions = [ - { - "name": "some_boolean", - "ask": "some question", - "type": "boolean", - "default": "bad default", - } - ] - answers = {} - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_boolean_input_test_ask(): - ask_text = "some question" - questions = [ - { - "name": "some_boolean", - "type": "boolean", - "ask": ask_text, - } - ] - answers = {} - - with patch.object(Moulinette.interface, "prompt", return_value=0) as prompt: - _parse_args_in_yunohost_format(answers, questions) - prompt.assert_called_with(ask_text + " [yes | no] (default: no)", False) - - -def test_parse_args_in_yunohost_format_boolean_input_test_ask_with_default(): - ask_text = "some question" - default_text = 1 - questions = [ - { - "name": "some_boolean", - "type": "boolean", - "ask": ask_text, - "default": default_text, - } - ] - answers = {} - - with patch.object(Moulinette.interface, "prompt", return_value=1) as prompt: - _parse_args_in_yunohost_format(answers, questions) - prompt.assert_called_with("%s [yes | no] (default: yes)" % ask_text, False) - - -def test_parse_args_in_yunohost_format_domain_empty(): - questions = [ - { - "name": "some_domain", - "type": "domain", - } - ] - main_domain = "my_main_domain.com" - expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) - answers = {} - - with patch.object( - domain, "_get_maindomain", return_value="my_main_domain.com" - ), patch.object(domain, "domain_list", return_value={"domains": [main_domain]}): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_domain(): - main_domain = "my_main_domain.com" - domains = [main_domain] - questions = [ - { - "name": "some_domain", - "type": "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 _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_domain_two_domains(): - main_domain = "my_main_domain.com" - other_domain = "some_other_domain.tld" - domains = [main_domain, other_domain] - - questions = [ - { - "name": "some_domain", - "type": "domain", - } - ] - 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 _parse_args_in_yunohost_format(answers, questions) == expected_result - - 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 _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_domain_two_domains_wrong_answer(): - main_domain = "my_main_domain.com" - other_domain = "some_other_domain.tld" - domains = [main_domain, other_domain] - - questions = [ - { - "name": "some_domain", - "type": "domain", - } - ] - answers = {"some_domain": "doesnt_exist.pouet"} - - with patch.object( - domain, "_get_maindomain", return_value=main_domain - ), patch.object(domain, "domain_list", return_value={"domains": domains}): - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_domain_two_domains_default_no_ask(): - main_domain = "my_main_domain.com" - other_domain = "some_other_domain.tld" - domains = [main_domain, other_domain] - - questions = [ - { - "name": "some_domain", - "type": "domain", - } - ] - answers = {} - 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 _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_domain_two_domains_default(): - main_domain = "my_main_domain.com" - other_domain = "some_other_domain.tld" - domains = [main_domain, other_domain] - - 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 - ), patch.object(domain, "domain_list", return_value={"domains": domains}): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_domain_two_domains_default_input(): - main_domain = "my_main_domain.com" - other_domain = "some_other_domain.tld" - domains = [main_domain, other_domain] - - questions = [{"name": "some_domain", "type": "domain", "ask": "choose a domain"}] - answers = {} - - with patch.object( - domain, "_get_maindomain", return_value=main_domain - ), patch.object(domain, "domain_list", return_value={"domains": domains}): - expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) - with patch.object(Moulinette.interface, "prompt", return_value=main_domain): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - expected_result = OrderedDict({"some_domain": (other_domain, "domain")}) - with patch.object(Moulinette.interface, "prompt", return_value=other_domain): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_user_empty(): - users = { - "some_user": { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - } - } - - questions = [ - { - "name": "some_user", - "type": "user", - } - ] - answers = {} - - with patch.object(user, "user_list", return_value={"users": users}): - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_user(): - username = "some_user" - users = { - username: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - } - } - - questions = [ - { - "name": "some_user", - "type": "user", - } - ] - answers = {"some_user": username} - - expected_result = OrderedDict({"some_user": (username, "user")}) - - with patch.object(user, "user_list", return_value={"users": users}): - with patch.object(user, "user_info", return_value={}): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_user_two_users(): - username = "some_user" - other_user = "some_other_user" - users = { - username: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - }, - other_user: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "z@ynh.local", - "fullname": "john doe", - }, - } - - questions = [ - { - "name": "some_user", - "type": "user", - } - ] - answers = {"some_user": other_user} - expected_result = OrderedDict({"some_user": (other_user, "user")}) - - with patch.object(user, "user_list", return_value={"users": users}): - with patch.object(user, "user_info", return_value={}): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - answers = {"some_user": username} - expected_result = OrderedDict({"some_user": (username, "user")}) - - with patch.object(user, "user_list", return_value={"users": users}): - with patch.object(user, "user_info", return_value={}): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_user_two_users_wrong_answer(): - username = "my_username.com" - other_user = "some_other_user" - users = { - username: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - }, - other_user: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "z@ynh.local", - "fullname": "john doe", - }, - } - - questions = [ - { - "name": "some_user", - "type": "user", - } - ] - answers = {"some_user": "doesnt_exist.pouet"} - - with patch.object(user, "user_list", return_value={"users": users}): - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_user_two_users_no_default(): - username = "my_username.com" - other_user = "some_other_user.tld" - users = { - username: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - }, - other_user: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "z@ynh.local", - "fullname": "john doe", - }, - } - - questions = [{"name": "some_user", "type": "user", "ask": "choose a user"}] - answers = {} - - with patch.object(user, "user_list", return_value={"users": users}): - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_user_two_users_default_input(): - username = "my_username.com" - other_user = "some_other_user.tld" - users = { - username: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "p@ynh.local", - "fullname": "the first name the last name", - }, - other_user: { - "ssh_allowed": False, - "username": "some_user", - "mailbox-quota": "0", - "mail": "z@ynh.local", - "fullname": "john doe", - }, - } - - questions = [{"name": "some_user", "type": "user", "ask": "choose a user"}] - answers = {} - - with patch.object(user, "user_list", return_value={"users": users}): - with patch.object(user, "user_info", return_value={}): - expected_result = OrderedDict({"some_user": (username, "user")}) - with patch.object(Moulinette.interface, "prompt", return_value=username): - assert ( - _parse_args_in_yunohost_format(answers, questions) - == expected_result - ) - - expected_result = OrderedDict({"some_user": (other_user, "user")}) - with patch.object(Moulinette.interface, "prompt", return_value=other_user): - assert ( - _parse_args_in_yunohost_format(answers, questions) - == expected_result - ) - - -def test_parse_args_in_yunohost_format_number(): - questions = [ - { - "name": "some_number", - "type": "number", - } - ] - answers = {"some_number": 1337} - expected_result = OrderedDict({"some_number": (1337, "number")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_number_no_input(): - questions = [ - { - "name": "some_number", - "type": "number", - } - ] - answers = {} - - expected_result = OrderedDict({"some_number": (0, "number")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_number_bad_input(): - questions = [ - { - "name": "some_number", - "type": "number", - } - ] - answers = {"some_number": "stuff"} - - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - answers = {"some_number": 1.5} - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_number_input(): - questions = [ - { - "name": "some_number", - "type": "number", - "ask": "some question", - } - ] - answers = {} - - expected_result = OrderedDict({"some_number": (1337, "number")}) - with patch.object(Moulinette.interface, "prompt", return_value="1337"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - with patch.object(Moulinette.interface, "prompt", return_value=1337): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - expected_result = OrderedDict({"some_number": (0, "number")}) - with patch.object(Moulinette.interface, "prompt", return_value=""): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_number_input_no_ask(): - questions = [ - { - "name": "some_number", - "type": "number", - } - ] - answers = {} - expected_result = OrderedDict({"some_number": (1337, "number")}) - - with patch.object(Moulinette.interface, "prompt", return_value="1337"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_number_no_input_optional(): - questions = [ - { - "name": "some_number", - "type": "number", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_number": (0, "number")}) # default to 0 - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_number_optional_with_input(): - questions = [ - { - "name": "some_number", - "ask": "some question", - "type": "number", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_number": (1337, "number")}) - - with patch.object(Moulinette.interface, "prompt", return_value="1337"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_number_optional_with_input_without_ask(): - questions = [ - { - "name": "some_number", - "type": "number", - "optional": True, - } - ] - answers = {} - expected_result = OrderedDict({"some_number": (0, "number")}) - - with patch.object(Moulinette.interface, "prompt", return_value="0"): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_number_no_input_default(): - questions = [ - { - "name": "some_number", - "ask": "some question", - "type": "number", - "default": 1337, - } - ] - answers = {} - expected_result = OrderedDict({"some_number": (1337, "number")}) - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - - -def test_parse_args_in_yunohost_format_number_bad_default(): - questions = [ - { - "name": "some_number", - "ask": "some question", - "type": "number", - "default": "bad default", - } - ] - answers = {} - with pytest.raises(YunohostError): - _parse_args_in_yunohost_format(answers, questions) - - -def test_parse_args_in_yunohost_format_number_input_test_ask(): - ask_text = "some question" - questions = [ - { - "name": "some_number", - "type": "number", - "ask": ask_text, - } - ] - answers = {} - - with patch.object(Moulinette.interface, "prompt", return_value="1111") as prompt: - _parse_args_in_yunohost_format(answers, questions) - prompt.assert_called_with("%s (default: 0)" % (ask_text), False) - - -def test_parse_args_in_yunohost_format_number_input_test_ask_with_default(): - ask_text = "some question" - default_value = 1337 - questions = [ - { - "name": "some_number", - "type": "number", - "ask": ask_text, - "default": default_value, - } - ] - answers = {} - - with patch.object(Moulinette.interface, "prompt", return_value="1111") as prompt: - _parse_args_in_yunohost_format(answers, questions) - prompt.assert_called_with("%s (default: %s)" % (ask_text, default_value), False) - - -@pytest.mark.skip # we should do something with this example -def test_parse_args_in_yunohost_format_number_input_test_ask_with_example(): - ask_text = "some question" - example_value = 1337 - questions = [ - { - "name": "some_number", - "type": "number", - "ask": ask_text, - "example": example_value, - } - ] - answers = {} - - with patch.object(Moulinette.interface, "prompt", return_value="1111") as prompt: - _parse_args_in_yunohost_format(answers, questions) - assert ask_text in prompt.call_args[0][0] - assert example_value in prompt.call_args[0][0] - - -@pytest.mark.skip # we should do something with this help -def test_parse_args_in_yunohost_format_number_input_test_ask_with_help(): - ask_text = "some question" - help_value = 1337 - questions = [ - { - "name": "some_number", - "type": "number", - "ask": ask_text, - "help": help_value, - } - ] - answers = {} - - with patch.object(Moulinette.interface, "prompt", return_value="1111") as prompt: - _parse_args_in_yunohost_format(answers, questions) - assert ask_text in prompt.call_args[0][0] - assert help_value in prompt.call_args[0][0] - - -def test_parse_args_in_yunohost_format_display_text(): - questions = [{"name": "some_app", "type": "display_text", "ask": "foobar"}] - answers = {} - - with patch.object(sys, "stdout", new_callable=StringIO) as stdout: - _parse_args_in_yunohost_format(answers, questions) - assert "foobar" in stdout.getvalue() diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py deleted file mode 100644 index eb92dd71f..000000000 --- a/src/yunohost/utils/legacy.py +++ /dev/null @@ -1,239 +0,0 @@ -import os -from moulinette import m18n -from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import write_to_json, read_yaml - -from yunohost.user import user_list -from yunohost.app import ( - _installed_apps, - _get_app_settings, - _set_app_settings, -) -from yunohost.permission import ( - permission_create, - user_permission_update, - permission_sync_to_user, -) - -logger = getActionLogger("yunohost.legacy") - -LEGACY_PERMISSION_LABEL = { - ("nextcloud", "skipped"): "api", # .well-known - ("libreto", "skipped"): "pad access", # /[^/]+ - ("leed", "skipped"): "api", # /action.php, for cron task ... - ("mailman", "protected"): "admin", # /admin - ("prettynoemiecms", "protected"): "admin", # /admin - ("etherpad_mypads", "skipped"): "admin", # /admin - ("baikal", "protected"): "admin", # /admin/ - ("couchpotato", "unprotected"): "api", # /api - ("freshrss", "skipped"): "api", # /api/, - ("portainer", "skipped"): "api", # /api/webhooks/ - ("jeedom", "unprotected"): "api", # /core/api/jeeApi.php - ("bozon", "protected"): "user interface", # /index.php - ( - "limesurvey", - "protected", - ): "admin", # /index.php?r=admin,/index.php?r=plugins,/scripts - ("kanboard", "unprotected"): "api", # /jsonrpc.php - ("seafile", "unprotected"): "medias", # /media - ("ttrss", "skipped"): "api", # /public.php,/api,/opml.php?op=publish - ("libreerp", "protected"): "admin", # /web/database/manager - ("z-push", "skipped"): "api", # $domain/[Aa]uto[Dd]iscover/.* - ("radicale", "skipped"): "?", # $domain$path_url - ( - "jirafeau", - "protected", - ): "user interface", # $domain$path_url/$","$domain$path_url/admin.php.*$ - ("opensondage", "protected"): "admin", # $domain$path_url/admin/ - ( - "lstu", - "protected", - ): "user interface", # $domain$path_url/login$","$domain$path_url/logout$","$domain$path_url/api$","$domain$path_url/extensions$","$domain$path_url/stats$","$domain$path_url/d/.*$","$domain$path_url/a$","$domain$path_url/$ - ( - "lutim", - "protected", - ): "user interface", # $domain$path_url/stats/?$","$domain$path_url/manifest.webapp/?$","$domain$path_url/?$","$domain$path_url/[d-m]/.*$ - ( - "lufi", - "protected", - ): "user interface", # $domain$path_url/stats$","$domain$path_url/manifest.webapp$","$domain$path_url/$","$domain$path_url/d/.*$","$domain$path_url/m/.*$ - ( - "gogs", - "skipped", - ): "api", # $excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-receive%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-upload%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/info/refs -} - - -def legacy_permission_label(app, permission_type): - return LEGACY_PERMISSION_LABEL.get( - (app, permission_type), "Legacy %s urls" % permission_type - ) - - -def migrate_legacy_permission_settings(app=None): - - logger.info(m18n.n("migrating_legacy_permission_settings")) - apps = _installed_apps() - - if app: - if app not in apps: - logger.error( - "Can't migrate permission for app %s because it ain't installed..." - % app - ) - apps = [] - else: - apps = [app] - - for app in apps: - - settings = _get_app_settings(app) or {} - if settings.get("label"): - user_permission_update( - app + ".main", label=settings["label"], sync_perm=False - ) - del settings["label"] - - def _setting(name): - s = settings.get(name) - return s.split(",") if s else [] - - skipped_urls = [uri for uri in _setting("skipped_uris") if uri != "/"] - skipped_urls += ["re:" + regex for regex in _setting("skipped_regex")] - unprotected_urls = [uri for uri in _setting("unprotected_uris") if uri != "/"] - unprotected_urls += ["re:" + regex for regex in _setting("unprotected_regex")] - protected_urls = [uri for uri in _setting("protected_uris") if uri != "/"] - protected_urls += ["re:" + regex for regex in _setting("protected_regex")] - - if skipped_urls != []: - permission_create( - app + ".legacy_skipped_uris", - additional_urls=skipped_urls, - auth_header=False, - label=legacy_permission_label(app, "skipped"), - show_tile=False, - allowed="visitors", - protected=True, - sync_perm=False, - ) - if unprotected_urls != []: - permission_create( - app + ".legacy_unprotected_uris", - additional_urls=unprotected_urls, - auth_header=True, - label=legacy_permission_label(app, "unprotected"), - show_tile=False, - allowed="visitors", - protected=True, - sync_perm=False, - ) - if protected_urls != []: - permission_create( - app + ".legacy_protected_uris", - additional_urls=protected_urls, - auth_header=True, - label=legacy_permission_label(app, "protected"), - show_tile=False, - allowed=[], - protected=True, - sync_perm=False, - ) - - legacy_permission_settings = [ - "skipped_uris", - "unprotected_uris", - "protected_uris", - "skipped_regex", - "unprotected_regex", - "protected_regex", - ] - for key in legacy_permission_settings: - if key in settings: - del settings[key] - - _set_app_settings(app, settings) - - permission_sync_to_user() - - -def translate_legacy_rules_in_ssowant_conf_json_persistent(): - - persistent_file_name = "/etc/ssowat/conf.json.persistent" - if not os.path.exists(persistent_file_name): - return - - # Ugly hack because for some reason so many people have tabs in their conf.json.persistent ... - os.system(r"sed -i 's/\t/ /g' /etc/ssowat/conf.json.persistent") - - # Ugly hack to try not to misarably fail migration - persistent = read_yaml(persistent_file_name) - - legacy_rules = [ - "skipped_urls", - "unprotected_urls", - "protected_urls", - "skipped_regex", - "unprotected_regex", - "protected_regex", - ] - - if not any(legacy_rule in persistent for legacy_rule in legacy_rules): - return - - if not isinstance(persistent.get("permissions"), dict): - persistent["permissions"] = {} - - skipped_urls = persistent.get("skipped_urls", []) + [ - "re:" + r for r in persistent.get("skipped_regex", []) - ] - protected_urls = persistent.get("protected_urls", []) + [ - "re:" + r for r in persistent.get("protected_regex", []) - ] - unprotected_urls = persistent.get("unprotected_urls", []) + [ - "re:" + r for r in persistent.get("unprotected_regex", []) - ] - - known_users = list(user_list()["users"].keys()) - - for legacy_rule in legacy_rules: - if legacy_rule in persistent: - del persistent[legacy_rule] - - if skipped_urls: - persistent["permissions"]["custom_skipped"] = { - "users": [], - "label": "Custom permissions - skipped", - "show_tile": False, - "auth_header": False, - "public": True, - "uris": skipped_urls - + persistent["permissions"].get("custom_skipped", {}).get("uris", []), - } - - if unprotected_urls: - persistent["permissions"]["custom_unprotected"] = { - "users": [], - "label": "Custom permissions - unprotected", - "show_tile": False, - "auth_header": True, - "public": True, - "uris": unprotected_urls - + persistent["permissions"].get("custom_unprotected", {}).get("uris", []), - } - - if protected_urls: - persistent["permissions"]["custom_protected"] = { - "users": known_users, - "label": "Custom permissions - protected", - "show_tile": False, - "auth_header": True, - "public": False, - "uris": protected_urls - + persistent["permissions"].get("custom_protected", {}).get("uris", []), - } - - write_to_json(persistent_file_name, persistent, sort_keys=True, indent=4) - - logger.warning( - "YunoHost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system" - ) diff --git a/tests/autofix_locale_format.py b/tests/autofix_locale_format.py deleted file mode 100644 index f777f06f1..000000000 --- a/tests/autofix_locale_format.py +++ /dev/null @@ -1,47 +0,0 @@ -import re -import json -import glob - -# List all locale files (except en.json being the ref) -locale_folder = "locales/" -locale_files = glob.glob(locale_folder + "*.json") -locale_files = [filename.split("/")[-1] for filename in locale_files] -locale_files.remove("en.json") - -reference = json.loads(open(locale_folder + "en.json").read()) - - -def fix_locale(locale_file): - - this_locale = json.loads(open(locale_folder + locale_file).read()) - fixed_stuff = False - - # We iterate over all keys/string in en.json - for key, string in reference.items(): - - # Ignore check if there's no translation yet for this key - if key not in this_locale: - continue - - # Then we check that every "{stuff}" (for python's .format()) - # should also be in the translated string, otherwise the .format - # will trigger an exception! - subkeys_in_ref = [k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)] - subkeys_in_this_locale = [k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key])] - - if set(subkeys_in_ref) != set(subkeys_in_this_locale) and (len(subkeys_in_ref) == len(subkeys_in_this_locale)): - for i, subkey in enumerate(subkeys_in_ref): - this_locale[key] = this_locale[key].replace('{%s}' % subkeys_in_this_locale[i], '{%s}' % subkey) - fixed_stuff = True - - if fixed_stuff: - json.dump( - this_locale, - open(locale_folder + locale_file, "w"), - indent=4, - ensure_ascii=False, - ) - - -for locale_file in locale_files: - fix_locale(locale_file) diff --git a/tests/reformat_locales.py b/tests/reformat_locales.py deleted file mode 100644 index 90251d040..000000000 --- a/tests/reformat_locales.py +++ /dev/null @@ -1,38 +0,0 @@ -import re - -def reformat(lang, transformations): - - locale = open(f"locales/{lang}.json").read() - for pattern, replace in transformations.items(): - locale = re.compile(pattern).sub(replace, locale) - - open(f"locales/{lang}.json", "w").write(locale) - -###################################################### - -godamn_spaces_of_hell = ["\u00a0", "\u2000", "\u2001", "\u2002", "\u2003", "\u2004", "\u2005", "\u2006", "\u2007", "\u2008", "\u2009", "\u200A", "\u202f", "\u202F", "\u3000"] - -transformations = {s: " " for s in godamn_spaces_of_hell} -transformations.update({ - "…": "...", -}) - - -reformat("en", transformations) - -###################################################### - -transformations.update({ - "courriel": "email", - "e-mail": "email", - "Courriel": "Email", - "E-mail": "Email", - "« ": "'", - "«": "'", - " »": "'", - "»": "'", - "’": "'", - #r"$(\w{1,2})'|( \w{1,2})'": r"\1\2’", -}) - -reformat("fr", transformations) diff --git a/tests/remove_stale_translated_strings.py b/tests/remove_stale_translated_strings.py deleted file mode 100644 index 48f2180e4..000000000 --- a/tests/remove_stale_translated_strings.py +++ /dev/null @@ -1,25 +0,0 @@ -import json -import glob -from collections import OrderedDict - -locale_folder = "../locales/" -locale_files = glob.glob(locale_folder + "*.json") -locale_files = [filename.split("/")[-1] for filename in locale_files] -locale_files.remove("en.json") - -reference = json.loads(open(locale_folder + "en.json").read()) - -for locale_file in locale_files: - - print(locale_file) - this_locale = json.loads( - open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict - ) - this_locale_fixed = {k: v for k, v in this_locale.items() if k in reference} - - json.dump( - this_locale_fixed, - open(locale_folder + locale_file, "w"), - indent=4, - ensure_ascii=False, - ) diff --git a/tests/test_actionmap.py b/tests/test_actionmap.py index 0b8abb152..e482bdfe1 100644 --- a/tests/test_actionmap.py +++ b/tests/test_actionmap.py @@ -2,4 +2,4 @@ import yaml def test_yaml_syntax(): - yaml.safe_load(open("data/actionsmap/yunohost.yml")) + yaml.safe_load(open("share/actionsmap.yml")) diff --git a/tests/test_helpers.d/ynhtest_apt.sh b/tests/test_helpers.d/ynhtest_apt.sh new file mode 100644 index 000000000..074bc2e60 --- /dev/null +++ b/tests/test_helpers.d/ynhtest_apt.sh @@ -0,0 +1,22 @@ +ynhtest_apt_install_apt_deps_regular() { + + dpkg --list | grep -q "ii *$app-ynh-deps" && apt remove $app-ynh-deps --assume-yes || true + dpkg --list | grep -q 'ii *nyancat' && apt remove nyancat --assume-yes || true + dpkg --list | grep -q 'ii *sl' && apt remove sl --assume-yes || true + + ! ynh_package_is_installed "$app-ynh-deps" + ! ynh_package_is_installed "nyancat" + ! ynh_package_is_installed "sl" + + ynh_install_app_dependencies "nyancat sl" + + ynh_package_is_installed "$app-ynh-deps" + ynh_package_is_installed "nyancat" + ynh_package_is_installed "sl" + + ynh_remove_app_dependencies + + ! ynh_package_is_installed "$app-ynh-deps" + ! ynh_package_is_installed "nyancat" + ! ynh_package_is_installed "sl" +} diff --git a/tests/test_helpers.d/ynhtest_config.sh b/tests/test_helpers.d/ynhtest_config.sh new file mode 100644 index 000000000..b64943a48 --- /dev/null +++ b/tests/test_helpers.d/ynhtest_config.sh @@ -0,0 +1,662 @@ + +################# +# _ __ _ _ # +# | '_ \| | | | # +# | |_) | |_| | # +# | .__/ \__, | # +# | | __/ | # +# |_| |___/ # +# # +################# + +_read_py() { + local file="$1" + local key="$2" + python3 -c "exec(open('$file').read()); print($key)" +} + +ynhtest_config_read_py() { + + local dummy_dir="$(mktemp -d -p $VAR_WWW)" + file="$dummy_dir/dummy.py" + + cat << EOF > $dummy_dir/dummy.py +# Some comment +FOO = None +ENABLED = False +# TITLE = "Old title" +TITLE = "Lorem Ipsum" +THEME = "colib'ris" +EMAIL = "root@example.com" # This is a comment without quotes +PORT = 1234 # This is a comment without quotes +URL = 'https://yunohost.org' +DICT = {} +DICT['ldap_base'] = "ou=users,dc=yunohost,dc=org" +DICT['ldap_conf'] = {} +DICT['ldap_conf']['user'] = "camille" +# YNH_ICI +DICT['TITLE'] = "Hello world" +EOF + + test "$(_read_py "$file" "FOO")" == "None" + test "$(ynh_read_var_in_file "$file" "FOO")" == "None" + + test "$(_read_py "$file" "ENABLED")" == "False" + test "$(ynh_read_var_in_file "$file" "ENABLED")" == "False" + + test "$(_read_py "$file" "TITLE")" == "Lorem Ipsum" + test "$(ynh_read_var_in_file "$file" "TITLE")" == "Lorem Ipsum" + + test "$(_read_py "$file" "THEME")" == "colib'ris" + test "$(ynh_read_var_in_file "$file" "THEME")" == "colib'ris" + + test "$(_read_py "$file" "EMAIL")" == "root@example.com" + test "$(ynh_read_var_in_file "$file" "EMAIL")" == "root@example.com" + + test "$(_read_py "$file" "PORT")" == "1234" + test "$(ynh_read_var_in_file "$file" "PORT")" == "1234" + + test "$(_read_py "$file" "URL")" == "https://yunohost.org" + test "$(ynh_read_var_in_file "$file" "URL")" == "https://yunohost.org" + + test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org" + + test "$(ynh_read_var_in_file "$file" "user")" == "camille" + + test "$(ynh_read_var_in_file "$file" "TITLE" "YNH_ICI")" == "Hello world" + + ! _read_py "$file" "NONEXISTENT" + test "$(ynh_read_var_in_file "$file" "NONEXISTENT")" == "YNH_NULL" + + ! _read_py "$file" "ENABLE" + test "$(ynh_read_var_in_file "$file" "ENABLE")" == "YNH_NULL" +} + +ynhtest_config_write_py() { + local dummy_dir="$(mktemp -d -p $VAR_WWW)" + file="$dummy_dir/dummy.py" + + cat << EOF > $dummy_dir/dummy.py +# Some comment +FOO = None +ENABLED = False +# TITLE = "Old title" +TITLE = "Lorem Ipsum" +THEME = "colib'ris" +EMAIL = "root@example.com" # This is a comment without quotes +PORT = 1234 # This is a comment without quotes +URL = 'https://yunohost.org' +DICT = {} +DICT['ldap_base'] = "ou=users,dc=yunohost,dc=org" +# YNH_ICI +DICT['TITLE'] = "Hello world" +EOF + + ynh_write_var_in_file "$file" "FOO" "bar" + test "$(_read_py "$file" "FOO")" == "bar" + test "$(ynh_read_var_in_file "$file" "FOO")" == "bar" + + ynh_write_var_in_file "$file" "ENABLED" "True" + test "$(_read_py "$file" "ENABLED")" == "True" + test "$(ynh_read_var_in_file "$file" "ENABLED")" == "True" + + ynh_write_var_in_file "$file" "TITLE" "Foo Bar" + test "$(_read_py "$file" "TITLE")" == "Foo Bar" + test "$(ynh_read_var_in_file "$file" "TITLE")" == "Foo Bar" + + ynh_write_var_in_file "$file" "THEME" "super-awesome-theme" + test "$(_read_py "$file" "THEME")" == "super-awesome-theme" + test "$(ynh_read_var_in_file "$file" "THEME")" == "super-awesome-theme" + + ynh_write_var_in_file "$file" "EMAIL" "sam@domain.tld" + test "$(_read_py "$file" "EMAIL")" == "sam@domain.tld" + test "$(ynh_read_var_in_file "$file" "EMAIL")" == "sam@domain.tld" + + ynh_write_var_in_file "$file" "PORT" "5678" + test "$(_read_py "$file" "PORT")" == "5678" + test "$(ynh_read_var_in_file "$file" "PORT")" == "5678" + + ynh_write_var_in_file "$file" "URL" "https://domain.tld/foobar" + test "$(_read_py "$file" "URL")" == "https://domain.tld/foobar" + test "$(ynh_read_var_in_file "$file" "URL")" == "https://domain.tld/foobar" + + ynh_write_var_in_file "$file" "ldap_base" "ou=users,dc=yunohost,dc=org" + test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org" + + ynh_write_var_in_file "$file" "TITLE" "YOLO" "YNH_ICI" + test "$(ynh_read_var_in_file "$file" "TITLE" "YNH_ICI")" == "YOLO" + + ! ynh_write_var_in_file "$file" "NONEXISTENT" "foobar" + ! _read_py "$file" "NONEXISTENT" + test "$(ynh_read_var_in_file "$file" "NONEXISTENT")" == "YNH_NULL" + + ! ynh_write_var_in_file "$file" "ENABLE" "foobar" + ! _read_py "$file" "ENABLE" + test "$(ynh_read_var_in_file "$file" "ENABLE")" == "YNH_NULL" + +} + +############### +# _ _ # +# (_) (_) # +# _ _ __ _ # +# | | '_ \| | # +# | | | | | | # +# |_|_| |_|_| # +# # +############### + +_read_ini() { + local file="$1" + local key="$2" + python3 -c "import configparser; c = configparser.ConfigParser(); c.read('$file'); print(c['main']['$key'])" +} + +ynhtest_config_read_ini() { + local dummy_dir="$(mktemp -d -p $VAR_WWW)" + file="$dummy_dir/dummy.ini" + + cat << EOF > $file +# Some comment +; Another comment +[main] +foo = null +enabled = False +# title = Old title +title = Lorem Ipsum +theme = colib'ris +email = root@example.com ; This is a comment without quotes +port = 1234 ; This is a comment without quotes +url = https://yunohost.org +[dict] + ldap_base = ou=users,dc=yunohost,dc=org +EOF + + test "$(_read_ini "$file" "foo")" == "null" + test "$(ynh_read_var_in_file "$file" "foo")" == "null" + + test "$(_read_ini "$file" "enabled")" == "False" + test "$(ynh_read_var_in_file "$file" "enabled")" == "False" + + test "$(_read_ini "$file" "title")" == "Lorem Ipsum" + test "$(ynh_read_var_in_file "$file" "title")" == "Lorem Ipsum" + + test "$(_read_ini "$file" "theme")" == "colib'ris" + test "$(ynh_read_var_in_file "$file" "theme")" == "colib'ris" + + #test "$(_read_ini "$file" "email")" == "root@example.com" + test "$(ynh_read_var_in_file "$file" "email")" == "root@example.com" + + #test "$(_read_ini "$file" "port")" == "1234" + test "$(ynh_read_var_in_file "$file" "port")" == "1234" + + test "$(_read_ini "$file" "url")" == "https://yunohost.org" + test "$(ynh_read_var_in_file "$file" "url")" == "https://yunohost.org" + + test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org" + + ! _read_ini "$file" "nonexistent" + test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL" + + ! _read_ini "$file" "enable" + test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL" + +} + +ynhtest_config_write_ini() { + local dummy_dir="$(mktemp -d -p $VAR_WWW)" + file="$dummy_dir/dummy.ini" + + cat << EOF > $file +# Some comment +; Another comment +[main] +foo = null +enabled = False +# title = Old title +title = Lorem Ipsum +theme = colib'ris +email = root@example.com # This is a comment without quotes +port = 1234 # This is a comment without quotes +url = https://yunohost.org +[dict] + ldap_base = ou=users,dc=yunohost,dc=org +EOF + + ynh_write_var_in_file "$file" "foo" "bar" + test "$(_read_ini "$file" "foo")" == "bar" + test "$(ynh_read_var_in_file "$file" "foo")" == "bar" + + ynh_write_var_in_file "$file" "enabled" "True" + test "$(_read_ini "$file" "enabled")" == "True" + test "$(ynh_read_var_in_file "$file" "enabled")" == "True" + + ynh_write_var_in_file "$file" "title" "Foo Bar" + test "$(_read_ini "$file" "title")" == "Foo Bar" + test "$(ynh_read_var_in_file "$file" "title")" == "Foo Bar" + + ynh_write_var_in_file "$file" "theme" "super-awesome-theme" + test "$(_read_ini "$file" "theme")" == "super-awesome-theme" + test "$(ynh_read_var_in_file "$file" "theme")" == "super-awesome-theme" + + ynh_write_var_in_file "$file" "email" "sam@domain.tld" + test "$(_read_ini "$file" "email")" == "sam@domain.tld # This is a comment without quotes" + test "$(ynh_read_var_in_file "$file" "email")" == "sam@domain.tld" + + ynh_write_var_in_file "$file" "port" "5678" + test "$(_read_ini "$file" "port")" == "5678 # This is a comment without quotes" + test "$(ynh_read_var_in_file "$file" "port")" == "5678" + + ynh_write_var_in_file "$file" "url" "https://domain.tld/foobar" + test "$(_read_ini "$file" "url")" == "https://domain.tld/foobar" + test "$(ynh_read_var_in_file "$file" "url")" == "https://domain.tld/foobar" + + ynh_write_var_in_file "$file" "ldap_base" "ou=users,dc=yunohost,dc=org" + test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org" + + ! ynh_write_var_in_file "$file" "nonexistent" "foobar" + ! _read_ini "$file" "nonexistent" + test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL" + + ! ynh_write_var_in_file "$file" "enable" "foobar" + ! _read_ini "$file" "enable" + test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL" + +} + +############################# +# _ # +# | | # +# _ _ __ _ _ __ ___ | | # +# | | | |/ _` | '_ ` _ \| | # +# | |_| | (_| | | | | | | | # +# \__, |\__,_|_| |_| |_|_| # +# __/ | # +# |___/ # +# # +############################# + +_read_yaml() { + local file="$1" + local key="$2" + python3 -c "import yaml; print(yaml.safe_load(open('$file'))['$key'])" +} + +ynhtest_config_read_yaml() { + local dummy_dir="$(mktemp -d -p $VAR_WWW)" + file="$dummy_dir/dummy.yml" + + cat << EOF > $file +# Some comment +foo: +enabled: false +# title: old title +title: Lorem Ipsum +theme: colib'ris +email: root@example.com # This is a comment without quotes +port: 1234 # This is a comment without quotes +url: https://yunohost.org +dict: + ldap_base: ou=users,dc=yunohost,dc=org +EOF + + test "$(_read_yaml "$file" "foo")" == "None" + test "$(ynh_read_var_in_file "$file" "foo")" == "" + + test "$(_read_yaml "$file" "enabled")" == "False" + test "$(ynh_read_var_in_file "$file" "enabled")" == "false" + + test "$(_read_yaml "$file" "title")" == "Lorem Ipsum" + test "$(ynh_read_var_in_file "$file" "title")" == "Lorem Ipsum" + + test "$(_read_yaml "$file" "theme")" == "colib'ris" + test "$(ynh_read_var_in_file "$file" "theme")" == "colib'ris" + + test "$(_read_yaml "$file" "email")" == "root@example.com" + test "$(ynh_read_var_in_file "$file" "email")" == "root@example.com" + + test "$(_read_yaml "$file" "port")" == "1234" + test "$(ynh_read_var_in_file "$file" "port")" == "1234" + + test "$(_read_yaml "$file" "url")" == "https://yunohost.org" + test "$(ynh_read_var_in_file "$file" "url")" == "https://yunohost.org" + + test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org" + + ! _read_yaml "$file" "nonexistent" + test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL" + + ! _read_yaml "$file" "enable" + test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL" +} + + +ynhtest_config_write_yaml() { + local dummy_dir="$(mktemp -d -p $VAR_WWW)" + file="$dummy_dir/dummy.yml" + + cat << EOF > $file +# Some comment +foo: +enabled: false +# title: old title +title: Lorem Ipsum +theme: colib'ris +email: root@example.com # This is a comment without quotes +port: 1234 # This is a comment without quotes +url: https://yunohost.org +dict: + ldap_base: ou=users,dc=yunohost,dc=org +EOF + + ynh_write_var_in_file "$file" "foo" "bar" + # cat $dummy_dir/dummy.yml # to debug + ! test "$(_read_yaml "$file" "foo")" == "bar" # writing broke the yaml syntax... "foo:bar" (no space aftr :) + test "$(ynh_read_var_in_file "$file" "foo")" == "bar" + + ynh_write_var_in_file "$file" "enabled" "true" + test "$(_read_yaml "$file" "enabled")" == "True" + test "$(ynh_read_var_in_file "$file" "enabled")" == "true" + + ynh_write_var_in_file "$file" "title" "Foo Bar" + test "$(_read_yaml "$file" "title")" == "Foo Bar" + test "$(ynh_read_var_in_file "$file" "title")" == "Foo Bar" + + ynh_write_var_in_file "$file" "theme" "super-awesome-theme" + test "$(_read_yaml "$file" "theme")" == "super-awesome-theme" + test "$(ynh_read_var_in_file "$file" "theme")" == "super-awesome-theme" + + ynh_write_var_in_file "$file" "email" "sam@domain.tld" + test "$(_read_yaml "$file" "email")" == "sam@domain.tld" + test "$(ynh_read_var_in_file "$file" "email")" == "sam@domain.tld" + + ynh_write_var_in_file "$file" "port" "5678" + test "$(_read_yaml "$file" "port")" == "5678" + test "$(ynh_read_var_in_file "$file" "port")" == "5678" + + ynh_write_var_in_file "$file" "url" "https://domain.tld/foobar" + test "$(_read_yaml "$file" "url")" == "https://domain.tld/foobar" + test "$(ynh_read_var_in_file "$file" "url")" == "https://domain.tld/foobar" + + ynh_write_var_in_file "$file" "ldap_base" "ou=foobar,dc=domain,dc=tld" + test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=foobar,dc=domain,dc=tld" + + ! ynh_write_var_in_file "$file" "nonexistent" "foobar" + test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL" + + ! ynh_write_var_in_file "$file" "enable" "foobar" + test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL" + test "$(ynh_read_var_in_file "$file" "enabled")" == "true" +} + +######################### +# _ # +# (_) # +# _ ___ ___ _ __ # +# | / __|/ _ \| '_ \ # +# | \__ \ (_) | | | | # +# | |___/\___/|_| |_| # +# _/ | # +# |__/ # +# # +######################### + +_read_json() { + local file="$1" + local key="$2" + python3 -c "import json; print(json.load(open('$file'))['$key'])" +} + +ynhtest_config_read_json() { + local dummy_dir="$(mktemp -d -p $VAR_WWW)" + file="$dummy_dir/dummy.json" + + cat << EOF > $file +{ + "foo": null, + "enabled": false, + "title": "Lorem Ipsum", + "theme": "colib'ris", + "email": "root@example.com", + "port": 1234, + "url": "https://yunohost.org", + "dict": { + "ldap_base": "ou=users,dc=yunohost,dc=org" + } +} +EOF + + + test "$(_read_json "$file" "foo")" == "None" + test "$(ynh_read_var_in_file "$file" "foo")" == "null" + + test "$(_read_json "$file" "enabled")" == "False" + test "$(ynh_read_var_in_file "$file" "enabled")" == "false" + + test "$(_read_json "$file" "title")" == "Lorem Ipsum" + test "$(ynh_read_var_in_file "$file" "title")" == "Lorem Ipsum" + + test "$(_read_json "$file" "theme")" == "colib'ris" + test "$(ynh_read_var_in_file "$file" "theme")" == "colib'ris" + + test "$(_read_json "$file" "email")" == "root@example.com" + test "$(ynh_read_var_in_file "$file" "email")" == "root@example.com" + + test "$(_read_json "$file" "port")" == "1234" + test "$(ynh_read_var_in_file "$file" "port")" == "1234" + + test "$(_read_json "$file" "url")" == "https://yunohost.org" + test "$(ynh_read_var_in_file "$file" "url")" == "https://yunohost.org" + + test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org" + + ! _read_json "$file" "nonexistent" + test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL" + + ! _read_json "$file" "enable" + test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL" +} + + +ynhtest_config_write_json() { + local dummy_dir="$(mktemp -d -p $VAR_WWW)" + file="$dummy_dir/dummy.json" + + cat << EOF > $file +{ + "foo": null, + "enabled": false, + "title": "Lorem Ipsum", + "theme": "colib'ris", + "email": "root@example.com", + "port": 1234, + "url": "https://yunohost.org", + "dict": { + "ldap_base": "ou=users,dc=yunohost,dc=org" + } +} +EOF + + ynh_write_var_in_file "$file" "foo" "bar" + cat $file + test "$(_read_json "$file" "foo")" == "bar" + test "$(ynh_read_var_in_file "$file" "foo")" == "bar" + + ynh_write_var_in_file "$file" "enabled" "true" + cat $file + test "$(_read_json "$file" "enabled")" == "true" + test "$(ynh_read_var_in_file "$file" "enabled")" == "true" + + ynh_write_var_in_file "$file" "title" "Foo Bar" + cat $file + test "$(_read_json "$file" "title")" == "Foo Bar" + test "$(ynh_read_var_in_file "$file" "title")" == "Foo Bar" + + ynh_write_var_in_file "$file" "theme" "super-awesome-theme" + cat $file + test "$(_read_json "$file" "theme")" == "super-awesome-theme" + test "$(ynh_read_var_in_file "$file" "theme")" == "super-awesome-theme" + + ynh_write_var_in_file "$file" "email" "sam@domain.tld" + cat $file + test "$(_read_json "$file" "email")" == "sam@domain.tld" + test "$(ynh_read_var_in_file "$file" "email")" == "sam@domain.tld" + + ynh_write_var_in_file "$file" "port" "5678" + test "$(_read_json "$file" "port")" == "5678" + test "$(ynh_read_var_in_file "$file" "port")" == "5678" + + ynh_write_var_in_file "$file" "url" "https://domain.tld/foobar" + test "$(_read_json "$file" "url")" == "https://domain.tld/foobar" + test "$(ynh_read_var_in_file "$file" "url")" == "https://domain.tld/foobar" + + ynh_write_var_in_file "$file" "ldap_base" "ou=foobar,dc=domain,dc=tld" + test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=foobar,dc=domain,dc=tld" + + ! ynh_write_var_in_file "$file" "nonexistent" "foobar" + test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL" + + ! ynh_write_var_in_file "$file" "enable" "foobar" + test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL" + test "$(ynh_read_var_in_file "$file" "enabled")" == "true" +} + +####################### +# _ # +# | | # +# _ __ | |__ _ __ # +# | '_ \| '_ \| '_ \ # +# | |_) | | | | |_) | # +# | .__/|_| |_| .__/ # +# | | | | # +# |_| |_| # +# # +####################### + +_read_php() { + local file="$1" + local key="$2" + php -r "include '$file'; echo var_export(\$$key);" | sed "s/^'//g" | sed "s/'$//g" +} + +ynhtest_config_read_php() { + local dummy_dir="$(mktemp -d -p $VAR_WWW)" + file="$dummy_dir/dummy.php" + + cat << EOF > $file + "ou=users,dc=yunohost,dc=org", + 'ldap_conf' => [] + ]; + \$dict['ldap_conf']['user'] = 'camille'; + const DB_HOST = 'localhost'; +?> +EOF + + test "$(_read_php "$file" "foo")" == "NULL" + test "$(ynh_read_var_in_file "$file" "foo")" == "NULL" + + test "$(_read_php "$file" "enabled")" == "false" + test "$(ynh_read_var_in_file "$file" "enabled")" == "false" + + test "$(_read_php "$file" "title")" == "Lorem Ipsum" + test "$(ynh_read_var_in_file "$file" "title")" == "Lorem Ipsum" + + test "$(_read_php "$file" "theme")" == "colib\\'ris" + test "$(ynh_read_var_in_file "$file" "theme")" == "colib'ris" + + test "$(_read_php "$file" "email")" == "root@example.com" + test "$(ynh_read_var_in_file "$file" "email")" == "root@example.com" + + test "$(_read_php "$file" "port")" == "1234" + test "$(ynh_read_var_in_file "$file" "port")" == "1234" + + test "$(_read_php "$file" "url")" == "https://yunohost.org" + test "$(ynh_read_var_in_file "$file" "url")" == "https://yunohost.org" + + test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org" + + test "$(ynh_read_var_in_file "$file" "user")" == "camille" + + test "$(ynh_read_var_in_file "$file" "DB_HOST")" == "localhost" + + ! _read_php "$file" "nonexistent" + test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL" + + ! _read_php "$file" "enable" + test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL" +} + + +ynhtest_config_write_php() { + local dummy_dir="$(mktemp -d -p $VAR_WWW)" + file="$dummy_dir/dummy.php" + + cat << EOF > $file + "ou=users,dc=yunohost,dc=org", + ]; +?> +EOF + + ynh_write_var_in_file "$file" "foo" "bar" + test "$(_read_php "$file" "foo")" == "bar" + test "$(ynh_read_var_in_file "$file" "foo")" == "bar" + + ynh_write_var_in_file "$file" "enabled" "true" + test "$(_read_php "$file" "enabled")" == "true" + test "$(ynh_read_var_in_file "$file" "enabled")" == "true" + + ynh_write_var_in_file "$file" "title" "Foo Bar" + cat $file + test "$(_read_php "$file" "title")" == "Foo Bar" + test "$(ynh_read_var_in_file "$file" "title")" == "Foo Bar" + + ynh_write_var_in_file "$file" "theme" "super-awesome-theme" + cat $file + test "$(_read_php "$file" "theme")" == "super-awesome-theme" + test "$(ynh_read_var_in_file "$file" "theme")" == "super-awesome-theme" + + ynh_write_var_in_file "$file" "email" "sam@domain.tld" + cat $file + test "$(_read_php "$file" "email")" == "sam@domain.tld" + test "$(ynh_read_var_in_file "$file" "email")" == "sam@domain.tld" + + ynh_write_var_in_file "$file" "port" "5678" + test "$(_read_php "$file" "port")" == "5678" + test "$(ynh_read_var_in_file "$file" "port")" == "5678" + + ynh_write_var_in_file "$file" "url" "https://domain.tld/foobar" + test "$(_read_php "$file" "url")" == "https://domain.tld/foobar" + test "$(ynh_read_var_in_file "$file" "url")" == "https://domain.tld/foobar" + + ynh_write_var_in_file "$file" "ldap_base" "ou=foobar,dc=domain,dc=tld" + test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=foobar,dc=domain,dc=tld" + + ! ynh_write_var_in_file "$file" "nonexistent" "foobar" + test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL" + + ! ynh_write_var_in_file "$file" "enable" "foobar" + test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL" + test "$(ynh_read_var_in_file "$file" "enabled")" == "true" +} 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" +} 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 +} diff --git a/tests/test_helpers.d/ynhtest_settings.sh b/tests/test_helpers.d/ynhtest_settings.sh new file mode 100644 index 000000000..e916c146b --- /dev/null +++ b/tests/test_helpers.d/ynhtest_settings.sh @@ -0,0 +1,29 @@ +ynhtest_settings() { + + test -n "$app" + + mkdir -p "/etc/yunohost/apps/$app" + echo "label: $app" > "/etc/yunohost/apps/$app/settings.yml" + + test -z "$(ynh_app_setting_get --key="foo")" + test -z "$(ynh_app_setting_get --key="bar")" + test -z "$(ynh_app_setting_get --app="$app" --key="baz")" + + ynh_app_setting_set --key="foo" --value="foovalue" + ynh_app_setting_set --app="$app" --key="bar" --value="barvalue" + ynh_app_setting_set "$app" baz bazvalue + + test "$(ynh_app_setting_get --key="foo")" == "foovalue" + test "$(ynh_app_setting_get --key="bar")" == "barvalue" + test "$(ynh_app_setting_get --app="$app" --key="baz")" == "bazvalue" + + ynh_app_setting_delete --key="foo" + ynh_app_setting_delete --app="$app" --key="bar" + ynh_app_setting_delete "$app" baz + + test -z "$(ynh_app_setting_get --key="foo")" + test -z "$(ynh_app_setting_get --key="bar")" + test -z "$(ynh_app_setting_get --app="$app" --key="baz")" + + rm -rf "/etc/yunohost/apps/$app" +} diff --git a/tests/test_helpers.d/ynhtest_user.sh b/tests/test_helpers.d/ynhtest_user.sh new file mode 100644 index 000000000..46f2a0cd6 --- /dev/null +++ b/tests/test_helpers.d/ynhtest_user.sh @@ -0,0 +1,25 @@ + +ynhtest_system_user_create() { + username=$(head -c 12 /dev/urandom | md5sum | head -c 12) + + ! ynh_system_user_exists --username="$username" + + ynh_system_user_create --username="$username" + + ynh_system_user_exists --username="$username" + + ynh_system_user_delete --username="$username" + + ! ynh_system_user_exists --username="$username" +} + +ynhtest_system_user_with_group() { + username=$(head -c 12 /dev/urandom | md5sum | head -c 12) + + ynh_system_user_create --username="$username" --groups="ssl-cert,ssh.app" + + grep -q "^ssl-cert:.*$username" /etc/group + grep -q "^ssh.app:.*$username" /etc/group + + ynh_system_user_delete --username="$username" +} diff --git a/tests/test_translation_format_consistency.py b/tests/test_translation_format_consistency.py deleted file mode 100644 index 86d1c3279..000000000 --- a/tests/test_translation_format_consistency.py +++ /dev/null @@ -1,52 +0,0 @@ -import re -import json -import glob -import pytest - -# List all locale files (except en.json being the ref) -locale_folder = "locales/" -locale_files = glob.glob(locale_folder + "*.json") -locale_files = [filename.split("/")[-1] for filename in locale_files] -locale_files.remove("en.json") - -reference = json.loads(open(locale_folder + "en.json").read()) - - -def find_inconsistencies(locale_file): - - this_locale = json.loads(open(locale_folder + locale_file).read()) - - # We iterate over all keys/string in en.json - for key, string in reference.items(): - - # Ignore check if there's no translation yet for this key - if key not in this_locale: - continue - - # Then we check that every "{stuff}" (for python's .format()) - # should also be in the translated string, otherwise the .format - # will trigger an exception! - subkeys_in_ref = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)) - subkeys_in_this_locale = set( - k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key]) - ) - - if any(k not in subkeys_in_ref for k in subkeys_in_this_locale): - yield """\n -========================== -Format inconsistency for string {key} in {locale_file}:" -en.json -> {string} -{locale_file} -> {translated_string} -""".format( - key=key, - string=string.encode("utf-8"), - locale_file=locale_file, - translated_string=this_locale[key].encode("utf-8"), - ) - - -@pytest.mark.parametrize("locale_file", locale_files) -def test_translation_format_consistency(locale_file): - inconsistencies = list(find_inconsistencies(locale_file)) - if inconsistencies: - raise Exception("".join(inconsistencies)) diff --git a/tox.ini b/tox.ini index c25d8bf8f..dc2c52074 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,15 @@ [tox] -envlist = py37-{lint,invalidcode},py37-black-{run,check} +envlist = py39-{lint,invalidcode},py39-black-{run,check} [testenv] skip_install=True deps = - py37-{lint,invalidcode}: flake8 - py37-black-{run,check}: black + py39-{lint,invalidcode}: flake8 + py39-black-{run,check}: black + py39-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-black-check: black --check --diff src doc data tests - py37-black-run: black src doc data tests + py39-lint: flake8 src doc maintenance tests --ignore E402,E501,E203,W503 --exclude src/vendor + py39-invalidcode: flake8 src bin maintenance --exclude src/tests,src/vendor --select F,E722,W605 + py39-black-check: black --check --diff bin src doc maintenance tests + py39-black-run: black bin src doc maintenance tests + py39-mypy: mypy --ignore-missing-import --install-types --non-interactive --follow-imports silent src/ --exclude (acme_tiny|migrations)