diff --git a/.coveragerc b/.coveragerc index fe22c8381..bc952e665 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,2 @@ [report] -omit=src/tests/*,src/vendor/*,/usr/lib/moulinette/yunohost/* +omit=src/tests/*,src/vendor/*,/usr/lib/moulinette/yunohost/*,/usr/lib/python3/dist-packages/yunohost/tests/*,/usr/lib/python3/dist-packages/yunohost/vendor/* diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..01b917f6e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,42 @@ +name: "CodeQL" + +on: + push: + branches: [ "dev" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "dev" ] + paths-ignore: + - 'src/tests/**' + schedule: + - cron: '43 12 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/n_updater.sh b/.github/workflows/n_updater.sh deleted file mode 100644 index a8b0b0eec..000000000 --- a/.github/workflows/n_updater.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -#================================================= -# N UPDATING HELPER -#================================================= - -# This script is meant to be run by GitHub Actions. -# It is derived from the Updater script from the YunoHost-Apps organization. -# It aims to automate the update of `n`, the Node version management system. - -#================================================= -# FETCHING LATEST RELEASE AND ITS ASSETS -#================================================= - -# Fetching information -source helpers/nodejs -current_version="$n_version" -repo="tj/n" -# Some jq magic is needed, because the latest upstream release is not always the latest version (e.g. security patches for older versions) -version=$(curl --silent "https://api.github.com/repos/$repo/releases" | jq -r '.[] | select( .prerelease != true ) | .tag_name' | sort -V | tail -1) - -# Later down the script, we assume the version has only digits and dots -# Sometimes the release name starts with a "v", so let's filter it out. -if [[ ${version:0:1} == "v" || ${version:0:1} == "V" ]]; then - version=${version:1} -fi - -# Setting up the environment variables -echo "Current version: $current_version" -echo "Latest release from upstream: $version" -echo "VERSION=$version" >> $GITHUB_ENV -# For the time being, let's assume the script will fail -echo "PROCEED=false" >> $GITHUB_ENV - -# Proceed only if the retrieved version is greater than the current one -if ! dpkg --compare-versions "$current_version" "lt" "$version" ; then - echo "::warning ::No new version available" - exit 0 -# Proceed only if a PR for this new version does not already exist -elif git ls-remote -q --exit-code --heads https://github.com/${GITHUB_REPOSITORY:-YunoHost/yunohost}.git ci-auto-update-n-v$version ; then - echo "::warning ::A branch already exists for this update" - exit 0 -fi - -#================================================= -# UPDATE SOURCE FILES -#================================================= - -asset_url="https://github.com/tj/n/archive/v${version}.tar.gz" - -echo "Handling asset at $asset_url" - -# Create the temporary directory -tempdir="$(mktemp -d)" - -# Download sources and calculate checksum -filename=${asset_url##*/} -curl --silent -4 -L $asset_url -o "$tempdir/$filename" -checksum=$(sha256sum "$tempdir/$filename" | head -c 64) - -# Delete temporary directory -rm -rf $tempdir - -echo "Calculated checksum for n v${version} is $checksum" - -#================================================= -# GENERIC FINALIZATION -#================================================= - -# Replace new version in helper -sed -i -E "s/^n_version=.*$/n_version=$version/" helpers/nodejs - -# Replace checksum in helper -sed -i -E "s/^n_checksum=.*$/n_checksum=$checksum/" helpers/nodejs - -# The Action will proceed only if the PROCEED environment variable is set to true -echo "PROCEED=true" >> $GITHUB_ENV -exit 0 diff --git a/.github/workflows/n_updater.yml b/.github/workflows/n_updater.yml index 35afd8ae7..ce3e9c925 100644 --- a/.github/workflows/n_updater.yml +++ b/.github/workflows/n_updater.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Fetch the source code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - name: Run the updater script @@ -21,7 +21,8 @@ jobs: git config --global user.name 'yunohost-bot' git config --global user.email 'yunohost-bot@users.noreply.github.com' # Run the updater script - /bin/bash .github/workflows/n_updater.sh + wget https://raw.githubusercontent.com/tj/n/master/bin/n --output-document=helpers/vendor/n/n + [[ -z "$(git diff helpers/vendor/n/n)" ]] || echo "PROCEED=true" >> $GITHUB_ENV - name: Commit changes id: commit if: ${{ env.PROCEED == 'true' }} diff --git a/.gitignore b/.gitignore index eae46b4c5..91b5b56e4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,10 @@ src/locales # Test src/tests/apps + +# Tmp/local doc stuff +doc/bash-completion.sh +doc/bash_completion.d +doc/openapi.js +doc/openapi.json +doc/swagger diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4d0f30679..3e030940b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,6 +16,9 @@ default: code_quality: tags: - docker + rules: + - if: $CI_COMMIT_TAG # Only for tags + code_quality_html: extends: code_quality @@ -23,6 +26,9 @@ code_quality_html: REPORT_FORMAT: html artifacts: paths: [gl-code-quality-report.html] + rules: + - if: $CI_COMMIT_TAG # Only for tags + # see: https://docs.gitlab.com/ee/ci/yaml/#switch-between-branch-pipelines-and-merge-request-pipelines workflow: @@ -37,7 +43,7 @@ workflow: - when: always variables: - YNH_BUILD_DIR: "ynh-build" + YNH_BUILD_DIR: "/ynh-build" include: - template: Code-Quality.gitlab-ci.yml diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index db691b9d2..610580dac 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -8,7 +8,7 @@ - DEBIAN_FRONTEND=noninteractive apt update artifacts: paths: - - $YNH_BUILD_DIR/*.deb + - ./*.deb .build_script: &build_script - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" install devscripts --no-install-recommends @@ -17,6 +17,8 @@ - VERSION_NIGHTLY="${VERSION}+$(date +%Y%m%d%H%M)" - dch --package "${PACKAGE}" --force-bad-version -v "${VERSION_NIGHTLY}" -D "unstable" --force-distribution "Daily build." - debuild --no-lintian -us -uc + - cp $YNH_BUILD_DIR/*.deb ${CI_PROJECT_DIR}/ + - cd ${CI_PROJECT_DIR} ######################################## # BUILD DEB @@ -31,7 +33,7 @@ build-yunohost: - mkdir -p $YNH_BUILD_DIR/$PACKAGE - cat archive.tar.gz | tar -xz -C $YNH_BUILD_DIR/$PACKAGE - rm archive.tar.gz - - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE + - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE - *build_script @@ -42,7 +44,7 @@ build-ssowat: script: - DEBIAN_DEPENDS=$(cat debian/control | tr "," "\n" | grep -Po "ssowat \([>,=,<]+ .*\)" | grep -Po "[0-9\.]+") - git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $DEBIAN_DEPENDS $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1 - - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE + - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE - *build_script build-moulinette: @@ -52,5 +54,5 @@ build-moulinette: script: - DEBIAN_DEPENDS=$(cat debian/control | tr "," "\n" | grep -Po "moulinette \([>,=,<]+ .*\)" | grep -Po "[0-9\.]+") - git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $DEBIAN_DEPENDS $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1 - - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE + - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE - *build_script diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 528d8f5aa..4f6ea6ba1 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -13,15 +13,18 @@ generate-helpers-doc: script: - cd doc - python3 generate_helper_doc.py + - python3 generate_resource_doc.py > resources.md - hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo - - cp helpers.md doc_repo/pages/06.contribute/10.packaging_apps/11.helpers/packaging_apps_helpers.md + - cp helpers.md doc_repo/pages/06.contribute/10.packaging_apps/80.resources/11.helpers/packaging_apps_helpers.md + - cp resources.md doc_repo/pages/06.contribute/10.packaging_apps/80.resources/15.appresources/packaging_apps_resources.md - cd doc_repo # replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ? - hub checkout -b "${CI_COMMIT_REF_NAME}" - - hub commit -am "[CI] Helper for ${CI_COMMIT_REF_NAME}" - - hub pull-request -m "[CI] Helper for ${CI_COMMIT_REF_NAME}" -p # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd + - hub commit -am "[CI] Update app helpers/resources for ${CI_COMMIT_REF_NAME}" + - hub pull-request -m "[CI] Update app helpers/resources for ${CI_COMMIT_REF_NAME}" -p # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd artifacts: paths: - doc/helpers.md + - doc/resources.md only: - tags diff --git a/.gitlab/ci/install.gitlab-ci.yml b/.gitlab/ci/install.gitlab-ci.yml index e2662e9e2..65409c6eb 100644 --- a/.gitlab/ci/install.gitlab-ci.yml +++ b/.gitlab/ci/install.gitlab-ci.yml @@ -17,7 +17,7 @@ upgrade: image: "after-install" script: - apt-get update -o Acquire::Retries=3 - - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ./$YNH_BUILD_DIR/*.deb + - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb install-postinstall: @@ -25,5 +25,5 @@ install-postinstall: image: "before-install" script: - apt-get update -o Acquire::Retries=3 - - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ./$YNH_BUILD_DIR/*.deb - - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace + - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb + - yunohost tools postinstall -d domain.tld -u syssa -F 'Syssa Mine' -p the_password --ignore-dyndns --force-diskspace diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index 2c2bdcc1d..7a8fbf1fb 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -42,7 +42,6 @@ black: - '[ $(git diff | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit - git commit -am "[CI] Format code with Black" || true - git push -f origin "ci-format-${CI_COMMIT_REF_NAME}":"ci-format-${CI_COMMIT_REF_NAME}" - - hub pull-request -m "[CI] Format code with Black" -b Yunohost:$CI_COMMIT_REF_NAME -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd + - hub pull-request -m "[CI] Format code with Black" -b Yunohost:dev -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd only: - variables: - - $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH + - tags diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index 27b9b4913..b0ffd3db5 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -1,7 +1,7 @@ .install_debs: &install_debs - apt-get update -o Acquire::Retries=3 - - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ./$YNH_BUILD_DIR/*.deb - - pip3 install -U mock pip pytest pytest-cov pytest-mock pytest-sugar requests-mock tox ansi2html black jinja2 + - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb + - pip3 install -U mock pip pytest pytest-cov pytest-mock pytest-sugar requests-mock tox ansi2html black jinja2 "packaging<22" .test-stage: stage: test @@ -34,9 +34,9 @@ full-tests: PYTEST_ADDOPTS: "--color=yes" before_script: - *install_debs - - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace + - yunohost tools postinstall -d domain.tld -u syssa -F 'Syssa Mine' -p the_password --ignore-dyndns --force-diskspace script: - - python3 -m pytest --cov=yunohost tests/ src/tests/ src/diagnosers/ --junitxml=report.xml + - python3 -m pytest --cov=yunohost tests/ src/tests/ --junitxml=report.xml - cd tests - bash test_helpers.sh needs: @@ -46,6 +46,7 @@ full-tests: artifacts: true - job: build-moulinette artifacts: true + coverage: '/TOTAL.*\s+(\d+%)/' artifacts: reports: junit: report.xml @@ -125,6 +126,15 @@ test-app-config: - src/app.py - src/utils/config.py +test-app-resources: + extends: .test-stage + script: + - python3 -m pytest src/tests/test_app_resources.py + only: + changes: + - src/app.py + - src/utils/resources.py + test-changeurl: extends: .test-stage script: diff --git a/.gitlab/ci/translation.gitlab-ci.yml b/.gitlab/ci/translation.gitlab-ci.yml index b6c683f57..83db2b5a4 100644 --- a/.gitlab/ci/translation.gitlab-ci.yml +++ b/.gitlab/ci/translation.gitlab-ci.yml @@ -26,7 +26,7 @@ autofix-translated-strings: - git checkout -b "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}" --no-track - python3 maintenance/missing_i18n_keys.py --fix - python3 maintenance/autofix_locale_format.py - - '[ $(git diff | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit + - '[ $(git diff --ignore-blank-lines --ignore-all-space --ignore-space-at-eol --ignore-cr-at-eol | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit - git commit -am "[CI] Reformat / remove stale translated strings" || true - git push -f origin "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}":"ci-remove-stale-translated-strings-${CI_COMMIT_REF_NAME}" - hub pull-request -m "[CI] Reformat / remove stale translated strings" -b Yunohost:$CI_COMMIT_REF_NAME -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd diff --git a/.lgtm.yml b/.lgtm.yml deleted file mode 100644 index 8fd57e49e..000000000 --- a/.lgtm.yml +++ /dev/null @@ -1,4 +0,0 @@ -extraction: - python: - python_setup: - version: "3" \ No newline at end of file diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md deleted file mode 100644 index 0a9ac7527..000000000 --- a/CONTRIBUTORS.md +++ /dev/null @@ -1,101 +0,0 @@ -YunoHost core contributors -========================== - -YunoHost is built and maintained by the YunoHost project community. -Everyone is encouraged to submit issues and changes, and to contribute in other ways -- see https://yunohost.org/contribute to find out how. - --- - -Initial YunoHost core was built by Kload & beudbeud, for YunoHost v2. - -Most of code was written by Kload and jerome, with help of numerous contributors. - -Translation is made by a bunch of lovely people all over the world. - -We would like to thank anyone who ever helped the YunoHost project <3 - - -YunoHost core Contributors --------------------------- - -- Jérôme Lebleu -- Kload -- Laurent 'Bram' Peuch -- Julien 'ju' Malik -- opi -- Aleks -- Adrien 'beudbeud' Beudin -- M5oul -- Valentin 'zamentur' / 'ljf' Grimaud -- Jocelyn Delalande -- infertux -- Taziden -- ZeHiro -- Josue-T -- nahoj -- a1ex -- JimboJoe -- vetetix -- jellium -- Sebastien 'sebian' Badia -- lmangani -- Julien Vaubourg -- thardev -- zimo2001 - - -YunoHost core Translators -------------------------- - -If you want to help translation, please visit https://translate.yunohost.org/projects/yunohost/yunohost/ - - -### Dutch - -- DUBWiSE -- Jeroen Keerl -- marut - -### English - -- Bugsbane -- rokaz - -### French - -- aoz roon -- Genma -- Jean-Baptiste Holcroft -- Jean P. -- Jérôme Lebleu -- Lapineige -- paddy - - -### German - -- david.bartke -- Fabian Gruber -- Felix Bartels -- Jeroen Keerl -- martin kistner -- Philip Gatzka - -### Hindi - -- Anmol - -### Italian - -- bricabrac -- Thomas Bille - -### Portuguese - -- Deleted User -- Trollken - -### Spanish - -- Juanu - diff --git a/README.md b/README.md index 969651eee..07ee04de0 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@
![Version](https://img.shields.io/github/v/tag/yunohost/yunohost?label=version&sort=semver) -[![Build status](https://shields.io/gitlab/pipeline/yunohost/yunohost/dev)](https://gitlab.com/yunohost/yunohost/-/pipelines) -![Test coverage](https://img.shields.io/gitlab/coverage/yunohost/yunohost/dev) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/YunoHost/yunohost.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/YunoHost/yunohost/context:python) -[![GitHub license](https://img.shields.io/github/license/YunoHost/yunohost)](https://github.com/YunoHost/yunohost/blob/dev/LICENSE) +[![Pipeline status](https://gitlab.com/yunohost/yunohost/badges/dev/pipeline.svg)](https://gitlab.com/yunohost/yunohost/-/pipelines) +![Test coverage](https://gitlab.com/yunohost/yunohost/badges/dev/coverage.svg) +[![Project license](https://img.shields.io/gitlab/license/yunohost/yunohost)](https://github.com/YunoHost/yunohost/blob/dev/LICENSE) +[![CodeQL](https://github.com/yunohost/yunohost/workflows/CodeQL/badge.svg)](https://github.com/YunoHost/yunohost/security/code-scanning) [![Mastodon Follow](https://img.shields.io/mastodon/follow/28084)](https://mastodon.social/@yunohost)
@@ -44,3 +44,24 @@ Webadmin ([Yunohost-Admin](https://github.com/YunoHost/yunohost-admin)) | Single ## License As [other components of YunoHost](https://yunohost.org/#/faq_en), this repository is licensed under GNU AGPL v3. + +## They support us <3 + +We are thankful for our sponsors providing us with infrastructure and grants! + +
+

+ + + +

+

+ + + + + +

+
+ +This project was funded through the [NGI0 PET](https://nlnet.nl/PET) Fund, a fund established by NLnet with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825310. If you're interested, [check out how to apply in this video](https://media.ccc.de/v/36c3-10795-ngi_zero_a_treasure_trove_of_it_innovation)! diff --git a/bin/yunohost b/bin/yunohost index 8cebdee8e..3f985e6e7 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -1,5 +1,4 @@ #! /usr/bin/python3 -# -*- coding: utf-8 -*- import os import sys @@ -25,6 +24,9 @@ def _parse_cli_args(): parser.add_argument( "--quiet", action="store_true", default=False, help="Don't produce any output" ) + parser.add_argument( + "--version", action="store_true", default=False, help="Display YunoHost packages versions (alias to 'yunohost tools versions')" + ) parser.add_argument( "--timeout", type=int, @@ -51,6 +53,7 @@ def _parse_cli_args(): # Stupid PATH management because sometimes (e.g. some cron job) PATH is only /usr/bin:/bin ... + default_path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" if os.environ["PATH"] != default_path: os.environ["PATH"] = default_path + ":" + os.environ["PATH"] @@ -67,6 +70,9 @@ if __name__ == "__main__": parser, opts, args = _parse_cli_args() + if opts.version: + args = ["tools", "versions"] + # Execute the action yunohost.cli( debug=opts.debug, diff --git a/bin/yunohost-api b/bin/yunohost-api index 8cf9d4f26..9f4d5eb26 100755 --- a/bin/yunohost-api +++ b/bin/yunohost-api @@ -1,5 +1,4 @@ #! /usr/bin/python3 -# -*- coding: utf-8 -*- import argparse import yunohost diff --git a/bin/yunopaste b/bin/yunopaste index 679f13544..edf8d55c8 100755 --- a/bin/yunopaste +++ b/bin/yunopaste @@ -19,7 +19,7 @@ paste_data() { [[ -z "$json" ]] && _die "Unable to post the data to the server." key=$(echo "$json" \ - | python -c 'import json,sys;o=json.load(sys.stdin);print o["key"]' \ + | python3 -c 'import json,sys;o=json.load(sys.stdin);print(o["key"])' \ 2>/dev/null) [[ -z "$key" ]] && _die "Unable to parse the server response." diff --git a/bin/yunoprompt b/bin/yunoprompt index 8062ab06e..3ab510d2a 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -56,7 +56,7 @@ EOF echo "$LOGO_AND_FINGERPRINTS" > /etc/issue -if [[ ! -f /etc/yunohost/installed ]] +if ! groups | grep -q all_users && [[ ! -f /etc/yunohost/installed ]] then chvt 2 diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index 72fd71c4d..e614c3796 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -10,7 +10,7 @@ mail_uid = 500 protocols = imap sieve {% if pop3_enabled == "True" %}pop3{% endif %} -mail_plugins = $mail_plugins quota +mail_plugins = $mail_plugins quota notify push_notification ############################################################################### diff --git a/conf/dovecot/dovecot.sieve b/conf/dovecot/dovecot.sieve index 639c28303..bf4754529 100644 --- a/conf/dovecot/dovecot.sieve +++ b/conf/dovecot/dovecot.sieve @@ -1,4 +1,4 @@ require "fileinto"; - if header :contains "X-Spam-Flag" "YES" { + if header :contains "X-Spam-Flag" "Yes" { fileinto "Junk"; } diff --git a/conf/fail2ban/postfix-sasl.conf b/conf/fail2ban/postfix-sasl.conf new file mode 100644 index 000000000..a9f470782 --- /dev/null +++ b/conf/fail2ban/postfix-sasl.conf @@ -0,0 +1,6 @@ +# Fail2Ban filter for postfix authentication failures +[INCLUDES] +before = common.conf +[Definition] +_daemon = postfix/smtpd +failregex = ^%(__prefix_line)swarning: [-._\w]+\[\]: SASL (?:LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5) authentication failed(: [ A-Za-z0-9+/]*={0,2})?\s*$ diff --git a/conf/fail2ban/yunohost-jails.conf b/conf/fail2ban/yunohost-jails.conf index 1cf1a1966..911f9cd85 100644 --- a/conf/fail2ban/yunohost-jails.conf +++ b/conf/fail2ban/yunohost-jails.conf @@ -8,6 +8,13 @@ enabled = true [postfix] enabled = true +[sasl] +enabled = true +port = smtp +filter = postfix-sasl +logpath = /var/log/mail.log +maxretry = 5 + [dovecot] enabled = true diff --git a/conf/nginx/plain/acme-challenge.conf.inc b/conf/nginx/plain/acme-challenge.conf.inc index 35c4b80c2..859aa6817 100644 --- a/conf/nginx/plain/acme-challenge.conf.inc +++ b/conf/nginx/plain/acme-challenge.conf.inc @@ -1,6 +1,6 @@ location ^~ '/.well-known/acme-challenge/' { default_type "text/plain"; - alias /tmp/acme-challenge-public/; + alias /var/www/.well-known/acme-challenge-public/; gzip off; } diff --git a/conf/nginx/plain/yunohost_http_errors.conf.inc b/conf/nginx/plain/yunohost_http_errors.conf.inc new file mode 100644 index 000000000..76f1015f3 --- /dev/null +++ b/conf/nginx/plain/yunohost_http_errors.conf.inc @@ -0,0 +1,7 @@ +error_page 502 /502.html; + +location = /502.html { + + root /usr/share/yunohost/html/; + +} diff --git a/conf/nginx/redirect_to_admin.conf b/conf/nginx/redirect_to_admin.conf index 22748daa3..1d7933c6a 100644 --- a/conf/nginx/redirect_to_admin.conf +++ b/conf/nginx/redirect_to_admin.conf @@ -1,3 +1,3 @@ location / { - return 302 https://$http_host/yunohost/admin; + return 302 https://$host/yunohost/admin; } diff --git a/conf/nginx/security.conf.inc b/conf/nginx/security.conf.inc index a35bb566e..44d7f86b4 100644 --- a/conf/nginx/security.conf.inc +++ b/conf/nginx/security.conf.inc @@ -26,10 +26,9 @@ ssl_dhparam /usr/share/yunohost/ffdhe2048.pem; # https://wiki.mozilla.org/Security/Guidelines/Web_Security # https://observatory.mozilla.org/ {% if experimental == "True" %} -more_set_headers "Content-Security-Policy : upgrade-insecure-requests; default-src https: data: blob: ; object-src https: data: 'unsafe-inline'; style-src https: data: 'unsafe-inline' ; script-src https: data: 'unsafe-inline' 'unsafe-eval'"; +more_set_headers "Content-Security-Policy : upgrade-insecure-requests; default-src https: data: blob: ; object-src https: data: 'unsafe-inline'; style-src https: data: 'unsafe-inline' ; script-src https: data: 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob:;"; {% else %} more_set_headers "Content-Security-Policy : upgrade-insecure-requests"; -more_set_headers "Content-Security-Policy-Report-Only : default-src https: wss: data: blob: ; object-src https: data: 'unsafe-inline'; style-src https: data: 'unsafe-inline' ; script-src https: data: 'unsafe-inline' 'unsafe-eval'"; {% endif %} more_set_headers "X-Content-Type-Options : nosniff"; more_set_headers "X-XSS-Protection : 1; mode=block"; diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index 379b597a7..ccba8a082 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -6,30 +6,34 @@ map $http_upgrade $connection_upgrade { server { listen 80; listen [::]:80; - server_name {{ domain }} xmpp-upload.{{ domain }}; + server_name {{ domain }}{% if xmpp_enabled == "True" %} xmpp-upload.{{ domain }} muc.{{ domain }}{% endif %}; access_by_lua_file /usr/share/ssowat/access.lua; include /etc/nginx/conf.d/acme-challenge.conf.inc; location ^~ '/.well-known/ynh-diagnosis/' { - alias /tmp/.well-known/ynh-diagnosis/; + alias /var/www/.well-known/ynh-diagnosis/; } + {% if mail_enabled == "True" %} location ^~ '/.well-known/autoconfig/mail/' { alias /var/www/.well-known/{{ domain }}/autoconfig/mail/; } + {% endif %} {# Note that this != "False" is meant to be failure-safe, in the case the redrect_to_https would happen to contain empty string or whatever value. We absolutely don't want to disable the HTTPS redirect *except* when it's explicitly being asked to be disabled. #} {% if redirect_to_https != "False" %} location / { - return 301 https://$http_host$request_uri; + return 301 https://$host$request_uri; } {# The app config snippets are not included in the HTTP conf unless HTTPS redirect is disabled, because app's location may blocks will conflict or bypass/ignore the HTTPS redirection. #} {% else %} include /etc/nginx/conf.d/{{ domain }}.d/*.conf; {% endif %} + include /etc/nginx/conf.d/yunohost_http_errors.conf.inc; + access_log /var/log/nginx/{{ domain }}-access.log; error_log /var/log/nginx/{{ domain }}-error.log; } @@ -44,10 +48,10 @@ server { ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; - {% if domain_cert_ca != "Self-signed" %} + {% if domain_cert_ca != "selfsigned" %} more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload"; {% endif %} - {% if domain_cert_ca == "Let's Encrypt" %} + {% if domain_cert_ca == "letsencrypt" %} # OCSP settings ssl_stapling on; ssl_stapling_verify on; @@ -56,9 +60,11 @@ server { resolver_timeout 5s; {% endif %} + {% if mail_enabled == "True" %} location ^~ '/.well-known/autoconfig/mail/' { alias /var/www/.well-known/{{ domain }}/autoconfig/mail/; } + {% endif %} access_by_lua_file /usr/share/ssowat/access.lua; @@ -67,11 +73,13 @@ server { include /etc/nginx/conf.d/yunohost_sso.conf.inc; include /etc/nginx/conf.d/yunohost_admin.conf.inc; include /etc/nginx/conf.d/yunohost_api.conf.inc; + include /etc/nginx/conf.d/yunohost_http_errors.conf.inc; access_log /var/log/nginx/{{ domain }}-access.log; error_log /var/log/nginx/{{ domain }}-error.log; } +{% if xmpp_enabled == "True" %} # vhost dedicated to XMPP http_upload server { listen 443 ssl http2; @@ -99,10 +107,10 @@ server { ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; - {% if domain_cert_ca != "Self-signed" %} + {% if domain_cert_ca != "selfsigned" %} more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload"; {% endif %} - {% if domain_cert_ca == "Let's Encrypt" %} + {% if domain_cert_ca == "letsencrypt" %} # OCSP settings ssl_stapling on; ssl_stapling_verify on; @@ -114,3 +122,4 @@ server { access_log /var/log/nginx/xmpp-upload.{{ domain }}-access.log; error_log /var/log/nginx/xmpp-upload.{{ domain }}-error.log; } +{% endif %} diff --git a/conf/nginx/yunohost_admin.conf.inc b/conf/nginx/yunohost_admin.conf.inc index b5eff7a5e..0c4a96fdc 100644 --- a/conf/nginx/yunohost_admin.conf.inc +++ b/conf/nginx/yunohost_admin.conf.inc @@ -7,9 +7,11 @@ location /yunohost/admin/ { index index.html; {% if webadmin_allowlist_enabled == "True" %} - {% for ip in webadmin_allowlist.split(',') %} - allow {{ ip }}; - {% endfor %} + {% if webadmin_allowlist.strip() -%} + {% for ip in webadmin_allowlist.strip().split(',') -%} + allow {{ ip.strip() }}; + {% endfor -%} + {% endif -%} deny all; {% endif %} @@ -19,6 +21,10 @@ location /yunohost/admin/ { more_set_headers "Cache-Control: no-store, no-cache, must-revalidate"; } + location /yunohost/admin/applogos/ { + alias /usr/share/yunohost/applogos/; + } + more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; connect-src 'self' https://paste.yunohost.org wss://$host; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; object-src 'none'; img-src 'self' data:;"; more_set_headers "Content-Security-Policy-Report-Only:"; } diff --git a/conf/nginx/yunohost_api.conf.inc b/conf/nginx/yunohost_api.conf.inc index 3a463c23b..b4567e0b8 100644 --- a/conf/nginx/yunohost_api.conf.inc +++ b/conf/nginx/yunohost_api.conf.inc @@ -4,7 +4,7 @@ location /yunohost/api/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_set_header Host $http_host; + proxy_set_header Host $host; {% if webadmin_allowlist_enabled == "True" %} {% for ip in webadmin_allowlist.split(',') %} diff --git a/conf/postfix/main.cf b/conf/postfix/main.cf index 3e53714d0..19b40aefb 100644 --- a/conf/postfix/main.cf +++ b/conf/postfix/main.cf @@ -81,7 +81,7 @@ alias_maps = hash:/etc/aliases alias_database = hash:/etc/aliases mydomain = {{ main_domain }} mydestination = localhost -{% if relay_host == "" %} +{% if relay_enabled != "True" %} relayhost = {% else %} relayhost = [{{ relay_host }}]:{{ relay_port }} @@ -102,7 +102,7 @@ message_size_limit = 35914708 virtual_mailbox_domains = ldap:/etc/postfix/ldap-domains.cf virtual_mailbox_maps = ldap:/etc/postfix/ldap-accounts.cf virtual_mailbox_base = -virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf +virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf,ldap:/etc/postfix/ldap-groups.cf virtual_alias_domains = virtual_minimum_uid = 100 virtual_uid_maps = static:vmail @@ -198,7 +198,7 @@ smtpd_client_recipient_rate_limit=150 # and after to send spam disable_vrfy_command = yes -{% if relay_user != "" %} +{% if relay_enabled == "True" %} # Relay email through an other smtp account # enable SASL authentication smtp_sasl_auth_enable = yes diff --git a/conf/postfix/plain/ldap-groups.cf b/conf/postfix/plain/ldap-groups.cf new file mode 100644 index 000000000..215081fac --- /dev/null +++ b/conf/postfix/plain/ldap-groups.cf @@ -0,0 +1,7 @@ +server_host = localhost +server_port = 389 +search_base = dc=yunohost,dc=org +query_filter = (&(objectClass=groupOfNamesYnh)(mail=%s)) +scope = sub +result_attribute = memberUid, mail +terminal_result_attribute = memberUid diff --git a/conf/rspamd/rspamd.sieve b/conf/rspamd/rspamd.sieve index 38943eefa..56a30c3c1 100644 --- a/conf/rspamd/rspamd.sieve +++ b/conf/rspamd/rspamd.sieve @@ -1,4 +1,4 @@ require ["fileinto"]; -if header :is "X-Spam" "yes" { +if header :is "X-Spam" "Yes" { fileinto "Junk"; } diff --git a/conf/slapd/config.ldif b/conf/slapd/config.ldif index e1fe3b1b5..1037e8bed 100644 --- a/conf/slapd/config.ldif +++ b/conf/slapd/config.ldif @@ -10,7 +10,7 @@ # Config database customization: # 1. Edit this file as you want. # 2. Apply your modifications. For this just run this following command in a shell: -# $ /usr/share/yunohost/hooks/conf_regen/06-slapd apply_config +# $ /usr/share/yunohost/hooks/conf_regen/06-slapd post true # # Note that if you customize this file, YunoHost's regen-conf will NOT # overwrite this file. But that also means that you should be careful about @@ -130,7 +130,6 @@ olcSuffix: dc=yunohost,dc=org # admin entry below # These access lines apply to database #1 only olcAccess: {0}to attrs=userPassword,shadowLastChange - by dn.base="cn=admin,dc=yunohost,dc=org" write by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write by anonymous auth by self write @@ -140,7 +139,6 @@ olcAccess: {0}to attrs=userPassword,shadowLastChange # owning it if they are authenticated. # Others should be able to see it. olcAccess: {1}to attrs=cn,gecos,givenName,mail,maildrop,displayName,sn - by dn.base="cn=admin,dc=yunohost,dc=org" write by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write by self write by * read @@ -160,9 +158,8 @@ olcAccess: {2}to dn.base="" # The admin dn has full write access, everyone else # can read everything. olcAccess: {3}to * - by dn.base="cn=admin,dc=yunohost,dc=org" write by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write - by group/groupOfNames/member.exact="cn=admin,ou=groups,dc=yunohost,dc=org" write + by group/groupOfNames/member.exact="cn=admins,ou=groups,dc=yunohost,dc=org" write by * read # olcAddContentAcl: FALSE diff --git a/conf/slapd/db_init.ldif b/conf/slapd/db_init.ldif index be0181dfe..8703afb85 100644 --- a/conf/slapd/db_init.ldif +++ b/conf/slapd/db_init.ldif @@ -5,15 +5,6 @@ objectClass: organization o: yunohost.org dc: yunohost -dn: cn=admin,ou=sudo,dc=yunohost,dc=org -cn: admin -objectClass: sudoRole -objectClass: top -sudoCommand: ALL -sudoUser: admin -sudoOption: !authenticate -sudoHost: ALL - dn: ou=users,dc=yunohost,dc=org objectClass: organizationalUnit objectClass: top @@ -39,27 +30,23 @@ objectClass: organizationalUnit objectClass: top ou: groups +dn: cn=admins,ou=sudo,dc=yunohost,dc=org +cn: admins +objectClass: sudoRole +objectClass: top +sudoCommand: ALL +sudoUser: %admins +sudoHost: ALL + dn: ou=sudo,dc=yunohost,dc=org objectClass: organizationalUnit objectClass: top ou: sudo -dn: cn=admin,dc=yunohost,dc=org -objectClass: organizationalRole -objectClass: posixAccount -objectClass: simpleSecurityObject -cn: admin -uid: admin -uidNumber: 1007 -gidNumber: 1007 -homeDirectory: /home/admin -loginShell: /bin/bash -userPassword: yunohost - dn: cn=admins,ou=groups,dc=yunohost,dc=org objectClass: posixGroup objectClass: top -memberUid: admin +objectClass: groupOfNamesYnh gidNumber: 4001 cn: admins diff --git a/conf/slapd/mailserver.ldif b/conf/slapd/mailserver.ldif index 849d1d9e1..09f5c64cc 100644 --- a/conf/slapd/mailserver.ldif +++ b/conf/slapd/mailserver.ldif @@ -89,4 +89,7 @@ olcObjectClasses: ( 1.3.6.1.4.1.40328.1.1.2.3 NAME 'mailGroup' SUP top AUXILIARY DESC 'Mail Group' MUST ( mail ) + MAY ( + mailalias $ maildrop + ) ) diff --git a/conf/ssh/sshd_config b/conf/ssh/sshd_config index b6d4111ee..eaa0c7380 100644 --- a/conf/ssh/sshd_config +++ b/conf/ssh/sshd_config @@ -3,7 +3,7 @@ Protocol 2 # PLEASE: if you wish to change the ssh port properly in YunoHost, use this command: -# yunohost settings set security.ssh.port -v +# yunohost settings set security.ssh.ssh_port -v Port {{ port }} {% if ipv6_enabled == "true" %}ListenAddress ::{% endif %} @@ -56,7 +56,7 @@ ChallengeResponseAuthentication no UsePAM yes # PLEASE: if you wish to force everybody to authenticate using ssh keys, run this command: -# yunohost settings set security.ssh.password_authentication -v no +# yunohost settings set security.ssh.ssh_password_authentication -v no {% if password_authentication == "False" %} PasswordAuthentication no {% else %} diff --git a/debian/changelog b/debian/changelog index 929d94c44..428d02b05 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,664 @@ +yunohost (11.1.22) stable; urgency=low + + - security: replace $http_host by $host in nginx conf, cf https://github.com/yandex/gixy/blob/master/docs/en/plugins/hostspoofing.md / Credit to A.Wolski (3957b10e) + - security: keep fail2ban rule when reloading firewall ([#1661](https://github.com/yunohost/yunohost/pull/1661)) + - regenconf: fix a stupid bug using chown instead of chmod ... (af93524c) + - postinstall: crash early if the username already exists on the system (e87ee09b) + - diagnosis: Support multiple TXT entries for TLD ([#1680](https://github.com/yunohost/yunohost/pull/1680)) + - apps: Support gitea's URL format ([#1683](https://github.com/yunohost/yunohost/pull/1683)) + - apps: fix a bug where YunoHost would complain that 'it needs X RAM but only Y left' with Y > X because some apps have a higher runtime RAM requirement than build time ... (4152cb0d) + - apps: Enhance app_shell() : prevent from taking the lock + improve php context with a 'phpflags' setting ([#1681](https://github.com/yunohost/yunohost/pull/1681)) + - apps resources: Allow passing an actual list in the manifest.toml for the apt resource packages ([#1670](https://github.com/yunohost/yunohost/pull/1670)) + - apps resources: fix a bug where port automigration between v1->v2 wouldnt work (36a17dfd) + - i18n: Translations updated for Basque, Galician, Japanese, Polish + + Thanks to all contributors <3 ! (Félix Piédallu, Grzegorz Cichocki, José M, Kayou, motcha, Nicolas Palix, orhtej2, tituspijean, xabirequejo, Yann Autissier) + + -- Alexandre Aubin Mon, 10 Jul 2023 17:43:56 +0200 + +yunohost (11.1.21.4) stable; urgency=low + + - regenconf: Get rid of previous tmp hack about /dev/null for people that went through the very first 11.1.21, because it's causing issue in unpriviledged LXC or similar context (8242cab7) + - apps: don't attempt to del password key if it doesn't exist (29338f79) + + -- Alexandre Aubin Wed, 14 Jun 2023 15:48:33 +0200 + +yunohost (11.1.21.3) stable; urgency=low + + - Fix again /var/www/.well-known/ynh-diagnosis/ perms which are too broad and could be exploited to serve malicious files x_x (84984ad8) + + -- Alexandre Aubin Mon, 12 Jun 2023 17:41:26 +0200 + +yunohost (11.1.21.2) stable; urgency=low + + - Aleks loves xargs syntax >_> (313a1647) + + -- Alexandre Aubin Mon, 12 Jun 2023 00:25:44 +0200 + +yunohost (11.1.21.1) stable; urgency=low + + - Fix stupid issue with code that changes /dev/null perms... (e6f134bc) + + -- Alexandre Aubin Mon, 12 Jun 2023 00:02:47 +0200 + +yunohost (11.1.21) stable; urgency=low + + - users: more verbose logs for user_group_update operations ([#1668](https://github.com/yunohost/yunohost/pull/1668)) + - apps: fix auto-catalog update cron job which was broken because --apps doesnt exist anymore (1552944f) + - apps: Add a 'yunohost app shell' command to open a shell into an app environment ([#1656](https://github.com/yunohost/yunohost/pull/1656)) + - security/regenconf: fix security issue where apps' system conf would be owned by the app, which can enable priviledge escalation (daf51e94) + - security/regenconf: force systemd, nginx, php and fail2ban conf to be owned by root (e649c092) + - security/nginx: use /var/www/.well-known folder for ynh diagnosis and acme challenge, because /tmp/ could be manipulated by user to serve maliciously crafted files (d42c9983) + - i18n: Translations updated for French, Polish, Ukrainian + + Thanks to all contributors <3 ! (Kay0u, Kuba Bazan, ppr, sudo, Tagada, tituspijean, Tymofii-Lytvynenko) + + -- Alexandre Aubin Sun, 11 Jun 2023 19:20:27 +0200 + +yunohost (11.1.20) stable; urgency=low + + - appsv2: fix funky current_version not being defined when hydrating pre-upgrade notifications (8fa823b4) + - helpers: using YNH_APP_ID instead of YNH_APP_INSTANCE_NAME during ynh_setup_source download, for more consistency and because tests was actually failing since a while because of this (e59a4f84) + - helpers: improve error message for corrupt source in ynh_setup_source, it's more relevant to cite the source url rather than the downloaded output path (d698c4c3) + - nginx: Update "worker" Content-Security-Policy header when in experimental security mode ([#1664](https://github.com/yunohost/yunohost/pull/1664)) + - i18n: Translations updated for French, Indonesian, Russian, Slovak + + Thanks to all contributors <3 ! (axolotle, Éric Gaspar, Ilya, Jose Riha, Neko Nekowazarashi, Yann Autissier) + + -- Alexandre Aubin Sat, 20 May 2023 18:57:26 +0200 + +yunohost (11.1.19) stable; urgency=low + + - helpers: Upgrade n to version 9.1.0 ([#1646](https://github.com/yunohost/yunohost/pull/1646)) + - appsv2: in perm resource, fix handling of additional urls containing vars to replace (8fbdd228) + - appsv2: fix version-specific upgrade notification hydration ([#1655](https://github.com/yunohost/yunohost/pull/1655)) + - appsv2/regenconf: prevent set -u to be enabled during regen-conf triggered from inside appsv2 scripts (a7350a7e) + - refactoring: various renaming in configpanel ([#1649](https://github.com/yunohost/yunohost/pull/1649)) + - i18n: Translations updated for Arabic, Basque, Indonesian + + Thanks to all contributors <3 ! (axolotle, ButterflyOfFire, Kayou, Neko Nekowazarashi, tituspijean, xabirequejo) + + -- Alexandre Aubin Mon, 08 May 2023 16:04:06 +0200 + +yunohost (11.1.18) stable; urgency=low + + - appsv2: always set an 'app' setting equal to app id to be able to use __APP__ in markdown templates ([#1645](https://github.com/yunohost/yunohost/pull/1645)) + - appsv2: fix edge-case when validating packager-provided infos for permissions resource (aa43e6c2) + - appsv2: Support using any variables/setting in permissions declaration ([#1637](https://github.com/yunohost/yunohost/pull/1637)) + - dns: Add support for Porkbun through Lexicon ([#1638](https://github.com/yunohost/yunohost/pull/1638)) + - diagnosis: Report out-of-catalog/broken/bad quality apps as warning instead of error ([#1641](https://github.com/yunohost/yunohost/pull/1641)) + - user: .ssh directory should be executable ([#1642](https://github.com/yunohost/yunohost/pull/1642)) + - i18n: Translations updated for Arabic, Basque, French, Galician + + Thanks to all contributors <3 ! (ButterflyOfFire, José M, ppr, tituspijean, xabirequejo) + + -- Alexandre Aubin Fri, 14 Apr 2023 17:20:58 +0200 + +yunohost (11.1.17) stable; urgency=low + + - domains: fix autodns for gandi root domain ([#1634](https://github.com/yunohost/yunohost/pull/1634)) + - helpers: fix previous change about using YNH_APP_ACTION ... which is not defined in config panel context (8c25aa9b) + - appsv2: for the dir/subdirs of data_dir, create parent folders if they don't exist (9a4267ff) + - quality: Split utils/config.py ([#1635](https://github.com/yunohost/yunohost/pull/1635)) + - quality: Rework questions/options tests ([#1629](https://github.com/yunohost/yunohost/pull/1629)) + + Thanks to all contributors <3 ! (axolotle, Kayou) + + -- Alexandre Aubin Wed, 05 Apr 2023 16:00:09 +0200 + +yunohost (11.1.16) stable; urgency=low + + - apps: fix i18n panel+section names ([#1630](https://github.com/yunohost/yunohost/pull/1630)) + - appsv2: don't remove yhh-deps virtual package if it doesn't exist. Otherwise when apt fails to install dependency, we end up with another error about failing to remove the ynh-deps package (3656c199) + - appsv2: add validation for expected types for permissions stuff (b2596f32) + - appsv2: add support for subdirs property in data_dir (4b46f322) + - appsv2: various fixes regarding sources toml parsing/caching (14bf2ee4) + - appsv2: add documentation about the new 'autoupdate' mechanism for app sources (63981aac) + - ynh_setup_source: fix buggy checksum mismatch handling, can't compute the sha256sum after we delete the file @_@ (1b2fa91f) + - users: fix quota parsing being wrong by a factor 1000 ... doveadm returns kilos, not bytes (821aedef) + - backup: fix boring issue where archive is a broken symlink... (a95d10e5) + + Thanks to all contributors <3 ! (axolotle) + + -- Alexandre Aubin Sun, 02 Apr 2023 20:29:33 +0200 + +yunohost (11.1.15) stable; urgency=low + + - doc: Fix version number in autogenerated resource doc (5b58e0e6) + - helpers: Fix documentation for ynh_setup_source (7491dd4c) + - helpers: fix ynh_setup_source, 'source_id' may contain slashes x_x (eaf7a290) + - helpers/nodejs: simplify 'n' script install and maintenance ([#1627](https://github.com/yunohost/yunohost/pull/1627)) + + -- Alexandre Aubin Sat, 11 Mar 2023 16:50:50 +0100 + +yunohost (11.1.14) stable; urgency=low + + - helpers: simplify --time display option for ynh_script_progression .. we don't care about displaying time when below 10 sc (8731f77a) + - appsv2: add support for a 'sources' app resources to modernize and replace app.src format ([#1615](https://github.com/yunohost/yunohost/pull/1615)) + - i18n: Translations updated for Arabic, Polish, Ukrainian + + Thanks to all contributors <3 ! (ButterflyOfFire, Grzegorz Cichocki, Tymofii-Lytvynenko) + + -- Alexandre Aubin Thu, 09 Mar 2023 15:34:17 +0100 + +yunohost (11.1.13) stable; urgency=low + + - appsv2: fix port already used detection ([#1622](https://github.com/yunohost/yunohost/pull/1622)) + - appsv2: when hydrating template, the data may be not-string, eg ports are int (72986842) + - [i18n] Translations updated for Arabic, French, Galician, German, Occitan + + Thanks to all contributors <3 ! (ButterflyOfFire, Christian Wehrli, José M, Kay0u, ppr) + + -- Alexandre Aubin Fri, 03 Mar 2023 22:57:14 +0100 + +yunohost (11.1.12.2) stable; urgency=low + + - helpers: omg base64 wraps the output by default :| (d04f2085) + + -- Alexandre Aubin Wed, 01 Mar 2023 22:12:51 +0100 + +yunohost (11.1.12.1) stable; urgency=low + + - helper: fix previous tweak about debugging diff for manually modified files on the CI @_@ (fd304008) + + -- Alexandre Aubin Wed, 01 Mar 2023 08:08:55 +0100 + +yunohost (11.1.12) stable; urgency=low + + - apps: add '--continue-on-failure' to 'yunohost app upgrade ([#1602](https://github.com/yunohost/yunohost/pull/1602)) + - appsv2: Create parent dirs when provisioning install_dir ([#1609](https://github.com/yunohost/yunohost/pull/1609)) + - appsv2: set `w` as default permission on `install_dir` folder ([#1611](https://github.com/yunohost/yunohost/pull/1611)) + - appsv2: Handle undefined main permission url ([#1620](https://github.com/yunohost/yunohost/pull/1620)) + - apps/helpers: tweak behavior of checksum helper in CI context to help debug why file appear as 'manually modified' ([#1618](https://github.com/yunohost/yunohost/pull/1618)) + - apps/helpers: more robust way to grep that the service correctly started ? ([#1617](https://github.com/yunohost/yunohost/pull/1617)) + - regenconf: sometimes ntp doesnt exist (97c0128c) + - nginx/security: fix empty webadmin allowlist breaking nginx conf... (e458d881) + - misc: automatic get rid of /etc/profile.d/check_yunohost_is_installed.sh when yunohost is postinstalled (20e8805e) + - settings: Fix pop3_enabled parsing returning 0/1 instead of True/False ... (b40c0de3) + - [i18n] Translations updated for French, Galician, Italian, Polish + + Thanks to all contributors <3 ! (Éric Gaspar, John Schmidt, José M, Krakinou, Kuba Bazan, Laurent Peuch, ppr, tituspijean) + + -- Alexandre Aubin Tue, 28 Feb 2023 23:08:02 +0100 + +yunohost (11.1.11.2) stable; urgency=low + + - Rebump version to flag as stable, not testing >_> + + -- Alexandre Aubin Fri, 24 Feb 2023 13:09:48 +0100 + +yunohost (11.1.11.1) testing; urgency=low + + - appsv2: fix previous commit about __DOMAIN__ because url may be None x_x (e05df676) + + -- Alexandre Aubin Fri, 24 Feb 2023 01:30:14 +0100 + +yunohost (11.1.11) stable; urgency=low + + - logs: fix decoding errors not handled when trying to read service logs ([#1606](https://github.com/yunohost/yunohost/pull/1606)) + - mail: fix dovecot-pop3d not being installed when enabling pop3 ([#1607](https://github.com/yunohost/yunohost/pull/1607)) + - apps: when creating the app's bash env for script, make sure to use the manifest from the workdir instead of app setting dir, which is important for consistency during edge case when upgrade from v1 to v2 fails (bab27014) + - appsv2: data_dir's owner should have rwx by default (139e54a2) + - appsv2: fix usage of __DOMAIN__ in permission url (943b9ff8) + + Thanks to all contributors <3 ! (Eric Geldmacher, ljf) + + -- Alexandre Aubin Thu, 23 Feb 2023 22:31:02 +0100 + +yunohost (11.1.10) stable; urgency=low + + - apps: add 'YNH_DEBIAN_VERSION' variable in apps contexts (df6a2a2c) + - appsv2: add support for a packages_from_raw_bash option in apt where one can add a multiline bash snippet to echo packages (4dfff201) + - appsv2: fix resource provisioning scripts picking up already-closed operation logger, resulting in confusing debugging output (888593ad) + - appsv2: fix reload_only_if_change option not working as expected, resulting in incorrect 'Firewall reloaded' messages (d725b454) + - appsv2: fix check that postgresql db exists... (1dc8b753) + + -- Alexandre Aubin Tue, 21 Feb 2023 18:57:33 +0100 + +yunohost (11.1.9) stable; urgency=low + + - apps: simplify the redaction of change_url scripts by adding a new ynh_change_url_nginx_config helper + predefining new/old/change domain/path variables (2b70ccbf) + - appsv2: revert commit that adds a bunch of warning about apt/database consistency, it's more relevant to have them in package linter instead (63f0f084) + - appsv2: fix system user group update, broke in commit from earlier (ec4c2684) + - log: Previous trick about getting rid of setting didnt work, forgot to use metadata instead of self.metadata (848adf89) + - ux: Moar boring postgresql messages displayed as warning (290d627f) + + Thanks to all contributors <3 ! (Bram) + + -- Alexandre Aubin Mon, 20 Feb 2023 20:32:28 +0100 + +yunohost (11.1.8.2) stable; urgency=low + + - regenconf: fix undefined var in apt regenconf (343065eb) + + -- Alexandre Aubin Sun, 19 Feb 2023 21:38:59 +0100 + +yunohost (11.1.8.1) stable; urgency=low + + - postgresql: moar regenconf fixes (e6ae3892) + - postgresql: ugly hack to hide boring warning messages when installing postgresql with apt the first time ... (13d50f4f) + + -- Alexandre Aubin Sun, 19 Feb 2023 19:41:05 +0100 + +yunohost (11.1.8) stable; urgency=low + + - apps: don't miserably crash when failing to read .md file such as DESCRIPTION.md (58ac633d) + - apps: fix edge case when upgrading using a local folder not modified since a while (d3ec5d05) + - appsv2: fix system user provisioning ... (d123fd76, 771b801e) + - appsv2: add check about database vs. apt consistency in resource / warn about lack of explicit dependency to mariadb-server (97b69e7c) + - appsv2: add home dir that defaults to /var/www/__APP__ for system user resource (ce7227c0) + - postgresql: fix regenconf hook, the arg format thingy changed a bit at some point ? (8a43b046) + - regenconf: in apt/php stuff, don't try to upgrade-alternatives if the default PHP version ain't available anymore (similar to commit e24ddd29) (18e034df) + - postinstall: raise a proper error when trying to use e.g. 'admin' as the first username which will conflict with the admins group mail aliases (475c93d5) + - i18n: Translations updated for Arabic, Basque + + Thanks to all contributors <3 ! (ButterflyOfFire, xabirequejo) + + -- Alexandre Aubin Sun, 19 Feb 2023 18:22:02 +0100 + +yunohost (11.1.7) stable; urgency=low + + - mail: fix complain about unused parameters in postfix: exclude_internal=yes / search_timeout=30 (0da6370d) + - mail: Add push notification plugins in dovecot ([#1594](https://github.com/yunohost/yunohost/pull/1594)) + - diagnosis: fix typo, diagnosis detail should be a list, not a string (d0ca120e) + - helpers: in apt/php stuff, don't try to upgrade-alternatives if the default PHP version ain't available anymore (e24ddd29) + - apps: fix inconsistent app removal during remove-after-failed-upgrade and remove-after-failed-backup contexts (7be7eb11) + - appsv2: we don't want to store user-provided passwords by default, but they should still be set in the env for the script to use it (9bd4344f) + - appsv2: fix i18n for arch mismatch, can't juste join() inside string formated with .format() (aa9bc47a) + - appsv2: missing raw_msg=True for exceptions (1d1a3756) + - appsv2: fix check that main permission url is '/' (ab8a6b94) + - appsv2: mysqlshow is fucking dumb and returns exit code 0 when DB doesnt exists ... (0ab20b73) + - appsv2: also replace __DOMAIN__ in resource properties (0c4a006a) + - appsv2: in php helpers, use the global $phpversion var/setting by default instead of $YNH_PHP_VERSION (60b21795) + - i18n: Translations updated for Arabic, Galician + + Thanks to all contributors <3 ! (ButterflyOfFire, John Hackett, José M) + + -- Alexandre Aubin Wed, 15 Feb 2023 21:08:04 +0100 + +yunohost (11.1.6.2) stable; urgency=low + + - permissions: fix trailing-slash issue in edge case where app has additional urls related to a different domain (a4fa6e07) + - backup: fix postinstall during full restore ... tmp admin user can't be named 'admin' because of conflicting alias with the admins group (65894007) + - doc: improve app resource doc (a154e811) + + -- Alexandre Aubin Thu, 09 Feb 2023 19:00:42 +0100 + +yunohost (11.1.6.1) stable; urgency=low + + - dns: fix CAA recommended DNS conf -> 0 is apparently a more sensible value than 128... (2eb7da06) + - users: Allow digits in user fullname (024db62a) + - backup: fix full backup restore postinstall calls that now need first username+fullname+password (48e488f8) + - i18n: Translations updated for Arabic, Basque, Chinese (Simplified) + + Thanks to all contributors <3 ! (ButterflyOfFire, Poesty Li, xabirequejo) + + -- Alexandre Aubin Wed, 08 Feb 2023 22:50:37 +0100 + +yunohost (11.1.6) stable; urgency=low + + - helpers: allow to use ynh_replace_string with @ ([#1588](https://github.com/yunohost/yunohost/pull/1588)) + - helpers: fix behavior of ynh_write_var_in_file when key is duplicated ([#1589](https://github.com/yunohost/yunohost/pull/1589), [#1591](https://github.com/yunohost/yunohost/pull/1591)) + - helpers: fix composer workdir variable for package v2 ([#1586](https://github.com/yunohost/yunohost/pull/1586)) + - configpanels: properly escape & for values used in ynh_write_var_in_file ([#1590](https://github.com/yunohost/yunohost/pull/1590)) + - appsv2/group question: don't include primary groups in choices (c179d4b8) + - appsv2: when initalizing permission, make sure to add 'all_users' when visitors is chosen (71042f08) + - backup/multimedia: test that /home/yunohots.multimedia does exists to avoid boring warning later (170eaf5d) + - domains: add missing logic to inject translated 'help' keys in config panel like we do for global settings (4dee434e) + - domain/dns: don't miserably crash when the domain is known by lexicon but not in registrar_list.toml (b5b69e95) + - admin->admins migration: try to losen up even more the search for first admin user x_x (1e520342) + - i18n: Translations updated for French, Polish + + Thanks to all contributors <3 ! (Éric Gaspar, Grzegorz Cichocki, Kayou, Krzysztof Nowakowski, ljf, ppr) + + -- Alexandre Aubin Tue, 07 Feb 2023 00:14:17 +0100 + +yunohost (11.1.5.5) stable; urgency=low + + - admin->admins migration: try to handle boring case where the 'first' user cant be identified because it doesnt have the root@ alias (8485ebc7) + - appsv2: ignore the old/ugly/legacy removal of apt deps when removing the php conf, because that's handled by the apt resource (3bbba640) + - appsv2: moar fixes for v1->v2 upgrade not getting the proper env context (fb54da2e) + + -- Alexandre Aubin Sat, 04 Feb 2023 18:51:03 +0100 + +yunohost (11.1.5.4) stable; urgency=low + + - appsv2: typo in ports resource doc x_x (0e787acb) + - appsv2: fix permission provisioning for fulldomain apps + fix apps not properly getting removed after failed resources init (476908bd) + + -- Alexandre Aubin Fri, 03 Feb 2023 20:43:04 +0100 + +yunohost (11.1.5.3) stable; urgency=low + + - helpers/appsv2: replacement of __PHPVERSION__ should use the phpversion setting, not YNH_PHP_VERSION (13d4e16e) + - appv2 resources: document the fact that the apt resource may create a phpversion setting when the dependencies contain php packages (2107a848) + + -- Alexandre Aubin Fri, 03 Feb 2023 03:05:11 +0100 + +yunohost (11.1.5.2) stable; urgency=low + + - maintenance: new year, update copyright header (ba4f1925) + - helpers: fix remaining __FINALPATH__ in php template (note that this is backward compatible because ynh_add_config will replace __INSTALL_DIR__ by $finalpath if $finalpath exists... (9b7668da) + + -- Alexandre Aubin Thu, 02 Feb 2023 23:58:29 +0100 + +yunohost (11.1.5.1) stable; urgency=low + + - debian: Bump moulinette/ssowat requirement to 11.1 (0826a541) + - helpers: Fixes $app unbound when running ynh_secure_remove ([#1582](https://github.com/yunohost/yunohost/pull/1582)) + - log/appv2: don't dump all settings in log metadata (a9ac55e4) + - appv2: resource upgrade will tweak settings, we have to re-update the env_dict after upgrading resources (3110460a) + - appv2: safety-backup-before-upgrade should only contain the app (1c95bcff) + - appv2: fix env not including vars for v1->v2 upgrade (2b2d49a5) + - backup: add name of the backup in create/delete message, otherwise that creates some spooky messages with 'Backup created' directly followed by 'Backup deleted' during safety-backup-before-upgrade in v2 apps (8090acb1) + - [i18n] Translations updated for Arabic, French, Galician, Polish + + Thanks to all contributors <3 ! (ButterflyOfFire, Éric Gaspar, Eryk Michalak, Florent, José M, ppr) + + -- Alexandre Aubin Thu, 02 Feb 2023 23:37:46 +0100 + +yunohost (11.1.5) stable; urgency=low + + - Release as stable ! + + - diagnosis: we can't yield an ERROR if there's no IPv6, otherwise that blocks all subsequent network-related diagnoser because of the dependency system ... (ade92e43) + - domains: fix domain_config.toml typos in conditions (480f7a43) + - certs: Don't try restarting metronome if no domain configured for it (452ba8bb) + + Thanks to all contributors <3 ! (Axolotle) + + -- Alexandre Aubin Wed, 01 Feb 2023 20:21:56 +0100 + +yunohost (11.1.4.1) testing; urgency=low + + - debian: don't dump upgradable apps during postinst's catalog update (82d30f02) + - ynh_setup_source: Output checksums when source is 'corrupt' ([#1578](https://github.com/yunohost/yunohost/pull/1578)) + - metronome: Auto-enable/disable metronome if there's no/at least one domain configured for XMPP (c990cee6) + + Thanks to all contributors <3 ! (tituspijean) + + -- Alexandre Aubin Wed, 01 Feb 2023 17:10:32 +0100 + +yunohost (11.1.4) testing; urgency=low + + - settings: Add DNS exposure setting given the IP version ([#1451](https://github.com/yunohost/yunohost/pull/1451)) + + Thanks to all contributors <3 ! (Tagada) + + -- Alexandre Aubin Mon, 30 Jan 2023 16:28:56 +0100 + +yunohost (11.1.3.1) testing; urgency=low + + - nginx: add xmpp-upload. and muc. server_name only if xmpp_enabled is enabled (c444dee4) + - [i18n] Translations updated for Arabic, Basque, French, Galician, Spanish, Turkish + + Thanks to all contributors <3 ! (Alperen İsa Nalbant, ButterflyOfFire, cristian amoyao, Éric Gaspar, José M, Kayou, ppr, quiwy, xabirequejo) + + -- Alexandre Aubin Mon, 30 Jan 2023 15:44:30 +0100 + +yunohost (11.1.3) testing; urgency=low + + - helpers: Include procedures in MySQL database backup ([#1570](https://github.com/yunohost/yunohost/pull/1570)) + - users: be able to change the loginShell of a user ([#1538](https://github.com/yunohost/yunohost/pull/1538)) + - debian: refresh catalog upon package upgrade (be5b1c1b) + + Thanks to all contributors <3 ! (Éric Gaspar, Kay0u, ljf, Metin Bektas) + + -- Alexandre Aubin Thu, 19 Jan 2023 23:08:10 +0100 + +yunohost (11.1.2.2) testing; urgency=low + + - Minor technical fixes (b37d4baf, 68342171) + - configpanel: stop the madness of returning a 500 error when trying to load config panel 0.1 ... otherwise this will crash the new app info view ... (f21fbed2) + - apps: fix trick to find the default branch from git repo @_@ (25c10166) + - debian: regen ssowatconf during package upgrade (4615d7b7) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Éric Gaspar, ppr) + + -- Alexandre Aubin Tue, 10 Jan 2023 13:23:28 +0100 + +yunohost (11.1.2.1) testing; urgency=low + + - i18n: fix (un)defined string issues (dd33476f) + - doc: Revive the old auto documentation of API with swagger + - apps: don't clone 'master' branch by default, use git ls-remote to check what's the default branch instead (a6db52b7) + - ssowat: add use_remote_user_var_in_nginx_conf flag on permission (f258eab6) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Mon, 09 Jan 2023 23:58:51 +0100 + +yunohost (11.1.2) testing; urgency=low + + - apps: Various fixes/improvements for appsv2, mostly related to webadmin integration ([#1526](https://github.com/yunohost/yunohost/pull/1526)) + - domains/regenconf: propagate mail/xmpp enable/disable toggle to actual system configs ([#1541](https://github.com/yunohost/yunohost/pull/1541)) + - settings: Add a virtual setting to enable passwordless sudo for admins (75cb3cb2) + - settings: Add a global setting to choose SSOwat's theme ([#1545](https://github.com/yunohost/yunohost/pull/1545)) + - certs: Improve trick to identify certs as self-signed (c38aba74) + - certs: be more resilient when mail cant be sent to root for some reason .. (d7ee1c23) + - certs/postfix: propagate postfix SNI stuff when renewing certificates (31794008) + - certs/xmpp: add to domain's certificate the alt subdomain muc ([#1163](https://github.com/yunohost/yunohost/pull/1163)) + - conf/ldap: fix issue where sudo doesn't work because sudo-ldap doesn't create /etc/sudo-ldap.conf :/ (d2417c33) + - configpanels: fix custom getter ([#1546](https://github.com/yunohost/yunohost/pull/1546)) + - configpanels: fix inconsistent return format for boolean, sometimes 1/0, sometimes True/False -> force normalization of values when calling get() for a single setting from a config panel (47b9b8b5) + - postfix/fail2ban: Add postfix SASL login failure to a fail2ban jail ([#1552](https://github.com/yunohost/yunohost/pull/1552)) + - mail: Fix flag case sensitivity in dovecot and rspamd sieve filter ([#1450](https://github.com/yunohost/yunohost/pull/1450)) + - misc: Don't disable avahi-daemon by force in conf_regen ([#1555](https://github.com/yunohost/yunohost/pull/1555)) + - misc: Fix yunopaste ([#1558](https://github.com/yunohost/yunohost/pull/1558)) + - misc: Don't take lock for read/GET operations (#1554) (0ac8e66a) + - i18n: Translations updated for Basque, French, Galician, Portuguese, Slovak, Spanish, Ukrainian + + Thanks to all contributors <3 ! (axolotle, DDATAA, Fabian Wilkens, Gabriel, José M, Jose Riha, ljf, Luis H. Porras, ppr, quiwy, Rafael Fontenelle, selfhoster1312, Tymofii-Lytvynenko, xabirequejo, Xavier Brochard) + + -- Alexandre Aubin Fri, 06 Jan 2023 00:12:53 +0100 + +yunohost (11.1.1.2) testing; urgency=low + + - group mailalias: the ldap class is in fact mailGroup, not mailAccount -_- (1cb5e43e) + + -- Alexandre Aubin Sat, 03 Dec 2022 15:57:22 +0100 + +yunohost (11.1.1.1) testing; urgency=low + + - Fix again the legacy patch for yunohost user create @_@ (46d6fab0) + + -- Alexandre Aubin Sat, 03 Dec 2022 14:13:09 +0100 + +yunohost (11.1.1) testing; urgency=low + + - groups: add mail-aliases management (#1539) (0f9d9388) + - apps: Allow apps to be installed on a path sharing a common base, eg /foo and /foo2 (#1537) (ae594111) + - admins/ldap: re-allow member of the admins group to edit ldap db (4f5cc166) + - nginx: Add 502 custom error page (#1530) (5063e128) + - misc/nodejs: Upgrade n to version 9.0.1 ([#1528](https://github.com/yunohost/yunohost/pull/1528)) + - misc/update: add --allow-releaseinfo-change option to apt update to prevent the classic nightmare when debian changes from stable to oldstable (ac6d6871) + - misc/dns: Add Webgo as Registrar to support it via LexiconAdd Webgo as Registrar (#1529) (c50f3771) + - misc/debug: Improve dpkg_is_broken instruction to also mention dpkg --audit (a772153b) + - misc/regeconf: fix yunohost hook incorectly tweaking mdns.yml ownership (9bd98162) + - misc/helpers: fix docker-image-extract helper (#1532) + - misc/yunoprompt: don't display postinstall tip to members of all_users group (because they can't check if /etc/yunohost/installed exists, but if they're member of the all_users group, then postinstall was already done) (4aaa8896) + - misc/diagnosis: make the dnsrecord diagnoser not complain about the damn 128 vs 0 stuff in CAA records (70a8225b) + - misc/settings: fix output format for 'yunohost settings list' (70bf38ce) + - misc/helpers: Better error message when psql is not there for database_exists (#992) (f49c121b) + - misc/multimedia: fix edgecase where setfacl crashes because of broken symlinks (94f21ea2) + - misc/legacy: auto-patch yunohost user create syntax in app scripts to use --fullname instead (d254fb1b) + - [i18n] Translations updated for Arabic, Basque, Chinese (Simplified), Dutch, French, Galician, German, Spanish, Ukrainian + + Thanks to all contributors <3 ! (André Koot, Augustin Trancart, Axolotle, ButterflyOfFire, Christian Wehrli, Éric Gaspar, José M, lee, mod242, quiwy, tituspijean, Tymofii-Lytvynenko, xabirequejo) + + -- Alexandre Aubin Fri, 02 Dec 2022 23:31:28 +0100 + +yunohost (11.1.0.2) testing; urgency=low + + - globalsettings: make sure to run migration 25 prior to the regenconf (f3750598) + - domaininfo: Some apps don't have path ([#1521](https://github.com/yunohost/yunohost/pull/1521)) + - Add sponsors to the README ([#1522](https://github.com/yunohost/yunohost/pull/1522)) + - postfix: fix relay conf not triggered because new setting system now returns '1' and not 'True' (cd43c8bd) + - postfix: fix permission issue preventing to properly create sasl_passwd.db (5394790f) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Félix Piédallu, Florian Masy, ppr, Tagada) + + -- Alexandre Aubin Fri, 04 Nov 2022 13:13:40 +0100 + +yunohost (11.1.0.1) testing; urgency=low + + - Bump version after propagating hotfix on 11.0.10.2 + + -- Alexandre Aubin Thu, 27 Oct 2022 15:46:26 +0200 + +yunohost (11.1.0) testing; urgency=low + + - apps: New 'v2' packaging format ([#1289](https://github.com/yunohost/yunohost/pull/1289)) + - helpers: Upgrade n to version 9.0.0 ([#1477](https://github.com/yunohost/yunohost/pull/1477)) + - helpers: Support extracting source from docker images in ynh_setup_source ([#1505](https://github.com/yunohost/yunohost/pull/1505)) + - configpanels: Refactor global settings the new config panel framework ([#1459](https://github.com/yunohost/yunohost/pull/1459)) + - configpanels: Add support for actions (= button widget) and apply it to domain cert management ([#1436](https://github.com/YunoHost/yunohost/pull/1436)) + - admin: Drop the 'admin' user, have 'admins' be a group of Yunohost users instead ([#1408](https://github.com/yunohost/yunohost/pull/1408)) + - admin: Implement a new 'virtual global setting' to change root password from the global setting config panel ([#1515](https://github.com/yunohost/yunohost/pull/1515)) + - domains: Be able to "list" domain as a tree structure + add new 'domain_info' API endpoint ([#1434](https://github.com/yunohost/yunohost/pull/1434)) + - users: Encourage to define a single 'full display name' instead of separate 'firstname/lastname' ([#1516](https://github.com/yunohost/yunohost/pull/1516)) + - security: Improve most used password check list ([#1517](https://github.com/yunohost/yunohost/pull/1517)) + - i18n: Translations updated for Slovak + + Thanks to all contributors <3 ! (axolotle, Dante, Jose Riha, Tagadda, yalh76) + + -- Alexandre Aubin Tue, 25 Oct 2022 17:57:29 +0200 + +yunohost (11.0.10.2) stable; urgency=low + + - Add another trick to autorestart yunohost-api at the end of the upgrade when ran from the api itself... (6f640c08) + + -- Alexandre Aubin Thu, 27 Oct 2022 15:46:26 +0200 + +yunohost (11.0.10.1) stable; urgency=low + + - self-upgrade: fix yunohost-api restart which was not triggered @_@ (472e9250) + + -- Alexandre Aubin Mon, 17 Oct 2022 23:56:37 +0200 + +yunohost (11.0.10) stable; urgency=low + + - configpanels: fix nested bind statements (0252a6fd) + - ynh_setup_source: Add option to fully replace the destination dir ([#1509](https://github.com/yunohost/yunohost/pull/1509)) + - tools_update: also yield a boolean to easily know if there's a major yunohost upgrade pending + list of pending migrations (cf change in webadmin to encourage people to check the release note on the forum before yoloupgrading) (86e45f9c) + - diagnosis: add reports when apt is configured with the 'testing' channel for yunohost, or with the 'stable' codename for debian (0adff31d) + - [i18n] Translations updated for French, Slovak + + Thanks to all contributors <3 ! (Dante, Jose Riha, ppr, yalh76) + + -- Alexandre Aubin Mon, 17 Oct 2022 16:56:47 +0200 + +yunohost (11.0.9.15) stable; urgency=low + + - [fix] Lidswitch if no reboot ([#1506](https://github.com/yunohost/yunohost/pull/1506)) + - [fix] postinstall: edge case where var would get undefined.. (b7bea608) + - [fix] backup: Try to fix again the infamous issue where from_yunohost_version gets filled with 'BASH_XTRACEFD' (14fb1cfd) + - [fix] Various english wording improvements ([#1507](https://github.com/yunohost/yunohost/pull/1507)) + - [i18n] Translations updated for Arabic, Slovak, Telugu, Turkish + + Thanks to all contributors <3 ! (Alice Kile, ButterflyOfFire, Jose Riha, ljf (zamentur), marty hiatt, Sedat Albayrak) + + -- Alexandre Aubin Fri, 30 Sep 2022 16:24:59 +0200 + +yunohost (11.0.9.14) stable; urgency=low + + - [fix] dns: confusion on XMPP CNAME records for nohost.me & co domains (f6057d25) + - [fix] helper ynh_get_ram: LANG= isn't enough to get en_US output, gotta use LC_ALL (e51cdd98) + + -- Alexandre Aubin Wed, 07 Sep 2022 13:08:31 +0200 + +yunohost (11.0.9.13) stable; urgency=low + + - [fix] defaultapp: domain may not exist in app_map dict output (efe0e601) + - [fix] regenconf: fix a stupid issue with slapcat displaying an error message because grep -q breaks the pipe (503b9031) + - [fix] regenconf: add a timeout to curl inside dnsmasq regenconf to prevent being stuck too long when no network on the machine (b77e8114) + - [fix] ynh_delete_file_checksum with non-existing option in helpers/config ([#1501](https://github.com/YunoHost/yunohost/pull/1501)) + - [i18n] Translations updated for Basque, Galician, Slovak + + Thanks to all contributors <3 ! (José M, Jose Riha, tituspijean, xabirequejo) + + -- Alexandre Aubin Sat, 03 Sep 2022 23:27:56 +0200 + +yunohost (11.0.9.12) stable; urgency=low + + - [fix] postinstall: check all partitions (not only physical ones) ([#1497](https://github.com/YunoHost/yunohost/pull/1497)) + - [i18n] Translations updated for Basque, French, Indonesian, Italian, Slovak + + Thanks to all contributors <3 ! (Salamandar) + + -- Alexandre Aubin Sun, 28 Aug 2022 14:50:38 +0200 + +yunohost (11.0.9.11) stable; urgency=low + + = Merge with Buster branch + - [fix] diagnosis: fix inaccurate message (ae92a0b8) + - [fix] logrotate helpers: getopts miserably explodes if 'legacy_args' is inconsistent with 'args_array' ... (530bf04a) + - [i18n] Translations updated for Basque, French, Indonesian, Italian, Slovak + + Thanks to all contributors <3 ! (Jose Riha, Leandro Noferini, liimee, Stephan Klein, xabirequejo) + + -- Alexandre Aubin Fri, 26 Aug 2022 16:32:19 +0200 + +yunohost (11.0.9.9) stable; urgency=low + + - Sync with Buster branch + - [fix] php7.3->7.4: autopatch nginx configs during restore (18e041c4) + + -- Alexandre Aubin Fri, 19 Aug 2022 20:50:52 +0200 + +yunohost (11.0.9.7) stable; urgency=low + + - [fix] logorate helper: was broken because wrong index é_è (efa80304) + - [i18n] Translations updated for French, Galician, Ukrainian + + Thanks to all contributors <3 ! (Éric Gaspar, José M, Tymofii-Lytvynenko) + + -- Alexandre Aubin Wed, 17 Aug 2022 19:24:11 +0200 + +yunohost (11.0.9.6) stable; urgency=low + + - Sync with Buster branch + - [fix] helpers: logrotate shitty inconsistent handling of 'supposedly legacy' --non-append option ... (8d1c75e7) + - [fix] apps: Better handling of super shitty edge case where an app settings.yml is empty for some unexpected mystic reason ... (9eb123f8) + + -- Alexandre Aubin Wed, 17 Aug 2022 01:26:28 +0200 + +yunohost (11.0.9.5) stable; urgency=low + + - Propagate fixes in buster->bullseye migration + - [fix] venv rebuild: synapse's folder is named matrix-synapse (c8031ace) + + -- Alexandre Aubin Sun, 14 Aug 2022 18:22:30 +0200 + +yunohost (11.0.9.3) stable; urgency=low + + - [fix] postgresql 11->13: Epic typo / missing import (3cb1a41a) + - [i18n] Translations updated for Basque, French, Galician + + Thanks to all contributors <3 ! (Éric Gaspar, José M, Kay0u, punkrockgirl) + + -- Alexandre Aubin Sat, 13 Aug 2022 22:37:05 +0200 + +yunohost (11.0.9.2) stable; urgency=low + + - [fix] venv rebuild: fix yunohost app force upgrade command (5d90971b) + - [fix] apt helpers: simplify ynh_remove_app_dependencies, we don't need to care about removing php-fpm services from yunohost, because 'yunohost service' now dynamically check what relevant phpX.Y-fpm service exist on the system (64e35815) + - [enh] diagnosis: add complains if some app installed are still requiring only yunohost 3.x (31aacb33) + - [fix] venv rebuild: migration should have an empty disclaimer when in auto mode (d2a6dcd4) + - [fix] postgresql 11->13 migration: skip if no yunohost app depend on postgresql (d161da03) + + Thanks to all contributors <3 ! (Éric Gaspar, ljf) + + -- Alexandre Aubin Sat, 13 Aug 2022 20:08:27 +0200 + +yunohost (11.0.9.1) stable; urgency=low + + - [fix] venv rebuild: /opt may not exist ... + + -- Alexandre Aubin Thu, 11 Aug 2022 16:00:40 +0200 + yunohost (11.0.9) stable; urgency=low - [fix] services: Skip php 7.3 which is most likely dead after buster->bullseye migration because users get spooked (51804925) @@ -134,6 +795,80 @@ yunohost (11.0.2) testing; urgency=low -- Alexandre Aubin Wed, 19 Jan 2022 20:52:39 +0100 +yunohost (4.4.2.14) stable; urgency=low + + - bullseye migration: remove derpy OVH repo... (76014920) + - bullseye migration: improve autofix procedure for the libc6 hell (02b3a138) + + -- Alexandre Aubin Sat, 03 Sep 2022 23:19:08 +0200 + +yunohost (4.4.2.13) stable; urgency=low + + - [fix] bullseye migration: a few annoying issues related to Sury (b5fabc87) + + -- Alexandre Aubin Mon, 29 Aug 2022 15:40:03 +0200 + +yunohost (4.4.2.12) stable; urgency=low + + - bullseye migration: add trick to automagically find the likely log of a previously failed migration to ease support (f5d94509) + + -- Alexandre Aubin Fri, 26 Aug 2022 19:22:30 +0200 + +yunohost (4.4.2.10) stable; urgency=low + + - bullseye migration: add proper explanations and advices after the damn 'The distribution is not Buster' message ... (6a594d0e) + + -- Alexandre Aubin Mon, 22 Aug 2022 10:28:50 +0200 + +yunohost (4.4.2.9) stable; urgency=low + + - apt helper: fix edge case with equivs package being flagged hold because of buster->bullseye migration (b306df2c) + - bullseye migration: fix check about free space in /boot/ ... (a2d4abc1) + + -- Alexandre Aubin Thu, 18 Aug 2022 19:24:47 +0200 + +yunohost (4.4.2.7) stable; urgency=low + + - upgrades: ignore boring insserv warnings during apt commands (87f0eff9) + - bullseye migration: higher treshold for low space detection in /boot/ because some people still experience the issue on 4.4.2.6 (d283c900) + + -- Alexandre Aubin Wed, 17 Aug 2022 01:21:36 +0200 + +yunohost (4.4.2.6) stable; urgency=low + + - [fix] bullseye migration: trash pip freeze stderr because it's confusing users ... (e68fc821) + - [fix] bullseye migration: add a check that there's at least 70MB available in /boot ... (02fcbd97) + - [fix] bullseye migration: better detection mechanism for the libc6 / libgcc hell issue (633a1fbf) + + -- Alexandre Aubin Sun, 14 Aug 2022 18:18:13 +0200 + +yunohost (4.4.2.3) stable; urgency=low + + - [fix] bullseye migration: add fix for stupid dnsmasq not picking new init script (origin/dev, origin/HEAD, dev) + - [fix] bullseye migration: add the patch for the build-essential / libc6-dev / libgcc-8-dev hell ... + - [fix] bullseye migration: add critical fix for RPi failing to get network on reboot + - [fix] bullseye migration: add ffsync to deprecated apps (77c2f5dc) + + -- Alexandre Aubin Sat, 13 Aug 2022 20:06:00 +0200 + +yunohost (4.4.2.1) stable; urgency=low + + - [fix] bullseye migration: /opt may not exist ... (5fd74577) + + -- Alexandre Aubin Thu, 11 Aug 2022 15:56:16 +0200 + +yunohost (4.4.2) stable; urgency=low + + - Release as stable + - [fix] bullseye migration: /etc/apt/sources.list may not exist (b928dd12) + - [fix] bullseye migration: Allow lime2 to upgrade even if kernel is hold (#1452) + - [fix] bullseye migration: Save python apps venv in a requirements file, in order to regenerate it in a follow-up migration ([#1479](https://github.com/YunoHost/yunohost/pull/1479)) + - [fix] bullseye migration: tweak message to prepare for stable release (80015a72) + + Thanks to all contributors <3 ! (ljf, theo-is-taken) + + -- Alexandre Aubin Tue, 09 Aug 2022 16:59:15 +0200 + yunohost (4.4.1) testing; urgency=low - [fix] php helpers: prevent epic catastrophies when the app changes php version (31d3719b) diff --git a/debian/control b/debian/control index 8b94dcdec..8880867a2 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Package: yunohost Essential: yes Architecture: all Depends: ${python3:Depends}, ${misc:Depends} - , moulinette (>= 11.0), ssowat (>= 11.0) + , moulinette (>= 11.1), ssowat (>= 11.1) , python3-psutil, python3-requests, python3-dnspython, python3-openssl , python3-miniupnpc, python3-dbus, python3-jinja2 , python3-toml, python3-packaging, python3-publicsuffix2 @@ -28,7 +28,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , rspamd, opendkim-tools, postsrsd, procmail, mailutils , redis-server , acl - , git, curl, wget, cron, unzip, jq, bc, at + , git, curl, wget, cron, unzip, jq, bc, at, procps , lsb-release, haveged, fake-hwclock, equivs, lsof, whois Recommends: yunohost-admin , ntp, inetutils-ping | iputils-ping diff --git a/debian/postinst b/debian/postinst index c62926a30..37d6ff895 100644 --- a/debian/postinst +++ b/debian/postinst @@ -20,6 +20,7 @@ do_configure() { fi else echo "Regenerating configuration, this might take a while..." + yunohost app ssowatconf yunohost tools regen-conf --output-as none echo "Launching migrations..." @@ -27,6 +28,9 @@ do_configure() { echo "Re-diagnosing server health..." yunohost diagnosis run --force + + echo "Refreshing app catalog..." + yunohost tools update apps --output-as none || true fi systemctl restart yunohost-portal-api @@ -40,6 +44,23 @@ do_configure() { systemctl restart yunohost-api else echo "(Delaying the restart of yunohost-api, this should automatically happen after the end of this upgrade)" + cat << EOF | at -M now >/dev/null 2>&1 +# Wait for apt / dpkg / yunohost to not be up anymore, hence the upgrade finished + +while pgrep -x apt || pgrep -x apt-get || pgrep dpkg || test -e /var/run/moulinette_yunohost.lock; +do + sleep 3 +done + +# Restart yunohost-api, though only if it wasnt already restarted by something else in the last 60 secs + +API_START_TIMESTAMP="\$(date --date="\$(systemctl show yunohost-api | grep ExecMainStartTimestamp= | awk -F= '{print \$2}')" +%s)" + +if [ "\$(( \$(date +%s) - \$API_START_TIMESTAMP ))" -ge 60 ]; +then + systemctl restart yunohost-api +fi +EOF fi fi } diff --git a/doc/api.html b/doc/api.html new file mode 100644 index 000000000..502d1247f --- /dev/null +++ b/doc/api.html @@ -0,0 +1,42 @@ + + + + + + Swagger UI + + + + + + +
+ + + + + + + diff --git a/doc/generate_api_doc.py b/doc/generate_api_doc.py new file mode 100644 index 000000000..bb5f1df29 --- /dev/null +++ b/doc/generate_api_doc.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" License + Copyright (C) 2013 YunoHost + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses +""" + +""" + Generate JSON specification files API +""" +import os +import sys +import yaml +import json + + +def main(): + with open("../share/actionsmap.yml") as f: + action_map = yaml.safe_load(f) + + # try: + # with open("/etc/yunohost/current_host", "r") as f: + # domain = f.readline().rstrip() + # except IOError: + # domain = requests.get("http://ip.yunohost.org").text + + with open("../debian/changelog") as f: + top_changelog = f.readline() + api_version = top_changelog[top_changelog.find("(") + 1 : top_changelog.find(")")] + + csrf = { + "name": "X-Requested-With", + "in": "header", + "required": True, + "schema": {"type": "string", "default": "Swagger API"}, + } + + resource_list = { + "openapi": "3.0.3", + "info": { + "title": "YunoHost API", + "description": "This is the YunoHost API used on all YunoHost instances. This API is essentially used by YunoHost Webadmin.", + "version": api_version, + }, + "servers": [ + { + "url": "https://{domain}/yunohost/api", + "variables": { + "domain": { + "default": "demo.yunohost.org", + "description": "Your yunohost domain", + } + }, + } + ], + "tags": [{"name": "public", "description": "Public route"}], + "paths": { + "/login": { + "post": { + "tags": ["public"], + "summary": "Logs in and returns the authentication cookie", + "parameters": [csrf], + "requestBody": { + "required": True, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "credentials": { + "type": "string", + "format": "password", + } + }, + "required": ["credentials"], + } + } + }, + }, + "security": [], + "responses": { + "200": { + "description": "Successfully login", + "headers": {"Set-Cookie": {"schema": {"type": "string"}}}, + } + }, + } + }, + "/installed": { + "get": { + "tags": ["public"], + "summary": "Test if the API is working", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Successfully working", + } + }, + } + }, + }, + } + + def convert_categories(categories, parent_category=""): + for category, category_params in categories.items(): + if parent_category: + category = f"{parent_category} {category}" + if "subcategory_help" in category_params: + category_params["category_help"] = category_params["subcategory_help"] + + if "category_help" not in category_params: + category_params["category_help"] = "" + resource_list["tags"].append( + {"name": category, "description": category_params["category_help"]} + ) + + for action, action_params in category_params["actions"].items(): + if "action_help" not in action_params: + action_params["action_help"] = "" + if "api" not in action_params: + continue + if not isinstance(action_params["api"], list): + action_params["api"] = [action_params["api"]] + + for i, api in enumerate(action_params["api"]): + print(api) + method, path = api.split(" ") + method = method.lower() + key_param = "" + if "{" in path: + key_param = path[path.find("{") + 1 : path.find("}")] + resource_list["paths"].setdefault(path, {}) + + notes = "" + + operationId = f"{category}_{action}" + if i > 0: + operationId += f"_{i}" + operation = { + "tags": [category], + "operationId": operationId, + "summary": action_params["action_help"], + "description": notes, + "responses": {"200": {"description": "successful operation"}}, + } + if action_params.get("deprecated"): + operation["deprecated"] = True + + operation["parameters"] = [] + if method == "post": + operation["parameters"] = [csrf] + + if "arguments" in action_params: + if method in ["put", "post", "patch"]: + operation["requestBody"] = { + "required": True, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": {}, + "required": [], + } + } + }, + } + for arg_name, arg_params in action_params["arguments"].items(): + if "help" not in arg_params: + arg_params["help"] = "" + param_type = "query" + allow_multiple = False + required = True + allowable_values = None + name = str(arg_name).replace("-", "_") + if name[0] == "_": + required = False + if "full" in arg_params: + name = arg_params["full"][2:] + else: + name = name[2:] + name = name.replace("-", "_") + + if "choices" in arg_params: + allowable_values = arg_params["choices"] + _type = "string" + if "type" in arg_params: + types = {"open": "file", "int": "int"} + _type = types[arg_params["type"]] + if ( + "action" in arg_params + and arg_params["action"] == "store_true" + ): + _type = "boolean" + + if "nargs" in arg_params: + if arg_params["nargs"] == "*": + allow_multiple = True + required = False + _type = "array" + if arg_params["nargs"] == "+": + allow_multiple = True + required = True + _type = "array" + if arg_params["nargs"] == "?": + allow_multiple = False + required = False + else: + allow_multiple = False + + if name == key_param: + param_type = "path" + required = True + allow_multiple = False + + if method in ["put", "post", "patch"]: + schema = operation["requestBody"]["content"][ + "multipart/form-data" + ]["schema"] + schema["properties"][name] = { + "type": _type, + "description": arg_params["help"], + } + if required: + schema["required"].append(name) + prop_schema = schema["properties"][name] + else: + parameters = { + "name": name, + "in": param_type, + "description": arg_params["help"], + "required": required, + "schema": { + "type": _type, + }, + "explode": allow_multiple, + } + prop_schema = parameters["schema"] + operation["parameters"].append(parameters) + + if allowable_values is not None: + prop_schema["enum"] = allowable_values + if "default" in arg_params: + prop_schema["default"] = arg_params["default"] + if arg_params.get("metavar") == "PASSWORD": + prop_schema["format"] = "password" + if arg_params.get("metavar") == "MAIL": + prop_schema["format"] = "mail" + # Those lines seems to slow swagger ui too much + # if 'pattern' in arg_params.get('extra', {}): + # prop_schema['pattern'] = arg_params['extra']['pattern'][0] + + resource_list["paths"][path][method.lower()] = operation + + # Includes subcategories + if "subcategories" in category_params: + convert_categories(category_params["subcategories"], category) + + del action_map["_global"] + convert_categories(action_map) + + openapi_json = json.dumps(resource_list) + # Save the OpenAPI json + with open(os.getcwd() + "/openapi.json", "w") as f: + f.write(openapi_json) + + openapi_js = f"var openapiJSON = {openapi_json}" + with open(os.getcwd() + "/openapi.js", "w") as f: + f.write(openapi_js) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/doc/generate_bash_completion.py b/doc/generate_bash_completion.py index d55973010..88aa273fd 100644 --- a/doc/generate_bash_completion.py +++ b/doc/generate_bash_completion.py @@ -31,7 +31,6 @@ def get_dict_actions(OPTION_SUBTREE, category): with open(ACTIONSMAP_FILE, "r") as stream: - # Getting the dictionary containning what actions are possible per category OPTION_TREE = yaml.safe_load(stream) @@ -65,7 +64,6 @@ with open(ACTIONSMAP_FILE, "r") as stream: os.makedirs(BASH_COMPLETION_FOLDER, exist_ok=True) with open(BASH_COMPLETION_FILE, "w") as generated_file: - # header of the file generated_file.write("#\n") generated_file.write("# completion for yunohost\n") diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index 371e8899b..110d1d4cd 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -20,12 +20,11 @@ def get_current_commit(): def render(helpers): - current_commit = get_current_commit() data = { "helpers": helpers, - "date": datetime.datetime.now().strftime("%m/%d/%Y"), + "date": datetime.datetime.now().strftime("%d/%m/%Y"), "version": open("../debian/changelog").readlines()[0].split()[1].strip("()"), } @@ -56,20 +55,17 @@ def render(helpers): class Parser: def __init__(self, filename): - self.filename = filename self.file = open(filename, "r").readlines() self.blocks = None def parse_blocks(self): - self.blocks = [] current_reading = "void" current_block = {"name": None, "line": -1, "comments": [], "code": []} for i, line in enumerate(self.file): - if line.startswith("#!/bin/bash"): continue @@ -117,7 +113,6 @@ class Parser: current_reading = "code" elif current_reading == "code": - if line == "}": # We're getting out of the function current_reading = "void" @@ -138,7 +133,6 @@ class Parser: continue def parse_block(self, b): - b["brief"] = "" b["details"] = "" b["usage"] = "" @@ -164,7 +158,6 @@ class Parser: elif subblock.startswith("usage"): for line in subblock.split("\n"): - if line.startswith("| arg"): linesplit = line.split() argname = linesplit[2] @@ -216,11 +209,13 @@ def malformed_error(line_number): def main(): - helper_files = sorted(glob.glob("../helpers/*")) helpers = [] for helper_file in helper_files: + if not os.path.isfile(helper_file): + continue + category_name = os.path.basename(helper_file) print("Parsing %s ..." % category_name) p = Parser(helper_file) diff --git a/doc/generate_manpages.py b/doc/generate_manpages.py index bdb1fcaee..782dd8a90 100644 --- a/doc/generate_manpages.py +++ b/doc/generate_manpages.py @@ -60,7 +60,6 @@ def main(): # man pages of "yunohost *" with open(ACTIONSMAP_FILE, "r") as actionsmap: - # Getting the dictionary containning what actions are possible per domain actionsmap = ordered_yaml_load(actionsmap) diff --git a/doc/generate_resource_doc.py b/doc/generate_resource_doc.py new file mode 100644 index 000000000..201d25265 --- /dev/null +++ b/doc/generate_resource_doc.py @@ -0,0 +1,72 @@ +import ast +import datetime +import subprocess + +version = open("../debian/changelog").readlines()[0].split()[1].strip("()") +today = datetime.datetime.now().strftime("%d/%m/%Y") + + +def get_current_commit(): + p = subprocess.Popen( + "git rev-parse --verify HEAD", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + stdout, stderr = p.communicate() + + current_commit = stdout.strip().decode("utf-8") + return current_commit + + +current_commit = get_current_commit() + + +print( + f"""--- +title: App resources +template: docs +taxonomy: + category: docs +routes: + default: '/packaging_apps_resources' +--- + +Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_resource_doc.py) on {today} (YunoHost version {version}) + +""" +) + + +fname = "../src/utils/resources.py" +content = open(fname).read() + +# NB: This magic is because we want to be able to run this script outside of a YunoHost context, +# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports... +tree = ast.parse(content) + +ResourceClasses = [ + c + for c in tree.body + if isinstance(c, ast.ClassDef) and c.bases and c.bases[0].id == "AppResource" +] + +ResourceDocString = {} + +for c in ResourceClasses: + assert c.body[1].targets[0].id == "type" + resource_id = c.body[1].value.value + docstring = ast.get_docstring(c) + + ResourceDocString[resource_id] = docstring + + +for resource_id, doc in sorted(ResourceDocString.items()): + doc = doc.replace("\n ", "\n") + + print("----------------") + print("") + print(f"## {resource_id.replace('_', ' ').title()}") + print("") + print(doc) + print("") diff --git a/helpers/apps b/helpers/apps index 85b74de15..7a93298c0 100644 --- a/helpers/apps +++ b/helpers/apps @@ -111,3 +111,96 @@ ynh_remove_apps() { done fi } + +# Spawn a Bash shell with the app environment loaded +# +# usage: ynh_spawn_app_shell --app="app" +# | arg: -a, --app= - the app ID +# +# examples: +# ynh_spawn_app_shell --app="APP" <<< 'echo "$USER"' +# ynh_spawn_app_shell --app="APP" < /tmp/some_script.bash +# +# Requires YunoHost version 11.0.* or higher, and that the app relies on packaging v2 or higher. +# The spawned shell will have environment variables loaded and environment files sourced +# from the app's service configuration file (defaults to $app.service, overridable by the packager with `service` setting). +# If the app relies on a specific PHP version, then `php` will be aliased that version. The PHP command will also be appended with the `phpflags` settings. +ynh_spawn_app_shell() { + # Declare an array to define the options of this helper. + local legacy_args=a + local -A args_array=([a]=app=) + local app + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Force Bash to be used to run this helper + if [[ ! $0 =~ \/?bash$ ]] + then + ynh_print_err --message="Please use Bash as shell" + exit 1 + fi + + # Make sure the app is installed + local installed_apps_list=($(yunohost app list --output-as json --quiet | jq -r .apps[].id)) + if [[ " ${installed_apps_list[*]} " != *" ${app} "* ]] + then + ynh_print_err --message="$app is not in the apps list" + exit 1 + fi + + # Make sure the app has its own user + if ! id -u "$app" &>/dev/null; then + ynh_print_err --message="There is no \"$app\" system user" + exit 1 + fi + + # Make sure the app has an install_dir setting + local install_dir=$(ynh_app_setting_get --app=$app --key=install_dir) + if [ -z "$install_dir" ] + then + ynh_print_err --message="$app has no install_dir setting (does it use packaging format >=2?)" + exit 1 + fi + + # Load the app's service name, or default to $app + local service=$(ynh_app_setting_get --app=$app --key=service) + [ -z "$service" ] && service=$app; + + # Export HOME variable + export HOME=$install_dir; + + # Load the Environment variables from the app's service + local env_var=$(systemctl show $service.service -p "Environment" --value) + [ -n "$env_var" ] && export $env_var; + + # Force `php` to its intended version + # We use `eval`+`export` since `alias` is not propagated to subshells, even with `export` + local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) + local phpflags=$(ynh_app_setting_get --app=$app --key=phpflags) + if [ -n "$phpversion" ] + then + eval "php() { php${phpversion} ${phpflags} \"\$@\"; }" + export -f php + fi + + # Source the EnvironmentFiles from the app's service + local env_files=($(systemctl show $service.service -p "EnvironmentFiles" --value)) + if [ ${#env_files[*]} -gt 0 ] + then + # set -/+a enables and disables new variables being automatically exported. Needed when using `source`. + set -a + for file in ${env_files[*]} + do + [[ $file = /* ]] && source $file + done + set +a + fi + + # cd into the WorkingDirectory set in the service, or default to the install_dir + local env_dir=$(systemctl show $service.service -p "WorkingDirectory" --value) + [ -z $env_dir ] && env_dir=$install_dir; + cd $env_dir + + # Spawn the app shell + su -s /bin/bash $app +} diff --git a/helpers/apt b/helpers/apt index 5ddcba381..a2f2d3de8 100644 --- a/helpers/apt +++ b/helpers/apt @@ -227,9 +227,8 @@ ynh_install_app_dependencies() { # Add a comma for each space between packages. But not add a comma if the space separate a version specification. (See below) dependencies="$(echo "$dependencies" | sed 's/\([^\<=\>]\)\ \([^(]\)/\1, \2/g')" local dependencies=${dependencies//|/ | } - local manifest_path="$YNH_APP_BASEDIR/manifest.json" - local version=$(jq -r '.version' "$manifest_path") + local version=$(ynh_read_manifest --manifest_key="version") if [ -z "${version}" ] || [ "$version" == "null" ]; then version="1.0" fi @@ -278,7 +277,10 @@ ynh_install_app_dependencies() { ynh_app_setting_set --app=$app --key=phpversion --value=$specific_php_version # Set the default php version back as the default version for php-cli. - update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION + if test -e /usr/bin/php$YNH_DEFAULT_PHP_VERSION + then + update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION + fi elif grep --quiet 'php' <<< "$dependencies"; then ynh_app_setting_set --app=$app --key=phpversion --value=$YNH_DEFAULT_PHP_VERSION fi @@ -361,16 +363,19 @@ ynh_remove_app_dependencies() { current_dependencies=${current_dependencies// | /|} fi - ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. + # Edge case where the app dep may be on hold, + # cf https://forum.yunohost.org/t/migration-error-cause-of-ffsync/20675/4 + if apt-mark showhold | grep -q -w ${dep_app}-ynh-deps + then + apt-mark unhold ${dep_app}-ynh-deps + fi - # Check if this app used a specific php version ... in which case we check - # if the corresponding php-fpm is still there. Otherwise, we remove the - # service from yunohost as well - - local specific_php_version=$(echo $current_dependencies | tr '-' ' ' | grep -o -E "\" | sed 's/php//g' | sort | uniq) - [[ "$specific_php_version" != "$YNH_DEFAULT_PHP_VERSION" ]] || specific_php_version="" - if [[ -n "$specific_php_version" ]] && ! ynh_package_is_installed --package="php${specific_php_version}-fpm"; then - yunohost service remove php${specific_php_version}-fpm + # Remove the fake package and its dependencies if they not still used. + # (except if dpkg doesn't know anything about the package, + # which should be symptomatic of a failed install, and we don't want bash to report an error) + if dpkg-query --show ${dep_app}-ynh-deps &>/dev/null + then + ynh_package_autopurge ${dep_app}-ynh-deps fi } diff --git a/helpers/backup b/helpers/backup index 01b51d5a1..ade3ce5e5 100644 --- a/helpers/backup +++ b/helpers/backup @@ -285,6 +285,14 @@ ynh_restore_file() { else mv "$archive_path" "${dest_path}" fi + + # Boring hack for nginx conf file mapped to php7.3 + # Note that there's no need to patch the fpm config because most php apps + # will call "ynh_add_fpm_config" during restore, effectively recreating the file from scratch + if [[ "${dest_path}" == "/etc/nginx/conf.d/"* ]] && grep 'php7.3.*sock' "${dest_path}" + then + sed -i 's/php7.3/php7.4/g' "${dest_path}" + fi } # Calculate and store a file checksum into the app settings @@ -319,6 +327,13 @@ ynh_store_file_checksum() { ynh_app_setting_set --app=$app --key=$checksum_setting_name --value=$(md5sum "$file" | cut --delimiter=' ' --fields=1) + if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then + # Using a base64 is in fact more reversible than "replace / and space by _" ... So we can in fact obtain the original file path in an easy reliable way ... + local file_path_base64=$(echo "$file" | base64 -w0) + mkdir -p /var/cache/yunohost/appconfbackup/ + cat $file > /var/cache/yunohost/appconfbackup/original_${file_path_base64} + fi + # If backup_file_checksum isn't empty, ynh_backup_if_checksum_is_different has made a backup if [ -n "${backup_file_checksum-}" ]; then # Print the diff between the previous file and the new one. @@ -353,11 +368,20 @@ ynh_backup_if_checksum_is_different() { backup_file_checksum="" if [ -n "$checksum_value" ]; then # Proceed only if a value was stored into the app settings if [ -e $file ] && ! echo "$checksum_value $file" | md5sum --check --status; then # If the checksum is now different + backup_file_checksum="/var/cache/yunohost/appconfbackup/$file.backup.$(date '+%Y%m%d.%H%M%S')" mkdir --parents "$(dirname "$backup_file_checksum")" cp --archive "$file" "$backup_file_checksum" # Backup the current file ynh_print_warn "File $file has been manually modified since the installation or last upgrade. So it has been duplicated in $backup_file_checksum" echo "$backup_file_checksum" # Return the name of the backup file + if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then + local file_path_base64=$(echo "$file" | base64 -w0) + if test -e /var/cache/yunohost/appconfbackup/original_${file_path_base64} + then + ynh_print_warn "Diff with the original file:" + diff --report-identical-files --unified --color=always /var/cache/yunohost/appconfbackup/original_${file_path_base64} $file >&2 || true + fi + fi fi fi } diff --git a/helpers/config b/helpers/config index 9c7272b85..77f118c5f 100644 --- a/helpers/config +++ b/helpers/config @@ -22,7 +22,7 @@ _ynh_app_config_get_one() { if [[ "$bind" == "settings" ]]; then ynh_die --message="File '${short_setting}' can't be stored in settings" fi - old[$short_setting]="$(ls "$(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" 2>/dev/null || echo YNH_NULL)" + old[$short_setting]="$(ls "$(echo $bind | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" 2>/dev/null || echo YNH_NULL)" file_hash[$short_setting]="true" # Get multiline text from settings or from a full file @@ -32,7 +32,7 @@ _ynh_app_config_get_one() { elif [[ "$bind" == *":"* ]]; then ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" else - old[$short_setting]="$(cat $(echo $bind | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2>/dev/null || echo YNH_NULL)" + old[$short_setting]="$(cat $(echo $bind | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/) 2>/dev/null || echo YNH_NULL)" fi # Get value from a kind of key/value file @@ -47,7 +47,7 @@ _ynh_app_config_get_one() { bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" fi - local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key_}" --after="${bind_after}")" fi @@ -73,11 +73,11 @@ _ynh_app_config_apply_one() { if [[ "$bind" == "settings" ]]; then ynh_die --message="File '${short_setting}' can't be stored in settings" fi - local bind_file="$(echo "$bind" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + local bind_file="$(echo "$bind" | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" if [[ "${!short_setting}" == "" ]]; then ynh_backup_if_checksum_is_different --file="$bind_file" ynh_secure_remove --file="$bind_file" - ynh_delete_file_checksum --file="$bind_file" --update_only + ynh_delete_file_checksum --file="$bind_file" ynh_print_info --message="File '$bind_file' removed" else ynh_backup_if_checksum_is_different --file="$bind_file" @@ -98,7 +98,7 @@ _ynh_app_config_apply_one() { if [[ "$bind" == *":"* ]]; then ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" fi - local bind_file="$(echo "$bind" | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + local bind_file="$(echo "$bind" | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" ynh_backup_if_checksum_is_different --file="$bind_file" echo "${!short_setting}" >"$bind_file" ynh_store_file_checksum --file="$bind_file" --update_only @@ -113,7 +113,7 @@ _ynh_app_config_apply_one() { bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" fi - local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" ynh_backup_if_checksum_is_different --file="$bind_file" ynh_write_var_in_file --file="${bind_file}" --key="${bind_key_}" --value="${!short_setting}" --after="${bind_after}" @@ -285,6 +285,18 @@ ynh_app_config_apply() { _ynh_app_config_apply } +ynh_app_action_run() { + local runner="run__$1" + # Get value from getter if exists + if type -t "$runner" 2>/dev/null | grep -q '^function$' 2>/dev/null; then + $runner + #ynh_return "result:" + #ynh_return "$(echo "${result}" | sed 's/^/ /g')" + else + ynh_die "No handler defined in app's script for action $1. If you are the maintainer of this app, you should define '$runner'" + fi +} + ynh_app_config_run() { declare -Ag old=() declare -Ag changed=() @@ -309,5 +321,7 @@ ynh_app_config_run() { ynh_app_config_apply ynh_script_progression --message="Configuration of $app completed" --last ;; + *) + ynh_app_action_run $1 esac } diff --git a/helpers/fail2ban b/helpers/fail2ban index 21177fa8d..31f55b312 100644 --- a/helpers/fail2ban +++ b/helpers/fail2ban @@ -99,8 +99,8 @@ ignoreregex = " >$YNH_APP_BASEDIR/conf/f2b_filter.conf fi - ynh_add_config --template="$YNH_APP_BASEDIR/conf/f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf" - ynh_add_config --template="$YNH_APP_BASEDIR/conf/f2b_filter.conf" --destination="/etc/fail2ban/filter.d/$app.conf" + ynh_add_config --template="f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf" + ynh_add_config --template="f2b_filter.conf" --destination="/etc/fail2ban/filter.d/$app.conf" ynh_systemd_action --service_name=fail2ban --action=reload --line_match="(Started|Reloaded) Fail2Ban Service" --log_path=systemd diff --git a/helpers/hardware b/helpers/hardware index 337630fa8..3ccf7ffe8 100644 --- a/helpers/hardware +++ b/helpers/hardware @@ -30,8 +30,8 @@ ynh_get_ram() { ram=0 # Use the total amount of ram elif [ $free -eq 1 ]; then - local free_ram=$(LANG=C vmstat --stats --unit M | grep "free memory" | awk '{print $1}') - local free_swap=$(LANG=C vmstat --stats --unit M | grep "free swap" | awk '{print $1}') + local free_ram=$(LC_ALL=C vmstat --stats --unit M | grep "free memory" | awk '{print $1}') + local free_swap=$(LC_ALL=C vmstat --stats --unit M | grep "free swap" | awk '{print $1}') local free_ram_swap=$((free_ram + free_swap)) # Use the total amount of free ram @@ -44,8 +44,8 @@ ynh_get_ram() { ram=$free_swap fi elif [ $total -eq 1 ]; then - local total_ram=$(LANG=C vmstat --stats --unit M | grep "total memory" | awk '{print $1}') - local total_swap=$(LANG=C vmstat --stats --unit M | grep "total swap" | awk '{print $1}') + local total_ram=$(LC_ALL=C vmstat --stats --unit M | grep "total memory" | awk '{print $1}') + local total_swap=$(LC_ALL=C vmstat --stats --unit M | grep "total swap" | awk '{print $1}') local total_ram_swap=$((total_ram + total_swap)) local ram=$total_ram_swap diff --git a/helpers/logging b/helpers/logging index 4601e0b39..ab5d564aa 100644 --- a/helpers/logging +++ b/helpers/logging @@ -308,8 +308,8 @@ ynh_script_progression() { local progression_bar="${progress_string2:0:$effective_progression}${progress_string1:0:$expected_progression}${progress_string0:0:$left_progression}" local print_exec_time="" - if [ $time -eq 1 ]; then - print_exec_time=" [$(date +%Hh%Mm,%Ss --date="0 + $exec_time sec")]" + if [ $time -eq 1 ] && [ "$exec_time" -gt 10 ]; then + print_exec_time=" [$(bc <<< "scale=1; $exec_time / 60" ) minutes]" fi ynh_print_info "[$progression_bar] > ${message}${print_exec_time}" diff --git a/helpers/logrotate b/helpers/logrotate index 6f9726beb..45f66d443 100644 --- a/helpers/logrotate +++ b/helpers/logrotate @@ -16,10 +16,27 @@ # Requires YunoHost version 2.6.4 or higher. # Requires YunoHost version 3.2.0 or higher for the argument `--specific_user` ynh_use_logrotate() { + + # Stupid patch to remplace --non-append by --nonappend + # Because for some reason --non-append was supposed to be legacy + # (why is it legacy ? Idk maybe because getopts cant parse args with - in their names..) + # but there was no good communication about this, and now --non-append + # is still the most-used option, yet it was parsed with batshit stupid code + # So instead this loops over the positional args, and replace --non-append + # with --nonappend so it's transperent for the rest of the function... + local all_args=( ${@} ) + for I in $(seq 0 $(($# - 1))) + do + if [[ "${all_args[$I]}" == "--non-append" ]] + then + all_args[$I]="--nonappend" + fi + done + set -- "${all_args[@]}" + # Declare an array to define the options of this helper. - local legacy_args=lnuya - local -A args_array=([l]=logfile= [n]=nonappend [u]=specific_user= [y]=non [a]=append) - # [y]=non [a]=append are only for legacy purpose, to not fail on the old option '--non-append' + local legacy_args=lnu + local -A args_array=([l]=logfile= [n]=nonappend [u]=specific_user=) local logfile local nonappend local specific_user @@ -30,14 +47,6 @@ ynh_use_logrotate() { specific_user="${specific_user:-}" # LEGACY CODE - PRE GETOPTS - if [ $# -gt 0 ] && [ "$1" == "--non-append" ]; then - nonappend=1 - # Destroy this argument for the next command. - shift - elif [ $# -gt 1 ] && [ "$2" == "--non-append" ]; then - nonappend=1 - fi - if [ $# -gt 0 ] && [ "$(echo ${1:0:1})" != "-" ]; then # If the given logfile parameter already exists as a file, or if it ends up with ".log", # we just want to manage a single file diff --git a/helpers/multimedia b/helpers/multimedia index abeb9ed2c..05479a84a 100644 --- a/helpers/multimedia +++ b/helpers/multimedia @@ -48,7 +48,7 @@ ynh_multimedia_build_main_dir() { # Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers. setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY" # Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl. - setfacl -RL -m m::rwx "$MEDIA_DIRECTORY" + setfacl -RL -m m::rwx "$MEDIA_DIRECTORY" || true } # Add a directory in yunohost.multimedia diff --git a/helpers/mysql b/helpers/mysql index 822159f27..a5290f794 100644 --- a/helpers/mysql +++ b/helpers/mysql @@ -133,7 +133,7 @@ ynh_mysql_dump_db() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - mysqldump --single-transaction --skip-dump-date "$database" + mysqldump --single-transaction --skip-dump-date --routines "$database" } # Create a user diff --git a/helpers/nginx b/helpers/nginx index 6daf6cc1e..bb0fe0577 100644 --- a/helpers/nginx +++ b/helpers/nginx @@ -20,7 +20,7 @@ ynh_add_nginx_config() { local finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" - ynh_add_config --template="$YNH_APP_BASEDIR/conf/nginx.conf" --destination="$finalnginxconf" + ynh_add_config --template="nginx.conf" --destination="$finalnginxconf" if [ "${path_url:-}" != "/" ]; then ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="$finalnginxconf" @@ -42,3 +42,37 @@ ynh_remove_nginx_config() { ynh_secure_remove --file="/etc/nginx/conf.d/$domain.d/$app.conf" ynh_systemd_action --service_name=nginx --action=reload } + + +# Move / regen the nginx config in a change url context +# +# usage: ynh_change_url_nginx_config +# +# Requires YunoHost version 11.1.9 or higher. +ynh_change_url_nginx_config() { + local old_nginx_conf_path=/etc/nginx/conf.d/$old_domain.d/$app.conf + local new_nginx_conf_path=/etc/nginx/conf.d/$new_domain.d/$app.conf + + # Change the path in the NGINX config file + if [ $change_path -eq 1 ] + then + # Make a backup of the original NGINX config file if modified + ynh_backup_if_checksum_is_different --file="$old_nginx_conf_path" + # Set global variables for NGINX helper + domain="$old_domain" + path="$new_path" + path_url="$new_path" + # Create a dedicated NGINX config + ynh_add_nginx_config + fi + + # Change the domain for NGINX + if [ $change_domain -eq 1 ] + then + ynh_delete_file_checksum --file="$old_nginx_conf_path" + mv "$old_nginx_conf_path" "$new_nginx_conf_path" + ynh_store_file_checksum --file="$new_nginx_conf_path" + fi + ynh_systemd_action --service_name=nginx --action=reload +} + diff --git a/helpers/nodejs b/helpers/nodejs index 42c25e51f..e3ccf82dd 100644 --- a/helpers/nodejs +++ b/helpers/nodejs @@ -1,32 +1,10 @@ #!/bin/bash -n_version=8.2.0 -n_checksum=75efd9e583836f3e6cc6d793df1501462fdceeb3460d5a2dbba99993997383b9 n_install_dir="/opt/node_n" node_version_path="$n_install_dir/n/versions/node" # N_PREFIX is the directory of n, it needs to be loaded as a environment variable. export N_PREFIX="$n_install_dir" -# Install Node version management -# -# [internal] -# -# usage: ynh_install_n -# -# Requires YunoHost version 2.7.12 or higher. -ynh_install_n() { - # Build an app.src for n - echo "SOURCE_URL=https://github.com/tj/n/archive/v${n_version}.tar.gz -SOURCE_SUM=${n_checksum}" >"$YNH_APP_BASEDIR/conf/n.src" - # Download and extract n - ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n - # Install n - ( - cd "$n_install_dir/git" - PREFIX=$N_PREFIX make install 2>&1 - ) -} - # Load the version of node for an app, and set variables. # # usage: ynh_use_nodejs @@ -133,14 +111,10 @@ ynh_install_nodejs() { test -x /usr/bin/node && mv /usr/bin/node /usr/bin/node_n test -x /usr/bin/npm && mv /usr/bin/npm /usr/bin/npm_n - # If n is not previously setup, install it - if ! $n_install_dir/bin/n --version >/dev/null 2>&1; then - ynh_install_n - elif dpkg --compare-versions "$($n_install_dir/bin/n --version)" lt $n_version; then - ynh_install_n - fi - - # Modify the default N_PREFIX in n script + # Install (or update if YunoHost vendor/ folder updated since last install) n + mkdir -p $n_install_dir/bin/ + cp /usr/share/yunohost/helpers.d/vendor/n/n $n_install_dir/bin/n + # Tweak for n to understand it's installed in $N_PREFIX ynh_replace_string --match_string="^N_PREFIX=\${N_PREFIX-.*}$" --replace_string="N_PREFIX=\${N_PREFIX-$N_PREFIX}" --target_file="$n_install_dir/bin/n" # Restore /usr/local/bin in PATH diff --git a/helpers/php b/helpers/php index 05e0939c8..417dbbc61 100644 --- a/helpers/php +++ b/helpers/php @@ -57,6 +57,7 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} # # Requires YunoHost version 4.1.0 or higher. ynh_add_fpm_config() { + local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. local legacy_args=vtufpd local -A args_array=([v]=phpversion= [t]=use_template [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service) @@ -81,11 +82,16 @@ ynh_add_fpm_config() { dedicated_service=${dedicated_service:-0} # Set the default PHP-FPM version by default - phpversion="${phpversion:-$YNH_PHP_VERSION}" + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then + phpversion="${phpversion:-$YNH_PHP_VERSION}" + else + phpversion="${phpversion:-$_globalphpversion}" + fi local old_phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) # If the PHP version changed, remove the old fpm conf + # (NB: This stuff is also handled by the apt helper, which is usually triggered before this helper) if [ -n "$old_phpversion" ] && [ "$old_phpversion" != "$phpversion" ]; then local old_php_fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir) local old_php_finalphpconf="$old_php_fpm_config_dir/pool.d/$app.conf" @@ -100,6 +106,7 @@ ynh_add_fpm_config() { # Legacy args (packager should just list their php dependency as regular apt dependencies... if [ -n "$package" ]; then # Install the additionnal packages from the default repository + ynh_print_warn --message "Argument --package of ynh_add_fpm_config is deprecated and to be removed in the future" ynh_install_app_dependencies "$package" fi @@ -156,7 +163,7 @@ ynh_add_fpm_config() { user = __APP__ group = __APP__ -chdir = __FINALPATH__ +chdir = __INSTALL_DIR__ listen = /var/run/php/php__PHPVERSION__-fpm-__APP__.sock listen.owner = www-data @@ -192,7 +199,7 @@ pm.process_idle_timeout = 10s if [ -e "$YNH_APP_BASEDIR/conf/php-fpm.ini" ]; then ynh_print_warn --message="Packagers ! Please do not use a separate php ini file, merge your directives in the pool file instead." - ynh_add_config --template="$YNH_APP_BASEDIR/conf/php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini" + ynh_add_config --template="php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini" fi if [ $dedicated_service -eq 1 ]; then @@ -206,7 +213,7 @@ syslog.ident = php-fpm-__APP__ include = __FINALPHPCONF__ " >$YNH_APP_BASEDIR/conf/php-fpm-$app.conf - ynh_add_config --template="$YNH_APP_BASEDIR/conf/php-fpm-$app.conf" --destination="$globalphpconf" + ynh_add_config --template="php-fpm-$app.conf" --destination="$globalphpconf" # Create a config for a dedicated PHP-FPM service for the app echo "[Unit] @@ -283,7 +290,7 @@ ynh_remove_fpm_config() { # If the PHP version used is not the default version for YunoHost # The second part with YNH_APP_PURGE is an ugly hack to guess that we're inside the remove script # (we don't actually care about its value, we just check its not empty hence it exists) - if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] && [ -n "${YNH_APP_PURGE:-}" ]; then + if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] && [ -n "${YNH_APP_PURGE:-}" ] && dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then # Remove app dependencies ... but ideally should happen via an explicit call from packager ynh_remove_app_dependencies fi @@ -474,13 +481,14 @@ YNH_COMPOSER_VERSION=${YNH_COMPOSER_VERSION:-$YNH_DEFAULT_COMPOSER_VERSION} # Execute a command with Composer # -# usage: ynh_composer_exec [--phpversion=phpversion] [--workdir=$final_path] --commands="commands" +# usage: ynh_composer_exec [--phpversion=phpversion] [--workdir=$install_dir] --commands="commands" # | arg: -v, --phpversion - PHP version to use with composer -# | arg: -w, --workdir - The directory from where the command will be executed. Default $final_path. +# | arg: -w, --workdir - The directory from where the command will be executed. Default $install_dir or $final_path # | arg: -c, --commands - Commands to execute. # # Requires YunoHost version 4.2 or higher. ynh_composer_exec() { + local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. local legacy_args=vwc declare -Ar args_array=([v]=phpversion= [w]=workdir= [c]=commands=) @@ -489,8 +497,13 @@ ynh_composer_exec() { local commands # Manage arguments with getopts ynh_handle_getopts_args "$@" - workdir="${workdir:-$final_path}" - phpversion="${phpversion:-$YNH_PHP_VERSION}" + workdir="${workdir:-${install_dir:-$final_path}}" + + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then + phpversion="${phpversion:-$YNH_PHP_VERSION}" + else + phpversion="${phpversion:-$_globalphpversion}" + fi COMPOSER_HOME="$workdir/.composer" COMPOSER_MEMORY_LIMIT=-1 \ php${phpversion} "$workdir/composer.phar" $commands \ @@ -499,14 +512,15 @@ ynh_composer_exec() { # Install and initialize Composer in the given directory # -# usage: ynh_install_composer [--phpversion=phpversion] [--workdir=$final_path] [--install_args="--optimize-autoloader"] [--composerversion=composerversion] +# usage: ynh_install_composer [--phpversion=phpversion] [--workdir=$install_dir] [--install_args="--optimize-autoloader"] [--composerversion=composerversion] # | arg: -v, --phpversion - PHP version to use with composer -# | arg: -w, --workdir - The directory from where the command will be executed. Default $final_path. +# | arg: -w, --workdir - The directory from where the command will be executed. Default $install_dir. # | arg: -a, --install_args - Additional arguments provided to the composer install. Argument --no-dev already include # | arg: -c, --composerversion - Composer version to install # # Requires YunoHost version 4.2 or higher. ynh_install_composer() { + local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. local legacy_args=vwac declare -Ar args_array=([v]=phpversion= [w]=workdir= [a]=install_args= [c]=composerversion=) @@ -516,8 +530,18 @@ ynh_install_composer() { local composerversion # Manage arguments with getopts ynh_handle_getopts_args "$@" - workdir="${workdir:-$final_path}" - phpversion="${phpversion:-$YNH_PHP_VERSION}" + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then + workdir="${workdir:-$final_path}" + else + workdir="${workdir:-$install_dir}" + fi + + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then + phpversion="${phpversion:-$YNH_PHP_VERSION}" + else + phpversion="${phpversion:-$_globalphpversion}" + fi + install_args="${install_args:-}" composerversion="${composerversion:-$YNH_COMPOSER_VERSION}" diff --git a/helpers/postgresql b/helpers/postgresql index 92a70a166..796a36214 100644 --- a/helpers/postgresql +++ b/helpers/postgresql @@ -195,7 +195,13 @@ ynh_psql_database_exists() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - if ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database"; then + # if psql is not there, we cannot check the db + # though it could exists. + if ! command -v psql + then + ynh_print_err -m "PostgreSQL is not installed, impossible to check for db existence." + return 1 + elif ! sudo --login --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database"; then return 1 else return 0 diff --git a/helpers/string b/helpers/string index 4dd5c0b4b..dc1658e3d 100644 --- a/helpers/string +++ b/helpers/string @@ -48,7 +48,7 @@ ynh_replace_string() { ynh_handle_getopts_args "$@" set +o xtrace # set +x - local delimit=@ + local delimit=$'\001' # Escape the delimiter if it's in the string. match_string=${match_string//${delimit}/"\\${delimit}"} replace_string=${replace_string//${delimit}/"\\${delimit}"} diff --git a/helpers/systemd b/helpers/systemd index 270b0144d..761e818ad 100644 --- a/helpers/systemd +++ b/helpers/systemd @@ -23,7 +23,7 @@ ynh_add_systemd_config() { service="${service:-$app}" template="${template:-systemd.service}" - ynh_add_config --template="$YNH_APP_BASEDIR/conf/$template" --destination="/etc/systemd/system/$service.service" + ynh_add_config --template="$template" --destination="/etc/systemd/system/$service.service" systemctl enable $service --quiet systemctl daemon-reload @@ -61,7 +61,7 @@ ynh_remove_systemd_config() { # | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started. # | arg: -p, --log_path= - Log file - Path to the log file. Default : `/var/log/$app/$app.log` # | arg: -t, --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 300 seconds. -# | arg: -e, --length= - Length of the error log : Default : 20 +# | arg: -e, --length= - Length of the error log displayed for debugging : Default : 20 # # Requires YunoHost version 3.5.0 or higher. ynh_systemd_action() { @@ -110,6 +110,8 @@ ynh_systemd_action() { action="reload-or-restart" fi + local time_start="$(date --utc --rfc-3339=seconds | cut -d+ -f1) UTC" + # If the service fails to perform the action if ! systemctl $action $service_name; then # Show syslog for this service @@ -128,9 +130,17 @@ ynh_systemd_action() { local i=0 for i in $(seq 1 $timeout); do # Read the log until the sentence is found, that means the app finished to start. Or run until the timeout - if grep --extended-regexp --quiet "$line_match" "$templog"; then - ynh_print_info --message="The service $service_name has correctly executed the action ${action}." - break + if [ "$log_path" == "systemd" ]; then + # For systemd services, we in fact dont rely on the templog, which for some reason is not reliable, but instead re-read journalctl every iteration, starting at the timestamp where we triggered the action + if journalctl --unit=$service_name --since="$time_start" --quiet --no-pager --no-hostname | grep --extended-regexp --quiet "$line_match"; then + ynh_print_info --message="The service $service_name has correctly executed the action ${action}." + break + fi + else + if grep --extended-regexp --quiet "$line_match" "$templog"; then + ynh_print_info --message="The service $service_name has correctly executed the action ${action}." + break + fi fi if [ $i -eq 30 ]; then echo "(this may take some time)" >&2 diff --git a/helpers/utils b/helpers/utils index 60cbedb5c..52d7c734f 100644 --- a/helpers/utils +++ b/helpers/utils @@ -22,7 +22,10 @@ YNH_APP_BASEDIR=${YNH_APP_BASEDIR:-$(realpath ..)} ynh_exit_properly() { local exit_code=$? - rm -rf "/var/cache/yunohost/download/" + if [[ "${YNH_APP_ACTION:-}" =~ ^install$|^upgrade$|^restore$ ]] + then + rm -rf "/var/cache/yunohost/download/" + fi if [ "$exit_code" -eq 0 ]; then exit 0 # Exit without error if the script ended correctly @@ -61,40 +64,89 @@ ynh_abort_if_errors() { trap ynh_exit_properly EXIT # Capturing exit signals on shell script } +# When running an app script with packaging format >= 2, auto-enable ynh_abort_if_errors except for remove script +if [[ "${YNH_CONTEXT:-}" != "regenconf" ]] && dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} ge 2 && [[ ${YNH_APP_ACTION} != "remove" ]] +then + ynh_abort_if_errors +fi + # Download, check integrity, uncompress and patch the source from app.src # -# usage: ynh_setup_source --dest_dir=dest_dir [--source_id=source_id] [--keep="file1 file2"] -# | arg: -d, --dest_dir= - Directory where to setup sources -# | arg: -s, --source_id= - Name of the source, defaults to `app` -# | arg: -k, --keep= - Space-separated list of files/folders that will be backup/restored in $dest_dir, such as a config file you don't want to overwrite. For example 'conf.json secrets.json logs/' +# usage: ynh_setup_source --dest_dir=dest_dir [--source_id=source_id] [--keep="file1 file2"] [--full_replace] +# | arg: -d, --dest_dir= - Directory where to setup sources +# | arg: -s, --source_id= - Name of the source, defaults to `main` (when the sources resource exists in manifest.toml) or (legacy) `app` otherwise +# | arg: -k, --keep= - Space-separated list of files/folders that will be backup/restored in $dest_dir, such as a config file you don't want to overwrite. For example 'conf.json secrets.json logs/' +# | arg: -r, --full_replace= - Remove previous sources before installing new sources +# +# #### New 'sources' resources +# +# (See also the resources documentation which may be more complete?) +# +# This helper will read infos from the 'sources' resources in the manifest.toml of the app +# and expect a structure like: +# +# ```toml +# [resources.sources] +# [resources.sources.main] +# url = "https://some.address.to/download/the/app/archive" +# sha256 = "0123456789abcdef" # The sha256 sum of the asset obtained from the URL +# ``` +# +# ##### Optional flags +# +# ```text +# format = "tar.gz"/xz/bz2 # automatically guessed from the extension of the URL, but can be set explicitly. Will use `tar` to extract +# "zip" # automatically guessed from the extension of the URL, but can be set explicitly. Will use `unzip` to extract +# "docker" # useful to extract files from an already-built docker image (instead of rebuilding them locally). Will use `docker-image-extract` to extract +# "whatever" # an arbitrary value, not really meaningful except to imply that the file won't be extracted +# +# in_subdir = true # default, there's an intermediate subdir in the archive before accessing the actual files +# false # sources are directly in the archive root +# n # (special cases) an integer representing a number of subdirs levels to get rid of +# +# extract = true # default if file is indeed an archive such as .zip, .tar.gz, .tar.bz2, ... +# = false # default if file 'format' is not set and the file is not to be extracted because it is not an archive but a script or binary or whatever asset. +# # in which case the file will only be `mv`ed to the location possibly renamed using the `rename` value +# +# rename = "whatever_your_want" # to be used for convenience when `extract` is false and the default name of the file is not practical +# platform = "linux/amd64" # (defaults to "linux/$YNH_ARCH") to be used in conjonction with `format = "docker"` to specify which architecture to extract for +# ``` +# +# You may also define assets url and checksum per-architectures such as: +# ```toml +# [resources.sources] +# [resources.sources.main] +# amd64.url = "https://some.address.to/download/the/app/archive/when/amd64" +# amd64.sha256 = "0123456789abcdef" +# armhf.url = "https://some.address.to/download/the/app/archive/when/armhf" +# armhf.sha256 = "fedcba9876543210" +# ``` +# +# In which case ynh_setup_source --dest_dir="$install_dir" will automatically pick the appropriate source depending on the arch +# +# +# +# #### Legacy format '.src' # # This helper will read `conf/${source_id}.src`, download and install the sources. # # The src file need to contains: # ``` # SOURCE_URL=Address to download the app archive -# SOURCE_SUM=Control sum -# # (Optional) Program to check the integrity (sha256sum, md5sum...). Default: sha256 -# SOURCE_SUM_PRG=sha256 -# # (Optional) Archive format. Default: tar.gz +# SOURCE_SUM=Sha256 sum # SOURCE_FORMAT=tar.gz -# # (Optional) Put false if sources are directly in the archive root. Default: true -# # Instead of true, SOURCE_IN_SUBDIR could be the number of sub directories to remove. # SOURCE_IN_SUBDIR=false -# # (Optionnal) Name of the local archive (offline setup support). Default: ${src_id}.${src_format} # SOURCE_FILENAME=example.tar.gz -# # (Optional) If it set as false don't extract the source. Default: true -# # (Useful to get a debian package or a python wheel.) # SOURCE_EXTRACT=(true|false) +# SOURCE_PLATFORM=linux/arm64/v8 # ``` # # The helper will: -# - Check if there is a local source archive in `/opt/yunohost-apps-src/$APP_ID/$SOURCE_FILENAME` -# - Download `$SOURCE_URL` if there is no local archive -# - Check the integrity with `$SOURCE_SUM_PRG -c --status` +# - Download the specific URL if there is no local archive +# - Check the integrity with the specific sha256 sum # - Uncompress the archive to `$dest_dir`. -# - If `$SOURCE_IN_SUBDIR` is true, the first level directory of the archive will be removed. -# - If `$SOURCE_IN_SUBDIR` is a numeric value, the N first level directories will be removed. +# - If `in_subdir` is true, the first level directory of the archive will be removed. +# - If `in_subdir` is a numeric value, the N first level directories will be removed. # - Patches named `sources/patches/${src_id}-*.patch` will be applied to `$dest_dir` # - Extra files in `sources/extra_files/$src_id` will be copied to dest_dir # @@ -102,26 +154,73 @@ ynh_abort_if_errors() { ynh_setup_source() { # Declare an array to define the options of this helper. local legacy_args=dsk - local -A args_array=([d]=dest_dir= [s]=source_id= [k]=keep=) + local -A args_array=([d]=dest_dir= [s]=source_id= [k]=keep= [r]=full_replace=) local dest_dir local source_id local keep + local full_replace # Manage arguments with getopts ynh_handle_getopts_args "$@" - source_id="${source_id:-app}" keep="${keep:-}" + full_replace="${full_replace:-0}" - local src_file_path="$YNH_APP_BASEDIR/conf/${source_id}.src" + if test -e $YNH_APP_BASEDIR/manifest.toml && cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq -e '.resources.sources' >/dev/null + then + source_id="${source_id:-main}" + local sources_json=$(cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq ".resources.sources[\"$source_id\"]") + if jq -re ".url" <<< "$sources_json" + then + local arch_prefix="" + else + local arch_prefix=".$YNH_ARCH" + fi - # Load value from configuration file (see above for a small doc about this file - # format) - local src_url=$(grep 'SOURCE_URL=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_sum=$(grep 'SOURCE_SUM=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_format=$(grep 'SOURCE_FORMAT=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_extract=$(grep 'SOURCE_EXTRACT=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$src_file_path" | cut --delimiter='=' --fields=2-) - local src_filename=$(grep 'SOURCE_FILENAME=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_url="$(jq -r "$arch_prefix.url" <<< "$sources_json" | sed 's/^null$//')" + local src_sum="$(jq -r "$arch_prefix.sha256" <<< "$sources_json" | sed 's/^null$//')" + local src_sumprg="sha256sum" + local src_format="$(jq -r ".format" <<< "$sources_json" | sed 's/^null$//')" + local src_in_subdir="$(jq -r ".in_subdir" <<< "$sources_json" | sed 's/^null$//')" + local src_extract="$(jq -r ".extract" <<< "$sources_json" | sed 's/^null$//')" + local src_platform="$(jq -r ".platform" <<< "$sources_json" | sed 's/^null$//')" + local src_rename="$(jq -r ".rename" <<< "$sources_json" | sed 's/^null$//')" + + [[ -n "$src_url" ]] || ynh_die "No URL defined for source $source_id$arch_prefix ?" + [[ -n "$src_sum" ]] || ynh_die "No sha256 sum defined for source $source_id$arch_prefix ?" + + if [[ -z "$src_format" ]] + then + if [[ "$src_url" =~ ^.*\.zip$ ]] || [[ "$src_url" =~ ^.*/zipball/.*$ ]] + then + src_format="zip" + elif [[ "$src_url" =~ ^.*\.tar\.gz$ ]] || [[ "$src_url" =~ ^.*\.tgz$ ]] || [[ "$src_url" =~ ^.*/tar\.gz/.*$ ]] || [[ "$src_url" =~ ^.*/tarball/.*$ ]] + then + src_format="tar.gz" + elif [[ "$src_url" =~ ^.*\.tar\.xz$ ]] + then + src_format="tar.xz" + elif [[ "$src_url" =~ ^.*\.tar\.bz2$ ]] + then + src_format="tar.bz2" + elif [[ -z "$src_extract" ]] + then + src_extract="false" + fi + fi + else + source_id="${source_id:-app}" + local src_file_path="$YNH_APP_BASEDIR/conf/${source_id}.src" + + # Load value from configuration file (see above for a small doc about this file + # format) + local src_url=$(grep 'SOURCE_URL=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_sum=$(grep 'SOURCE_SUM=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_format=$(grep 'SOURCE_FORMAT=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_rename=$(grep 'SOURCE_FILENAME=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_extract=$(grep 'SOURCE_EXTRACT=' "$src_file_path" | cut --delimiter='=' --fields=2-) + local src_platform=$(grep 'SOURCE_PLATFORM=' "$src_file_path" | cut --delimiter='=' --fields=2-) + fi # Default value src_sumprg=${src_sumprg:-sha256sum} @@ -129,33 +228,54 @@ ynh_setup_source() { src_format=${src_format:-tar.gz} src_format=$(echo "$src_format" | tr '[:upper:]' '[:lower:]') src_extract=${src_extract:-true} - if [ "$src_filename" = "" ]; then - src_filename="${source_id}.${src_format}" + + if [[ "$src_extract" != "true" ]] && [[ "$src_extract" != "false" ]] + then + ynh_die "For source $source_id, expected either 'true' or 'false' for the extract parameter" fi + # (Unused?) mecanism where one can have the file in a special local cache to not have to download it... - local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${src_filename}" + local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${source_id}" - mkdir -p /var/cache/yunohost/download/${YNH_APP_ID}/ - src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${src_filename}" + # Gotta use this trick with 'dirname' because source_id may contain slashes x_x + mkdir -p $(dirname /var/cache/yunohost/download/${YNH_APP_ID}/${source_id}) + src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${source_id}" - if test -e "$local_src"; then + if [ "$src_format" = "docker" ]; then + src_platform="${src_platform:-"linux/$YNH_ARCH"}" + elif test -e "$local_src"; then cp $local_src $src_filename else [ -n "$src_url" ] || ynh_die "Couldn't parse SOURCE_URL from $src_file_path ?" - # NB. we have to declare the var as local first, - # otherwise 'local foo=$(false) || echo 'pwet'" does'nt work - # because local always return 0 ... - local out - # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) - out=$(wget --tries 3 --no-dns-cache --timeout 900 --no-verbose --output-document=$src_filename $src_url 2>&1) \ - || ynh_die --message="$out" - fi + # If the file was prefetched but somehow doesn't match the sum, rm and redownload it + if [ -e "$src_filename" ] && ! echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status + then + rm -f "$src_filename" + fi - # Check the control sum - echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status \ - || ynh_die --message="Corrupt source" + # Only redownload the file if it wasnt prefetched + if [ ! -e "$src_filename" ] + then + # NB. we have to declare the var as local first, + # otherwise 'local foo=$(false) || echo 'pwet'" does'nt work + # because local always return 0 ... + local out + # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) + out=$(wget --tries 3 --no-dns-cache --timeout 900 --no-verbose --output-document=$src_filename $src_url 2>&1) \ + || ynh_die --message="$out" + fi + + # Check the control sum + if ! echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status + then + local actual_sum="$(${src_sumprg} ${src_filename} | cut --delimiter=' ' --fields=1)" + local actual_size="$(du -hs ${src_filename} | cut --fields=1)" + rm -f ${src_filename} + ynh_die --message="Corrupt source for ${src_url}: Expected sha256sum to be ${src_sum} but got ${actual_sum} (size: ${actual_size})." + fi + fi # Keep files to be backup/restored at the end of the helper # Assuming $dest_dir already exists @@ -172,16 +292,30 @@ ynh_setup_source() { done fi + if [ "$full_replace" -eq 1 ]; then + ynh_secure_remove --file="$dest_dir" + fi + # Extract source into the app dir mkdir --parents "$dest_dir" + if [ -n "${install_dir:-}" ] && [ "$dest_dir" == "$install_dir" ]; then + _ynh_apply_default_permissions $dest_dir + fi if [ -n "${final_path:-}" ] && [ "$dest_dir" == "$final_path" ]; then _ynh_apply_default_permissions $dest_dir fi - if ! "$src_extract"; then - mv $src_filename $dest_dir - elif [ "$src_format" = "zip" ]; then + if [[ "$src_extract" == "false" ]]; then + if [[ -z "$src_rename" ]] + then + mv $src_filename $dest_dir + else + mv $src_filename $dest_dir/$src_rename + fi + elif [[ "$src_format" == "docker" ]]; then + /usr/share/yunohost/helpers.d/vendor/docker-image-extract/docker-image-extract -p $src_platform -o $dest_dir $src_url 2>&1 + elif [[ "$src_format" == "zip" ]]; then # Zip format # Using of a temp directory, because unzip doesn't manage --strip-components if $src_in_subdir; then @@ -311,7 +445,7 @@ ynh_local_curl() { # | arg: -d, --destination= - Destination of the config file # # examples: -# ynh_add_config --template=".env" --destination="$final_path/.env" use the template file "../conf/.env" +# ynh_add_config --template=".env" --destination="$install_dir/.env" use the template file "../conf/.env" # ynh_add_config --template="/etc/nginx/sites-available/default" --destination="etc/nginx/sites-available/mydomain.conf" # # The template can be by default the name of a file in the conf directory @@ -326,7 +460,7 @@ ynh_local_curl() { # __NAMETOCHANGE__ by $app # __USER__ by $app # __FINALPATH__ by $final_path -# __PHPVERSION__ by $YNH_PHP_VERSION +# __PHPVERSION__ by $YNH_PHP_VERSION (packaging v1 only, packaging v2 uses phpversion setting implicitly set by apt resource) # __YNH_NODE_LOAD_PATH__ by $ynh_node_load_PATH # ``` # And any dynamic variables that should be defined before calling this helper like: @@ -395,7 +529,7 @@ ynh_add_config() { # __NAMETOCHANGE__ by $app # __USER__ by $app # __FINALPATH__ by $final_path -# __PHPVERSION__ by $YNH_PHP_VERSION +# __PHPVERSION__ by $YNH_PHP_VERSION (packaging v1 only, packaging v2 uses phpversion setting implicitly set by apt resource) # __YNH_NODE_LOAD_PATH__ by $ynh_node_load_PATH # # And any dynamic variables that should be defined before calling this helper like: @@ -425,10 +559,13 @@ ynh_replace_vars() { ynh_replace_string --match_string="__NAMETOCHANGE__" --replace_string="$app" --target_file="$file" ynh_replace_string --match_string="__USER__" --replace_string="$app" --target_file="$file" fi + # Legacy if test -n "${final_path:-}"; then ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$file" + ynh_replace_string --match_string="__INSTALL_DIR__" --replace_string="$final_path" --target_file="$file" fi - if test -n "${YNH_PHP_VERSION:-}"; then + # Legacy / Packaging v1 only + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2 && test -n "${YNH_PHP_VERSION:-}"; then ynh_replace_string --match_string="__PHPVERSION__" --replace_string="$YNH_PHP_VERSION" --target_file="$file" fi if test -n "${ynh_node_load_PATH:-}"; then @@ -513,7 +650,7 @@ ynh_read_var_in_file() { # Get the line number after which we search for the variable local line_number=1 if [[ -n "$after" ]]; then - line_number=$(grep -n $after $file | cut -d: -f1) + line_number=$(grep -m1 -n $after $file | cut -d: -f1) if [[ -z "$line_number" ]]; then set -o xtrace # set -x return 1 @@ -543,7 +680,7 @@ ynh_read_var_in_file() { var_part+='\s*' # Extract the part after assignation sign - local expression_with_comment="$(tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL | head -n1)" + local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" if [[ "$expression_with_comment" == "YNH_NULL" ]]; then set -o xtrace # set -x echo YNH_NULL @@ -589,15 +726,14 @@ ynh_write_var_in_file() { set +o xtrace # set +x # Get the line number after which we search for the variable - local line_number=1 + local after_line_number=1 if [[ -n "$after" ]]; then - line_number=$(grep -n $after $file | cut -d: -f1) - if [[ -z "$line_number" ]]; then + after_line_number=$(grep -m1 -n $after $file | cut -d: -f1) + if [[ -z "$after_line_number" ]]; then set -o xtrace # set -x return 1 fi fi - local range="${line_number},\$ " local filename="$(basename -- "$file")" local ext="${filename##*.}" @@ -622,17 +758,21 @@ ynh_write_var_in_file() { var_part+='\s*' # Extract the part after assignation sign - local expression_with_comment="$(tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL | head -n1)" + local expression_with_comment="$((tail +$after_line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" if [[ "$expression_with_comment" == "YNH_NULL" ]]; then set -o xtrace # set -x return 1 fi + local value_line_number="$(tail +$after_line_number ${file} | grep -m1 -n -i -P $var_part'\K.*$' | cut -d: -f1)" + value_line_number=$((after_line_number + value_line_number)) + local range="${after_line_number},${value_line_number} " # Remove comments if needed local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" endline=${expression_with_comment#"$expression"} endline="$(echo "$endline" | sed 's/\\/\\\\/g')" value="$(echo "$value" | sed 's/\\/\\\\/g')" + value=${value//&/"\&"} local first_char="${expression:0:1}" delimiter=$'\001' if [[ "$first_char" == '"' ]]; then @@ -694,7 +834,7 @@ _acceptable_path_to_delete() { local forbidden_paths=$(ls -d / /* /{var,home,usr}/* /etc/{default,sudoers.d,yunohost,cron*}) # Legacy : A couple apps still have data in /home/$app ... - if [[ -n "$app" ]] + if [[ -n "${app:-}" ]] then forbidden_paths=$(echo "$forbidden_paths" | grep -v "/home/$app") fi @@ -758,12 +898,25 @@ ynh_read_manifest() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - if [ ! -e "$manifest" ]; then + if [ ! -e "${manifest:-}" ]; then # If the manifest isn't found, try the common place for backup and restore script. - manifest="$YNH_APP_BASEDIR/manifest.json" + if [ -e "$YNH_APP_BASEDIR/manifest.json" ] + then + manifest="$YNH_APP_BASEDIR/manifest.json" + elif [ -e "$YNH_APP_BASEDIR/manifest.toml" ] + then + manifest="$YNH_APP_BASEDIR/manifest.toml" + else + ynh_die --message "No manifest found !?" + fi fi - jq ".$manifest_key" "$manifest" --raw-output + if echo "$manifest" | grep -q '\.json$' + then + jq ".$manifest_key" "$manifest" --raw-output + else + cat "$manifest" | python3 -c 'import json, toml, sys; print(json.dumps(toml.load(sys.stdin)))' | jq ".$manifest_key" --raw-output + fi } # Read the upstream version from the manifest or `$YNH_APP_MANIFEST_VERSION` @@ -907,9 +1060,9 @@ ynh_compare_current_package_version() { _ynh_apply_default_permissions() { local target=$1 - local ynh_requirement=$(jq -r '.requirements.yunohost' $YNH_APP_BASEDIR/manifest.json | tr -d '>= ') + local ynh_requirement=$(ynh_read_manifest --manifest_key="requirements.yunohost" | tr -d '<>= ') - if [ -z "$ynh_requirement" ] || [ "$ynh_requirement" == "null" ] || dpkg --compare-versions $ynh_requirement ge 4.2; then + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} ge 2 || [ -z "$ynh_requirement" ] || [ "$ynh_requirement" == "null" ] || dpkg --compare-versions $ynh_requirement ge 4.2; then chmod o-rwx $target chmod g-w $target chown -R root:root $target @@ -918,10 +1071,20 @@ _ynh_apply_default_permissions() { fi fi - # Crons should be owned by root otherwise they probably don't run - if echo "$target" | grep -q '^/etc/cron' + # Crons should be owned by root + # Also we don't want systemd conf, nginx conf or others stuff to be owned by the app, + # otherwise they could self-edit their own systemd conf and escalate privilege + if echo "$target" | grep -q '^/etc/cron\|/etc/php\|/etc/nginx/conf.d\|/etc/fail2ban\|/etc/systemd/system' then chmod 400 $target chown root:root $target fi } + +int_to_bool() { + sed -e 's/^1$/True/g' -e 's/^0$/False/g' +} + +toml_to_json() { + python3 -c 'import toml, json, sys; print(json.dumps(toml.load(sys.stdin)))' +} diff --git a/helpers/vendor/docker-image-extract/LICENSE b/helpers/vendor/docker-image-extract/LICENSE new file mode 100644 index 000000000..82579b059 --- /dev/null +++ b/helpers/vendor/docker-image-extract/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Emmanuel Frecon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/helpers/vendor/docker-image-extract/README.md b/helpers/vendor/docker-image-extract/README.md new file mode 100644 index 000000000..6f6cb5074 --- /dev/null +++ b/helpers/vendor/docker-image-extract/README.md @@ -0,0 +1 @@ +This is taken from https://github.com/efrecon/docker-image-extract diff --git a/helpers/vendor/docker-image-extract/docker-image-extract b/helpers/vendor/docker-image-extract/docker-image-extract new file mode 100755 index 000000000..4842a8e04 --- /dev/null +++ b/helpers/vendor/docker-image-extract/docker-image-extract @@ -0,0 +1,262 @@ +#!/bin/sh +# +# This script pulls and extracts all files from an image in Docker Hub. +# +# Copyright (c) 2020-2022, Jeremy Lin +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +PLATFORM_DEFAULT="linux/amd64" +PLATFORM="${PLATFORM_DEFAULT}" +OUT_DIR="./output" + +usage() { + echo "This script pulls and extracts all files from an image in Docker Hub." + echo + echo "$0 [OPTIONS...] IMAGE[:REF]" + echo + echo "IMAGE can be a community user image (like 'some-user/some-image') or a" + echo "Docker official image (like 'hello-world', which contains no '/')." + echo + echo "REF is either a tag name or a full SHA-256 image digest (with a 'sha256:' prefix)." + echo "The default ref is the 'latest' tag." + echo + echo "Options:" + echo + echo " -p PLATFORM Pull image for the specified platform (default: ${PLATFORM})" + echo " For a given image on Docker Hub, the 'Tags' tab lists the" + echo " platforms supported for that image." + echo " -o OUT_DIR Extract image to the specified output dir (default: ${OUT_DIR})" + echo " -h Show help with usage examples" +} + +usage_detailed() { + usage + echo + echo "Examples:" + echo + echo "# Pull and extract all files in the 'hello-world' image tagged 'latest'." + echo "\$ $0 hello-world:latest" + echo + echo "# Same as above; ref defaults to the 'latest' tag." + echo "\$ $0 hello-world" + echo + echo "# Pull the 'hello-world' image for the 'linux/arm64/v8' platform." + echo "\$ $0 -p linux/arm64/v8 hello-world" + echo + echo "# Pull an image by digest." + echo "\$ $0 hello-world:sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042" +} + +if [ $# -eq 0 ]; then + usage_detailed + exit 0 +fi + +while getopts ':ho:p:' opt; do + case $opt in + o) + OUT_DIR="${OPTARG}" + ;; + p) + PLATFORM="${OPTARG}" + ;; + h) + usage_detailed + exit 0 + ;; + \?) + echo "ERROR: Invalid option '-$OPTARG'." + echo + usage + exit 1 + ;; + \:) echo "ERROR: Argument required for option '-$OPTARG'." + echo + usage + exit 1 + ;; + esac +done +shift $(($OPTIND - 1)) + +if [ $# -eq 0 ]; then + echo "ERROR: Image to pull must be specified." + echo + usage + exit 1 +fi + +have_curl() { + command -v curl >/dev/null +} + +have_wget() { + command -v wget >/dev/null +} + +if ! have_curl && ! have_wget; then + echo "This script requires either curl or wget." + exit 1 +fi + +image_spec="$1" +image="${image_spec%%:*}" +if [ "${image#*/}" = "${image}" ]; then + # Docker official images are in the 'library' namespace. + image="library/${image}" +fi +ref="${image_spec#*:}" +if [ "${ref}" = "${image_spec}" ]; then + echo "Defaulting ref to tag 'latest'..." + ref=latest +fi + +# Split platform (OS/arch/variant) into separate variables. +# A platform specifier doesn't always include the `variant` component. +OLD_IFS="${IFS}" +IFS=/ read -r OS ARCH VARIANT <":"" (assumes key/val won't contain double quotes). + # The colon may have whitespace on either side. + grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]\+\"" | + # Extract just by deleting the last '"', and then greedily deleting + # everything up to '"'. + sed -e 's/"$//' -e 's/.*"//' +} + +# Fetch a URL to stdout. Up to two header arguments may be specified: +# +# fetch [name1: value1] [name2: value2] +# +fetch() { + if have_curl; then + if [ $# -eq 2 ]; then + set -- -H "$2" "$1" + elif [ $# -eq 3 ]; then + set -- -H "$2" -H "$3" "$1" + fi + curl -sSL "$@" + else + if [ $# -eq 2 ]; then + set -- --header "$2" "$1" + elif [ $# -eq 3 ]; then + set -- --header "$2" --header "$3" "$1" + fi + wget -qO- "$@" + fi +} + +# https://docs.docker.com/docker-hub/api/latest/#tag/repositories +manifest_list_url="https://hub.docker.com/v2/repositories/${image}/tags/${ref}" + +# If we're pulling the image for the default platform, or the ref is already +# a SHA-256 image digest, then we don't need to look up anything. +if [ "${PLATFORM}" = "${PLATFORM_DEFAULT}" ] || [ -z "${ref##sha256:*}" ]; then + digest="${ref}" +else + echo "Getting multi-arch manifest list..." + digest=$(fetch "${manifest_list_url}" | + # Break up the single-line JSON output into separate lines by adding + # newlines before and after the chars '[', ']', '{', and '}'. + sed -e 's/\([][{}]\)/\n\1\n/g' | + # Extract the "images":[...] list. + sed -n '/"images":/,/]/ p' | + # Each image's details are now on a separate line, e.g. + # "architecture":"arm64","features":"","variant":"v8","digest":"sha256:054c85801c4cb41511b176eb0bf13a2c4bbd41611ddd70594ec3315e88813524","os":"linux","os_features":"","os_version":null,"size":828724,"status":"active","last_pulled":"2022-09-02T22:46:48.240632Z","last_pushed":"2022-09-02T00:42:45.69226Z" + # The image details are interspersed with lines of stray punctuation, + # so grep for an arbitrary string that must be in these lines. + grep architecture | + # Search for an image that matches the platform. + while read -r image; do + # Arch is probably most likely to be unique, so check that first. + arch="$(echo ${image} | extract 'architecture')" + if [ "${arch}" != "${ARCH}" ]; then continue; fi + + os="$(echo ${image} | extract 'os')" + if [ "${os}" != "${OS}" ]; then continue; fi + + variant="$(echo ${image} | extract 'variant')" + if [ "${variant}" = "${VARIANT}" ]; then + echo ${image} | extract 'digest' + break + fi + done) +fi + +if [ -n "${digest}" ]; then + echo "Platform ${PLATFORM} resolved to '${digest}'..." +else + echo "No image digest found. Verify that the image, ref, and platform are valid." + exit 1 +fi + +# https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate +api_token_url="https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" + +# https://github.com/docker/distribution/blob/master/docs/spec/api.md#pulling-an-image-manifest +manifest_url="https://registry-1.docker.io/v2/${image}/manifests/${digest}" + +# https://github.com/docker/distribution/blob/master/docs/spec/api.md#pulling-a-layer +blobs_base_url="https://registry-1.docker.io/v2/${image}/blobs" + +echo "Getting API token..." +token=$(fetch "${api_token_url}" | extract 'token') +auth_header="Authorization: Bearer $token" +v2_header="Accept: application/vnd.docker.distribution.manifest.v2+json" + +echo "Getting image manifest for $image:$ref..." +layers=$(fetch "${manifest_url}" "${auth_header}" "${v2_header}" | + # Extract `digest` values only after the `layers` section appears. + sed -n '/"layers":/,$ p' | + extract 'digest') + +if [ -z "${layers}" ]; then + echo "No layers returned. Verify that the image and ref are valid." + exit 1 +fi + +mkdir -p "${OUT_DIR}" + +for layer in $layers; do + hash="${layer#sha256:}" + echo "Fetching and extracting layer ${hash}..." + fetch "${blobs_base_url}/${layer}" "${auth_header}" | gzip -d | tar -C "${OUT_DIR}" -xf - + # Ref: https://github.com/moby/moby/blob/master/image/spec/v1.2.md#creating-an-image-filesystem-changeset + # https://github.com/moby/moby/blob/master/pkg/archive/whiteouts.go + # Search for "whiteout" files to indicate files deleted in this layer. + OLD_IFS="${IFS}" + find "${OUT_DIR}" -name '.wh.*' | while IFS= read -r f; do + dir="${f%/*}" + wh_file="${f##*/}" + file="${wh_file#.wh.}" + # Delete both the whiteout file and the whited-out file. + rm -rf "${dir}/${wh_file}" "${dir}/${file}" + done + IFS="${OLD_IFS}" +done + +echo "Image contents extracted into ${OUT_DIR}." diff --git a/helpers/vendor/n/LICENSE b/helpers/vendor/n/LICENSE new file mode 100644 index 000000000..8e04e8467 --- /dev/null +++ b/helpers/vendor/n/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/helpers/vendor/n/README.md b/helpers/vendor/n/README.md new file mode 100644 index 000000000..9a29a3936 --- /dev/null +++ b/helpers/vendor/n/README.md @@ -0,0 +1 @@ +This is taken from https://github.com/tj/n/ diff --git a/helpers/vendor/n/n b/helpers/vendor/n/n new file mode 100755 index 000000000..2a877c45b --- /dev/null +++ b/helpers/vendor/n/n @@ -0,0 +1,1635 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2155 +# Disabled "Declare and assign separately to avoid masking return values": https://github.com/koalaman/shellcheck/wiki/SC2155 + +# +# log +# + +log() { + printf " ${SGR_CYAN}%10s${SGR_RESET} : ${SGR_FAINT}%s${SGR_RESET}\n" "$1" "$2" +} + +# +# verbose_log +# Can suppress with --quiet. +# Like log but to stderr rather than stdout, so can also be used from "display" routines. +# + +verbose_log() { + if [[ "${SHOW_VERBOSE_LOG}" == "true" ]]; then + >&2 printf " ${SGR_CYAN}%10s${SGR_RESET} : ${SGR_FAINT}%s${SGR_RESET}\n" "$1" "$2" + fi +} + +# +# Exit with the given +# + +abort() { + >&2 printf "\n ${SGR_RED}Error: %s${SGR_RESET}\n\n" "$*" && exit 1 +} + +# +# Synopsis: trace message ... +# Debugging output to stderr, not used in production code. +# + +function trace() { + >&2 printf "trace: %s\n" "$*" +} + +# +# Synopsis: echo_red message ... +# Highlight message in colour (on stdout). +# + +function echo_red() { + printf "${SGR_RED}%s${SGR_RESET}\n" "$*" +} + +# +# Synopsis: n_grep +# grep wrapper to ensure consistent grep options and circumvent aliases. +# + +function n_grep() { + GREP_OPTIONS='' command grep "$@" +} + +# +# Setup and state +# + +VERSION="v9.1.0" + +N_PREFIX="${N_PREFIX-/usr/local}" +N_PREFIX=${N_PREFIX%/} +readonly N_PREFIX + +N_CACHE_PREFIX="${N_CACHE_PREFIX-${N_PREFIX}}" +N_CACHE_PREFIX=${N_CACHE_PREFIX%/} +CACHE_DIR="${N_CACHE_PREFIX}/n/versions" +readonly N_CACHE_PREFIX CACHE_DIR + +N_NODE_MIRROR=${N_NODE_MIRROR:-${NODE_MIRROR:-https://nodejs.org/dist}} +N_NODE_MIRROR=${N_NODE_MIRROR%/} +readonly N_NODE_MIRROR + +N_NODE_DOWNLOAD_MIRROR=${N_NODE_DOWNLOAD_MIRROR:-https://nodejs.org/download} +N_NODE_DOWNLOAD_MIRROR=${N_NODE_DOWNLOAD_MIRROR%/} +readonly N_NODE_DOWNLOAD_MIRROR + +# Using xz instead of gzip is enabled by default, if xz compatibility checks pass. +# User may set N_USE_XZ to 0 to disable, or set to anything else to enable. +# May also be overridden by command line flags. + +# Normalise external values to true/false +if [[ "${N_USE_XZ}" = "0" ]]; then + N_USE_XZ="false" +elif [[ -n "${N_USE_XZ+defined}" ]]; then + N_USE_XZ="true" +fi +# Not setting to readonly. Overriden by CLI flags, and update_xz_settings_for_version. + +N_MAX_REMOTE_MATCHES=${N_MAX_REMOTE_MATCHES:-20} +# modified by update_mirror_settings_for_version +g_mirror_url=${N_NODE_MIRROR} +g_mirror_folder_name="node" + +# Options for curl and wget. +# Defining commands in variables is fraught (https://mywiki.wooledge.org/BashFAQ/050) +# but we can follow the simple case and store arguments in an array. + +GET_SHOWS_PROGRESS="false" +# --location to follow redirects +# --fail to avoid happily downloading error page from web server for 404 et al +# --show-error to show why failed (on stderr) +CURL_OPTIONS=( "--location" "--fail" "--show-error" ) +if [[ -t 1 ]]; then + CURL_OPTIONS+=( "--progress-bar" ) + command -v curl &> /dev/null && GET_SHOWS_PROGRESS="true" +else + CURL_OPTIONS+=( "--silent" ) +fi +WGET_OPTIONS=( "-q" "-O-" ) + +# Legacy support using unprefixed env. No longer documented in README. +if [ -n "$HTTP_USER" ];then + if [ -z "$HTTP_PASSWORD" ]; then + abort "Must specify HTTP_PASSWORD when supplying HTTP_USER" + fi + CURL_OPTIONS+=( "-u $HTTP_USER:$HTTP_PASSWORD" ) + WGET_OPTIONS+=( "--http-password=$HTTP_PASSWORD" + "--http-user=$HTTP_USER" ) +elif [ -n "$HTTP_PASSWORD" ]; then + abort "Must specify HTTP_USER when supplying HTTP_PASSWORD" +fi + +# Set by set_active_node +g_active_node= + +# set by various lookups to allow mixed logging and return value from function, especially for engine and node +g_target_node= + +DOWNLOAD=false # set to opt-out of activate (install), and opt-in to download (run, exec) +ARCH= +SHOW_VERBOSE_LOG="true" + +# ANSI escape codes +# https://en.wikipedia.org/wiki/ANSI_escape_code +# https://no-color.org +# https://bixense.com/clicolors + +USE_COLOR="true" +if [[ -n "${CLICOLOR_FORCE+defined}" && "${CLICOLOR_FORCE}" != "0" ]]; then + USE_COLOR="true" +elif [[ -n "${NO_COLOR+defined}" || "${CLICOLOR}" = "0" || ! -t 1 ]]; then + USE_COLOR="false" +fi +readonly USE_COLOR +# Select Graphic Rendition codes +if [[ "${USE_COLOR}" = "true" ]]; then + # KISS and use codes rather than tput, avoid dealing with missing tput or TERM. + readonly SGR_RESET="\033[0m" + readonly SGR_FAINT="\033[2m" + readonly SGR_RED="\033[31m" + readonly SGR_CYAN="\033[36m" +else + readonly SGR_RESET= + readonly SGR_FAINT= + readonly SGR_RED= + readonly SGR_CYAN= +fi + +# +# set_arch to override $(uname -a) +# + +set_arch() { + if test -n "$1"; then + ARCH="$1" + else + abort "missing -a|--arch value" + fi +} + +# +# Synopsis: set_insecure +# Globals modified: +# - CURL_OPTIONS +# - WGET_OPTIONS +# + +function set_insecure() { + CURL_OPTIONS+=( "--insecure" ) + WGET_OPTIONS+=( "--no-check-certificate" ) +} + +# +# Synposis: display_major_version numeric-version +# +display_major_version() { + local version=$1 + version="${version#v}" + version="${version%%.*}" + echo "${version}" +} + +# +# Synopsis: update_mirror_settings_for_version version +# e.g. means using download mirror and folder is nightly +# Globals modified: +# - g_mirror_url +# - g_mirror_folder_name +# + +function update_mirror_settings_for_version() { + if is_download_folder "$1" ; then + g_mirror_folder_name="$1" + g_mirror_url="${N_NODE_DOWNLOAD_MIRROR}/${g_mirror_folder_name}" + elif is_download_version "$1"; then + [[ "$1" =~ ^([^/]+)/(.*) ]] + local remote_folder="${BASH_REMATCH[1]}" + g_mirror_folder_name="${remote_folder}" + g_mirror_url="${N_NODE_DOWNLOAD_MIRROR}/${g_mirror_folder_name}" + fi +} + +# +# Synopsis: update_xz_settings_for_version numeric-version +# Globals modified: +# - N_USE_XZ +# + +function update_xz_settings_for_version() { + # tarballs in xz format were available in later version of iojs, but KISS and only use xz from v4. + if [[ "${N_USE_XZ}" = "true" ]]; then + local major_version="$(display_major_version "$1")" + if [[ "${major_version}" -lt 4 ]]; then + N_USE_XZ="false" + fi + fi +} + +# +# Synopsis: update_arch_settings_for_version numeric-version +# Globals modified: +# - ARCH +# + +function update_arch_settings_for_version() { + local tarball_platform="$(display_tarball_platform)" + if [[ -z "${ARCH}" && "${tarball_platform}" = "darwin-arm64" ]]; then + # First native builds were for v16, but can use x64 in rosetta for older versions. + local major_version="$(display_major_version "$1")" + if [[ "${major_version}" -lt 16 ]]; then + ARCH=x64 + fi + fi +} + +# +# Synopsis: is_lts_codename version +# + +function is_lts_codename() { + # https://github.com/nodejs/Release/blob/master/CODENAMES.md + # e.g. argon, Boron + [[ "$1" =~ ^([Aa]rgon|[Bb]oron|[Cc]arbon|[Dd]ubnium|[Ee]rbium|[Ff]ermium|[Gg]allium|[Hh]ydrogen|[Ii]ron|[Jj]od)$ ]] +} + +# +# Synopsis: is_download_folder version +# + +function is_download_folder() { + # e.g. nightly + [[ "$1" =~ ^(next-nightly|nightly|rc|release|test|v8-canary)$ ]] +} + +# +# Synopsis: is_download_version version +# + +function is_download_version() { + # e.g. nightly/, nightly/latest, nightly/v11 + if [[ "$1" =~ ^([^/]+)/(.*) ]]; then + local remote_folder="${BASH_REMATCH[1]}" + is_download_folder "${remote_folder}" + return + fi + return 2 +} + +# +# Synopsis: is_numeric_version version +# + +function is_numeric_version() { + # e.g. 6, v7.1, 8.11.3 + [[ "$1" =~ ^[v]{0,1}[0-9]+(\.[0-9]+){0,2}$ ]] +} + +# +# Synopsis: is_exact_numeric_version version +# + +function is_exact_numeric_version() { + # e.g. 6, v7.1, 8.11.3 + [[ "$1" =~ ^[v]{0,1}[0-9]+\.[0-9]+\.[0-9]+$ ]] +} + +# +# Synopsis: is_node_support_version version +# Reference: https://github.com/nodejs/package-maintenance/issues/236#issue-474783582 +# + +function is_node_support_version() { + [[ "$1" =~ ^(active|lts_active|lts_latest|lts|current|supported)$ ]] +} + +# +# Synopsis: display_latest_node_support_alias version +# Map aliases onto existing n aliases, current and lts +# + +function display_latest_node_support_alias() { + case "$1" in + "active") printf "current" ;; + "lts_active") printf "lts" ;; + "lts_latest") printf "lts" ;; + "lts") printf "lts" ;; + "current") printf "current" ;; + "supported") printf "current" ;; + *) printf "unexpected-version" + esac +} + +# +# Functions used when showing versions installed +# + +enter_fullscreen() { + # Set cursor to be invisible + tput civis 2> /dev/null + # Save screen contents + tput smcup 2> /dev/null + stty -echo +} + +leave_fullscreen() { + # Set cursor to normal + tput cnorm 2> /dev/null + # Restore screen contents + tput rmcup 2> /dev/null + stty echo +} + +handle_sigint() { + leave_fullscreen + S="$?" + kill 0 + exit $S +} + +handle_sigtstp() { + leave_fullscreen + kill -s SIGSTOP $$ +} + +# +# Output usage information. +# + +display_help() { + cat <<-EOF + +Usage: n [options] [COMMAND] [args] + +Commands: + + n Display downloaded Node.js versions and install selection + n latest Install the latest Node.js release (downloading if necessary) + n lts Install the latest LTS Node.js release (downloading if necessary) + n Install Node.js (downloading if necessary) + n install Install Node.js (downloading if necessary) + n run [args ...] Execute downloaded Node.js with [args ...] + n which Output path for downloaded node + n exec [args...] Execute command with modified PATH, so downloaded node and npm first + n rm Remove the given downloaded version(s) + n prune Remove all downloaded versions except the installed version + n --latest Output the latest Node.js version available + n --lts Output the latest LTS Node.js version available + n ls Output downloaded versions + n ls-remote [version] Output matching versions available for download + n uninstall Remove the installed Node.js + +Options: + + -V, --version Output version of n + -h, --help Display help information + -p, --preserve Preserve npm and npx during install of Node.js + -q, --quiet Disable curl output. Disable log messages processing "auto" and "engine" labels. + -d, --download Download if necessary, and don't make active + -a, --arch Override system architecture + --all ls-remote displays all matches instead of last 20 + --insecure Turn off certificate checking for https requests (may be needed from behind a proxy server) + --use-xz/--no-use-xz Override automatic detection of xz support and enable/disable use of xz compressed node downloads. + +Aliases: + + install: i + latest: current + ls: list + lsr: ls-remote + lts: stable + rm: - + run: use, as + which: bin + +Versions: + + Numeric version numbers can be complete or incomplete, with an optional leading 'v'. + Versions can also be specified by label, or codename, + and other downloadable releases by / + + 4.9.1, 8, v6.1 Numeric versions + lts Newest Long Term Support official release + latest, current Newest official release + auto Read version from file: .n-node-version, .node-version, .nvmrc, or package.json + engine Read version from package.json + boron, carbon Codenames for release streams + lts_latest Node.js support aliases + + and nightly, rc/10 et al + +EOF +} + +err_no_installed_print_help() { + display_help + abort "no downloaded versions yet, see above help for commands" +} + +# +# Synopsis: next_version_installed selected_version +# Output version after selected (which may be blank under some circumstances). +# + +function next_version_installed() { + display_cache_versions | n_grep "$1" -A 1 | tail -n 1 +} + +# +# Synopsis: prev_version_installed selected_version +# Output version before selected (which may be blank under some circumstances). +# + +function prev_version_installed() { + display_cache_versions | n_grep "$1" -B 1 | head -n 1 +} + +# +# Output n version. +# + +display_n_version() { + echo "$VERSION" && exit 0 +} + +# +# Synopsis: set_active_node +# Checks cached downloads for a binary matching the active node. +# Globals modified: +# - g_active_node +# + +function set_active_node() { + g_active_node= + local node_path="$(command -v node)" + if [[ -x "${node_path}" ]]; then + local installed_version=$(node --version) + installed_version=${installed_version#v} + for dir in "${CACHE_DIR}"/*/ ; do + local folder_name="${dir%/}" + folder_name="${folder_name##*/}" + if diff &> /dev/null \ + "${CACHE_DIR}/${folder_name}/${installed_version}/bin/node" \ + "${node_path}" ; then + g_active_node="${folder_name}/${installed_version}" + break + fi + done + fi +} + +# +# Display sorted versions directories paths. +# + +display_versions_paths() { + find "$CACHE_DIR" -maxdepth 2 -type d \ + | sed 's|'"$CACHE_DIR"'/||g' \ + | n_grep -E "/[0-9]+\.[0-9]+\.[0-9]+" \ + | sed 's|/|.|' \ + | sort -k 1,1 -k 2,2n -k 3,3n -k 4,4n -t . \ + | sed 's|\.|/|' +} + +# +# Display installed versions with +# + +display_versions_with_selected() { + local selected="$1" + echo + for version in $(display_versions_paths); do + if test "$version" = "$selected"; then + printf " ${SGR_CYAN}ο${SGR_RESET} %s\n" "$version" + else + printf " ${SGR_FAINT}%s${SGR_RESET}\n" "$version" + fi + done + echo + printf "Use up/down arrow keys to select a version, return key to install, d to delete, q to quit" +} + +# +# Synopsis: display_cache_versions +# + +function display_cache_versions() { + for folder_and_version in $(display_versions_paths); do + echo "${folder_and_version}" + done +} + +# +# Display current node --version and others installed. +# + +menu_select_cache_versions() { + enter_fullscreen + set_active_node + local selected="${g_active_node}" + + clear + display_versions_with_selected "${selected}" + + trap handle_sigint INT + trap handle_sigtstp SIGTSTP + + ESCAPE_SEQ=$'\033' + UP=$'A' + DOWN=$'B' + CTRL_P=$'\020' + CTRL_N=$'\016' + + while true; do + read -rsn 1 key + case "$key" in + "$ESCAPE_SEQ") + # Handle ESC sequences followed by other characters, i.e. arrow keys + read -rsn 1 -t 1 tmp + # See "[" if terminal in normal mode, and "0" in application mode + if [[ "$tmp" == "[" || "$tmp" == "O" ]]; then + read -rsn 1 -t 1 arrow + case "$arrow" in + "$UP") + clear + selected="$(prev_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + "$DOWN") + clear + selected="$(next_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + esac + fi + ;; + "d") + if [[ -n "${selected}" ]]; then + clear + # Note: prev/next is constrained to min/max + local after_delete_selection="$(next_version_installed "${selected}")" + if [[ "${after_delete_selection}" == "${selected}" ]]; then + after_delete_selection="$(prev_version_installed "${selected}")" + fi + remove_versions "${selected}" + + if [[ "${after_delete_selection}" == "${selected}" ]]; then + clear + leave_fullscreen + echo "All downloaded versions have been deleted from cache." + exit + fi + + selected="${after_delete_selection}" + display_versions_with_selected "${selected}" + fi + ;; + # Vim or Emacs 'up' key + "k"|"$CTRL_P") + clear + selected="$(prev_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + # Vim or Emacs 'down' key + "j"|"$CTRL_N") + clear + selected="$(next_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + "q") + clear + leave_fullscreen + exit + ;; + "") + # enter key returns empty string + leave_fullscreen + [[ -n "${selected}" ]] && activate "${selected}" + exit + ;; + esac + done +} + +# +# Move up a line and erase. +# + +erase_line() { + printf "\033[1A\033[2K" +} + +# +# Disable PaX mprotect for +# + +disable_pax_mprotect() { + test -z "$1" && abort "binary required" + local binary="$1" + + # try to disable mprotect via XATTR_PAX header + local PAXCTL="$(PATH="/sbin:/usr/sbin:$PATH" command -v paxctl-ng 2>&1)" + local PAXCTL_ERROR=1 + if [ -x "$PAXCTL" ]; then + $PAXCTL -l && $PAXCTL -m "$binary" >/dev/null 2>&1 + PAXCTL_ERROR="$?" + fi + + # try to disable mprotect via PT_PAX header + if [ "$PAXCTL_ERROR" != 0 ]; then + PAXCTL="$(PATH="/sbin:/usr/sbin:$PATH" command -v paxctl 2>&1)" + if [ -x "$PAXCTL" ]; then + $PAXCTL -Cm "$binary" >/dev/null 2>&1 + fi + fi +} + +# +# clean_copy_folder +# + +clean_copy_folder() { + local source="$1" + local target="$2" + if [[ -d "${source}" ]]; then + rm -rf "${target}" + cp -fR "${source}" "${target}" + fi +} + +# +# Activate +# + +activate() { + local version="$1" + local dir="$CACHE_DIR/$version" + local original_node="$(command -v node)" + local installed_node="${N_PREFIX}/bin/node" + log "copying" "$version" + + + # Ideally we would just copy from cache to N_PREFIX, but there are some complications + # - various linux versions use symlinks for folders in /usr/local and also error when copy folder onto symlink + # - we have used cp for years, so keep using it for backwards compatibility (instead of say rsync) + # - we allow preserving npm + # - we want to be somewhat robust to changes in tarball contents, so use find instead of hard-code expected subfolders + # + # This code was purist and concise for a long time. + # Now twice as much code, but using same code path for all uses, and supporting more setups. + + # Copy lib before bin so symlink targets exist. + # lib + mkdir -p "$N_PREFIX/lib" + # Copy everything except node_modules. + find "$dir/lib" -mindepth 1 -maxdepth 1 \! -name node_modules -exec cp -fR "{}" "$N_PREFIX/lib" \; + if [[ -z "${N_PRESERVE_NPM}" ]]; then + mkdir -p "$N_PREFIX/lib/node_modules" + # Copy just npm, skipping possible added global modules after download. Clean copy to avoid version change problems. + clean_copy_folder "$dir/lib/node_modules/npm" "$N_PREFIX/lib/node_modules/npm" + fi + # Takes same steps for corepack (experimental in node 16.9.0) as for npm, to avoid version problems. + if [[ -e "$dir/lib/node_modules/corepack" && -z "${N_PRESERVE_COREPACK}" ]]; then + mkdir -p "$N_PREFIX/lib/node_modules" + clean_copy_folder "$dir/lib/node_modules/corepack" "$N_PREFIX/lib/node_modules/corepack" + fi + + # bin + mkdir -p "$N_PREFIX/bin" + # Remove old node to avoid potential problems with firewall getting confused on Darwin by overwrite. + rm -f "$N_PREFIX/bin/node" + # Copy bin items by hand, in case user has installed global npm modules into cache. + cp -f "$dir/bin/node" "$N_PREFIX/bin" + [[ -e "$dir/bin/node-waf" ]] && cp -f "$dir/bin/node-waf" "$N_PREFIX/bin" # v0.8.x + if [[ -z "${N_PRESERVE_COREPACK}" ]]; then + [[ -e "$dir/bin/corepack" ]] && cp -fR "$dir/bin/corepack" "$N_PREFIX/bin" # from 16.9.0 + fi + if [[ -z "${N_PRESERVE_NPM}" ]]; then + [[ -e "$dir/bin/npm" ]] && cp -fR "$dir/bin/npm" "$N_PREFIX/bin" + [[ -e "$dir/bin/npx" ]] && cp -fR "$dir/bin/npx" "$N_PREFIX/bin" + fi + + # include + mkdir -p "$N_PREFIX/include" + find "$dir/include" -mindepth 1 -maxdepth 1 -exec cp -fR "{}" "$N_PREFIX/include" \; + + # share + mkdir -p "$N_PREFIX/share" + # Copy everything except man, at it is a symlink on some Linux (e.g. archlinux). + find "$dir/share" -mindepth 1 -maxdepth 1 \! -name man -exec cp -fR "{}" "$N_PREFIX/share" \; + mkdir -p "$N_PREFIX/share/man" + find "$dir/share/man" -mindepth 1 -maxdepth 1 -exec cp -fR "{}" "$N_PREFIX/share/man" \; + + disable_pax_mprotect "${installed_node}" + + local active_node="$(command -v node)" + if [[ -e "${active_node}" && -e "${installed_node}" && "${active_node}" != "${installed_node}" ]]; then + # Installed and active are different which might be a PATH problem. List both to give user some clues. + log "installed" "$("${installed_node}" --version) to ${installed_node}" + log "active" "$("${active_node}" --version) at ${active_node}" + else + local npm_version_str="" + local installed_npm="${N_PREFIX}/bin/npm" + local active_npm="$(command -v npm)" + if [[ -z "${N_PRESERVE_NPM}" && -e "${active_npm}" && -e "${installed_npm}" && "${active_npm}" = "${installed_npm}" ]]; then + npm_version_str=" (with npm $(npm --version))" + fi + + log "installed" "$("${installed_node}" --version)${npm_version_str}" + + # Extra tips for changed location. + if [[ -e "${active_node}" && -e "${original_node}" && "${active_node}" != "${original_node}" ]]; then + printf '\nNote: the node command changed location and the old location may be remembered in your current shell.\n' + log old "${original_node}" + log new "${active_node}" + printf 'If "node --version" shows the old version then start a new shell, or reset the location hash with:\nhash -r (for bash, zsh, ash, dash, and ksh)\nrehash (for csh and tcsh)\n' + fi + fi +} + +# +# Install +# + +install() { + [[ -z "$1" ]] && abort "version required" + local version + get_latest_resolved_version "$1" || return 2 + version="${g_target_node}" + [[ -n "${version}" ]] || abort "no version found for '$1'" + update_mirror_settings_for_version "$1" + update_xz_settings_for_version "${version}" + update_arch_settings_for_version "${version}" + + local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + + # Note: decompression flags ignored with default Darwin tar which autodetects. + if test "$N_USE_XZ" = "true"; then + local tarflag="-Jx" + else + local tarflag="-zx" + fi + + if test -d "$dir"; then + if [[ ! -e "$dir/n.lock" ]] ; then + if [[ "$DOWNLOAD" == "false" ]] ; then + activate "${g_mirror_folder_name}/${version}" + fi + exit + fi + fi + + log installing "${g_mirror_folder_name}-v$version" + + local url="$(tarball_url "$version")" + is_ok "${url}" || abort "download preflight failed for '$version' (${url})" + + log mkdir "$dir" + mkdir -p "$dir" || abort "sudo required (or change ownership, or define N_PREFIX)" + touch "$dir/n.lock" + + cd "${dir}" || abort "Failed to cd to ${dir}" + + log fetch "$url" + do_get "${url}" | tar "$tarflag" --strip-components=1 --no-same-owner -f - + pipe_results=( "${PIPESTATUS[@]}" ) + if [[ "${pipe_results[0]}" -ne 0 ]]; then + abort "failed to download archive for $version" + fi + if [[ "${pipe_results[1]}" -ne 0 ]]; then + abort "failed to extract archive for $version" + fi + [ "$GET_SHOWS_PROGRESS" = "true" ] && erase_line + rm -f "$dir/n.lock" + + disable_pax_mprotect bin/node + + if [[ "$DOWNLOAD" == "false" ]]; then + activate "${g_mirror_folder_name}/$version" + fi +} + +# +# Be more silent. +# + +set_quiet() { + SHOW_VERBOSE_LOG="false" + command -v curl > /dev/null && CURL_OPTIONS+=( "--silent" ) && GET_SHOWS_PROGRESS="false" +} + +# +# Synopsis: do_get [option...] url +# Call curl or wget with combination of global and passed options. +# + +function do_get() { + if command -v curl &> /dev/null; then + curl "${CURL_OPTIONS[@]}" "$@" + elif command -v wget &> /dev/null; then + wget "${WGET_OPTIONS[@]}" "$@" + else + abort "curl or wget command required" + fi +} + +# +# Synopsis: do_get_index [option...] url +# Call curl or wget with combination of global and passed options, +# with options tweaked to be more suitable for getting index. +# + +function do_get_index() { + if command -v curl &> /dev/null; then + # --silent to suppress progress et al + curl --silent --compressed "${CURL_OPTIONS[@]}" "$@" + elif command -v wget &> /dev/null; then + wget "${WGET_OPTIONS[@]}" "$@" + else + abort "curl or wget command required" + fi +} + +# +# Synopsis: remove_versions version ... +# + +function remove_versions() { + [[ -z "$1" ]] && abort "version(s) required" + while [[ $# -ne 0 ]]; do + local version + get_latest_resolved_version "$1" || break + version="${g_target_node}" + if [[ -n "${version}" ]]; then + update_mirror_settings_for_version "$1" + local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + if [[ -s "${dir}" ]]; then + rm -rf "${dir}" + else + echo "$1 (${version}) not in downloads cache" + fi + else + echo "No version found for '$1'" + fi + shift + done +} + +# +# Synopsis: prune_cache +# + +function prune_cache() { + set_active_node + + for folder_and_version in $(display_versions_paths); do + if [[ "${folder_and_version}" != "${g_active_node}" ]]; then + echo "${folder_and_version}" + rm -rf "${CACHE_DIR:?}/${folder_and_version}" + fi + done +} + +# +# Synopsis: find_cached_version version +# Finds cache directory for resolved version. +# Globals modified: +# - g_cached_version + +function find_cached_version() { + [[ -z "$1" ]] && abort "version required" + local version + get_latest_resolved_version "$1" || exit 1 + version="${g_target_node}" + [[ -n "${version}" ]] || abort "no version found for '$1'" + + update_mirror_settings_for_version "$1" + g_cached_version="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + if [[ ! -d "${g_cached_version}" && "${DOWNLOAD}" == "true" ]]; then + (install "${version}") + fi + [[ -d "${g_cached_version}" ]] || abort "'$1' (${version}) not in downloads cache" +} + + +# +# Synopsis: display_bin_path_for_version version +# + +function display_bin_path_for_version() { + find_cached_version "$1" + echo "${g_cached_version}/bin/node" +} + +# +# Synopsis: run_with_version version [args...] +# Run the given of node with [args ..] +# + +function run_with_version() { + find_cached_version "$1" + shift # remove version from parameters + exec "${g_cached_version}/bin/node" "$@" +} + +# +# Synopsis: exec_with_version command [args...] +# Modify the path to include and execute command. +# + +function exec_with_version() { + find_cached_version "$1" + shift # remove version from parameters + PATH="${g_cached_version}/bin:$PATH" exec "$@" +} + +# +# Synopsis: is_ok url +# Check the HEAD response of . +# + +function is_ok() { + # Note: both curl and wget can follow redirects, as present on some mirrors (e.g. https://npm.taobao.org/mirrors/node). + # The output is complicated with redirects, so keep it simple and use command status rather than parse output. + if command -v curl &> /dev/null; then + do_get --silent --head "$1" > /dev/null || return 1 + else + do_get --spider "$1" > /dev/null || return 1 + fi +} + +# +# Synopsis: can_use_xz +# Test system to see if xz decompression is supported by tar. +# + +function can_use_xz() { + # Be conservative and only enable if xz is likely to work. Unfortunately we can't directly query tar itself. + # For research, see https://github.com/shadowspawn/nvh/issues/8 + local uname_s="$(uname -s)" + if [[ "${uname_s}" = "Linux" ]] && command -v xz &> /dev/null ; then + # tar on linux is likely to support xz if it is available as a command + return 0 + elif [[ "${uname_s}" = "Darwin" ]]; then + local macos_version="$(sw_vers -productVersion)" + local macos_major_version="$(echo "${macos_version}" | cut -d '.' -f 1)" + local macos_minor_version="$(echo "${macos_version}" | cut -d '.' -f 2)" + if [[ "${macos_major_version}" -gt 10 || "${macos_minor_version}" -gt 8 ]]; then + # tar on recent Darwin has xz support built-in + return 0 + fi + fi + return 2 # not supported +} + +# +# Synopsis: display_tarball_platform +# + +function display_tarball_platform() { + # https://en.wikipedia.org/wiki/Uname + + local os="unexpected_os" + local uname_a="$(uname -a)" + case "${uname_a}" in + Linux*) os="linux" ;; + Darwin*) os="darwin" ;; + SunOS*) os="sunos" ;; + AIX*) os="aix" ;; + CYGWIN*) >&2 echo_red "Cygwin is not supported by n" ;; + MINGW*) >&2 echo_red "Git BASH (MSYS) is not supported by n" ;; + esac + + local arch="unexpected_arch" + local uname_m="$(uname -m)" + case "${uname_m}" in + x86_64) arch=x64 ;; + i386 | i686) arch="x86" ;; + aarch64) arch=arm64 ;; + armv8l) arch=arm64 ;; # armv8l probably supports arm64, and there is no specific armv8l build so give it a go + *) + # e.g. armv6l, armv7l, arm64 + arch="${uname_m}" + ;; + esac + # Override from command line, or version specific adjustment. + [ -n "$ARCH" ] && arch="$ARCH" + + echo "${os}-${arch}" +} + +# +# Synopsis: display_compatible_file_field +# display for current platform, as per field in index.tab, which is different than actual download +# + +function display_compatible_file_field { + local compatible_file_field="$(display_tarball_platform)" + if [[ -z "${ARCH}" && "${compatible_file_field}" = "darwin-arm64" ]]; then + # Look for arm64 for native but also x64 for older versions which can run in rosetta. + # (Downside is will get an install error if install version above 16 with x64 and not arm64.) + compatible_file_field="osx-arm64-tar|osx-x64-tar" + elif [[ "${compatible_file_field}" =~ darwin-(.*) ]]; then + compatible_file_field="osx-${BASH_REMATCH[1]}-tar" + fi + echo "${compatible_file_field}" +} + +# +# Synopsis: tarball_url version +# + +function tarball_url() { + local version="$1" + local ext=gz + [ "$N_USE_XZ" = "true" ] && ext="xz" + echo "${g_mirror_url}/v${version}/node-v${version}-$(display_tarball_platform).tar.${ext}" +} + +# +# Synopsis: get_file_node_version filename +# Sets g_target_node +# + +function get_file_node_version() { + g_target_node= + local filepath="$1" + verbose_log "found" "${filepath}" + # read returns a non-zero status but does still work if there is no line ending + local version + <"${filepath}" read -r version + # trim possible trailing \d from a Windows created file + version="${version%%[[:space:]]}" + verbose_log "read" "${version}" + g_target_node="${version}" +} + +# +# Synopsis: get_package_engine_version\ +# Sets g_target_node +# + +function get_package_engine_version() { + g_target_node= + local filepath="$1" + verbose_log "found" "${filepath}" + command -v node &> /dev/null || abort "an active version of node is required to read 'engines' from package.json" + local range + range="$(node -e "package = require('${filepath}'); if (package && package.engines && package.engines.node) console.log(package.engines.node)")" + verbose_log "read" "${range}" + [[ -n "${range}" ]] || return 2 + if [[ "*" == "${range}" ]]; then + verbose_log "target" "current" + g_target_node="current" + return + fi + + local version + if [[ "${range}" =~ ^([>~^=]|\>\=)?v?([0-9]+(\.[0-9]+){0,2})(.[xX*])?$ ]]; then + local operator="${BASH_REMATCH[1]}" + version="${BASH_REMATCH[2]}" + case "${operator}" in + '' | =) ;; + \> | \>=) version="current" ;; + \~) [[ "${version}" =~ ^([0-9]+\.[0-9]+)\.[0-9]+$ ]] && version="${BASH_REMATCH[1]}" ;; + ^) [[ "${version}" =~ ^([0-9]+) ]] && version="${BASH_REMATCH[1]}" ;; + esac + verbose_log "target" "${version}" + else + command -v npx &> /dev/null || abort "an active version of npx is required to use complex 'engine' ranges from package.json" + verbose_log "resolving" "${range}" + local version_per_line="$(n lsr --all)" + local versions_one_line=$(echo "${version_per_line}" | tr '\n' ' ') + # Using semver@7 so works with older versions of node. + # shellcheck disable=SC2086 + version=$(npm_config_yes=true npx --quiet semver@7 -r "${range}" ${versions_one_line} | tail -n 1) + fi + g_target_node="${version}" +} + +# +# Synopsis: get_nvmrc_version +# Sets g_target_node +# + +function get_nvmrc_version() { + g_target_node= + local filepath="$1" + verbose_log "found" "${filepath}" + local version + <"${filepath}" read -r version + verbose_log "read" "${version}" + # Translate from nvm aliases + case "${version}" in + lts/\*) version="lts" ;; + lts/*) version="${version:4}" ;; + node) version="current" ;; + *) ;; + esac + g_target_node="${version}" +} + +# +# Synopsis: get_engine_version [error-message] +# Sets g_target_node +# + +function get_engine_version() { + g_target_node= + local error_message="${1-package.json not found}" + local parent + parent="${PWD}" + while [[ -n "${parent}" ]]; do + if [[ -e "${parent}/package.json" ]]; then + get_package_engine_version "${parent}/package.json" + else + parent=${parent%/*} + continue + fi + break + done + [[ -n "${parent}" ]] || abort "${error_message}" + [[ -n "${g_target_node}" ]] || abort "did not find supported version of node in 'engines' field of package.json" +} + +# +# Synopsis: get_auto_version +# Sets g_target_node +# + +function get_auto_version() { + g_target_node= + # Search for a version control file first + local parent + parent="${PWD}" + while [[ -n "${parent}" ]]; do + if [[ -e "${parent}/.n-node-version" ]]; then + get_file_node_version "${parent}/.n-node-version" + elif [[ -e "${parent}/.node-version" ]]; then + get_file_node_version "${parent}/.node-version" + elif [[ -e "${parent}/.nvmrc" ]]; then + get_nvmrc_version "${parent}/.nvmrc" + else + parent=${parent%/*} + continue + fi + break + done + # Fallback to package.json + [[ -n "${parent}" ]] || get_engine_version "no file found for auto version (.n-node-version, .node-version, .nvmrc, or package.json)" + [[ -n "${g_target_node}" ]] || abort "file found for auto did not contain target version of node" +} + +# +# Synopsis: get_latest_resolved_version version +# Sets g_target_node +# + +function get_latest_resolved_version() { + g_target_node= + local version=${1} + simple_version=${version#node/} # Only place supporting node/ [sic] + if is_exact_numeric_version "${simple_version}"; then + # Just numbers, already resolved, no need to lookup first. + simple_version="${simple_version#v}" + g_target_node="${simple_version}" + else + # Complicated recognising exact version, KISS and lookup. + g_target_node=$(N_MAX_REMOTE_MATCHES=1 display_remote_versions "$version") + fi +} + +# +# Synopsis: display_remote_index +# index.tab reference: https://github.com/nodejs/nodejs-dist-indexer +# Index fields are: version date files npm v8 uv zlib openssl modules lts security +# KISS and just return fields we currently care about: version files lts +# + +display_remote_index() { + local index_url="${g_mirror_url}/index.tab" + # tail to remove header line + do_get_index "${index_url}" | tail -n +2 | cut -f 1,3,10 + if [[ "${PIPESTATUS[0]}" -ne 0 ]]; then + # Reminder: abort will only exit subshell, but consistent error display + abort "failed to download version index (${index_url})" + fi +} + +# +# Synopsis: display_match_limit limit +# + +function display_match_limit(){ + if [[ "$1" -gt 1 && "$1" -lt 32000 ]]; then + echo "Listing remote... Displaying $1 matches (use --all to see all)." + fi +} + +# +# Synopsis: display_remote_versions version +# + +function display_remote_versions() { + local version="$1" + update_mirror_settings_for_version "${version}" + local match='.' + local match_count="${N_MAX_REMOTE_MATCHES}" + + # Transform some labels before processing further. + if is_node_support_version "${version}"; then + version="$(display_latest_node_support_alias "${version}")" + match_count=1 + elif [[ "${version}" = "auto" ]]; then + # suppress stdout logging so lsr layout same as usual for scripting + get_auto_version || return 2 + version="${g_target_node}" + elif [[ "${version}" = "engine" ]]; then + # suppress stdout logging so lsr layout same as usual for scripting + get_engine_version || return 2 + version="${g_target_node}" + fi + + if [[ -z "${version}" ]]; then + match='.' + elif [[ "${version}" = "lts" || "${version}" = "stable" ]]; then + match_count=1 + # Codename is last field, first one with a name is newest lts + match="${TAB_CHAR}[a-zA-Z]+\$" + elif [[ "${version}" = "latest" || "${version}" = "current" ]]; then + match_count=1 + match='.' + elif is_numeric_version "${version}"; then + version="v${version#v}" + # Avoid restriction message if exact version + is_exact_numeric_version "${version}" && match_count=1 + # Quote any dots in version so they are literal for expression + match="${version//\./\.}" + # Avoid 1.2 matching 1.23 + match="^${match}[^0-9]" + elif is_lts_codename "${version}"; then + # Capitalise (could alternatively make grep case insensitive) + codename="$(echo "${version:0:1}" | tr '[:lower:]' '[:upper:]')${version:1}" + # Codename is last field + match="${TAB_CHAR}${codename}\$" + elif is_download_folder "${version}"; then + match='.' + elif is_download_version "${version}"; then + version="${version#"${g_mirror_folder_name}"/}" + if [[ "${version}" = "latest" || "${version}" = "current" ]]; then + match_count=1 + match='.' + else + version="v${version#v}" + match="${version//\./\.}" + match="^${match}" # prefix + if is_numeric_version "${version}"; then + # Exact numeric match + match="${match}[^0-9]" + fi + fi + else + abort "invalid version '$1'" + fi + display_match_limit "${match_count}" + + # Implementation notes: + # - using awk rather than head so do not close pipe early on curl + # - restrict search to compatible files as not always available, or not at same time + # - return status of curl command (i.e. PIPESTATUS[0]) + display_remote_index \ + | n_grep -E "$(display_compatible_file_field)" \ + | n_grep -E "${match}" \ + | awk "NR<=${match_count}" \ + | cut -f 1 \ + | n_grep -E -o '[^v].*' + return "${PIPESTATUS[0]}" +} + +# +# Synopsis: delete_with_echo target +# + +function delete_with_echo() { + if [[ -e "$1" ]]; then + echo "$1" + rm -rf "$1" + fi +} + +# +# Synopsis: uninstall_installed +# Uninstall the installed node and npm (leaving alone the cache), +# so undo install, and may expose possible system installed versions. +# + +uninstall_installed() { + # npm: https://docs.npmjs.com/misc/removing-npm + # rm -rf /usr/local/{lib/node{,/.npm,_modules},bin,share/man}/npm* + # node: https://stackabuse.com/how-to-uninstall-node-js-from-mac-osx/ + # Doing it by hand rather than scanning cache, so still works if cache deleted first. + # This covers tarballs for at least node 4 through 10. + + while true; do + read -r -p "Do you wish to delete node and npm from ${N_PREFIX}? " yn + case $yn in + [Yy]* ) break ;; + [Nn]* ) exit ;; + * ) echo "Please answer yes or no.";; + esac + done + + echo "" + echo "Uninstalling node and npm" + delete_with_echo "${N_PREFIX}/bin/node" + delete_with_echo "${N_PREFIX}/bin/npm" + delete_with_echo "${N_PREFIX}/bin/npx" + delete_with_echo "${N_PREFIX}/bin/corepack" + delete_with_echo "${N_PREFIX}/include/node" + delete_with_echo "${N_PREFIX}/lib/dtrace/node.d" + delete_with_echo "${N_PREFIX}/lib/node_modules/npm" + delete_with_echo "${N_PREFIX}/lib/node_modules/corepack" + delete_with_echo "${N_PREFIX}/share/doc/node" + delete_with_echo "${N_PREFIX}/share/man/man1/node.1" + delete_with_echo "${N_PREFIX}/share/systemtap/tapset/node.stp" +} + +# +# Synopsis: show_permission_suggestions +# + +function show_permission_suggestions() { + echo "Suggestions:" + echo "- run n with sudo, or" + echo "- define N_PREFIX to a writeable location, or" +} + +# +# Synopsis: show_diagnostics +# Show environment and check for common problems. +# + +function show_diagnostics() { + echo "This information is to help you diagnose issues, and useful when reporting an issue." + echo "Note: some output may contain passwords. Redact before sharing." + + printf "\n\nCOMMAND LOCATIONS AND VERSIONS\n" + + printf "\nbash\n" + command -v bash && bash --version + + printf "\nn\n" + command -v n && n --version + + printf "\nnode\n" + if command -v node &> /dev/null; then + command -v node && node --version + node -e 'if (process.versions.v8) console.log("JavaScript engine: v8");' + + printf "\nnpm\n" + command -v npm && npm --version + fi + + printf "\ntar\n" + if command -v tar &> /dev/null; then + command -v tar && tar --version + else + echo_red "tar not found. Needed for extracting downloads." + fi + + printf "\ncurl or wget\n" + if command -v curl &> /dev/null; then + command -v curl && curl --version + elif command -v wget &> /dev/null; then + command -v wget && wget --version + else + echo_red "Neither curl nor wget found. Need one of them for downloads." + fi + + printf "\nuname\n" + uname -a + + printf "\n\nSETTINGS\n" + + printf "\nn\n" + echo "node mirror: ${N_NODE_MIRROR}" + echo "node downloads mirror: ${N_NODE_DOWNLOAD_MIRROR}" + echo "install destination: ${N_PREFIX}" + [[ -n "${N_PREFIX}" ]] && echo "PATH: ${PATH}" + echo "ls-remote max matches: ${N_MAX_REMOTE_MATCHES}" + [[ -n "${N_PRESERVE_NPM}" ]] && echo "installs preserve npm by default" + [[ -n "${N_PRESERVE_COREPACK}" ]] && echo "installs preserve corepack by default" + + printf "\nProxy\n" + # disable "var is referenced but not assigned": https://github.com/koalaman/shellcheck/wiki/SC2154 + # shellcheck disable=SC2154 + [[ -n "${http_proxy}" ]] && echo "http_proxy: ${http_proxy}" + # shellcheck disable=SC2154 + [[ -n "${https_proxy}" ]] && echo "https_proxy: ${https_proxy}" + if command -v curl &> /dev/null; then + # curl supports lower case and upper case! + # shellcheck disable=SC2154 + [[ -n "${all_proxy}" ]] && echo "all_proxy: ${all_proxy}" + [[ -n "${ALL_PROXY}" ]] && echo "ALL_PROXY: ${ALL_PROXY}" + [[ -n "${HTTP_PROXY}" ]] && echo "HTTP_PROXY: ${HTTP_PROXY}" + [[ -n "${HTTPS_PROXY}" ]] && echo "HTTPS_PROXY: ${HTTPS_PROXY}" + if [[ -e "${CURL_HOME}/.curlrc" ]]; then + echo "have \$CURL_HOME/.curlrc" + elif [[ -e "${HOME}/.curlrc" ]]; then + echo "have \$HOME/.curlrc" + fi + elif command -v wget &> /dev/null; then + if [[ -e "${WGETRC}" ]]; then + echo "have \$WGETRC" + elif [[ -e "${HOME}/.wgetrc" ]]; then + echo "have \$HOME/.wgetrc" + fi + fi + + printf "\n\nCHECKS\n" + + printf "\nChecking n install destination is in PATH...\n" + local install_bin="${N_PREFIX}/bin" + local path_wth_guards=":${PATH}:" + if [[ "${path_wth_guards}" =~ :${install_bin}/?: ]]; then + printf "good\n" + else + echo_red "'${install_bin}' is not in PATH" + fi + if command -v node &> /dev/null; then + printf "\nChecking n install destination priority in PATH...\n" + local node_dir="$(dirname "$(command -v node)")" + + local index=0 + local path_entry + local path_entries + local install_bin_index=0 + local node_index=999 + IFS=':' read -ra path_entries <<< "${PATH}" + for path_entry in "${path_entries[@]}"; do + (( index++ )) + [[ "${path_entry}" =~ ^${node_dir}/?$ ]] && node_index="${index}" + [[ "${path_entry}" =~ ^${install_bin}/?$ ]] && install_bin_index="${index}" + done + if [[ "${node_index}" -lt "${install_bin_index}" ]]; then + echo_red "There is a version of node installed which will be found in PATH before the n installed version." + else + printf "good\n" + fi + fi + + # Check npm too. Simpler check than for PATH and node, more like the runtime logging for active/installed node. + if [[ -z "${N_PRESERVE_NPM}" ]]; then + printf "\nChecking npm install destination...\n" + local installed_npm="${N_PREFIX}/bin/npm" + local active_npm="$(command -v npm)" + if [[ -e "${active_npm}" && -e "${installed_npm}" && "${active_npm}" != "${installed_npm}" ]]; then + echo_red "There is an active version of npm shadowing the version installed by n. Check order of entries in PATH." + log "installed" "${installed_npm}" + log "active" "${active_npm}" + else + printf "good\n" + fi + fi + + printf "\nChecking permissions for cache folder...\n" + # Most likely problem is ownership rather than than permissions as such. + local cache_root="${N_PREFIX}/n" + if [[ -e "${N_PREFIX}" && ! -w "${N_PREFIX}" && ! -e "${cache_root}" ]]; then + echo_red "You do not have write permission to create: ${cache_root}" + show_permission_suggestions + echo "- make a folder you own:" + echo " sudo mkdir -p \"${cache_root}\"" + echo " sudo chown $(whoami) \"${cache_root}\"" + elif [[ -e "${cache_root}" && ! -w "${cache_root}" ]]; then + echo_red "You do not have write permission to: ${cache_root}" + show_permission_suggestions + echo "- change folder ownership to yourself:" + echo " sudo chown -R $(whoami) \"${cache_root}\"" + elif [[ ! -e "${cache_root}" ]]; then + echo "Cache folder does not exist: ${cache_root}" + echo "This is normal if you have not done an install yet, as cache is only created when needed." + elif [[ -e "${CACHE_DIR}" && ! -w "${CACHE_DIR}" ]]; then + echo_red "You do not have write permission to: ${CACHE_DIR}" + show_permission_suggestions + echo "- change folder ownership to yourself:" + echo " sudo chown -R $(whoami) \"${CACHE_DIR}\"" + else + echo "good" + fi + + if [[ -e "${N_PREFIX}" ]]; then + # Most likely problem is ownership rather than than permissions as such. + printf "\nChecking permissions for install folders...\n" + local install_writeable="true" + for subdir in bin lib include share; do + if [[ -e "${N_PREFIX}/${subdir}" && ! -w "${N_PREFIX}/${subdir}" ]]; then + install_writeable="false" + echo_red "You do not have write permission to: ${N_PREFIX}/${subdir}" + break + fi + done + if [[ "${install_writeable}" = "true" ]]; then + echo "good" + else + show_permission_suggestions + echo "- change folder ownerships to yourself:" + echo " (cd \"${N_PREFIX}\" && sudo chown -R $(whoami) bin lib include share)" + fi + fi + + printf "\nChecking mirror is reachable...\n" + if is_ok "${N_NODE_MIRROR}/"; then + printf "good\n" + else + echo_red "mirror not reachable" + printf "Showing failing command and output\n" + if command -v curl &> /dev/null; then + ( set -x; do_get --head "${N_NODE_MIRROR}/" ) + else + ( set -x; do_get --spider "${N_NODE_MIRROR}/" ) + printf "\n" + fi + fi +} + +# +# Handle arguments. +# + +# First pass. Process the options so they can come before or after commands, +# particularly for `n lsr --all` and `n install --arch x686` +# which feel pretty natural. + +unprocessed_args=() +positional_arg="false" + +while [[ $# -ne 0 ]]; do + case "$1" in + --all) N_MAX_REMOTE_MATCHES=32000 ;; + -V|--version) display_n_version ;; + -h|--help|help) display_help; exit ;; + -q|--quiet) set_quiet ;; + -d|--download) DOWNLOAD="true" ;; + --insecure) set_insecure ;; + -p|--preserve) N_PRESERVE_NPM="true" N_PRESERVE_COREPACK="true" ;; + --no-preserve) N_PRESERVE_NPM="" N_PRESERVE_COREPACK="" ;; + --use-xz) N_USE_XZ="true" ;; + --no-use-xz) N_USE_XZ="false" ;; + --latest) display_remote_versions latest; exit ;; + --stable) display_remote_versions lts; exit ;; # [sic] old terminology + --lts) display_remote_versions lts; exit ;; + -a|--arch) shift; set_arch "$1";; # set arch and continue + exec|run|as|use) + unprocessed_args+=( "$1" ) + positional_arg="true" + ;; + *) + if [[ "${positional_arg}" == "true" ]]; then + unprocessed_args+=( "$@" ) + break + fi + unprocessed_args+=( "$1" ) + ;; + esac + shift +done + +if [[ -z "${N_USE_XZ+defined}" ]]; then + N_USE_XZ="true" # Default to using xz + can_use_xz || N_USE_XZ="false" +fi + +set -- "${unprocessed_args[@]}" + +if test $# -eq 0; then + test -z "$(display_versions_paths)" && err_no_installed_print_help + menu_select_cache_versions +else + while test $# -ne 0; do + case "$1" in + bin|which) display_bin_path_for_version "$2"; exit ;; + run|as|use) shift; run_with_version "$@"; exit ;; + exec) shift; exec_with_version "$@"; exit ;; + doctor) show_diagnostics; exit ;; + rm|-) shift; remove_versions "$@"; exit ;; + prune) prune_cache; exit ;; + latest) install latest; exit ;; + stable) install stable; exit ;; + lts) install lts; exit ;; + ls|list) display_versions_paths; exit ;; + lsr|ls-remote|list-remote) shift; display_remote_versions "$1"; exit ;; + uninstall) uninstall_installed; exit ;; + i|install) shift; install "$1"; exit ;; + N_TEST_DISPLAY_LATEST_RESOLVED_VERSION) shift; get_latest_resolved_version "$1" > /dev/null || exit 2; echo "${g_target_node}"; exit ;; + *) install "$1"; exit ;; + esac + shift + done +fi diff --git a/hooks/backup/18-data_multimedia b/hooks/backup/18-data_multimedia index f80cff0b3..94162d517 100644 --- a/hooks/backup/18-data_multimedia +++ b/hooks/backup/18-data_multimedia @@ -9,7 +9,7 @@ source /usr/share/yunohost/helpers # Backup destination backup_dir="${1}/data/multimedia" -if [ -e "/home/yunohost.multimedia/.nobackup" ]; then +if [ ! -e "/home/yunohost.multimedia" ] || [ -e "/home/yunohost.multimedia/.nobackup" ]; then exit 0 fi diff --git a/hooks/backup/20-conf_ynh_settings b/hooks/backup/20-conf_ynh_settings index 76ab0aaca..0820978e7 100644 --- a/hooks/backup/20-conf_ynh_settings +++ b/hooks/backup/20-conf_ynh_settings @@ -13,6 +13,6 @@ backup_dir="${1}/conf/ynh" ynh_backup "/etc/yunohost/firewall.yml" "${backup_dir}/firewall.yml" ynh_backup "/etc/yunohost/current_host" "${backup_dir}/current_host" [ ! -d "/etc/yunohost/domains" ] || ynh_backup "/etc/yunohost/domains" "${backup_dir}/domains" -[ ! -e "/etc/yunohost/settings.json" ] || ynh_backup "/etc/yunohost/settings.json" "${backup_dir}/settings.json" +[ ! -e "/etc/yunohost/settings.yml" ] || ynh_backup "/etc/yunohost/settings.yml" "${backup_dir}/settings.yml" [ ! -d "/etc/yunohost/dyndns" ] || ynh_backup "/etc/yunohost/dyndns" "${backup_dir}/dyndns" [ ! -d "/etc/dkim" ] || ynh_backup "/etc/dkim" "${backup_dir}/dkim" diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index dec6536e6..7bd835f8f 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -42,7 +42,7 @@ do_init_regen() { # Backup folders mkdir -p /home/yunohost.backup/archives chmod 750 /home/yunohost.backup/archives - chown root:root /home/yunohost.backup/archives # This is later changed to admin:root once admin user exists + chown root:root /home/yunohost.backup/archives # This is later changed to root:admins once the admins group exists # Empty ssowat json persistent conf echo "{}" >'/etc/ssowat/conf.json.persistent' @@ -104,7 +104,7 @@ EOF # Cron job that upgrade the app list everyday cat >$pending_dir/etc/cron.daily/yunohost-fetch-apps-catalog < /dev/null) & +sleep \$((RANDOM%3600)); yunohost tools update apps > /dev/null EOF # Cron job that renew lets encrypt certificates if there's any that needs renewal @@ -132,12 +132,15 @@ EOF fi # Skip ntp if inside a container (inspired from the conf of systemd-timesyncd) - mkdir -p ${pending_dir}/etc/systemd/system/ntp.service.d/ - cat >${pending_dir}/etc/systemd/system/ntp.service.d/ynh-override.conf <${pending_dir}/etc/systemd/system/ntp.service.d/ynh-override.conf </dev/null) + chown root:root $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null | grep -vw mdns.yml) chmod 600 $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null) # Apps folder, custom hooks folder @@ -235,13 +248,19 @@ do_post_regen() { 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 - } - + if systemctl | grep -q 'ntp.service' + then + [[ ! "$regen_conf_files" =~ "ntp.service.d/ynh-override.conf" ]] || { + systemctl daemon-reload + systemctl restart ntp + } + fi + [[ ! "$regen_conf_files" =~ "nftables.service.d/ynh-override.conf" ]] || systemctl daemon-reload - [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || systemctl daemon-reload + [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || { + systemctl daemon-reload + systemctl restart systemd-logind + } [[ ! "$regen_conf_files" =~ "yunohost-firewall.service" ]] || systemctl daemon-reload [[ ! "$regen_conf_files" =~ "yunohost-api.service" ]] || systemctl daemon-reload [[ ! "$regen_conf_files" =~ "yunohost-portal-api.service" ]] || systemctl daemon-reload @@ -267,6 +286,11 @@ do_post_regen() { rm -f /etc/dpkg/origins/default ln -s /etc/dpkg/origins/yunohost /etc/dpkg/origins/default fi + + if test -e /etc/yunohost/installed && test -e /etc/profile.d/check_yunohost_is_installed.sh + then + rm /etc/profile.d/check_yunohost_is_installed.sh + fi } do_$1_regen ${@:2} diff --git a/hooks/conf_regen/03-ssh b/hooks/conf_regen/03-ssh index 9a7f5ce4d..d0351b4e5 100755 --- a/hooks/conf_regen/03-ssh +++ b/hooks/conf_regen/03-ssh @@ -14,15 +14,10 @@ do_pre_regen() { ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null || true) - # Support legacy setting (this setting might be disabled by a user during a migration) - if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then - ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null || true)" - fi - # Support different strategy for security configurations - export compatibility="$(yunohost settings get 'security.ssh.compatibility')" - export port="$(yunohost settings get 'security.ssh.port')" - export password_authentication="$(yunohost settings get 'security.ssh.password_authentication')" + export compatibility="$(yunohost settings get 'security.ssh.ssh_compatibility')" + export port="$(yunohost settings get 'security.ssh.ssh_port')" + export password_authentication="$(yunohost settings get 'security.ssh.ssh_password_authentication' | int_to_bool)" export ssh_keys export ipv6_enabled ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config" diff --git a/hooks/conf_regen/06-slapd b/hooks/conf_regen/06-slapd index 616b383ec..9ba61863b 100755 --- a/hooks/conf_regen/06-slapd +++ b/hooks/conf_regen/06-slapd @@ -58,14 +58,6 @@ EOF nscd -i passwd || true systemctl restart slapd - - # We don't use mkhomedir_helper because 'admin' may not be recognized - # when this script is ran in a chroot (e.g. ISO install) - # We also refer to admin as uid 1007 for the same reason - if [ ! -d /home/admin ]; then - cp -r /etc/skel /home/admin - chown -R 1007:1007 /home/admin - fi } _regenerate_slapd_conf() { @@ -131,6 +123,10 @@ do_post_regen() { chown -R openldap:openldap /etc/ldap/schema/ chown -R openldap:openldap /etc/ldap/slapd.d/ + # Fix weird scenarios where /etc/sudo-ldap.conf doesn't exists (yet is supposed to be + # created by the sudo-ldap package) : https://github.com/YunoHost/issues/issues/2091 + [ -e /etc/sudo-ldap.conf ] || ln -s /etc/ldap/ldap.conf /etc/sudo-ldap.conf + # If we changed the systemd ynh-override conf if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/slapd.service.d/ynh-override.conf$"; then systemctl daemon-reload @@ -139,7 +135,7 @@ do_post_regen() { fi # For some reason, old setups don't have the admins group defined... - if ! slapcat | grep -q 'cn=admins,ou=groups,dc=yunohost,dc=org'; then + if ! slapcat -H "ldap:///cn=admins,ou=groups,dc=yunohost,dc=org" | grep -q 'cn=admins,ou=groups,dc=yunohost,dc=org'; then slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org <<< \ "dn: cn=admins,ou=groups,dc=yunohost,dc=org cn: admins @@ -172,22 +168,6 @@ objectClass: top" echo "Reloading slapd" systemctl force-reload slapd - - # on slow hardware/vm this regen conf would exit before the admin user that - # is stored in ldap is available because ldap seems to slow to restart - # so we'll wait either until we are able to log as admin or until a timeout - # is reached - # we need to do this because the next hooks executed after this one during - # postinstall requires to run as admin thus breaking postinstall on slow - # hardware which mean yunohost can't be correctly installed on those hardware - # and this sucks - # wait a maximum time of 5 minutes - # yes, force-reload behave like a restart - number_of_wait=0 - while ! su admin -c '' && ((number_of_wait < 60)); do - sleep 5 - ((number_of_wait += 1)) - done } do_$1_regen ${@:2} diff --git a/hooks/conf_regen/10-apt b/hooks/conf_regen/10-apt index bdd6d399c..93ff053b8 100755 --- a/hooks/conf_regen/10-apt +++ b/hooks/conf_regen/10-apt @@ -2,6 +2,8 @@ set -e +readonly YNH_DEFAULT_PHP_VERSION=7.4 + do_pre_regen() { pending_dir=$1 @@ -68,7 +70,10 @@ do_post_regen() { fi # Make sure php7.4 is the default version when using php in cli - update-alternatives --set php /usr/bin/php7.4 + if test -e /usr/bin/php$YNH_DEFAULT_PHP_VERSION + then + update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION + fi } do_$1_regen ${@:2} diff --git a/hooks/conf_regen/12-metronome b/hooks/conf_regen/12-metronome index 220d18d58..b039ace31 100755 --- a/hooks/conf_regen/12-metronome +++ b/hooks/conf_regen/12-metronome @@ -26,8 +26,14 @@ do_pre_regen() { | sed "s/{{ main_domain }}/${main_domain}/g" \ >"${metronome_dir}/metronome.cfg.lua" - # add domain conf files + # Trick such that old conf files are flagged as to remove for domain in $YNH_DOMAINS; do + touch "${metronome_conf_dir}/${domain}.cfg.lua" + done + + # add domain conf files + domain_list="$(yunohost domain list --features xmpp --output-as json | jq -r ".domains[]")" + for domain in $domain_list; do cat domain.tpl.cfg.lua \ | sed "s/{{ domain }}/${domain}/g" \ >"${metronome_conf_dir}/${domain}.cfg.lua" @@ -68,8 +74,22 @@ do_post_regen() { chown -R metronome: /var/lib/metronome/ chown -R metronome: /etc/metronome/conf.d/ - [[ -z "$regen_conf_files" ]] \ - || systemctl restart metronome + if [[ -z "$(ls /etc/metronome/conf.d/*.cfg.lua 2>/dev/null)" ]] + then + if systemctl is-enabled metronome &>/dev/null + then + systemctl disable metronome --now 2>/dev/null + fi + else + if ! systemctl is-enabled metronome &>/dev/null + then + systemctl enable metronome --now 2>/dev/null + sleep 3 + fi + + [[ -z "$regen_conf_files" ]] \ + || systemctl restart metronome + fi } do_$1_regen ${@:2} diff --git a/hooks/conf_regen/15-nginx b/hooks/conf_regen/15-nginx index c1d943681..9eabcd8b7 100755 --- a/hooks/conf_regen/15-nginx +++ b/hooks/conf_regen/15-nginx @@ -56,8 +56,8 @@ do_pre_regen() { # install / update plain conf files cp plain/* "$nginx_conf_dir" # remove the panel overlay if this is specified in settings - panel_overlay=$(yunohost settings get 'ssowat.panel_overlay.enabled') - if [ "$panel_overlay" == "false" ] || [ "$panel_overlay" == "False" ]; then + panel_overlay=$(yunohost settings get 'misc.portal.ssowat_panel_overlay_enabled' | int_to_bool) + if [ "$panel_overlay" == "False" ]; then echo "#" >"${nginx_conf_dir}/yunohost_panel.conf.inc" fi @@ -65,14 +65,16 @@ do_pre_regen() { main_domain=$(cat /etc/yunohost/current_host) # Support different strategy for security configurations - export redirect_to_https="$(yunohost settings get 'security.nginx.redirect_to_https')" - export compatibility="$(yunohost settings get 'security.nginx.compatibility')" - export experimental="$(yunohost settings get 'security.experimental.enabled')" + export redirect_to_https="$(yunohost settings get 'security.nginx.nginx_redirect_to_https' | int_to_bool)" + export compatibility="$(yunohost settings get 'security.nginx.nginx_compatibility')" + export experimental="$(yunohost settings get 'security.experimental.security_experimental_enabled' | int_to_bool)" ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc" cert_status=$(yunohost domain cert status --json) # add domain conf files + xmpp_domain_list="$(yunohost domain list --features xmpp --output-as json | jq -r ".domains[]")" + mail_domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]")" for domain in $YNH_DOMAINS; do domain_conf_dir="${nginx_conf_dir}/${domain}.d" mkdir -p "$domain_conf_dir" @@ -84,17 +86,32 @@ do_pre_regen() { export domain_cert_ca=$(echo $cert_status \ | jq ".certificates.\"$domain\".CA_type" \ | tr -d '"') + if echo "$xmpp_domain_list" | grep -q "^$domain$" + then + export xmpp_enabled="True" + else + export xmpp_enabled="False" + fi + if echo "$mail_domain_list" | grep -q "^$domain$" + then + export mail_enabled="True" + else + export mail_enabled="False" + fi ynh_render_template "server.tpl.conf" "${nginx_conf_dir}/${domain}.conf" - ynh_render_template "autoconfig.tpl.xml" "${mail_autoconfig_dir}/config-v1.1.xml" + if [ $mail_enabled == "True" ] + then + ynh_render_template "autoconfig.tpl.xml" "${mail_autoconfig_dir}/config-v1.1.xml" + fi touch "${domain_conf_dir}/yunohost_local.conf" # Clean legacy conf files done - export webadmin_allowlist_enabled=$(yunohost settings get security.webadmin.allowlist.enabled) + export webadmin_allowlist_enabled=$(yunohost settings get security.webadmin.webadmin_allowlist_enabled | int_to_bool) if [ "$webadmin_allowlist_enabled" == "True" ]; then - export webadmin_allowlist=$(yunohost settings get security.webadmin.allowlist) + export webadmin_allowlist=$(yunohost settings get security.webadmin.webadmin_allowlist) fi ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc" ynh_render_template "yunohost_api.conf.inc" "${nginx_conf_dir}/yunohost_api.conf.inc" @@ -127,6 +144,12 @@ do_pre_regen() { do_post_regen() { regen_conf_files=$1 + if ls -l /etc/nginx/conf.d/*.d/*.conf + then + chown root:root /etc/nginx/conf.d/*.d/*.conf + chmod 644 /etc/nginx/conf.d/*.d/*.conf + fi + [ -z "$regen_conf_files" ] && exit 0 # create NGINX conf directories for domains diff --git a/hooks/conf_regen/19-postfix b/hooks/conf_regen/19-postfix index 177ea23e9..3a2aead5d 100755 --- a/hooks/conf_regen/19-postfix +++ b/hooks/conf_regen/19-postfix @@ -22,17 +22,19 @@ do_pre_regen() { main_domain=$(cat /etc/yunohost/current_host) # Support different strategy for security configurations - export compatibility="$(yunohost settings get 'security.postfix.compatibility')" + export compatibility="$(yunohost settings get 'security.postfix.postfix_compatibility')" # Add possibility to specify a relay # Could be useful with some isp with no 25 port open or more complex setup export relay_port="" export relay_user="" - export relay_host="$(yunohost settings get 'smtp.relay.host')" - if [ -n "${relay_host}" ]; then - relay_port="$(yunohost settings get 'smtp.relay.port')" - relay_user="$(yunohost settings get 'smtp.relay.user')" - relay_password="$(yunohost settings get 'smtp.relay.password')" + export relay_host="" + export relay_enabled="$(yunohost settings get 'email.smtp.smtp_relay_enabled' | int_to_bool)" + if [ "${relay_enabled}" == "True" ]; then + relay_host="$(yunohost settings get 'email.smtp.smtp_relay_host')" + relay_port="$(yunohost settings get 'email.smtp.smtp_relay_port')" + relay_user="$(yunohost settings get 'email.smtp.smtp_relay_user')" + relay_password="$(yunohost settings get 'email.smtp.smtp_relay_password')" # Avoid to display "Relay account paswword" to other users touch ${postfix_dir}/sasl_passwd @@ -44,17 +46,17 @@ do_pre_regen() { cat <<<"[${relay_host}]:${relay_port} ${relay_user}:${relay_password}" >${postfix_dir}/sasl_passwd fi export main_domain - export domain_list="$YNH_DOMAINS" + export domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')" ynh_render_template "main.cf" "${postfix_dir}/main.cf" ynh_render_template "sni" "${postfix_dir}/sni" cat postsrsd \ | sed "s/{{ main_domain }}/${main_domain}/g" \ - | sed "s/{{ domain_list }}/${YNH_DOMAINS}/g" \ + | sed "s/{{ domain_list }}/${domain_list}/g" \ >"${default_dir}/postsrsd" # adapt it for IPv4-only hosts - ipv6="$(yunohost settings get 'smtp.allow_ipv6')" + ipv6="$(yunohost settings get 'email.smtp.smtp_allow_ipv6' | int_to_bool)" if [ "$ipv6" == "False" ] || [ ! -f /proc/net/if_inet6 ]; then sed -i \ 's/ \[::ffff:127.0.0.0\]\/104 \[::1\]\/128//g' \ @@ -68,6 +70,8 @@ do_pre_regen() { do_post_regen() { regen_conf_files=$1 + chown postfix /etc/postfix + if [ -e /etc/postfix/sasl_passwd ]; then chmod 750 /etc/postfix/sasl_passwd* chown postfix:root /etc/postfix/sasl_passwd* diff --git a/hooks/conf_regen/25-dovecot b/hooks/conf_regen/25-dovecot index 37c73b6d8..49ff0c9ba 100755 --- a/hooks/conf_regen/25-dovecot +++ b/hooks/conf_regen/25-dovecot @@ -16,9 +16,9 @@ do_pre_regen() { cp dovecot-ldap.conf "${dovecot_dir}/dovecot-ldap.conf" cp dovecot.sieve "${dovecot_dir}/global_script/dovecot.sieve" - export pop3_enabled="$(yunohost settings get 'pop3.enabled')" + export pop3_enabled="$(yunohost settings get 'email.pop3.pop3_enabled' | int_to_bool)" export main_domain=$(cat /etc/yunohost/current_host) - export domain_list="$YNH_DOMAINS" + export domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')" ynh_render_template "dovecot.conf" "${dovecot_dir}/dovecot.conf" diff --git a/hooks/conf_regen/31-rspamd b/hooks/conf_regen/31-rspamd index 536aec7c2..6807ce0cd 100755 --- a/hooks/conf_regen/31-rspamd +++ b/hooks/conf_regen/31-rspamd @@ -26,7 +26,8 @@ do_post_regen() { chown _rspamd /etc/dkim # create DKIM key for domains - for domain in $YNH_DOMAINS; do + domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')" + for domain in $domain_list; do domain_key="/etc/dkim/${domain}.mail.key" [ ! -f "$domain_key" ] && { # We use a 1024 bit size because nsupdate doesn't seem to be able to diff --git a/hooks/conf_regen/35-postgresql b/hooks/conf_regen/35-postgresql index 0da0767cc..3a3843d69 100755 --- a/hooks/conf_regen/35-postgresql +++ b/hooks/conf_regen/35-postgresql @@ -20,7 +20,7 @@ do_pre_regen() { } do_post_regen() { - regen_conf_files=$1 + #regen_conf_files=$1 # Make sure postgresql is started and enabled # (N.B. : to check the active state, we check the cluster state because @@ -34,6 +34,8 @@ do_post_regen() { if [ ! -f "$PSQL_ROOT_PWD_FILE" ] || [ -z "$(cat $PSQL_ROOT_PWD_FILE)" ]; then ynh_string_random >$PSQL_ROOT_PWD_FILE fi + + [ ! -e $PSQL_ROOT_PWD_FILE ] || { chown root:postgres $PSQL_ROOT_PWD_FILE; chmod 440 $PSQL_ROOT_PWD_FILE; } sudo --login --user=postgres psql -c"ALTER user postgres WITH PASSWORD '$(cat $PSQL_ROOT_PWD_FILE)'" postgres @@ -47,20 +49,4 @@ do_post_regen() { ynh_systemd_action --service_name=postgresql --action=reload } -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/37-mdns b/hooks/conf_regen/37-mdns index 3a877970b..bd813e588 100755 --- a/hooks/conf_regen/37-mdns +++ b/hooks/conf_regen/37-mdns @@ -53,9 +53,6 @@ do_post_regen() { systemctl daemon-reload fi - systemctl disable avahi-daemon.socket --quiet --now 2>/dev/null || true - systemctl disable avahi-daemon --quiet --now 2>/dev/null || true - # Legacy stuff to enable the new yunomdns service on legacy systems if [[ -e /etc/avahi/avahi-daemon.conf ]] && grep -q 'yunohost' /etc/avahi/avahi-daemon.conf; then systemctl enable yunomdns --now --quiet diff --git a/hooks/conf_regen/43-dnsmasq b/hooks/conf_regen/43-dnsmasq index 9aca18031..90e3ed2d7 100755 --- a/hooks/conf_regen/43-dnsmasq +++ b/hooks/conf_regen/43-dnsmasq @@ -21,9 +21,9 @@ do_pre_regen() { cat plain/resolv.dnsmasq.conf | grep "^nameserver" | shuf >${pending_dir}/etc/resolv.dnsmasq.conf # retrieve variables - ipv4=$(curl -s -4 https://ip.yunohost.org 2>/dev/null || true) + ipv4=$(curl --max-time 10 -s -4 https://ip.yunohost.org 2>/dev/null || true) ynh_validate_ip4 "$ipv4" || ipv4='127.0.0.1' - ipv6=$(curl -s -6 https://ip6.yunohost.org 2>/dev/null || true) + ipv6=$(curl --max-time 10 -s -6 https://ip6.yunohost.org 2>/dev/null || true) ynh_validate_ip6 "$ipv6" || ipv6='' interfaces="$(ip -j addr show | jq -r '[.[].ifname]|join(" ")')" wireless_interfaces="lo" @@ -62,7 +62,8 @@ do_post_regen() { regen_conf_files=$1 # Force permission (to cover some edge cases where root's umask is like 027 and then dnsmasq cant read this file) - chown 644 /etc/resolv.dnsmasq.conf + chown root /etc/resolv.dnsmasq.conf + chmod 644 /etc/resolv.dnsmasq.conf # Fuck it, those domain/search entries from dhclient are usually annoying # lying shit from the ISP trying to MiTM diff --git a/hooks/conf_regen/52-fail2ban b/hooks/conf_regen/52-fail2ban index 8129e977d..db3cf0da7 100755 --- a/hooks/conf_regen/52-fail2ban +++ b/hooks/conf_regen/52-fail2ban @@ -14,15 +14,22 @@ do_pre_regen() { mkdir -p "${fail2ban_dir}/jail.d" cp yunohost.conf "${fail2ban_dir}/filter.d/yunohost.conf" + cp postfix-sasl.conf "${fail2ban_dir}/filter.d/postfix-sasl.conf" cp jail.conf "${fail2ban_dir}/jail.conf" - export ssh_port="$(yunohost settings get 'security.ssh.port')" + export ssh_port="$(yunohost settings get 'security.ssh.ssh_port')" ynh_render_template "yunohost-jails.conf" "${fail2ban_dir}/jail.d/yunohost-jails.conf" } do_post_regen() { regen_conf_files=$1 + if ls -l /etc/fail2ban/jail.d/*.conf + then + chown root:root /etc/fail2ban/jail.d/*.conf + chmod 644 /etc/fail2ban/jail.d/*.conf + fi + [[ -z "$regen_conf_files" ]] \ || systemctl reload fail2ban } diff --git a/hooks/restore/20-conf_ynh_settings b/hooks/restore/20-conf_ynh_settings index 2d731bd54..aba2b7a46 100644 --- a/hooks/restore/20-conf_ynh_settings +++ b/hooks/restore/20-conf_ynh_settings @@ -3,6 +3,6 @@ backup_dir="$1/conf/ynh" cp -a "${backup_dir}/current_host" /etc/yunohost/current_host cp -a "${backup_dir}/firewall.yml" /etc/yunohost/firewall.yml [ ! -d "${backup_dir}/domains" ] || cp -a "${backup_dir}/domains" /etc/yunohost/domains -[ ! -e "${backup_dir}/settings.json" ] || cp -a "${backup_dir}/settings.json" "/etc/yunohost/settings.json" +[ ! -e "${backup_dir}/settings.yml" ] || cp -a "${backup_dir}/settings.yml" "/etc/yunohost/settings.yml" [ ! -d "${backup_dir}/dyndns" ] || cp -raT "${backup_dir}/dyndns" "/etc/yunohost/dyndns" [ ! -d "${backup_dir}/dkim" ] || cp -raT "${backup_dir}/dkim" "/etc/dkim" diff --git a/locales/ar.json b/locales/ar.json index e37cdbcc7..2d3e1381e 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -1,51 +1,47 @@ { "action_invalid": "إجراء غير صالح '{action}'", "admin_password": "كلمة السر الإدارية", - "admin_password_change_failed": "لا يمكن تعديل الكلمة السرية", - "admin_password_changed": "عُدلت كلمة السر الإدارية", "app_already_installed": "{app} تم تنصيبه مِن قبل", "app_already_up_to_date": "{app} حديثٌ", "app_argument_required": "المُعامِل '{name}' مطلوب", "app_extraction_failed": "تعذر فك الضغط عن ملفات التنصيب", - "app_install_files_invalid": "ملفات التنصيب خاطئة", + "app_install_files_invalid": "لا يمكن تنصيب هذه الملفات", "app_not_correctly_installed": "يبدو أن التطبيق {app} لم يتم تنصيبه بشكل صحيح", "app_not_installed": "إنّ التطبيق {app} غير مُنصَّب", "app_not_properly_removed": "لم يتم حذف تطبيق {app} بشكلٍ جيّد", "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_failed": "تعذرت عملية تحديث {app}: {error}", "app_upgrade_some_app_failed": "تعذرت عملية ترقية بعض التطبيقات", "app_upgraded": "تم تحديث التطبيق {app}", - "ask_firstname": "الإسم", - "ask_lastname": "اللقب", "ask_main_domain": "النطاق الرئيسي", "ask_new_admin_password": "كلمة السر الإدارية الجديدة", "ask_password": "كلمة السر", - "backup_applying_method_copy": "جارٍ نسخ كافة الملفات إلى النسخة الإحتياطية …", + "backup_applying_method_copy": "جارٍ نسخ كافة الملفات المراد نسخها احتياطيا …", "backup_applying_method_tar": "جارٍ إنشاء ملف TAR للنسخة الاحتياطية…", - "backup_created": "تم إنشاء النسخة الإحتياطية", + "backup_created": "تم إنشاء النسخة الإحتياطية: {name}", "backup_method_copy_finished": "إنتهت عملية النسخ الإحتياطي", "backup_nothings_done": "ليس هناك أي شيء للحفظ", "backup_output_directory_required": "يتوجب عليك تحديد مجلد لتلقي النسخ الإحتياطية", - "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق {domain}", + "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق '{domain}'", "certmanager_cert_install_success_selfsigned": "نجحت عملية تثبيت الشهادة الموقعة ذاتيا الخاصة بالنطاق '{domain}'", "certmanager_cert_renew_success": "نجحت عملية تجديد شهادة Let's Encrypt الخاصة باسم النطاق '{domain}'", "certmanager_cert_signing_failed": "فشل إجراء توقيع الشهادة الجديدة", - "certmanager_no_cert_file": "تعذرت عملية قراءة شهادة نطاق {domain} (الملف : {file})", + "certmanager_no_cert_file": "تعذرت عملية قراءة ملف شهادة نطاق {domain} (الملف : {file})", "domain_created": "تم إنشاء النطاق", - "domain_creation_failed": "تعذرت عملية إنشاء النطاق", + "domain_creation_failed": "تعذرت عملية إنشاء النطاق {domain}: {error}", "domain_deleted": "تم حذف النطاق", "domain_exists": "اسم النطاق موجود سلفًا", "domains_available": "النطاقات المتوفرة :", "done": "تم", "downloading": "عملية التنزيل جارية …", "dyndns_ip_updated": "لقد تم تحديث عنوان الإيبي الخاص بك على نظام أسماء النطاقات الديناميكي", - "dyndns_key_generating": "عملية توليد مفتاح نظام أسماء النطاقات جارية. يمكن للعملية أن تستغرق بعضا من الوقت…", + "dyndns_key_generating": "جارٍ إنشاء مفتاح DNS ... قد يستغرق الأمر بعض الوقت.", "dyndns_key_not_found": "لم يتم العثور على مفتاح DNS الخاص باسم النطاق هذا", - "extracting": "عملية فك الضغط جارية …", + "extracting": "عملية فك الضغط جارية…", "installation_complete": "إكتملت عملية التنصيب", "main_domain_change_failed": "تعذّر تغيير النطاق الأساسي", "main_domain_changed": "تم تغيير النطاق الأساسي", @@ -53,17 +49,17 @@ "pattern_domain": "يتوجب أن يكون إسم نطاق صالح (مثل my-domain.org)", "pattern_email": "يتوجب أن يكون عنوان بريد إلكتروني صالح (مثل someone@domain.org)", "pattern_password": "يتوجب أن تكون مكونة من 3 حروف على الأقل", - "restore_extracting": "جارٍ فك الضغط عن الملفات التي نحتاجها من النسخة الاحتياطية…", + "restore_extracting": "جارٍ فك الضغط عن الملفات اللازمة من النسخة الاحتياطية…", "server_shutdown": "سوف ينطفئ الخادوم", "server_shutdown_confirm": "سوف ينطفئ الخادوم حالا. متأكد ؟ [{answers}]", "server_reboot": "سيعاد تشغيل الخادوم", "server_reboot_confirm": "سيعاد تشغيل الخادوم في الحين. هل أنت متأكد ؟ [{answers}]", "service_add_failed": "تعذرت إضافة خدمة '{service}'", "service_already_stopped": "إنّ خدمة '{service}' متوقفة مِن قبلُ", - "service_disabled": "لن يتم إطلاق خدمة '{service}' أثناء بداية تشغيل النظام.", - "service_enabled": "تم تنشيط خدمة '{service}'", + "service_disabled": "لن يتم إطلاق خدمة '{service}' أثناء بداية تشغيل النظام بتاتا.", + "service_enabled": "سيتم الآن بدء تشغيل الخدمة '{service}' تلقائيًا أثناء تمهيد النظام.", "service_removed": "تمت إزالة خدمة '{service}'", - "service_started": "تم إطلاق تشغيل خدمة '{service}'", + "service_started": "تم إطلاق تشغيل خدمة '{service}'", "service_stopped": "تمّ إيقاف خدمة '{service}'", "system_upgraded": "تمت عملية ترقية النظام", "unlimit": "دون تحديد الحصة", @@ -75,10 +71,10 @@ "user_deleted": "تم حذف المستخدم", "user_deletion_failed": "لا يمكن حذف المستخدم", "user_unknown": "المستخدم {user} مجهول", - "user_update_failed": "لا يمكن تحديث المستخدم", - "user_updated": "تم تحديث المستخدم", - "yunohost_installing": "عملية تنصيب يونوهوست جارية …", - "yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب أو هو مثبت حاليا بشكل خاطئ. قم بتنفيذ الأمر 'yunohost tools postinstall'", + "user_update_failed": "لا يمكن تحديث المستخدم {user}: {error}", + "user_updated": "تم تحديث معلومات المستخدم", + "yunohost_installing": "عملية تنصيب واي يونوهوست جارية …", + "yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب بشكل جيد. فضلًا قم بتنفيذ الأمر 'yunohost tools postinstall'", "migrations_list_conflict_pending_done": "لا يمكنك استخدام --previous و --done معًا على نفس سطر الأوامر.", "service_description_metronome": "يُدير حسابات الدردشة الفورية XMPP", "service_description_nginx": "يقوم بتوفير النفاذ و السماح بالوصول إلى كافة مواقع الويب المستضافة على خادومك", @@ -112,7 +108,6 @@ "service_description_rspamd": "يقوم بتصفية البريد المزعج و إدارة ميزات أخرى للبريد", "service_description_yunohost-firewall": "يُدير فتح وإغلاق منافذ الاتصال إلى الخدمات", "aborting": "إلغاء.", - "admin_password_too_long": "يرجى اختيار كلمة سرية أقصر مِن 127 حرف", "app_not_upgraded": "", "app_start_install": "جارٍ تثبيت {app}…", "app_start_remove": "جارٍ حذف {app}…", @@ -120,20 +115,17 @@ "app_upgrade_several_apps": "سوف يتم تحديث التطبيقات التالية: {apps}", "ask_new_domain": "نطاق جديد", "ask_new_path": "مسار جديد", - "global_settings_setting_security_password_admin_strength": "قوة الكلمة السرية الإدارية", - "global_settings_setting_security_password_user_strength": "قوة الكلمة السرية للمستخدم", "password_too_simple_1": "يجب أن يكون طول الكلمة السرية على الأقل 8 حروف", "already_up_to_date": "كل شيء على ما يرام. ليس هناك ما يتطلّب تحديثًا.", "service_description_slapd": "يخزّن المستخدمين والنطاقات والمعلومات المتعلقة بها", "service_reloaded": "تم إعادة تشغيل خدمة '{service}'", - "service_restarted": "تم إعادة تشغيل خدمة '{service}'", + "service_restarted": "تم إعادة تشغيل خدمة '{service}'", "group_unknown": "الفريق '{group}' مجهول", "group_deletion_failed": "فشلت عملية حذف الفريق '{group}': {error}", "group_deleted": "تم حذف الفريق '{group}'", "group_created": "تم إنشاء الفريق '{group}'", "dyndns_could_not_check_available": "لا يمكن التحقق مِن أنّ {domain} متوفر على {provider}.", "backup_mount_archive_for_restore": "جارٍ تهيئة النسخة الاحتياطية للاسترجاع…", - "root_password_replaced_by_admin_password": "لقد تم استبدال كلمة سر الجذر root بالكلمة الإدارية لـ admin.", "app_action_broke_system": "يبدو أنّ هذا الإجراء أدّى إلى تحطيم هذه الخدمات المهمة: {services}", "diagnosis_basesystem_host": "هذا الخادم يُشغّل ديبيان {debian_version}", "diagnosis_basesystem_kernel": "هذا الخادم يُشغّل نواة لينكس {kernel_version}", @@ -159,5 +151,111 @@ "diagnosis_description_dnsrecords": "تسجيلات خدمة DNS", "diagnosis_description_ip": "الإتصال بالإنترنت", "diagnosis_description_basesystem": "النظام الأساسي", - "field_invalid": "الحقل غير صحيح : '{}'" -} + "global_settings_setting_admin_strength": "قوة الكلمة السرية الإدارية", + "global_settings_setting_user_strength": "قوة الكلمة السرية للمستخدم", + "field_invalid": "الحقل غير صحيح : '{}'", + "diagnosis_ignored_issues": "(+ {nb_ignored} مشاكل تم تجاهلها)", + "domain_config_mail_in": "البريد الوارد", + "domain_config_mail_out": "البريد الخارج", + "domain_unknown": "النطاق '{domain}' مجهول", + "disk_space_not_sufficient_install": "ليس هناك مساحة كافية لتنصيب هذا التطبيق", + "diagnosis_unknown_categories": "الفئات التالية غير معروفة: {categories}", + "ask_fullname": "الاسم الكامل (اللقب والاسم)", + "diagnosis_ports_unreachable": "المنفذ {port} غير متاح الوصول إليه مِن الخارج.", + "domain_config_api_protocol": "بروتوكول API", + "domain_config_auth_application_secret": "المفتاح السري للتطبيق", + "domain_config_auth_consumer_key": "مفتاح المستخدِم", + "domain_config_auth_entrypoint": "نقطة الدخول API", + "domain_config_auth_key": "مفتاح التوثيق", + "domain_config_cert_issuer": "الهيئة الموثِّقة", + "domain_config_cert_renew": "تجديد شهادة Let's Encrypt", + "domain_config_cert_summary": "حالة الشهادة", + "domain_config_cert_summary_ok": "حسنًا، يبدو أنّ الشهادة الحالية جيدة!", + "domain_config_cert_validity": "مدة الصلاحية", + "domain_config_xmpp": "المراسَلة الفورية (XMPP)", + "global_settings_setting_root_password": "كلمة السر الجديدة لـ root", + "global_settings_setting_root_password_confirm": "كلمة السر الجديدة لـ root (تأكيد)", + "global_settings_setting_security_experimental_enabled": "ميزات أمان تجريبية", + "global_settings_setting_ssh_password_authentication": "الاستيثاق بكلمة سرية", + "global_settings_setting_ssh_port": "منفذ SSH", + "global_settings_setting_webadmin_allowlist": "قائمة عناوين الإيبي المسموح لها النفاذ إلى واجهة الويب الإدارية", + "ask_admin_username": "اسم المستخدِم للمدير", + "backup_archive_open_failed": "تعذر فتح النسخة الاحتياطية", + "diagnosis_mail_queue_unavailable_details": "خطأ: {error}", + "diagnosis_services_bad_status": "خدمة {service} {status} :(", + "diagnosis_services_running": "خدمة {service} شغّالة!", + "domain_config_cert_install": "تنصيب شهادة Let's Encrypt", + "domain_config_cert_summary_letsencrypt": "هنيئا! إنّك تستخدم الآن شهادة Let's Encrypt صالحة!", + "domain_config_default_app": "التطبيق الافتراضي", + "domain_dns_push_success": "تم تحديث ادخالات سِجِلات نظام أسماء النطاقات!", + "dyndns_unavailable": "النطاق '{domain}' غير متوفر.", + "global_settings_setting_pop3_enabled": "تفعيل POP3", + "diagnosis_ports_ok": "المنفذ {port} مفتوح ومتاح الوصول إليه مِن الخارج.", + "global_settings_setting_smtp_allow_ipv6": "سماح IPv6", + "disk_space_not_sufficient_update": "ليس هناك مساحة كافية لتحديث هذا التطبيق", + "domain_cert_gen_failed": "لا يمكن إعادة توليد الشهادة", + "diagnosis_apps_issue": "تم العثور على مشكلة في تطبيق {app}", + "tools_upgrade": "تحديث حُزم النظام", + "service_description_yunomdns": "يسمح لك بالوصول إلى خادمك الخاص باستخدام 'yunohost.local' في شبكتك المحلية", + "good_practices_about_user_password": "أنت الآن على وشك تحديد كلمة مرور مستخدم جديدة. يجب أن تتكون كلمة المرور من 8 أحرف على الأقل - أخذا بعين الإعتبار أنه من الممارسات الجيدة استخدام كلمة مرور أطول (أي عبارة مرور) و / أو مجموعة متنوعة من الأحرف (الأحرف الكبيرة والصغيرة والأرقام والأحرف الخاصة).", + "root_password_changed": "تم تغيير كلمة مرور الجذر", + "root_password_desynchronized": "تم تغيير كلمة مرور المدير ، لكن لم يتمكن YunoHost من نشرها على كلمة مرور الجذر!", + "user_import_bad_line": "سطر غير صحيح {line}: {details}", + "user_import_success": "تم استيراد المستخدمين بنجاح", + "visitors": "الزوار", + "password_too_simple_3": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل وأن تحتوي على أرقام و حروف كبيرة وصغيرة وأحرف خاصة", + "password_too_simple_4": "يجب أن تتكون كلمة المرور من 12 حرفًا على الأقل وأن تحتوي على أرقام وحروف كبيرة وصغيرة وأحرف خاصة", + "service_unknown": "الخدمة '{service}' غير معروفة", + "unbackup_app": "لن يتم حفظ التطبيق '{app}'", + "unrestore_app": "لن يتم استعادة التطبيق '{app}'", + "yunohost_already_installed": "إنّ YunoHost مُنصّب مِن قَبل", + "hook_name_unknown": "إسم الإجراء '{name}' غير معروف", + "app_manifest_install_ask_admin": "اختر مستخدمًا إداريًا لهذا التطبيق", + "domain_config_cert_summary_abouttoexpire": "مدة صلاحية الشهادة الحالية على وشك الإنتهاء ومِن المفتَرض أن يتم تجديدها تلقائيا قريبا.", + "app_manifest_install_ask_path": "اختر مسار URL (بعد النطاق) حيث ينبغي تنصيب هذا التطبيق", + "app_manifest_install_ask_domain": "اختر اسم النطاق الذي ينبغي فيه تنصيب هذا التطبيق", + "app_manifest_install_ask_is_public": "هل يجب أن يكون هذا التطبيق ظاهرًا للزوار المجهولين؟", + "domain_config_default_app_help": "سيعاد توجيه الناس تلقائيا إلى هذا التطبيق عند فتح اسم النطاق هذا. وإذا لم يُحدَّد أي تطبيق، يعاد توجيه الناس إلى استمارة تسجيل الدخولفي بوابة المستخدمين.", + "domain_config_xmpp_help": "ملاحطة: بعض ميزات الـ(إكس إم بي بي) ستتطلب أن تُحدّث سجلاتك الخاصة لـ DNS وتُعيد توليد شهادة Lets Encrypt لتفعيلها", + "certmanager_cert_install_failed": "أخفقت عملية تنصيب شهادة Let's Encrypt على {domains}", + "app_manifest_install_ask_password": "اختيار كلمة إدارية لهذا التطبيق", + "app_id_invalid": "مُعرّف التطبيق غير صالح", + "ask_admin_fullname": "الإسم الكامل للمدير", + "admins": "المدراء", + "all_users": "كافة مستخدمي واي يونوهوست", + "ask_user_domain": "اسم النطاق الذي سيُستخدَم لعنوان بريد المستخدِم وكذا لحساب XMPP", + "app_change_url_success": "تم تعديل الرابط التشعبي لتطبيق {app} إلى {domain}{path}", + "backup_app_failed": "لا يُمكن حِفظ {app}", + "pattern_password_app": "آسف، كلمات السر لا يمكن أن تحتوي على الحروف التالية: {forbidden_chars}", + "diagnosis_http_could_not_diagnose_details": "خطأ: {error}", + "mail_unavailable": "عنوان البريد الإلكتروني هذا مخصص لفريق المدراء", + "mailbox_disabled": "صندوق البريد معطل للمستخدم {user}", + "migration_0021_cleaning_up": "تنظيف ذاكرة التخزين المؤقت وكذا الحُزم التي تَعُد مفيدة…", + "migration_0021_yunohost_upgrade": "بداية تحديث نواة YunoHost…", + "migration_ldap_migration_failed_trying_to_rollback": "فشِلَت الهجرة… محاولة استعادة الرجوع إلى النظام.", + "migration_ldap_rollback_success": "تمت العودة إلى حالة النظام الأصلي.", + "migrations_success_forward": "اكتملت الهجرة {id}", + "password_too_simple_2": "يجب أن يكون طول كلمة المرور 8 حروف على الأقل وأن تحتوي على أرقام وحروف علوية ودنيا", + "pattern_lastname": "يجب أن يكون لقبًا صالحًا (على الأقل 3 حروف)", + "migration_0021_start": "بداية الهجرة إلى Bullseye", + "migrations_running_forward": "جارٍ تنفيذ الهجرة {id}…", + "password_confirmation_not_the_same": "كلمة المرور وتأكيدها غير متطابقان", + "password_too_long": "فضلا قم باختيار كلمة مرور طولها أقل مِن 127 حرفًا", + "pattern_fullname": "يجب أن يكون اسماً كاملاً صالحاً (على الأقل 3 حروف)", + "migration_0021_main_upgrade": "بداية التحديث الرئيسي…", + "migration_0021_patching_sources_list": "تحديث ملف sources.lists…", + "pattern_firstname": "يجب أن يكون اسماً أولياً صالحاً (على الأقل 3 حروف)", + "yunohost_configured": "تم إعداد YunoHost الآن", + "global_settings_setting_backup_compress_tar_archives": "ضغط النُسخ الاحتياطية", + "diagnosis_description_apps": "التطبيقات", + "danger": "خطر:", + "diagnosis_basesystem_hardware": "بنية الخادم هي {virt} {arch}", + "diagnosis_basesystem_hardware_model": "طراز الخادم {model}", + "diagnosis_mail_queue_ok": "هناك {nb_pending} رسائل بريد إلكتروني معلقة في قوائم انتظار البريد", + "diagnosis_mail_ehlo_ok": "يمكن الوصول إلى خادم بريد SMTP من الخارج وبالتالي فهو قادر على استقبال رسائل البريد الإلكتروني!", + "diagnosis_dns_good_conf": "تم إعداد سجلات نظام أسماء النطاقات DNS بشكل صحيح للنطاق {domain} (category {category})", + "diagnosis_ip_dnsresolution_working": "تحليل اسم النطاق يعمل!", + "diagnosis_mail_blacklist_listed_by": "إن عنوان الـ IP الخاص بك أو نطاقك {item} مُدرَج ضمن قائمة سوداء على {blacklist_name}", + "diagnosis_mail_outgoing_port_25_ok": "خادم بريد SMTP قادر على إرسال رسائل البريد الإلكتروني (منفذ البريد الصادر 25 غير محظور).", + "user_already_exists": "المستخدم '{user}' موجود مِن قَبل" +} \ No newline at end of file diff --git a/locales/ca.json b/locales/ca.json index b660032d2..821e5c3eb 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -1,8 +1,6 @@ { "action_invalid": "Acció '{action}' invàlida", "admin_password": "Contrasenya d'administració", - "admin_password_change_failed": "No es pot canviar la contrasenya", - "admin_password_changed": "S'ha canviat la contrasenya d'administració", "app_already_installed": "{app} ja està instal·lada", "app_already_installed_cant_change_url": "Aquesta aplicació ja està instal·lada. La URL no és pot canviar únicament amb aquesta funció. Mireu a `app changeurl` si està disponible.", "app_already_up_to_date": "{app} ja està actualitzada", @@ -22,7 +20,6 @@ "app_not_properly_removed": "{app} no s'ha pogut suprimir correctament", "app_removed": "{app} ha estat suprimida", "app_requirements_checking": "Verificació dels paquets requerits per {app}...", - "app_requirements_unmeet": "No es compleixen els requeriments per {app}, el paquet {pkgname} ({version}) ha de ser {spec}", "app_sources_fetch_failed": "No s'han pogut carregar els fitxers font, l'URL és correcta?", "app_unknown": "Aplicació desconeguda", "app_unsupported_remote_type": "El tipus remot utilitzat per l'aplicació no està suportat", @@ -30,8 +27,6 @@ "app_upgrade_failed": "No s'ha pogut actualitzar {app}: {error}", "app_upgrade_some_app_failed": "No s'han pogut actualitzar algunes aplicacions", "app_upgraded": "S'ha actualitzat {app}", - "ask_firstname": "Nom", - "ask_lastname": "Cognom", "ask_main_domain": "Domini principal", "ask_new_admin_password": "Nova contrasenya d'administrador", "ask_password": "Contrasenya", @@ -113,7 +108,6 @@ "confirm_app_install_danger": "PERILL! Aquesta aplicació encara és experimental (si no és que no funciona directament)! No hauríeu d'instal·lar-la a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema... Si accepteu el risc, escriviu «{answers}»", "confirm_app_install_thirdparty": "PERILL! Aquesta aplicació no es part del catàleg d'aplicacions de YunoHost. La instal·lació d'aplicacions de terceres parts pot comprometre la integritat i seguretat del seu sistema. No hauríeu d'instal·lar-ne a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema… Si accepteu el risc, escriviu «{answers}»", "custom_app_url_required": "Heu de especificar una URL per actualitzar la vostra aplicació personalitzada {app}", - "admin_password_too_long": "Trieu una contrasenya de menys de 127 caràcters", "dpkg_is_broken": "No es pot fer això en aquest instant perquè dpkg/APT (els gestors de paquets del sistema) sembla estar mal configurat... Podeu intentar solucionar-ho connectant-vos per SSH i executant «sudo apt install --fix-broken» i/o «sudo dpkg --configure -a».", "domain_cannot_remove_main": "No es pot eliminar «{domain}» ja que és el domini principal, primer s'ha d'establir un nou domini principal utilitzant «yunohost domain main-domain -n »; aquí hi ha una llista dels possibles dominis: {other_domains}", "domain_cert_gen_failed": "No s'ha pogut generar el certificat", @@ -142,26 +136,11 @@ "dyndns_domain_not_provided": "El proveïdor de DynDNS {provider} no pot oferir el domini {domain}.", "dyndns_unavailable": "El domini {domain} no està disponible.", "extracting": "Extracció en curs...", - "experimental_feature": "Atenció: Aquesta funcionalitat és experimental i no es considera estable, no s'ha d'utilitzar a excepció de saber el que esteu fent.", "field_invalid": "Camp incorrecte « {} »", "file_does_not_exist": "El camí {path} no existeix.", "firewall_reload_failed": "No s'ha pogut tornar a carregar el tallafocs", "firewall_reloaded": "S'ha tornat a carregar el tallafocs", "firewall_rules_cmd_failed": "Han fallat algunes comandes per aplicar regles del tallafocs. Més informació en el registre.", - "global_settings_bad_choice_for_enum": "Opció pel paràmetre {setting} incorrecta, s'ha rebut «{choice}», però les opcions disponibles són: {available_choices}", - "global_settings_bad_type_for_setting": "El tipus del paràmetre {setting} és incorrecte. S'ha rebut {received_type}, però s'esperava {expected_type}", - "global_settings_cant_open_settings": "No s'ha pogut obrir el fitxer de configuració, raó: {reason}", - "global_settings_cant_serialize_settings": "No s'ha pogut serialitzar les dades de configuració, raó: {reason}", - "global_settings_cant_write_settings": "No s'ha pogut escriure el fitxer de configuració, raó: {reason}", - "global_settings_key_doesnt_exists": "La clau « {settings_key} » no existeix en la configuració global, podeu veure totes les claus disponibles executant « yunohost settings list »", - "global_settings_reset_success": "S'ha fet una còpia de seguretat de la configuració anterior a {path}", - "global_settings_setting_security_nginx_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor web NGINX. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", - "global_settings_setting_security_password_admin_strength": "Robustesa de la contrasenya d'administrador", - "global_settings_setting_security_password_user_strength": "Robustesa de la contrasenya de l'usuari", - "global_settings_setting_security_ssh_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor SSH. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", - "global_settings_unknown_setting_from_settings_file": "Clau de configuració desconeguda: «{setting_key}», refusada i guardada a /etc/yunohost/settings-unknown.json", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permetre la clau d'hoste DSA (obsolet) per la configuració del servei SSH", - "global_settings_unknown_type": "Situació inesperada, la configuració {setting} sembla tenir el tipus {unknown_type} però no és un tipus reconegut pel sistema.", "good_practices_about_admin_password": "Esteu a punt de definir una nova contrasenya d'administrador. La contrasenya ha de tenir un mínim de 8 caràcters; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).", "hook_exec_failed": "No s'ha pogut executar el script: {path}", "hook_exec_not_terminated": "El script no s'ha acabat correctament: {path}", @@ -186,7 +165,6 @@ "log_available_on_yunopaste": "Aquest registre està disponible via {url}", "log_backup_restore_system": "Restaura el sistema a partir d'una còpia de seguretat", "log_backup_restore_app": "Restaura « {} » a partir d'una còpia de seguretat", - "log_remove_on_failed_restore": "Elimina « {} » després de que la restauració a partir de la còpia de seguretat hagi fallat", "log_remove_on_failed_install": "Elimina « {} » després de que la instal·lació hagi fallat", "log_domain_add": "Afegir el domini « {} » a la configuració del sistema", "log_domain_remove": "Elimina el domini « {} » de la configuració del sistema", @@ -207,7 +185,6 @@ "log_tools_reboot": "Reinicia el servidor", "already_up_to_date": "No hi ha res a fer. Tot està actualitzat.", "dpkg_lock_not_available": "No es pot utilitzar aquesta comanda en aquest moment ja que sembla que un altre programa està utilitzant el lock de dpkg (el gestor de paquets del sistema)", - "global_settings_setting_security_postfix_compatibility": "Solució de compromís entre compatibilitat i seguretat pel servidor Postfix. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", "mail_alias_remove_failed": "No s'han pogut eliminar els àlies del correu «{mail}»", "mail_domain_unknown": "El domini «{domain}» de l'adreça de correu no és vàlid. Utilitzeu un domini administrat per aquest servidor.", "mail_forward_remove_failed": "No s'han pogut eliminar el reenviament de correu «{mail}»", @@ -266,7 +243,6 @@ "restore_running_hooks": "Execució dels hooks de restauració...", "restore_system_part_failed": "No s'ha pogut restaurar la part «{part}» del sistema", "root_password_desynchronized": "S'ha canviat la contrasenya d'administració, però YunoHost no ha pogut propagar-ho cap a la contrasenya root!", - "root_password_replaced_by_admin_password": "La contrasenya root s'ha substituït per la contrasenya d'administració.", "server_shutdown": "S'aturarà el servidor", "server_shutdown_confirm": "S'aturarà el servidor immediatament, n'esteu segur? [{answers}]", "server_reboot": "Es reiniciarà el servidor", @@ -471,7 +447,6 @@ "diagnosis_services_running": "El servei {service} s'està executant!", "diagnosis_services_conf_broken": "La configuració pel servei {service} està trencada!", "diagnosis_ports_needed_by": "És necessari exposar aquest port per a les funcions {category} (servei {service})", - "global_settings_setting_pop3_enabled": "Activa el protocol POP3 per al servidor de correu", "log_app_action_run": "Executa l'acció de l'aplicació «{}»", "diagnosis_never_ran_yet": "Sembla que el servidor s'ha configurat recentment i encara no hi cap informe de diagnòstic per mostrar. S'ha d'executar un diagnòstic complet primer, ja sigui des de la pàgina web d'administració o utilitzant la comanda «yunohost diagnosis run» al terminal.", "diagnosis_description_web": "Web", @@ -506,7 +481,6 @@ "diagnosis_http_hairpinning_issue": "Sembla que la vostra xarxa no té el hairpinning activat.", "diagnosis_http_nginx_conf_not_up_to_date": "La configuració NGINX d'aquest domini sembla que ha estat modificada manualment, i no deixa que YunoHost diagnostiqui si és accessible amb HTTP.", "diagnosis_http_nginx_conf_not_up_to_date_details": "Per arreglar el problema, mireu les diferències amb la línia d'ordres utilitzant yunohost tools regen-conf nginx --dry-run --with-diff i si els canvis us semblen bé els podeu fer efectius utilitzant yunohost tools regen-conf nginx --force.", - "global_settings_setting_smtp_allow_ipv6": "Permet l'ús de IPv6 per rebre i enviar correus electrònics", "diagnosis_mail_ehlo_unreachable_details": "No s'ha pogut establir una connexió amb el vostre servidor en el port 25 amb IPv{ipversion}. Sembla que el servidor no és accessible.
1. La causa més comú per aquest problema és que el port 25 no està correctament redireccionat cap al vostre servidor.
2. També us hauríeu d'assegurar que el servei postfix estigui funcionant.
3. En configuracions més complexes: assegureu-vos que que no hi hagi cap tallafoc ni reverse-proxy interferint.", "diagnosis_mail_ehlo_wrong_details": "El EHLO rebut pel servidor de diagnòstic remot amb IPv{ipversion} és diferent al domini del vostre servidor.
EHLO rebut: {wrong_ehlo}
Esperat: {right_ehlo}
La causa més habitual d'aquest problema és que el port 25 no està correctament reenviat cap al vostre servidor. També podeu comprovar que no hi hagi un tallafocs o un reverse-proxy interferint.", "diagnosis_mail_fcrdns_dns_missing": "No hi ha cap DNS invers definit per IPv{ipversion}. Alguns correus electrònics poden no entregar-se o poden ser marcats com a correu brossa.", @@ -533,8 +507,6 @@ "app_packaging_format_not_supported": "No es pot instal·lar aquesta aplicació ja que el format del paquet no és compatible amb la versió de YunoHost del sistema. Hauríeu de considerar actualitzar el sistema.", "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost dyndns update --force.", "regenconf_need_to_explicitly_specify_ssh": "La configuració ssh ha estat modificada manualment, però heu d'especificar explícitament la categoria «ssh» amb --force per fer realment els canvis.", - "global_settings_setting_backup_compress_tar_archives": "Comprimir els arxius (.tar.gz) en lloc d'arxius no comprimits (.tar) al crear noves còpies de seguretat. N.B.: activar aquesta opció permet fer arxius de còpia de seguretat més lleugers, però el procés inicial de còpia de seguretat serà significativament més llarg i més exigent a nivell de CPU.", - "global_settings_setting_smtp_relay_host": "L'amfitrió de tramesa SMTP que s'ha d'utilitzar per enviar correus electrònics en lloc d'aquesta instància de YunoHost. És útil si esteu en una de les següents situacions: el port 25 està bloquejat per el vostre proveïdor d'accés a internet o proveïdor de servidor privat virtual, si teniu una IP residencial llistada a DUHL, si no podeu configurar el DNS invers o si el servidor no està directament exposat a internet i voleu utilitzar-ne un altre per enviar correus electrònics.", "unknown_main_domain_path": "Domini o ruta desconeguda per a «{app}». Heu d'especificar un domini i una ruta per a poder especificar una URL per al permís.", "show_tile_cant_be_enabled_for_regex": "No podeu activar «show_title» ara, perquè la URL per al permís «{permission}» és una expressió regular", "show_tile_cant_be_enabled_for_url_not_defined": "No podeu activar «show_title» ara, perquè primer s'ha de definir una URL per al permís «{permission}»", @@ -568,5 +540,13 @@ "diagnosis_sshd_config_inconsistent": "Sembla que el port SSH s'ha modificat manualment a /etc/ssh/sshd_config. Des de YunoHost 4.2, hi ha un nou paràmetre global «security.ssh.port» per evitar modificar manualment la configuració.", "diagnosis_sshd_config_insecure": "Sembla que la configuració SSH s'ha modificat manualment, i no es segura ha que no conté la directiva «AllowGroups» o «AllowUsers» per limitar l'accés a usuaris autoritzats.", "backup_create_size_estimation": "L'arxiu tindrà aproximadament {size} de dades.", - "app_restore_script_failed": "S'ha produït un error en el script de restauració de l'aplicació" + "app_restore_script_failed": "S'ha produït un error en el script de restauració de l'aplicació", + "global_settings_setting_backup_compress_tar_archives_help": "Comprimir els arxius (.tar.gz) en lloc d'arxius no comprimits (.tar) al crear noves còpies de seguretat. N.B.: activar aquesta opció permet fer arxius de còpia de seguretat més lleugers, però el procés inicial de còpia de seguretat serà significativament més llarg i més exigent a nivell de CPU.", + "global_settings_setting_nginx_compatibility_help": "Solució de compromís entre compatibilitat i seguretat pel servidor web NGINX. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", + "global_settings_setting_admin_strength": "Robustesa de la contrasenya d'administrador", + "global_settings_setting_user_strength": "Robustesa de la contrasenya de l'usuari", + "global_settings_setting_postfix_compatibility_help": "Solució de compromís entre compatibilitat i seguretat pel servidor Postfix. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", + "global_settings_setting_ssh_compatibility_help": "Solució de compromís entre compatibilitat i seguretat pel servidor SSH. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", + "global_settings_setting_smtp_allow_ipv6_help": "Permet l'ús de IPv6 per rebre i enviar correus electrònics", + "global_settings_setting_smtp_relay_enabled_help": "L'amfitrió de tramesa SMTP que s'ha d'utilitzar per enviar correus electrònics en lloc d'aquesta instància de YunoHost. És útil si esteu en una de les següents situacions: el port 25 està bloquejat per el vostre proveïdor d'accés a internet o proveïdor de servidor privat virtual, si teniu una IP residencial llistada a DUHL, si no podeu configurar el DNS invers o si el servidor no està directament exposat a internet i voleu utilitzar-ne un altre per enviar correus electrònics." } \ No newline at end of file diff --git a/locales/cs.json b/locales/cs.json index 47262064e..680d54743 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -2,9 +2,6 @@ "password_too_simple_1": "Heslo musí být aspoň 8 znaků dlouhé", "app_already_installed": "{app} je již nainstalován/a", "already_up_to_date": "Neprovedena žádná akce. Vše je již aktuální.", - "admin_password_too_long": "Zvolte prosím heslo kratší než 127 znaků", - "admin_password_changed": "Administrační heslo bylo změněno", - "admin_password_change_failed": "Nebylo možné změnit heslo", "admin_password": "Administrační heslo", "additional_urls_already_removed": "Další URL '{url}' již bylo odebráno u oprávnění '{permission}'", "additional_urls_already_added": "Další URL '{url}' již bylo přidáno pro oprávnění '{permission}'", @@ -49,19 +46,16 @@ "group_already_exist": "Skupina {group} již existuje", "good_practices_about_user_password": "Nyní zvolte nové heslo uživatele. Heslo by mělo být minimálně 8 znaků dlouhé, avšak je dobrou taktikou jej mít delší (např. použít více slov) a použít kombinaci znaků (velké, malé, čísla a speciální znaky).", "good_practices_about_admin_password": "Nyní zvolte nové administrační heslo. Heslo by mělo být minimálně 8 znaků dlouhé, avšak je dobrou taktikou jej mít delší (např. použít více slov) a použít kombinaci znaků (velké, malé, čísla a speciílní znaky).", - "global_settings_unknown_type": "Neočekávaná situace, nastavení {setting} deklaruje typ {unknown_type} ale toto není systémem podporováno.", - "global_settings_setting_backup_compress_tar_archives": "Komprimovat nové zálohy (.tar.gz) namísto nekomprimovaných (.tar). Poznámka: povolení této volby znamená objemově menší soubory záloh, avšak zálohování bude trvat déle a bude více zatěžovat CPU.", "global_settings_setting_smtp_relay_password": "SMTP relay heslo uživatele/hostitele", "global_settings_setting_smtp_relay_user": "SMTP relay uživatelské jméno/účet", "global_settings_setting_smtp_relay_port": "SMTP relay port", - "global_settings_setting_smtp_relay_host": "Použít SMTP relay hostitele pro odesílání emailů místo této YunoHost instance. Užitečné v různých situacích: port 25 je blokován vaším ISP nebo VPS poskytovatelem, IP adresa je na blacklistu (např. DUHL), nemůžete nastavit reverzní DNS záznam nebo tento server není přímo připojen do internetu a vy chcete použít jiný server k odesílání emailů.", - "global_settings_setting_smtp_allow_ipv6": "Povolit použití IPv6 pro příjem a odesílání emailů", "global_settings_setting_ssowat_panel_overlay_enabled": "Povolit SSOwat překryvný panel", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Povolit použití (zastaralého) DSA klíče hostitele pro konfiguraci SSH služby", - "global_settings_unknown_setting_from_settings_file": "Neznámý klíč v nastavení: '{setting_key}', zrušte jej a uložte v /etc/yunohost/settings-unknown.json", - "global_settings_setting_security_ssh_port": "SSH port", - "global_settings_setting_security_postfix_compatibility": "Kompromis mezi kompatibilitou a bezpečností Postfix serveru. Ovlivní šifry a další související bezpečnostní nastavení", - "global_settings_setting_security_ssh_compatibility": "Kompromis mezi kompatibilitou a bezpečností SSH serveru. Ovlivní šifry a další související bezpečnostní nastavení", - "global_settings_setting_security_password_user_strength": "Síla uživatelského hesla", - "global_settings_setting_security_password_admin_strength": "Síla administračního hesla" + "global_settings_setting_backup_compress_tar_archives_help": "Komprimovat nové zálohy (.tar.gz) namísto nekomprimovaných (.tar). Poznámka: povolení této volby znamená objemově menší soubory záloh, avšak zálohování bude trvat déle a bude více zatěžovat CPU.", + "global_settings_setting_admin_strength": "Síla administračního hesla", + "global_settings_setting_user_strength": "Síla uživatelského hesla", + "global_settings_setting_postfix_compatibility_help": "Kompromis mezi kompatibilitou a bezpečností Postfix serveru. Ovlivní šifry a další související bezpečnostní nastavení", + "global_settings_setting_ssh_compatibility_help": "Kompromis mezi kompatibilitou a bezpečností SSH serveru. Ovlivní šifry a další související bezpečnostní nastavení", + "global_settings_setting_ssh_port": "SSH port", + "global_settings_setting_smtp_allow_ipv6_help": "Povolit použití IPv6 pro příjem a odesílání emailů", + "global_settings_setting_smtp_relay_enabled_help": "Použít SMTP relay hostitele pro odesílání emailů místo této YunoHost instance. Užitečné v různých situacích: port 25 je blokován vaším ISP nebo VPS poskytovatelem, IP adresa je na blacklistu (např. DUHL), nemůžete nastavit reverzní DNS záznam nebo tento server není přímo připojen do internetu a vy chcete použít jiný server k odesílání emailů." } \ No newline at end of file diff --git a/locales/de.json b/locales/de.json index 4aa75270b..b61d0a431 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,14 +1,12 @@ { "action_invalid": "Ungültige Aktion '{action}'", "admin_password": "Administrator-Passwort", - "admin_password_change_failed": "Ändern des Passworts nicht möglich", - "admin_password_changed": "Das Administrator-Kennwort wurde geändert", "app_already_installed": "{app} ist schon installiert", "app_argument_choice_invalid": "Wähle einen gültigen Wert für das Argument '{name}': '{value}' ist nicht unter den verfügbaren Auswahlmöglichkeiten ({choices})", "app_argument_invalid": "Wähle einen gültigen Wert für das Argument '{name}': {error}", "app_argument_required": "Argument '{name}' wird benötigt", "app_extraction_failed": "Installationsdateien konnten nicht entpackt werden", - "app_id_invalid": "Falsche App-ID", + "app_id_invalid": "Falsche Applikations-ID", "app_install_files_invalid": "Diese Dateien können nicht installiert werden", "app_not_installed": "{app} konnte nicht in der Liste installierter Apps gefunden werden: {all_apps}", "app_removed": "{app} wurde entfernt", @@ -16,8 +14,6 @@ "app_unknown": "Unbekannte App", "app_upgrade_failed": "{app} konnte nicht aktualisiert werden: {error}", "app_upgraded": "{app} aktualisiert", - "ask_firstname": "Vorname", - "ask_lastname": "Nachname", "ask_main_domain": "Hauptdomain", "ask_new_admin_password": "Neues Verwaltungskennwort", "ask_password": "Passwort", @@ -137,8 +133,7 @@ "not_enough_disk_space": "Nicht genügend freier Speicherplatz unter '{path}'", "backup_creation_failed": "Konnte Backup-Archiv nicht erstellen", "app_not_correctly_installed": "{app} scheint nicht korrekt installiert zu sein", - "app_requirements_checking": "Überprüfe notwendige Pakete für {app}...", - "app_requirements_unmeet": "Anforderungen für {app} werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein", + "app_requirements_checking": "Überprüfe Voraussetzungen für {app}...", "app_unsupported_remote_type": "Für die App wurde ein nicht unterstützer Steuerungstyp verwendet", "backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path})", "domains_available": "Verfügbare Domains:", @@ -150,8 +145,8 @@ "certmanager_certificate_fetching_or_enabling_failed": "Die Aktivierung des neuen Zertifikats für die {domain} ist fehlgeschlagen...", "certmanager_attempt_to_renew_nonLE_cert": "Das Zertifikat der Domain '{domain}' wurde nicht von Let's Encrypt ausgestellt. Es kann nicht automatisch erneuert werden!", "certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain} läuft nicht in Kürze ab! (Benutze --force um diese Nachricht zu umgehen)", - "certmanager_domain_http_not_working": "Es scheint, als ob die Domäne {domain} nicht über HTTP erreicht werden kann. Bitte überprüfe, ob deine DNS- und nginx-Konfiguration in Ordnung ist. (Wenn du weißt, was du tust, nutze '--no-checks' um die Überprüfung zu überspringen.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "Der DNS-A-Eintrag der Domain {domain} unterscheidet sich von dieser Server-IP. Für weitere Informationen überprüfe bitte die 'DNS records' (basic) Kategorie in der Diagnose. Wenn du kürzlich deinen A-Eintrag verändert hast, warte bitte etwas, damit die Änderungen wirksam werden (Du kannst die DNS-Propagation mittels Website überprüfen) (Wenn du weißt, was du tust, kannst du '--no-checks' benutzen, um diese Überprüfung zu überspringen.)", + "certmanager_domain_http_not_working": "Es scheint, als ob die Domäne '{domain}' über HTTP nicht erreichbar ist. Bitte schauen Sie sich die 'Web'-Kategorie in der Diagnose an für weitere Informationen. (Wenn Sie wissen, was Sie tun, nutzen Sie '--no-checks' um die Überprüfung zu deaktivieren.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Die DNS-Einträge der Domäne '{domain}' unterscheiden sich von der IP dieses Servers. Für weitere Informationen überprüfen Sie bitte die Kategorie \"DNS-Einträge\" (basic) in der Diagnose. Wenn Sie kürzlich Ihren A-Eintrag verändert haben, warten Sie bitte ein wenig, bis die Änderungen wirksam werden (es gibt Online-Checks für die DNS-Propagation). (Wenn Sie wissen, was Sie tun, können Sie '--no-checks' verwenden, um diese Überprüfung zu überspringen.)", "certmanager_cannot_read_cert": "Es ist ein Fehler aufgetreten, als es versucht wurde das aktuelle Zertifikat für die Domain {domain} zu öffnen (Datei: {file}), Grund: {reason}", "certmanager_cert_install_success_selfsigned": "Das selbstsignierte Zertifikat für die Domäne '{domain}' wurde erfolgreich installiert", "certmanager_cert_install_success": "Let's-Encrypt-Zertifikat für die Domäne {domain} ist jetzt installiert", @@ -161,7 +156,7 @@ "certmanager_no_cert_file": "Die Zertifikatsdatei für die Domain {domain} (Datei: {file}) konnte nicht gelesen werden", "domain_cannot_remove_main": "Die Domäne '{domain}' konnten nicht entfernt werden, weil es die Haupt-Domäne ist. Du musst zuerst eine andere Domäne zur Haupt-Domäne machen. Dies ist über den Befehl 'yunohost domain main-domain -n ' möglich. Hier ist eine Liste möglicher Domänen: {other_domains}", "certmanager_self_ca_conf_file_not_found": "Die Konfigurationsdatei der Zertifizierungsstelle für selbstsignierte Zertifikate wurde nicht gefunden (Datei {file})", - "certmanager_acme_not_configured_for_domain": "Die ACME Challenge kann im Moment nicht für {domain} ausgeführt werden, weil in deiner nginx-Konfiguration das entsprechende Code-Snippet fehlt... Bitte stelle sicher, dass deine nginx-Konfiguration mit 'yunohost tools regen-conf nginx --dry-run --with-diff' auf dem neuesten Stand ist.", + "certmanager_acme_not_configured_for_domain": "Die ACME-Challenge für {domain} kann momentan nicht ausgeführt werden, weil in Ihrer nginx-Konfiguration das entsprechende Code-Snippet fehlt... Bitte stellen Sie sicher, dass Ihre nginx-Konfiguration mit 'yunohost tools regen-conf nginx --dry-run --with-diff' auf dem neuesten Stand ist.", "certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht aufgelöst werden (Datei: {file})", "domain_hostname_failed": "Neuer Hostname wurde nicht gesetzt. Das kann zukünftige Probleme verursachen (es kann auch sein, dass es funktioniert).", "app_already_installed_cant_change_url": "Diese Applikation ist bereits installiert. Die URL kann durch diese Funktion nicht modifiziert werden. Überprüfe ob `app changeurl` verfügbar ist.", @@ -176,17 +171,14 @@ "backup_archive_system_part_not_available": "Der System-Teil '{part}' ist in diesem Backup nicht enthalten", "backup_archive_writing_error": "Die Dateien '{source} (im Ordner '{dest}') konnten nicht in das komprimierte Archiv-Backup '{archive}' hinzugefügt werden", "app_change_url_success": "{app} URL ist nun {domain}{path}", - "global_settings_bad_type_for_setting": "Falscher Typ der Einstellung {setting}. Empfangen: {received_type}, aber erwarteter Typ: {expected_type}", - "global_settings_bad_choice_for_enum": "Wert des Einstellungsparameters {setting} ungültig. Du hast '{choice}' eingegeben. Aber nur folgende Werte sind gültig: {available_choices}", "file_does_not_exist": "Die Datei {path} existiert nicht.", - "experimental_feature": "Warnung: Der Maintainer hat diese Funktion als experimentell gekennzeichnet. Sie ist nicht stabil. Du solltest sie nur verwenden, wenn du weißt, was du tust.", "dyndns_domain_not_provided": "Der DynDNS-Anbieter {provider} kann die Domäne(n) {domain} nicht bereitstellen.", "dyndns_could_not_check_available": "Konnte nicht überprüfen, ob {domain} auf {provider} verfügbar ist.", "domain_dns_conf_is_just_a_recommendation": "Dieser Befehl zeigt dir die *empfohlene* Konfiguration. Er konfiguriert *nicht* das DNS für dich. Es liegt in deiner Verantwortung, die DNS-Zone bei deinem DNS-Registrar nach dieser Empfehlung zu konfigurieren.", "dpkg_lock_not_available": "Dieser Befehl kann momentan nicht ausgeführt werden, da anscheinend ein anderes Programm die Sperre von dpkg (dem Systempaket-Manager) verwendet", "confirm_app_install_thirdparty": "Warnung! Diese Applikation ist nicht Teil des App-Katalogs von YunoHost. Die Installation von Drittanbieter Applikationen kann die Integrität und Sicherheit Ihres Systems gefährden. Sie sollten sie NICHT installieren, wenn Sie nicht wissen, was Sie tun. Es wird KEIN SUPPORT geleistet, wenn diese Applikation nicht funktioniert oder Ihr System beschädigt! Wenn Sie dieses Risiko trotzdem eingehen wollen, geben Sie '{answers}' ein", "confirm_app_install_danger": "WARNUNG! Diese Applikation ist noch experimentell (wenn nicht sogar ausdrücklich nicht funktionsfähig)! Du solltest sie wahrscheinlich NICHT installieren, es sei denn, du weißt, was du tust. Es wird keine Unterstützung angeboten, falls diese Applikation nicht funktionieren oder dein System beschädigen sollte... Falls du bereit bist, dieses Risiko einzugehen, tippe '{answers}'", - "confirm_app_install_warning": "Warnung: Diese Applikation funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers}] ", + "confirm_app_install_warning": "Warnung: Diese Applikation funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single-Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers}] ", "backup_with_no_restore_script_for_app": "{app} hat kein Wiederherstellungsskript. Das Backup dieser App kann nicht automatisch wiederhergestellt werden.", "backup_with_no_backup_script_for_app": "Die App {app} hat kein Sicherungsskript. Ignoriere es.", "backup_unable_to_organize_files": "Dateien im Archiv konnten nicht mit der schnellen Methode organisiert werden", @@ -217,11 +209,9 @@ "aborting": "Breche ab.", "app_action_cannot_be_ran_because_required_services_down": "Diese erforderlichen Dienste sollten zur Durchführung dieser Aktion laufen: {services}. Versuche, sie neu zu starten, um fortzufahren (und möglicherweise zu untersuchen, warum sie nicht verfügbar sind).", "already_up_to_date": "Nichts zu tun. Alles ist bereits auf dem neusten Stand.", - "admin_password_too_long": "Bitte ein Passwort kürzer als 127 Zeichen wählen", "app_action_broke_system": "Diese Aktion scheint diese wichtigen Dienste unterbrochen zu haben: {services}", "apps_already_up_to_date": "Alle Apps sind bereits aktuell", "backup_copying_to_organize_the_archive": "Kopieren von {size} MB, um das Archiv zu organisieren", - "global_settings_setting_security_ssh_compatibility": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den SSH-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", "group_deleted": "Gruppe '{group}' gelöscht", "group_deletion_failed": "Konnte Gruppe '{group}' nicht löschen: {error}", "dyndns_provider_unreachable": "DynDNS-Anbieter {provider} kann nicht erreicht werden: Entweder ist dein YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.", @@ -232,33 +222,21 @@ "group_update_failed": "Kann Gruppe '{group}' nicht aktualisieren: {error}", "log_does_exists": "Es gibt kein Operationsprotokoll mit dem Namen'{log}', verwende 'yunohost log list', um alle verfügbaren Operationsprotokolle anzuzeigen", "log_operation_unit_unclosed_properly": "Die Operationseinheit wurde nicht richtig geschlossen", - "global_settings_setting_security_postfix_compatibility": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Postfix-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", - "global_settings_unknown_type": "Unerwartete Situation, die Einstellung {setting} scheint den Typ {unknown_type} zu haben, ist aber kein vom System unterstützter Typ.", "dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo apt install --fix-broken` sowie/oder `sudo dpkg --configure -a` ausführst.", - "global_settings_unknown_setting_from_settings_file": "Unbekannter Schlüssel in den Einstellungen: '{setting_key}', verwerfen und speichern in /etc/yunohost/settings-unknown.json", "log_link_to_log": "Vollständiges Log dieser Operation: '{desc}'", "log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwende den Befehl 'yunohost log show {name}'", - "global_settings_setting_security_nginx_compatibility": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Webserver NGINX. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Erlaubt die Verwendung eines (veralteten) DSA-Hostkeys für die SSH-Daemon-Konfiguration", "log_app_remove": "Entferne die Applikation '{}'", - "global_settings_cant_open_settings": "Einstellungsdatei konnte nicht geöffnet werden, Grund: {reason}", - "global_settings_cant_write_settings": "Einstellungsdatei konnte nicht gespeichert werden, Grund: {reason}", "log_app_install": "Installiere die Applikation '{}'", - "global_settings_reset_success": "Frühere Einstellungen werden nun auf {path} gesichert", "log_app_upgrade": "Upgrade der Applikation '{}'", "good_practices_about_admin_password": "Du bist nun dabei, ein neues Administratorpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - es ist jedoch empfehlenswert, ein längeres Passwort (z.B. eine Passphrase) und/oder verschiedene Arten von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", "log_corrupted_md_file": "Die mit Protokollen verknüpfte YAML-Metadatendatei ist beschädigt: '{md_file}\nFehler: {error}''", - "global_settings_cant_serialize_settings": "Einstellungsdaten konnten nicht serialisiert werden, Grund: {reason}", "log_help_to_get_failed_log": "Der Vorgang'{desc}' konnte nicht abgeschlossen werden. Bitte teile das vollständige Protokoll dieser Operation mit dem Befehl 'yunohost log share {name}', um Hilfe zu erhalten", "backup_no_uncompress_archive_dir": "Dieses unkomprimierte Archivverzeichnis gibt es nicht", "log_app_change_url": "Ändere die URL der Applikation '{}'", - "global_settings_setting_security_password_user_strength": "Stärke des Anmeldepassworts", "good_practices_about_user_password": "Du bist nun dabei, ein neues Nutzerpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - es ist jedoch empfehlenswert, ein längeres Passwort (z.B. eine Passphrase) und/oder verschiedene Arten von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", "log_link_to_failed_log": "Der Vorgang konnte nicht abgeschlossen werden '{desc}'. Bitte gib das vollständige Protokoll dieser Operation mit Klicken Sie hier an, um Hilfe zu erhalten", "backup_cant_mount_uncompress_archive": "Das unkomprimierte Archiv konnte nicht als schreibgeschützt gemountet werden", "backup_csv_addition_failed": "Es konnten keine Dateien zur Sicherung in die CSV-Datei hinzugefügt werden", - "global_settings_setting_security_password_admin_strength": "Stärke des Admin-Passworts", - "global_settings_key_doesnt_exists": "Der Schlüssel'{settings_key}' existiert nicht in den globalen Einstellungen, du kannst alle verfügbaren Schlüssel sehen, indem du 'yunohost settings list' ausführst", "log_app_makedefault": "Mache '{}' zur Standard-Applikation", "hook_json_return_error": "Konnte die Rückkehr vom Einsprungpunkt {path} nicht lesen. Fehler: {msg}. Unformatierter Inhalt: {raw_content}", "app_full_domain_unavailable": "Es tut uns leid, aber diese Applikation erfordert die Installation auf einer eigenen Domain, aber einige andere Applikationen sind bereits auf der Domäne'{domain}' installiert. Eine mögliche Lösung ist das Hinzufügen und Verwenden einer Subdomain, die dieser Applikation zugeordnet ist.", @@ -289,7 +267,7 @@ "diagnosis_cache_still_valid": "(Cache noch gültig für {category} Diagnose. Es wird keine neue Diagnose durchgeführt!)", "diagnosis_cant_run_because_of_dep": "Kann Diagnose für {category} nicht ausführen während wichtige Probleme zu {dep} noch nicht behoben sind.", "diagnosis_found_errors_and_warnings": "Habe {errors} erhebliche(s) Problem(e) (und {warnings} Warnung(en)) in Verbindung mit {category} gefunden!", - "diagnosis_ip_broken_dnsresolution": "Domänen-Namens-Auflösung scheint aus einem bestimmten Grund nicht zu funktionieren... Blockiert eine Firewall die DNS Anfragen?", + "diagnosis_ip_broken_dnsresolution": "Domänennamen-Auflösung scheint aus einem bestimmten Grund nicht zu funktionieren... Blockiert vielleicht eine Firewall die DNS-Anfragen?", "diagnosis_ip_broken_resolvconf": "Domänen-Namensauflösung scheint nicht zu funktionieren, was daran liegen könnte, dass in /etc/resolv.conf kein Eintrag auf 127.0.0.1 zeigt.", "diagnosis_ip_weird_resolvconf_details": "Die Datei /etc/resolv.conf muss ein Symlink auf /etc/resolvconf/run/resolv.conf sein, welcher auf 127.0.0.1 (dnsmasq) zeigt. Falls du die DNS-Resolver manuell konfigurieren möchtest, bearbeite bitte /etc/resolv.dnsmasq.conf.", "diagnosis_dns_good_conf": "DNS Einträge korrekt konfiguriert für die Domäne {domain} (Kategorie {category})", @@ -303,7 +281,7 @@ "backup_archive_corrupted": "Das Backup-Archiv '{archive}' scheint beschädigt: {error}", "backup_archive_cant_retrieve_info_json": "Die Informationen für das Archiv '{archive}' konnten nicht geladen werden... Die Datei info.json wurde nicht gefunden (oder ist kein gültiges json).", "app_packaging_format_not_supported": "Diese App kann nicht installiert werden da das Paketformat nicht von der YunoHost-Version unterstützt wird. Am besten solltest du dein System aktualisieren.", - "certmanager_domain_not_diagnosed_yet": "Für die Domain {domain} gibt es noch keine Diagnose-Resultate. Bitte widerhole die Diagnose für die Kategorien 'DNS records' und 'Web' im Diagnose-Bereich um zu überprüfen ob die Domain für Let's Encrypt bereit ist. (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen.)", + "certmanager_domain_not_diagnosed_yet": "Für die Domäne {domain} gibt es noch keine Diagnose-Resultate. Bitte wiederholen Sie die Diagnose für die Kategorien 'DNS-Einträge' und 'Web' im Diagnose-Bereich um zu überprüfen ob die Domäne für Let's Encrypt bereit ist. (Wenn Sie wissen was Sie tun, können Sie --no-checks benutzen, um diese Überprüfung zu überspringen.)", "mail_unavailable": "Diese E-Mail Adresse ist reserviert und wird dem ersten Konto automatisch zugewiesen", "diagnosis_services_conf_broken": "Die Konfiguration für den Dienst {service} ist fehlerhaft!", "diagnosis_services_running": "Dienst {service} läuft!", @@ -313,7 +291,7 @@ "diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!", "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden", "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost dyndns update --force ein Update erzwingen.", - "diagnosis_dns_point_to_doc": "Bitte schaue in der Dokumentation unter https://yunohost.org/dns_config nach, wenn du Hilfe bei der Konfiguration der DNS-Einträge benötigst.", + "diagnosis_dns_point_to_doc": "Bitte schauen Sie in der Dokumentation unter https://yunohost.org/dns_config nach, wenn Sie Hilfe bei der Konfiguration der DNS-Einträge benötigen.", "diagnosis_dns_discrepancy": "Der folgende DNS Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}", "diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration solltest du einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}", "diagnosis_dns_bad_conf": "Einige DNS-Einträge für die Domäne {domain} fehlen oder sind nicht korrekt (Kategorie {category})", @@ -324,14 +302,14 @@ "diagnosis_services_bad_status": "Der Dienst {service} ist {status} :(", "diagnosis_diskusage_verylow": "Der Speicher {mountpoint} (auf Gerät {device}) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von ingesamt {total}). Du solltest ernsthaft in Betracht ziehen, etwas Seicherplatz frei zu machen!", "diagnosis_http_ok": "Die Domäne {domain} ist über HTTP von außerhalb des lokalen Netzwerks erreichbar.", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es dir nicht gestatten, den ausgehenden Port 25 zu öffnen, da diese sich nicht um die Netzneutralität kümmern.
- Einige davon bieten als Alternative an, ein Mailserver-Relay zu verwenden, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine, die Privatsphäre berücksichtigende, Alternative ist die Verwendung eines VPN *mit einer dedizierten öffentlichen IP* um solche Einschränkungen zu umgehen. Schaue unter https://yunohost.org/#/vpn_advantage nach.
- Du kannst auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln", - "diagnosis_http_timeout": "Wartezeit wurde beim Versuch, von außen eine Verbindung zum Server aufzubauen, überschritten. Er scheint nicht erreichbar zu sein.
1. Die häufigste Ursache für dieses Problem ist daß der Port 80 (und 433) nicht richtig zu deinem Server weitergeleitet werden.
2. Du solltest auch sicherstellen, daß der Dienst nginx läuft.
3. In komplexeren Umgebungen: Stelle sicher, daß keine Firewall oder Reverse-Proxy stört.", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es Ihnen nicht gestatten, den ausgehenden Port 25 zu öffnen, weil Ihnen die Netzneutralität nichts bedeutet.
- Einige davon bieten als Alternative an, ein Mailserver-Relay zu verwenden, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine Alternative, welche die Privatsphäre berücksichtigt, wäre die Verwendung eines VPN *mit einer öffentlichen dedizierten IP* um solche Einschränkungen zu umgehen. Schauen Sie unter https://yunohost.org/#/vpn_advantage nach.
- Sie können auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln", + "diagnosis_http_timeout": "Wartezeit wurde beim Versuch überschritten, von Aussen eine Verbindung zu Ihrem Server aufzubauen. Er scheint nicht erreichbar zu sein.
1. Die häufigste Ursache für dieses Problem ist, dass die Ports 80 und 433 nicht richtig zu Ihrem Server weitergeleitet werden.
2. Sie sollten zudem sicherstellen, dass der Dienst nginx läuft.
3. In komplexeren Umgebungen: Stellen Sie sicher, dass keine Firewall oder Reverse-Proxy stört .", "service_reloaded_or_restarted": "Der Dienst '{service}' wurde erfolgreich neu geladen oder gestartet", "service_restarted": "Der Dienst '{service}' wurde neu gestartet", "certmanager_warning_subdomain_dns_record": "Die Subdomäne \"{subdomain}\" löst nicht zur gleichen IP Adresse auf wie \"{domain}\". Einige Funktionen sind nicht verfügbar bis du dies behebst und die Zertifikate neu erzeugst.", - "diagnosis_ports_ok": "Port {port} ist von außen erreichbar.", + "diagnosis_ports_ok": "Port {port} ist von Aussen erreichbar.", "diagnosis_ram_verylow": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total})", - "diagnosis_mail_outgoing_port_25_blocked_details": "Du solltest zuerst versuchen den ausgehenden Port 25 auf deiner Router-Konfigurationsoberfläche oder deiner Hosting-Anbieter-Konfigurationsoberfläche zu öffnen. (Bei einigen Hosting-Anbietern kann es sein, daß sie verlangen, daß man dafür ein Support-Ticket sendet).", + "diagnosis_mail_outgoing_port_25_blocked_details": "Sie sollten zuerst versuchen, den ausgehenden Port 25 in Ihrer Router-Konfigurationsoberfläche oder in der Konfigurationsoberfläche Ihres Hosting-Anbieters zu öffnen. (Bei einigen Hosting-Anbietern kann es sein, dass man von Ihnen verlangt, dass Sie dafür ein Support-Ticket erstellen).", "diagnosis_mail_ehlo_ok": "Der SMTP-Server ist von von außen erreichbar und darum auch in der Lage E-Mails zu empfangen!", "diagnosis_mail_ehlo_bad_answer": "Ein nicht-SMTP-Dienst antwortete auf Port 25 per IPv{ipversion}", "diagnosis_swap_notsomuch": "Das System hat nur {total} Swap. Du solltest dir überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.", @@ -358,13 +336,13 @@ "log_letsencrypt_cert_renew": "Erneuern des Let's Encrypt-Zeritifikates von '{}'", "log_selfsigned_cert_install": "Das selbstsignierte Zertifikat auf der Domäne '{}' installieren", "log_letsencrypt_cert_install": "Das Let’s Encrypt auf der Domäne '{}' installieren", - "diagnosis_mail_fcrdns_nok_details": "Du solltest zuerst versuchen, in deiner Internet-Router-Oberfläche oder in deiner Hosting-Anbieter-Oberfläche den Reverse-DNS-Eintrag mit {ehlo_domain}zu konfigurieren. (Gewisse Hosting-Anbieter können dafür möglicherweise verlangen, dass du dafür ein Support-Ticket erstellst).", - "diagnosis_mail_fcrdns_dns_missing": "Es wurde kein Reverse-DNS-Eintrag definiert für IPv{ipversion}. Einige E-Mails könnten möglicherweise zurückgewiesen oder als Spam markiert werden.", + "diagnosis_mail_fcrdns_nok_details": "Sie sollten zuerst versuchen, auf Ihrer Internet-Router-Oberfläche, in Ihrer Internet-Box oder auf Ihrer Hosting-Anbieter-Oberfläche den Reverse-DNS-Eintrag mit {ehlo_domain}zu konfigurieren. (Gewisse Hosting-Anbieter können möglicherweise verlangen, dass Sie dafür ein Support-Ticket erstellen).", + "diagnosis_mail_fcrdns_dns_missing": "Kein Reverse-DNS-Eintrag ist definiert für IPv{ipversion}. Einige E-Mails könnten eventuell nicht zugestellt oder als Spam markiert werden.", "diagnosis_mail_fcrdns_ok": "Dein Reverse-DNS-Eintrag ist korrekt konfiguriert!", "diagnosis_mail_ehlo_could_not_diagnose_details": "Fehler: {error}", - "diagnosis_mail_ehlo_could_not_diagnose": "Konnte nicht überprüfen, ob der Postfix-Mail-Server von aussen per IPv{ipversion} erreichbar ist.", + "diagnosis_mail_ehlo_could_not_diagnose": "Es war nicht möglich zu diagnostizieren, ob der Postfix-Mailserver von Aussen über IPv{ipversion} erreichbar ist.", "diagnosis_mail_ehlo_wrong_details": "Die vom Remote-Diagnose-Server per IPv{ipversion} empfangene EHLO weicht von der Domäne deines Servers ab.
Empfangene EHLO: {wrong_ehlo}
Erwartet: {right_ehlo}
Die geläufigste Ursache für dieses Problem ist, dass der Port 25 nicht korrekt auf deinem Server weitergeleitet wird. Du kannst zusätzlich auch prüfen, dass keine Firewall oder Reverse-Proxy stört.", - "diagnosis_mail_ehlo_bad_answer_details": "Das könnte daran liegen, dass anstelle deines Servers eine andere Maschine antwortet.", + "diagnosis_mail_ehlo_bad_answer_details": "Das könnte daran liegen, dass anstelle Ihres Servers ein anderes Gerät antwortet.", "ask_user_domain": "Domäne, welche für die E-Mail-Adresse und den XMPP-Account des Kontos verwendet werden soll", "app_manifest_install_ask_is_public": "Soll diese Applikation für Gäste sichtbar sein?", "app_manifest_install_ask_admin": "Wähle einen Administrator für diese Applikation", @@ -372,9 +350,9 @@ "diagnosis_mail_blacklist_listed_by": "Deine IP-Adresse oder Domäne {item} ist auf der Blacklist auf {blacklist_name}", "diagnosis_mail_blacklist_ok": "Die IP-Adressen und die Domänen, welche von diesem Server verwendet werden, scheinen nicht auf einer Blacklist zu sein", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Aktueller Reverse-DNS-Eintrag: {rdns_domain}
Erwarteter Wert: {ehlo_domain}", - "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Der Reverse-DNS-Eintrag für IPv{ipversion} ist nicht korrekt konfiguriert. Einige E-Mails könnten abgewiesen oder als Spam markiert werden.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Einige Provider werden es dir nicht erlauben, deinen Reverse-DNS-Eintrag zu konfigurieren (oder ihre Funktionalität könnte defekt sein ...). Falls du deinen Reverse-DNS-Eintrag für IPv4 korrekt konfiguiert ist, kannst du versuchen, die Verwendung von IPv6 für das Versenden von E-Mails auszuschalten, indem du den Befehl yunohost settings set smtp.allow_ipv6 -v off ausführst. Bemerkung: Die Folge dieser letzten Lösung ist, dass du mit Servern, welche ausschliesslich über IPv6 verfügen, keine E-Mails mehr versenden oder empfangen kannst.", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Einige Anbieter werden es dir nicht erlauben, deinen Reverse-DNS zu konfigurieren (oder deren Funktionalität ist defekt...). Falls du deswegen auf Probleme stoßen solltest, ziehe folgende Lösungen in Betracht:
- Manche ISPs stellen als Alternative die Benutzung eines Mail-Server-Relays zur Verfügung, was jedoch mit sich zieht, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine privatsphärenfreundlichere Alternative ist die Benutzung eines VPN *mit einer dedizierten öffentlichen IP* um Einschränkungen dieser Art zu umgehen. Schaue hier nach https://yunohost.org/#/vpn_advantage
- Schließlich ist es auch möglich zu einem anderen Anbieter zu wechseln", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Reverse-DNS-Eintrag ist nicht korrekt konfiguriert für IPv{ipversion}. Einige E-Mails könnten eventuell nicht zugestellt oder als Spam markiert werden.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Einige Provider werden es Ihnen vermutlich nicht erlauben, den Reverse-DNS-Eintrag zu konfigurieren (oder vielleicht ist diese Funktion beschädigt...). Falls Sie Ihren Reverse-DNS-Eintrag für IPv4 korrekt konfiguriert haben, können Sie versuchen, die Verwendung von IPv6 für das Versenden von E-Mails auszuschalten, indem Sie den Befehl yunohost settings set smtp.allow_ipv6 -v off ausführen. Bemerkung: Die Folge dieser letzten Lösung ist, dass Sie mit Servern, welche nur über IPv6 verfügen, keine E-Mails mehr versenden oder empfangen können.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Einige Anbieter werden es nicht zulassen, den Reverse-DNS zu konfigurieren (oder diese Funktion ist defekt...). Falls du deswegen auf Probleme stoßen solltest, ziehe folgende Lösungen in Betracht:
- Manche ISPs stellen als Alternative die Benutzung eines Mail-Server-Relays zur Verfügung, was jedoch mit sich zieht, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine privatsphärenfreundlichere Alternative ist die Benutzung eines VPN *mit einer dedizierten öffentlichen IP* um Einschränkungen dieser Art zu umgehen. Schaue hier nach https://yunohost.org/#/vpn_advantage
- Schließlich ist es auch möglich zu einem anderen Anbieter zu wechseln", "diagnosis_mail_queue_unavailable_details": "Fehler: {error}", "diagnosis_mail_queue_unavailable": "Die Anzahl der anstehenden Nachrichten in der Warteschlange kann nicht abgefragt werden", "diagnosis_mail_queue_ok": "{nb_pending} anstehende E-Mails in der Warteschlange", @@ -405,7 +383,7 @@ "diagnosis_package_installed_from_sury": "Einige System-Pakete sollten gedowngradet werden", "diagnosis_ports_forwarding_tip": "Um dieses Problem zu beheben, musst du höchstwahrscheinlich die Port-Weiterleitung auf deinem Internet-Router einrichten wie in https://yunohost.org/isp_box_config beschrieben", "diagnosis_regenconf_manually_modified_details": "Das ist wahrscheinlich OK wenn du weißt, was du tust! YunoHost wird in Zukunft diese Datei nicht mehr automatisch updaten... Aber sei bitte vorsichtig, da die zukünftigen Upgrades von YunoHost wichtige empfohlene Änderungen enthalten könnten. Wenn du möchtest, kannst du die Unterschiede mit yunohost tools regen-conf {category} --dry-run --with-diff inspizieren und mit yunohost tools regen-conf {category} --force auf das Zurücksetzen die empfohlene Konfiguration erzwingen", - "diagnosis_mail_blacklist_website": "Nachdem du herausgefunden hast, weshalb du auf die Blacklist gesetzt wurdest und dies behoben hast, zögere nicht, nachzufragen, ob deine IP-Adresse oder Ihre Domäne von auf {blacklist_website} entfernt wird", + "diagnosis_mail_blacklist_website": "Nachdem Sie herausgefunden haben, weshalb Sie auf die Blacklist gesetzt wurden und dies behoben haben, zögern Sie nicht, nachzufragen, ob Ihre IP oder Ihre Domäne von {blacklist_website} entfernt werden kann", "diagnosis_unknown_categories": "Folgende Kategorien sind unbekannt: {categories}", "diagnosis_http_hairpinning_issue": "In deinem lokalen Netzwerk scheint Hairpinning nicht aktiviert zu sein.", "diagnosis_ports_needed_by": "Diesen Port zu öffnen ist nötig, um die Funktionalität des Typs {category} (service {service}) zu gewährleisten", @@ -423,8 +401,8 @@ "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": "Es sieht so aus, als wäre dieser Server erst kürzlich eingerichtet worden und es gibt noch keinen Diagnosebericht, der angezeigt werden könnte. Sie sollten zunächst eine vollständige Diagnose durchführen, entweder über die Web-Oberfläche oder mit \"yunohost diagnosis run\" von der Kommandozeile aus.", - "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, 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_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein, um die Unterschiede anzuzeigen. Wenn Sie damit einverstanden sind, können Sie mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", + "diagnosis_backports_in_sources_list": "Sie haben vermutlich apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie, was Sie tun.", "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}", "group_user_not_in_group": "Konto {user} ist nicht in der Gruppe {group}", "group_user_already_in_group": "Konto {user} ist bereits in der Gruppe {group}", @@ -436,14 +414,9 @@ "global_settings_setting_smtp_relay_password": "SMTP Relay Host Passwort", "global_settings_setting_smtp_relay_user": "SMTP Relay Benutzer Account", "global_settings_setting_smtp_relay_port": "SMTP Relay Port", - "global_settings_setting_smtp_allow_ipv6": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden", - "global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver", "domain_cannot_remove_main_add_new_one": "Sie können '{domain}' nicht entfernen, da es die Hauptdomäne und Ihre einzige Domäne ist. Sie müssen zuerst eine andere Domäne mit 'yunohost domain add ' hinzufügen, dann als Hauptdomäne mit 'yunohost domain main-domain -n ' festlegen und dann können Sie die Domäne '{domain}' mit 'yunohost domain remove {domain}' entfernen'.'", "diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.", "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen.", - "global_settings_setting_smtp_relay_host": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn du in einer der folgenden Situationen bist: Dein ISP- oder VPS-Provider hat deinen Port 25 geblockt, eine deinen residentiellen IPs ist auf DUHL gelistet, du kannst keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und du möchtest einen anderen verwenden, um E-Mails zu versenden.", - "global_settings_setting_backup_compress_tar_archives": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen.", - "log_remove_on_failed_restore": "Entfernen von '{}' nach einer fehlgeschlagenen Wiederherstellung aus einem Sicherungsarchiv", "log_backup_restore_app": "Wiederherstellen von '{}' aus einem Sicherungsarchiv", "log_backup_restore_system": "System aus einem Sicherungsarchiv wiederherstellen", "log_available_on_yunopaste": "Das Protokoll ist nun via {url} verfügbar", @@ -548,7 +521,6 @@ "migration_ldap_migration_failed_trying_to_rollback": "Migrieren war nicht möglich... Versuch, ein Rollback des Systems durchzuführen.", "migration_ldap_backup_before_migration": "Vor der eigentlichen Migration ein Backup der LDAP-Datenbank und der Applikations-Einstellungen erstellen.", "global_settings_setting_ssowat_panel_overlay_enabled": "Das SSOwat-Overlay-Panel aktivieren", - "global_settings_setting_security_ssh_port": "SSH-Port", "diagnosis_sshd_config_inconsistent_details": "Bitte führe yunohost settings set security.ssh.port -v YOUR_SSH_PORT aus, um den SSH-Port festzulegen, und prüfe yunohost tools regen-conf ssh --dry-run --with-diff und yunohost tools regen-conf ssh --force um deine Konfiguration auf die YunoHost-Empfehlung zurückzusetzen.", "regex_incompatible_with_tile": "/!\\ Packagers! Für Berechtigung '{permission}' ist show_tile auf 'true' gesetzt und deshalb kannst du keine regex-URL als Hauptdomäne setzen", "permission_cant_add_to_all_users": "Die Berechtigung {permission} kann nicht für allen Konten hinzugefügt werden.", @@ -571,7 +543,6 @@ "server_reboot": "Der Server wird neu gestartet", "server_shutdown_confirm": "Der Server wird sofort heruntergefahren, sind Sie 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": "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.", @@ -580,8 +551,6 @@ "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": "Das Konto '{user}' ist bereits vorhanden", "update_apt_cache_warning": "Beim Versuch den Cache für APT (Debians Paketmanager) zu aktualisieren, ist etwas schief gelaufen. Hier ist ein Dump der Zeilen aus sources.list, die Ihnen vielleicht dabei helfen, das Problem zu identifizieren:\n{sourceslist}", - "global_settings_setting_security_webadmin_allowlist": "IP-Adressen, die auf die Verwaltungsseite zugreifen dürfen. Kommasepariert.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Erlaube nur bestimmten IP-Adressen den Zugriff auf die Verwaltungsseite.", "disk_space_not_sufficient_update": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu aktualisieren", "disk_space_not_sufficient_install": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu installieren", "danger": "Warnung:", @@ -595,14 +564,13 @@ "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_apps_not_in_app_catalog": "Diese Applikation steht nicht im Applikationskatalog von YunoHost. Sie sollten in Betracht ziehen, sie zu deinstallieren, weil sie keine Aktualisierungen mehr erhält und die Integrität und die Sicherheit Ihres Systems kompromittieren könnte.", + "diagnosis_apps_outdated_ynh_requirement": "Die installierte Version dieser Applikation erfordert nur YunoHost >=2.x oder 3.x, was darauf hinweisen könnte, dass die Applikation nicht nach aktuell empfohlenen Paketierungspraktiken und mit aktuellen Helpern erstellt worden ist. Sie sollten wirklich in Betracht ziehen, sie zu aktualisieren.", "diagnosis_description_apps": "Applikationen", "config_cant_set_value_on_section": "Du kannst einen einzelnen Wert nicht auf einen gesamten Konfigurationsbereich anwenden.", - "diagnosis_apps_deprecated_practices": "Die installierte Version dieser App verwendet immer noch gewisse veraltete Paketierungspraktiken. Du solltest die App wirklich aktualisieren.", + "diagnosis_apps_deprecated_practices": "Die installierte Version dieser Applikation verwendet gewisse veraltete Paketierungspraktiken. Sie sollten sie wirklich aktualisieren.", "app_config_unable_to_apply": "Konnte die Werte des Konfigurations-Panels nicht anwenden.", "app_config_unable_to_read": "Konnte die Werte des Konfigurations-Panels nicht auslesen.", "config_unknown_filter_key": "Der Filterschlüssel '{filter_key}' ist inkorrekt.", @@ -618,8 +586,6 @@ "domain_unknown": "Domäne '{domain}' unbekannt", "ldap_server_is_down_restart_it": "Der LDAP-Dienst ist nicht erreichbar, versuche ihn neu zu starten...", "user_import_bad_file": "Deine CSV-Datei ist nicht korrekt formatiert und wird daher ignoriert, um einen möglichen Datenverlust zu vermeiden", - "global_settings_setting_security_experimental_enabled": "Aktiviere experimentelle Sicherheitsfunktionen (nur aktivieren, wenn Du weißt was Du tust!)", - "global_settings_setting_security_nginx_redirect_to_https": "HTTP-Anfragen standardmäßig auf HTTPs umleiten (NICHT AUSSCHALTEN, sofern Du nicht weißt was Du tust!)", "user_import_missing_columns": "Die folgenden Spalten fehlen: {columns}", "user_import_nothing_to_do": "Es muss kein Konto importiert werden", "user_import_partial_failed": "Der Import von Konten ist teilweise fehlgeschlagen", @@ -637,7 +603,6 @@ "domain_dns_push_success": "DNS-Einträge aktualisiert!", "domain_dns_push_failed": "Die Aktualisierung der DNS-Einträge ist leider gescheitert.", "domain_dns_push_partial_failure": "DNS-Einträge teilweise aktualisiert: einige Warnungen/Fehler wurden gemeldet.", - "domain_config_features_disclaimer": "Bisher hat das Aktivieren/Deaktivieren von Mail- oder XMPP-Funktionen nur Auswirkungen auf die empfohlene und automatische DNS-Konfiguration, nicht auf die Systemkonfigurationen!", "domain_config_mail_in": "Eingehende E-Mails", "domain_config_mail_out": "Ausgehende E-Mails", "domain_config_xmpp": "Instant Messaging (XMPP)", @@ -651,7 +616,6 @@ "domain_config_auth_consumer_key": "Verbraucherschlüssel", "invalid_number_min": "Muss größer sein als {min}", "invalid_number_max": "Muss kleiner sein als {max}", - "invalid_password": "Ungültiges Passwort", "ldap_attribute_already_exists": "LDAP-Attribut '{attribute}' existiert bereits mit dem Wert '{value}'", "user_import_success": "Konten erfolgreich importiert", "domain_registrar_is_not_configured": "Der DNS-Registrar ist noch nicht für die Domäne '{domain}' konfiguriert.", @@ -666,14 +630,12 @@ "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": "Aktualisieren von Systempaketen", @@ -684,5 +646,63 @@ "migration_description_0022_php73_to_php74_pools": "Migriere php7.3-fpm 'pool' Konfiguration nach php7.4", "migration_description_0023_postgresql_11_to_13": "Migrieren von Datenbanken von PostgreSQL 11 nach 13", "service_description_postgresql": "Speichert Applikations-Daten (SQL Datenbank)", - "migration_0023_not_enough_space": "Stelle sicher, dass unter {path} genug Speicherplatz zur Verfügung steht, um die Migration auszuführen." -} + "migration_0023_not_enough_space": "Stelle sicher, dass unter {path} genug Speicherplatz zur Verfügung steht, um die Migration auszuführen.", + "global_settings_setting_backup_compress_tar_archives_help": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen.", + "global_settings_setting_security_experimental_enabled_help": "Aktiviere experimentelle Sicherheitsfunktionen (nur aktivieren, wenn Du weißt was Du tust!)", + "global_settings_setting_nginx_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Webserver NGINX. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", + "global_settings_setting_nginx_redirect_to_https_help": "HTTP-Anfragen standardmäßig auf HTTPs umleiten (NICHT AUSSCHALTEN, sofern Du nicht weißt was Du tust!)", + "global_settings_setting_admin_strength": "Stärke des Admin-Passworts", + "global_settings_setting_user_strength": "Stärke des Anmeldepassworts", + "global_settings_setting_postfix_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Postfix-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", + "global_settings_setting_ssh_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den SSH-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", + "global_settings_setting_ssh_password_authentication_help": "Passwort-Authentifizierung für SSH zulassen", + "global_settings_setting_ssh_port": "SSH-Port", + "global_settings_setting_webadmin_allowlist_help": "IP-Adressen, die auf die Verwaltungsseite zugreifen dürfen. Kommasepariert.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Erlaube nur bestimmten IP-Adressen den Zugriff auf die Verwaltungsseite.", + "global_settings_setting_smtp_allow_ipv6_help": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden", + "global_settings_setting_smtp_relay_enabled_help": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn du in einer der folgenden Situationen bist: Dein ISP- oder VPS-Provider hat deinen Port 25 geblockt, eine deinen residentiellen IPs ist auf DUHL gelistet, du kannst keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und du möchtest einen anderen verwenden, um E-Mails zu versenden.", + "admins": "Administratoren", + "all_users": "Alle YunoHost-Nutzer", + "app_action_failed": "Fehlgeschlagene Aktion {action} für Applikation {app}", + "app_manifest_install_ask_init_admin_permission": "Wer soll Zugriff auf die administrativen Funktionen für diese App erhalten? (Dies kann später wieder geändert werden)", + "app_manifest_install_ask_init_main_permission": "Wer soll Zugriff auf diese App erhalten? (Dies kann später wieder geändert werden)", + "ask_admin_fullname": "Vollständiger Name des Administrators", + "ask_admin_username": "Benutzername des Administrators", + "ask_fullname": "Vollständiger Name (Vorname und Nachname)", + "certmanager_cert_install_failed": "Installation des Let's Encrypt-Zertifikat fehlgeschlagen für {domains}", + "certmanager_cert_install_failed_selfsigned": "Installation des selbst-signierten Zertifikats fehlgeschlagen für {domains}", + "certmanager_cert_renew_failed": "Erneuern des Let's Encrypt-Zertifikat fehlgeschlagen für {domains}", + "config_action_disabled": "Konnte die Aktion '{action}' nicht durchführen, weil sie deaktiviert ist. Stellen Sie sicher, dass sie ihre Einschränkungen einhält. Hilfe: {help}", + "config_action_failed": "Ausführung der Aktion '{action}' fehlgeschlagen: {error}", + "config_forbidden_readonly_type": "Der Typ '{type}' kann nicht auf Nur-Lesen eingestellt werden. Verwenden Sie bitte einen anderen Typ, um diesen Wert zu generieren (relevante ID des Arguments: '{id}').", + "diagnosis_using_stable_codename": "apt (Paketmanager des Systems) ist gegenwärtig konfiguriert um die Pakete des Code-Namens 'stable' zu installieren, anstelle die des Code-Namen der aktuellen Debian-Version (bullseye).", + "domain_config_acme_eligible": "Geeignet für ACME", + "diagnosis_using_stable_codename_details": "Dies wird meistens durch eine fehlerhafte Konfiguration seitens des Hosting-Providers verursacht. Dies stellt eine Gefahr dar, weil sobald die nächste Debian-Version zum neuen 'stable' wird, wird apt alle System-Pakete aktualisieren wollen, ohne eine ordnungsgemässe Migration zu durchlaufen. Es wird sehr empfohlen dies zu berichtigen, indem Sie die Datei der apt-Quellen des Debian-Basis-Repositorys entsprechend anpassen indem Sie das stable-Keyword durch bullseye ersetzen. Die zugehörige Konfigurationsdatei sollte /etc/apt/sources.list oder eine Datei im Verzeichnis /etc/apt/sources.list.d/sein.", + "diagnosis_using_yunohost_testing": "apt (der Paketmanager des Systems) ist aktuell so konfiguriert, dass die 'testing'-Upgrades für YunoHost core installiert werden.", + "diagnosis_using_yunohost_testing_details": "Dies ist wahrscheinlich OK, wenn Sie wissen, was Sie tun. Aber beachten Sie bitte die Release-Notes bevor sie zukünftige YunoHost-Upgrades installieren! Wenn Sie die 'testing'-Upgrades deaktivieren möchten, sollten sie das testing-Schlüsselwort aus /etc/apt/sources.list.d/yunohost.list entfernen.", + "global_settings_setting_security_experimental_enabled": "Experimentelle Sicherheitsfunktionen", + "domain_config_acme_eligible_explain": "Es scheint, als ob diese Domäne nicht bereit ist für ein Let's Encrypt-Zertifikat. Bitte überprüfen Sie Ihre DNS-Konfiguration und ob Ihr Server über HTTP erreichbar ist . Die Abschnitte 'DNS-Einträge' und 'Web' auf der Diagnose-Seite können Ihnen dabei helfen, zu verstehen, was falsch konfiguriert ist.", + "domain_config_cert_install": "Installation des Let's Encrypt-Zertifikats", + "domain_config_cert_issuer": "Zertifizierungsstelle", + "domain_config_cert_no_checks": "Tests und andere Diagnose-Überprüfungen ignorieren", + "domain_config_cert_renew": "Erneuern des Let's Encrypt-Zertifikats", + "domain_config_cert_renew_help": "Das Zertifikat wird automatisch während den letzten 15 Tagen seiner Gültigkeit erneuert. Sie können es manuell erneuern, wenn Sie möchten. (nicht empfohlen).", + "domain_config_cert_summary": "Zertifikats-Status", + "visitors": "Besucher", + "domain_config_cert_summary_abouttoexpire": "Das aktuelle Zertifikat läuft bald ab. Es sollte bald automatisch erneuert werden.", + "domain_config_cert_summary_expired": "ACHTUNG: Das aktuelle Zertifikat ist nicht gültig! HTTPS wird gar nicht funktionieren!", + "domain_config_cert_summary_letsencrypt": "Toll! Sie benutzen ein gültiges Let's Encrypt-Zertifikat!", + "domain_config_cert_summary_ok": "Gut, das aktuelle Zertifikat sieht gut aus!", + "app_change_url_require_full_domain": "{app} kann nicht auf diese neue URL verschoben werden, weil sie eine vollständige eigene Domäne benötigt (z.B. mit Pfad = /)", + "app_not_upgraded_broken_system_continue": "Die App '{failed_app}' konnte nicht aktualisiert werden und hat Ihr System in einen beschädigten Zustand versetzt (folglich wird --continue-on-failure ignoriert) und als Konsequenz wurde die Aktualisierung der folgenden Apps abgelehnt: {apps}", + "app_yunohost_version_not_supported": "Diese App setzt YunoHost >= {required} voraus aber die gegenwärtig installierte Version ist {current}", + "app_failed_to_upgrade_but_continue": "Die App {failed_app} konnte nicht aktualisiert werden und es wird anforderungsgemäss zur nächsten Aktualisierung fortgefahren. Starten sie 'yunohost log show {operation_logger_name}' um den Fehlerbericht zu sehen", + "app_not_upgraded_broken_system": "Die App '{failed_app}' konnte nicht aktualisiert werden und hat Ihr System in einen beschädigten Zustand versetzt und als Konzequenz wurde die Aktualisierung der folgenden Apps abgelehnt: {apps}", + "apps_failed_to_upgrade": "Diese Apps konnten nicht aktualisiert werden: {apps}", + "app_arch_not_supported": "Diese App kann nur auf bestimmten Architekturen {required} installiert werden, aber Ihre gegenwärtige Serverarchitektur ist {current}", + "app_not_enough_disk": "Diese App benötigt {required} freien Speicherplatz.", + "app_not_enough_ram": "Diese App benötigt {required} RAM um installiert/aktualisiert zu werden, aber es sind aktuell nur {current} verfügbar.", + "app_change_url_failed": "Kann die URL für {app} nicht ändern: {error}", + "app_change_url_script_failed": "Es ist ein Fehler im URL-Änderungs-Script aufgetreten", + "app_resource_failed": "Automatische Ressourcen-Allokation (provisioning), die Unterbindung des Zugriffts auf Ressourcen (deprovisioning) oder die Aktualisierung der Ressourcen für {app} schlug fehl: {error}" +} \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index c34cf0198..bfc564afd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -4,25 +4,32 @@ "additional_urls_already_added": "Additionnal URL '{url}' already added in the additional URL for permission '{permission}'", "additional_urls_already_removed": "Additionnal URL '{url}' already removed in the additional URL for permission '{permission}'", "admin_password": "Administration password", - "admin_password_change_failed": "Unable to change password", - "admin_password_changed": "The administration password was changed", - "admin_password_too_long": "Please choose a password shorter than 127 characters", + "admins": "Admins", + "all_users": "All YunoHost users", "already_up_to_date": "Nothing to do. Everything is already up-to-date.", "app_action_broke_system": "This action seems to have broken these important services: {services}", "app_action_cannot_be_ran_because_required_services_down": "These required services should be running to run this action: {services}. Try restarting them to continue (and possibly investigate why they are down).", + "app_action_failed": "Failed to run action {action} for app {app}", "app_already_installed": "{app} is already installed", "app_already_installed_cant_change_url": "This app is already installed. The URL cannot be changed just by this function. Check in `app changeurl` if it's available.", "app_already_up_to_date": "{app} is already up-to-date", + "app_arch_not_supported": "This app can only be installed on architectures {required} but your server architecture is {current}", "app_argument_choice_invalid": "Pick a valid value for argument '{name}': '{value}' is not among the available choices ({choices})", "app_argument_invalid": "Pick a valid value for the argument '{name}': {error}", - "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reason", + "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reasons", "app_argument_required": "Argument '{name}' is required", + "app_change_url_failed": "Could not change the url for {app}: {error}", "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain}{path}'), nothing to do.", "app_change_url_no_script": "The app '{app_name}' doesn't support URL modification yet. Maybe you should upgrade it.", + "app_change_url_require_full_domain": "{app} cannot be moved to this new URL because it requires a full domain (i.e. with path = /)", + "app_change_url_script_failed": "An error occured inside the change url script", "app_change_url_success": "{app} URL is now {domain}{path}", "app_config_unable_to_apply": "Failed to apply config panel values.", "app_config_unable_to_read": "Failed to read config panel values.", + "app_corrupt_source": "YunoHost was able to download the asset '{source_id}' ({url}) for {app}, but the asset doesn't match the expected checksum. This could mean that some temporary network failure happened on your server, OR the asset was somehow changed by the upstream maintainer (or a malicious actor?) and YunoHost packagers need to investigate and update the app manifest to reflect this change.\n Expected sha256 checksum: {expected_sha256}\n Downloaded sha256 checksum: {computed_sha256}\n Downloaded file size: {size}", "app_extraction_failed": "Could not extract the installation files", + "app_failed_to_download_asset": "Failed to download asset '{source_id}' ({url}) for {app}: {out}", + "app_failed_to_upgrade_but_continue": "App {failed_app} failed to upgrade, continue to next upgrades as requested. Run 'yunohost log show {operation_logger_name}' to see failure log", "app_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.", "app_id_invalid": "Invalid app ID", "app_install_failed": "Unable to install {app}: {error}", @@ -33,21 +40,27 @@ "app_make_default_location_already_used": "Unable to make '{app}' the default app on the domain, '{domain}' is already in use by '{other_app}'", "app_manifest_install_ask_admin": "Choose an administrator user for this app", "app_manifest_install_ask_domain": "Choose the domain where this app should be installed", + "app_manifest_install_ask_init_admin_permission": "Who should have access to admin features for this app? (This can later be changed)", + "app_manifest_install_ask_init_main_permission": "Who should have access to this app? (This can later be changed)", "app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?", "app_manifest_install_ask_password": "Choose an administration password for this app", "app_manifest_install_ask_path": "Choose the URL path (after the domain) where this app should be installed", "app_not_correctly_installed": "{app} seems to be incorrectly installed", + "app_not_enough_disk": "This app requires {required} free space.", + "app_not_enough_ram": "This app requires {required} RAM to install/upgrade but only {current} is available right now.", "app_not_installed": "Could not find {app} in the list of installed apps: {all_apps}", "app_not_properly_removed": "{app} has not been properly removed", "app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps' upgrades have been cancelled: {apps}", + "app_not_upgraded_broken_system": "The app '{failed_app}' failed to upgrade and put the system in a broken state, and as a consequence the following apps' upgrades have been cancelled: {apps}", + "app_not_upgraded_broken_system_continue": "The app '{failed_app}' failed to upgrade and put the system in a broken state (so --continue-on-failure is ignored), and as a consequence the following apps' upgrades have been cancelled: {apps}", "app_packaging_format_not_supported": "This app cannot be installed because its packaging format is not supported by your YunoHost version. You should probably consider upgrading your system.", - "app_remove_after_failed_install": "Removing the app following the installation failure...", + "app_remove_after_failed_install": "Removing the app after installation failure...", "app_removed": "{app} uninstalled", - "app_requirements_checking": "Checking required packages for {app}...", - "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", + "app_requirements_checking": "Checking requirements for {app}...", + "app_resource_failed": "Provisioning, deprovisioning, or updating resources for {app} failed: {error}", "app_restore_failed": "Could not restore {app}: {error}", "app_restore_script_failed": "An error occured inside the app restore script", - "app_sources_fetch_failed": "Could not fetch sources files, is the URL correct?", + "app_sources_fetch_failed": "Could not fetch source files, is the URL correct?", "app_start_backup": "Collecting files to be backed up for {app}...", "app_start_install": "Installing {app}...", "app_start_remove": "Removing {app}...", @@ -60,14 +73,18 @@ "app_upgrade_several_apps": "The following apps will be upgraded: {apps}", "app_upgrade_some_app_failed": "Some apps could not be upgraded", "app_upgraded": "{app} upgraded", + "app_yunohost_version_not_supported": "This app requires YunoHost >= {required} but current installed version is {current}", "apps_already_up_to_date": "All apps are already up-to-date", "apps_catalog_failed_to_download": "Unable to download the {apps_catalog} app catalog: {error}", "apps_catalog_init_success": "App catalog system initialized!", "apps_catalog_obsolete_cache": "The app catalog cache is empty or obsolete.", "apps_catalog_update_success": "The application catalog has been updated!", "apps_catalog_updating": "Updating application catalog...", - "ask_firstname": "First name", - "ask_lastname": "Last name", + "apps_failed_to_upgrade": "Those applications failed to upgrade:{apps}", + "apps_failed_to_upgrade_line": "\n * {app_id} (to see corresponding log do a 'yunohost log show {operation_logger_name}')", + "ask_admin_fullname": "Admin full name", + "ask_admin_username": "Admin username", + "ask_fullname": "Full name", "ask_main_domain": "Main domain", "ask_new_admin_password": "New administration password", "ask_new_domain": "New domain", @@ -82,7 +99,7 @@ "backup_applying_method_tar": "Creating the backup TAR archive...", "backup_archive_app_not_found": "Could not find {app} in the backup archive", "backup_archive_broken_link": "Could not access the backup archive (broken link to {path})", - "backup_archive_cant_retrieve_info_json": "Could not load infos for archive '{archive}'... The info.json cannot be retrieved (or is not a valid json).", + "backup_archive_cant_retrieve_info_json": "Could not load info for archive '{archive}'... The info.json file cannot be retrieved (or is not a valid json).", "backup_archive_corrupted": "It looks like the backup archive '{archive}' is corrupted : {error}", "backup_archive_name_exists": "A backup archive with this name already exists.", "backup_archive_name_unknown": "Unknown local backup archive named '{name}'", @@ -95,14 +112,14 @@ "backup_copying_to_organize_the_archive": "Copying {size}MB to organize the archive", "backup_couldnt_bind": "Could not bind {src} to {dest}.", "backup_create_size_estimation": "The archive will contain about {size} of data.", - "backup_created": "Backup created", + "backup_created": "Backup created: {name}", "backup_creation_failed": "Could not create the backup archive", "backup_csv_addition_failed": "Could not add files to backup into the CSV file", "backup_csv_creation_failed": "Could not create the CSV file needed for restoration", "backup_custom_backup_error": "Custom backup method could not get past the 'backup' step", "backup_custom_mount_error": "Custom backup method could not get past the 'mount' step", "backup_delete_error": "Could not delete '{path}'", - "backup_deleted": "Backup deleted", + "backup_deleted": "Backup deleted: {name}", "backup_hook_unknown": "The backup hook '{hook}' is unknown", "backup_method_copy_finished": "Backup copy finalized", "backup_method_custom_finished": "Custom backup method '{method}' finished", @@ -120,28 +137,34 @@ "backup_unable_to_organize_files": "Could not use the quick method to organize files in the archive", "backup_with_no_backup_script_for_app": "The app '{app}' has no backup script. Ignoring.", "backup_with_no_restore_script_for_app": "{app} has no restoration script, you will not be able to automatically restore the backup of this app.", - "certmanager_acme_not_configured_for_domain": "The ACME challenge cannot be ran for {domain} right now because its nginx conf lacks the corresponding code snippet... Please make sure that your nginx configuration is up to date using `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "The ACME challenge cannot be run for {domain} right now because its nginx conf lacks the corresponding code snippet... Please make sure that your nginx configuration is up to date using `yunohost tools regen-conf nginx --dry-run --with-diff`.", "certmanager_attempt_to_renew_nonLE_cert": "The certificate for the domain '{domain}' is not issued by Let's Encrypt. Cannot renew it automatically!", "certmanager_attempt_to_renew_valid_cert": "The certificate for the domain '{domain}' is not about to expire! (You may use --force if you know what you're doing)", "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain}! (Use --force to bypass)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain} (file: {file}), reason: {reason}", + "certmanager_cert_install_failed": "Let's Encrypt certificate install failed for {domains}", + "certmanager_cert_install_failed_selfsigned": "Self-signed certificate install failed for {domains}", "certmanager_cert_install_success": "Let's Encrypt certificate now installed for the domain '{domain}'", "certmanager_cert_install_success_selfsigned": "Self-signed certificate now installed for the domain '{domain}'", + "certmanager_cert_renew_failed": "Let's Encrypt certificate renew failed for {domains}", "certmanager_cert_renew_success": "Let's Encrypt certificate renewed for the domain '{domain}'", "certmanager_cert_signing_failed": "Could not sign the new certificate", "certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain} did not work...", "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain} is not self-signed. Are you sure you want to replace it? (Use '--force' to do so.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' is different from this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off those checks.)", - "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off those checks.)", - "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off those checks.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' are different to this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off these checks.)", + "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off these checks.)", + "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off these checks.)", "certmanager_hit_rate_limit": "Too many certificates already issued for this exact set of domains {domain} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", "certmanager_no_cert_file": "Could not read the certificate file for the domain {domain} (file: {file})", "certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file})", "certmanager_unable_to_parse_self_CA_name": "Could not parse name of self-signing authority (file: {file})", "certmanager_warning_subdomain_dns_record": "Subdomain '{subdomain}' does not resolve to the same IP address as '{domain}'. Some features will not be available until you fix this and regenerate the certificate.", + "config_action_disabled": "Could not run action '{action}' since it is disabled, make sure to meet its constraints. help: {help}", + "config_action_failed": "Failed to run action '{action}': {error}", "config_apply_failed": "Applying the new configuration failed: {error}", "config_cant_set_value_on_section": "You can't set a single value on an entire config section.", "config_forbidden_keyword": "The keyword '{keyword}' is reserved, you can't create or use a config panel with a question with this id.", + "config_forbidden_readonly_type": "The type '{type}' can't be set as readonly, use another type to render this value (relevant arg id: '{id}').", "config_no_panel": "No config panel found.", "config_unknown_filter_key": "The filter key '{filter_key}' is incorrect.", "config_validate_color": "Should be a valid RGB hexadecimal color", @@ -149,20 +172,21 @@ "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}] ", + "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated into YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ", + "confirm_app_insufficient_ram": "DANGER! This app requires {required} RAM to install/upgrade but only {current} is available right now. Even if this app could run, its installation/upgrade process requires a large amount of RAM so your server may freeze and fail miserably. If you are willing to take that risk anyway, type '{answers}'", + "confirm_notifications_read": "WARNING: You should check the app notifications above before continuing, there might be important stuff to know. [{answers}]", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app}", "danger": "Danger:", "diagnosis_apps_allgood": "All installed apps respect basic packaging practices", "diagnosis_apps_bad_quality": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", "diagnosis_apps_broken": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", - "diagnosis_apps_deprecated_practices": "This app's installed version still uses some super-old deprecated packaging practices. You should really consider upgrading it.", + "diagnosis_apps_deprecated_practices": "This app's installed version still uses some very old, deprecated packaging practices. You should really consider upgrading it.", "diagnosis_apps_issue": "An issue was found for app {app}", - "diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and got removed, you should consider uninstalling this app as it won't receive upgrade, and may compromise the integrity and security of your system.", - "diagnosis_apps_outdated_ynh_requirement": "This app's installed version only requires yunohost >= 2.x, which tends to indicate that it's not up to date with recommended packaging practices and helpers. You should really consider upgrading it.", - "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage from installing packages from backports, because it's likely to create unstabilities or conflicts on your system.", + "diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and was removed, you should consider uninstalling this app as it won't receive upgrades and may compromise the integrity and security of your system.", + "diagnosis_apps_outdated_ynh_requirement": "This app's installed version only requires yunohost >= 2.x or 3.x, which tends to indicate that it's not up to date with recommended packaging practices and helpers. You should really consider upgrading it.", + "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage installing packages from backports, because it's likely to create unstabilities or conflicts on your system.", "diagnosis_basesystem_hardware": "Server hardware architecture is {virt} {arch}", "diagnosis_basesystem_hardware_model": "Server model is {model}", "diagnosis_basesystem_host": "Server is running Debian {debian_version}", @@ -190,7 +214,7 @@ "diagnosis_dns_discrepancy": "The following DNS record does not seem to follow the recommended configuration:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", "diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})", "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}", - "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help about configuring DNS records.", + "diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help configuring DNS records.", "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force.", "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", @@ -214,14 +238,14 @@ "diagnosis_http_hairpinning_issue": "Your local network does not seem to have hairpinning enabled.", "diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at https://yunohost.org/dns_local_network", "diagnosis_http_nginx_conf_not_up_to_date": "This domain's nginx configuration appears to have been modified manually, and prevents YunoHost from diagnosing if it's reachable on HTTP.", - "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference with the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok, apply the changes with yunohost tools regen-conf nginx --force.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference from the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok with it, apply the changes with yunohost tools regen-conf nginx --force.", "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.", "diagnosis_http_partially_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network in IPv{failed}, though it works in IPv{passed}.", "diagnosis_http_special_use_tld": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to be exposed outside the local network.", - "diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", + "diagnosis_http_timeout": "Timed-out while trying to contact your server from the outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", "diagnosis_http_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network.", "diagnosis_ignored_issues": "(+ {nb_ignored} ignored issue(s))", - "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests ?", + "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests?", "diagnosis_ip_broken_resolvconf": "Domain name resolution seems to be broken on your server, which seems related to /etc/resolv.conf not pointing to 127.0.0.1.", "diagnosis_ip_connected_ipv4": "The server is connected to the Internet through IPv4!", "diagnosis_ip_connected_ipv6": "The server is connected to the Internet through IPv6!", @@ -231,32 +255,33 @@ "diagnosis_ip_no_ipv4": "The server does not have working IPv4.", "diagnosis_ip_no_ipv6": "The server does not have working IPv6.", "diagnosis_ip_no_ipv6_tip": "Having a working IPv6 is not mandatory for your server to work, but it is better for the health of the Internet as a whole. IPv6 should usually be automatically configured by the system or your provider if it's available. Otherwise, you might need to configure a few things manually as explained in the documentation here: https://yunohost.org/#/ipv6. If you cannot enable IPv6 or if it seems too technical for you, you can also safely ignore this warning.", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 should usually be automatically configured by the system or your provider if it's available. Otherwise, you might need to configure a few things manually as explained in the documentation here: https://yunohost.org/#/ipv6.", "diagnosis_ip_not_connected_at_all": "The server does not seem to be connected to the Internet at all!?", "diagnosis_ip_weird_resolvconf": "DNS resolution seems to be working, but it looks like you're using a custom /etc/resolv.conf.", "diagnosis_ip_weird_resolvconf_details": "The file /etc/resolv.conf should be a symlink to /etc/resolvconf/run/resolv.conf itself pointing to 127.0.0.1 (dnsmasq). If you want to manually configure DNS resolvers, please edit /etc/resolv.dnsmasq.conf.", "diagnosis_mail_blacklist_listed_by": "Your IP or domain {item} is blacklisted on {blacklist_name}", "diagnosis_mail_blacklist_ok": "The IPs and domains used by this server do not appear to be blacklisted", "diagnosis_mail_blacklist_reason": "The blacklist reason is: {reason}", - "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixed it, feel free to ask for your IP or domaine to be removed on {blacklist_website}", + "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixing it, feel free to ask for your IP or domain to be removed on {blacklist_website}", "diagnosis_mail_ehlo_bad_answer": "A non-SMTP service answered on port 25 on IPv{ipversion}", - "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an other machine answering instead of your server.", - "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from outside in IPv{ipversion}.", + "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an another machine answering instead of your server.", + "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from the outside in IPv{ipversion}.", "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}", "diagnosis_mail_ehlo_ok": "The SMTP mail server is reachable from the outside and therefore is able to receive emails!", "diagnosis_mail_ehlo_unreachable": "The SMTP mail server is unreachable from the outside on IPv{ipversion}. It won't be able to receive emails.", "diagnosis_mail_ehlo_unreachable_details": "Could not open a connection on port 25 to your server in IPv{ipversion}. It appears to be unreachable.
1. The most common cause for this issue is that port 25 is not correctly forwarded to your server.
2. You should also make sure that service postfix is running.
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", "diagnosis_mail_ehlo_wrong": "A different SMTP mail server answers on IPv{ipversion}. Your server will probably not be able to receive emails.", "diagnosis_mail_ehlo_wrong_details": "The EHLO received by the remote diagnoser in IPv{ipversion} is different from your server's domain.
Received EHLO: {wrong_ehlo}
Expected: {right_ehlo}
The most common cause for this issue is that port 25 is not correctly forwarded to your server. Alternatively, make sure that no firewall or reverse-proxy is interfering.", - "diagnosis_mail_fcrdns_different_from_ehlo_domain": "The reverse DNS is not correctly configured in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Reverse DNS is not correctly configured for IPv{ipversion}. Some emails may fail to get delivered or be flagged as spam.", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Current reverse DNS: {rdns_domain}
Expected value: {ehlo_domain}", - "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.", + "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or be flagged as spam.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If you are experiencing issues because of this, consider the following solutions:
- Some ISP provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- Or it's possible to switch to a different provider", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If your reverse DNS is correctly configured for IPv4, you can try disabling the use of IPv6 when sending emails by running yunohost settings set smtp.allow_ipv6 -v off. Note: this last solution means that you won't be able to send or receive emails from the few IPv6-only servers out there.", - "diagnosis_mail_fcrdns_nok_details": "You should first try to configure the reverse DNS with {ehlo_domain} in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If your reverse DNS is correctly configured for IPv4, you can try disabling the use of IPv6 when sending emails by running yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Note: this last solution means that you won't be able to send or receive emails from the few IPv6-only servers out there.", + "diagnosis_mail_fcrdns_nok_details": "You should first try to configure reverse DNS with {ehlo_domain} in your internet router interface or your hosting provider interface. (Some hosting providers may require you to send them a support ticket for this).", "diagnosis_mail_fcrdns_ok": "Your reverse DNS is correctly configured!", "diagnosis_mail_outgoing_port_25_blocked": "The SMTP mail server cannot send emails to other servers because outgoing port 25 is blocked in IPv{ipversion}.", - "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider", + "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting providers may require you to send them a support ticket for this).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass these kinds of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider", "diagnosis_mail_outgoing_port_25_ok": "The SMTP mail server is able to send emails (outgoing port 25 is not blocked).", "diagnosis_mail_queue_ok": "{nb_pending} pending emails in the mail queues", "diagnosis_mail_queue_too_big": "Too many pending emails in mail queue ({nb_pending} emails)", @@ -270,38 +295,45 @@ "diagnosis_ports_could_not_diagnose_details": "Error: {error}", "diagnosis_ports_forwarding_tip": "To fix this issue, you most probably need to configure port forwarding on your internet router as described in https://yunohost.org/isp_box_config", "diagnosis_ports_needed_by": "Exposing this port is needed for {category} features (service {service})", - "diagnosis_ports_ok": "Port {port} is reachable from outside.", - "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from outside in IPv{failed}.", - "diagnosis_ports_unreachable": "Port {port} is not reachable from outside.", - "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process that ate up to much memory. Summary of the processes killed:\n{kills_summary}", + "diagnosis_ports_ok": "Port {port} is reachable from the outside.", + "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from the outside in IPv{failed}.", + "diagnosis_ports_unreachable": "Port {port} is not reachable from the outside.", + "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process consuming too much memory. Summary of the processes killed:\n{kills_summary}", "diagnosis_ram_low": "The system has {available} ({available_percent}%) RAM available (out of {total}). Be careful.", "diagnosis_ram_ok": "The system still has {available} ({available_percent}%) RAM available out of {total}.", "diagnosis_ram_verylow": "The system has only {available} ({available_percent}%) RAM available! (out of {total})", - "diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!", + "diagnosis_regenconf_allgood": "All configuration files are in line with the recommended configuration!", "diagnosis_regenconf_manually_modified": "Configuration file {file} appears to have been manually modified.", "diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with yunohost tools regen-conf {category} --dry-run --with-diff and force the reset to the recommended configuration with yunohost tools regen-conf {category} --force", "diagnosis_rootfstotalspace_critical": "The root filesystem only has a total of {space} which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16 GB for the root filesystem.", "diagnosis_rootfstotalspace_warning": "The root filesystem only has a total of {space}. This may be okay, but be careful because ultimately you may run out of disk space quickly... It's recommended to have at least 16 GB for the root filesystem.", - "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown criticial security vulnerability", - "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more infos.", + "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown critical security vulnerability", + "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more info.", "diagnosis_services_bad_status": "Service {service} is {status} :(", "diagnosis_services_bad_status_tip": "You can try to restart the service, and if it doesn't work, have a look at the service logs in the webadmin (from the command line, you can do this with yunohost service restart {service} and yunohost service log {service}).", "diagnosis_services_conf_broken": "Configuration is broken for service {service}!", "diagnosis_services_running": "Service {service} is running!", - "diagnosis_sshd_config_inconsistent": "It looks like the SSH port was manually modified in /etc/ssh/sshd_config. Since YunoHost 4.2, a new global setting 'security.ssh.port' is available to avoid manually editing the configuration.", - "diagnosis_sshd_config_inconsistent_details": "Please run yunohost settings set security.ssh.port -v YOUR_SSH_PORT to define the SSH port, and check yunohost tools regen-conf ssh --dry-run --with-diff and yunohost tools regen-conf ssh --force to reset your conf to the YunoHost recommendation.", + "diagnosis_sshd_config_inconsistent": "It looks like the SSH port was manually modified in /etc/ssh/sshd_config. Since YunoHost 4.2, a new global setting 'security.ssh.ssh_port' is available to avoid manually editing the configuration.", + "diagnosis_sshd_config_inconsistent_details": "Please run yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT to define the SSH port, and check yunohost tools regen-conf ssh --dry-run --with-diff and yunohost tools regen-conf ssh --force to reset your conf to the YunoHost recommendation.", "diagnosis_sshd_config_insecure": "The SSH configuration appears to have been manually modified, and is insecure because it contains no 'AllowGroups' or 'AllowUsers' directive to limit access to authorized users.", "diagnosis_swap_none": "The system has no swap at all. You should consider adding at least {recommended} of swap to avoid situations where the system runs out of memory.", "diagnosis_swap_notsomuch": "The system has only {total} swap. You should consider having at least {recommended} to avoid situations where the system runs out of memory.", "diagnosis_swap_ok": "The system has {total} of swap!", - "diagnosis_swap_tip": "Please be careful and aware that if the server is hosting swap on an SD card or SSD storage, it may drastically reduce the life expectancy of the device`.", + "diagnosis_swap_tip": "Please be careful and aware that if the server is hosting swap on an SD card or SSD storage, it may drastically reduce the life expectancy of the device.", "diagnosis_unknown_categories": "The following categories are unknown: {categories}", + "diagnosis_using_stable_codename": "apt (the system's package manager) is currently configured to install packages from codename 'stable', instead of the codename of the current Debian version (bullseye).", + "diagnosis_using_stable_codename_details": "This is usually caused by incorrect configuration from your hosting provider. This is dangerous, because as soon as the next Debian version becomes the new 'stable', apt will want to upgrade all system packages without going through a proper migration procedure. It is recommended to fix this by editing the apt source for base Debian repository, and replace the stable keyword by bullseye. The corresponding configuration file should be /etc/apt/sources.list, or a file in /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing": "apt (the system's package manager) is currently configured to install any 'testing' upgrade for YunoHost core.", + "diagnosis_using_yunohost_testing_details": "This is probably OK if you know what you are doing, but pay attention to the release notes before installing YunoHost upgrades! If you want to disable 'testing' upgrades, you should remove the testing keyword from /etc/apt/sources.list.d/yunohost.list.", "disk_space_not_sufficient_install": "There is not enough disk space left to install this application", "disk_space_not_sufficient_update": "There is not enough disk space left to update this application", - "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated in YunoHost.", + "domain_cannot_add_muc_upload": "You cannot add domains starting with 'muc.'. This kind of name is reserved for the XMPP multi-users chat feature integrated into YunoHost.", + "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated into YunoHost.", "domain_cannot_remove_main": "You cannot remove '{domain}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n '; here is the list of candidate domains: {other_domains}", "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add ', then set is as the main domain using 'yunohost domain main-domain -n ' and then you can remove the domain '{domain}' using 'yunohost domain remove {domain}'.'", "domain_cert_gen_failed": "Could not generate certificate", + "domain_config_acme_eligible": "ACME eligibility", + "domain_config_acme_eligible_explain": "This domain doesn't seem ready for a Let's Encrypt certificate. Please check your DNS configuration and HTTP server reachability. The 'DNS records' and 'Web' section in the diagnosis page can help you understand what is misconfigured.", "domain_config_api_protocol": "API protocol", "domain_config_auth_application_key": "Application key", "domain_config_auth_application_secret": "Application secret key", @@ -310,11 +342,24 @@ "domain_config_auth_key": "Authentication key", "domain_config_auth_secret": "Authentication secret", "domain_config_auth_token": "Authentication token", + "domain_config_cert_install": "Install Let's Encrypt certificate", + "domain_config_cert_issuer": "Certification authority", + "domain_config_cert_no_checks": "Ignore diagnosis checks", + "domain_config_cert_renew": "Renew Let's Encrypt certificate", + "domain_config_cert_renew_help": "Certificate will be automatically renewed during the last 15 days of validity. You can manually renew it if you want to. (Not recommended).", + "domain_config_cert_summary": "Certificate status", + "domain_config_cert_summary_abouttoexpire": "Current certificate is about to expire. It should soon be renewed automatically.", + "domain_config_cert_summary_expired": "CRITICAL: Current certificate is not valid! HTTPS won't work at all!", + "domain_config_cert_summary_letsencrypt": "Great! You're using a valid Let's Encrypt certificate!", + "domain_config_cert_summary_ok": "Okay, current certificate looks good!", + "domain_config_cert_summary_selfsigned": "WARNING: Current certificate is self-signed. Browsers will display a spooky warning to new visitors!", + "domain_config_cert_validity": "Validity", "domain_config_default_app": "Default app", - "domain_config_features_disclaimer": "So far, enabling/disabling mail or XMPP features only impact the recommended and automatic DNS configuration, not system configurations!", + "domain_config_default_app_help": "People will automatically be redirected to this app when opening this domain. If no app is specified, people are redirected to the user portal login form.", "domain_config_mail_in": "Incoming emails", "domain_config_mail_out": "Outgoing emails", "domain_config_xmpp": "Instant messaging (XMPP)", + "domain_config_xmpp_help": "NB: some XMPP features will require that you update your DNS records and regenerate your Lets Encrypt certificate to be enabled", "domain_created": "Domain created", "domain_creation_failed": "Unable to create domain {domain}: {error}", "domain_deleted": "Domain deleted", @@ -347,7 +392,7 @@ "domains_available": "Available domains:", "done": "Done", "downloading": "Downloading...", - "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.", + "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a` and/or `sudo dpkg --audit`.", "dpkg_lock_not_available": "This command can't be run right now because another program seems to be using the lock of dpkg (the system package manager)", "dyndns_could_not_check_available": "Could not check if {domain} is available on {provider}.", "dyndns_domain_not_provided": "DynDNS provider {provider} cannot provide domain {domain}.", @@ -360,42 +405,55 @@ "dyndns_registered": "DynDNS domain registered", "dyndns_registration_failed": "Could not register DynDNS domain: {error}", "dyndns_unavailable": "The domain '{domain}' is unavailable.", - "experimental_feature": "Warning: This feature is experimental and not considered stable, you should not use it unless you know what you are doing.", "extracting": "Extracting...", "field_invalid": "Invalid field '{}'", "file_does_not_exist": "The file {path} does not exist.", "firewall_reload_failed": "Could not reload the firewall", "firewall_reloaded": "Firewall reloaded", "firewall_rules_cmd_failed": "Some firewall rule commands have failed. More info in log.", - "global_settings_bad_choice_for_enum": "Bad choice for setting {setting}, received '{choice}', but available choices are: {available_choices}", - "global_settings_bad_type_for_setting": "Bad type for setting {setting}, received {received_type}, expected {expected_type}", - "global_settings_cant_open_settings": "Could not open settings file, reason: {reason}", - "global_settings_cant_serialize_settings": "Could not serialize settings data, reason: {reason}", - "global_settings_cant_write_settings": "Could not save settings file, reason: {reason}", - "global_settings_key_doesnt_exists": "The key '{settings_key}' does not exist in the global settings, you can see all the available keys by running 'yunohost settings list'", - "global_settings_reset_success": "Previous settings now backed up to {path}", - "global_settings_setting_backup_compress_tar_archives": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", - "global_settings_setting_pop3_enabled": "Enable the POP3 protocol for the mail server", - "global_settings_setting_security_experimental_enabled": "Enable experimental security features (don't enable this if you don't know what you're doing!)", - "global_settings_setting_security_nginx_compatibility": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", - "global_settings_setting_security_nginx_redirect_to_https": "Redirect HTTP requests to HTTPs by default (DO NOT TURN OFF unless you really know what you're doing!)", - "global_settings_setting_security_password_admin_strength": "Admin password strength", - "global_settings_setting_security_password_user_strength": "User password strength", - "global_settings_setting_security_postfix_compatibility": "Compatibility vs. security tradeoff for the Postfix server. Affects the ciphers (and other security-related aspects)", - "global_settings_setting_security_ssh_compatibility": "Compatibility vs. security tradeoff for the SSH server. Affects the ciphers (and other security-related aspects)", - "global_settings_setting_security_ssh_password_authentication": "Allow password authentication for SSH", - "global_settings_setting_security_ssh_port": "SSH port", - "global_settings_setting_security_webadmin_allowlist": "IP adresses allowed to access the webadmin. Comma-separated.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Allow only some IPs to access the webadmin.", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Allow the use of (deprecated) DSA hostkey for the SSH daemon configuration", - "global_settings_setting_smtp_allow_ipv6": "Allow the use of IPv6 to receive and send mail", - "global_settings_setting_smtp_relay_host": "SMTP relay host to use in order to send mail instead of this yunohost instance. Useful if you are in one of this situation: your 25 port is blocked by your ISP or VPS provider, you have a residential IP listed on DUHL, you are not able to configure reverse DNS or this server is not directly exposed on the internet and you want use an other one to send mails.", - "global_settings_setting_smtp_relay_password": "SMTP relay host password", + "global_settings_reset_success": "Reset global settings", + "global_settings_setting_admin_strength": "Admin password strength requirements", + "global_settings_setting_admin_strength_help": "These requirements are only enforced when initializing or changing the password", + "global_settings_setting_backup_compress_tar_archives": "Compress backups", + "global_settings_setting_backup_compress_tar_archives_help": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", + "global_settings_setting_dns_exposure": "IP versions to consider for DNS configuration and diagnosis", + "global_settings_setting_dns_exposure_help": "NB: This only affects the recommended DNS configuration and diagnosis checks. This does not affect system configurations.", + "global_settings_setting_nginx_compatibility": "NGINX Compatibility", + "global_settings_setting_nginx_compatibility_help": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", + "global_settings_setting_nginx_redirect_to_https": "Force HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "Redirect HTTP requests to HTTPs by default (DO NOT TURN OFF unless you really know what you're doing!)", + "global_settings_setting_passwordless_sudo": "Allow admins to use 'sudo' without re-typing their passwords", + "global_settings_setting_pop3_enabled": "Enable POP3", + "global_settings_setting_pop3_enabled_help": "Enable the POP3 protocol for the mail server", + "global_settings_setting_portal_theme": "Portal theme", + "global_settings_setting_portal_theme_help": "More info regarding creating custom portal themes at https://yunohost.org/theming", + "global_settings_setting_postfix_compatibility": "Postfix Compatibility", + "global_settings_setting_postfix_compatibility_help": "Compatibility vs. security tradeoff for the Postfix server. Affects the ciphers (and other security-related aspects)", + "global_settings_setting_root_access_explain": "On Linux systems, 'root' is the absolute admin. In YunoHost context, direct 'root' SSH login is by default disable - except from the local network of the server. Members of the 'admins' group can use the sudo command to act as root from the command line. However, it can be helpful to have a (robust) root password to debug the system if for some reason regular admins can not login anymore.", + "global_settings_setting_root_password": "New root password", + "global_settings_setting_root_password_confirm": "New root password (confirm)", + "global_settings_setting_security_experimental_enabled": "Experimental security features", + "global_settings_setting_security_experimental_enabled_help": "Enable experimental security features (don't enable this if you don't know what you're doing!)", + "global_settings_setting_smtp_allow_ipv6": "Allow IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Allow the use of IPv6 to receive and send mail", + "global_settings_setting_smtp_relay_enabled": "Enable SMTP relay", + "global_settings_setting_smtp_relay_enabled_help": "Enable the SMTP relay to use in order to send mail instead of this yunohost instance. Useful if you are in one of this situation: your 25 port is blocked by your ISP or VPS provider, you have a residential IP listed on DUHL, you are not able to configure reverse DNS or this server is not directly exposed on the internet and you want use an other one to send mails.", + "global_settings_setting_smtp_relay_host": "SMTP relay host", + "global_settings_setting_smtp_relay_password": "SMTP relay password", "global_settings_setting_smtp_relay_port": "SMTP relay port", - "global_settings_setting_smtp_relay_user": "SMTP relay user account", - "global_settings_setting_ssowat_panel_overlay_enabled": "Enable SSOwat panel overlay", - "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key}', discard it and save it in /etc/yunohost/settings-unknown.json", - "global_settings_unknown_type": "Unexpected situation, the setting {setting} appears to have the type {unknown_type} but it is not a type supported by the system.", + "global_settings_setting_smtp_relay_user": "SMTP relay user", + "global_settings_setting_ssh_compatibility": "SSH Compatibility", + "global_settings_setting_ssh_compatibility_help": "Compatibility vs. security tradeoff for the SSH server. Affects the ciphers (and other security-related aspects). See https://infosec.mozilla.org/guidelines/openssh for more info.", + "global_settings_setting_ssh_password_authentication": "Password authentication", + "global_settings_setting_ssh_password_authentication_help": "Allow password authentication for SSH", + "global_settings_setting_ssh_port": "SSH port", + "global_settings_setting_ssowat_panel_overlay_enabled": "Enable the small 'YunoHost' portal shortcut square on apps", + "global_settings_setting_user_strength": "User password strength requirements", + "global_settings_setting_user_strength_help": "These requirements are only enforced when initializing or changing the password", + "global_settings_setting_webadmin_allowlist": "Webadmin IP allowlist", + "global_settings_setting_webadmin_allowlist_enabled": "Enable Webadmin IP allowlist", + "global_settings_setting_webadmin_allowlist_enabled_help": "Allow only some IPs to access the webadmin.", + "global_settings_setting_webadmin_allowlist_help": "IP adresses allowed to access the webadmin.", "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).", "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to a variation of characters (uppercase, lowercase, digits and special characters).", "group_already_exist": "Group {group} already exists", @@ -409,22 +467,29 @@ "group_creation_failed": "Could not create the group '{group}': {error}", "group_deleted": "Group '{group}' deleted", "group_deletion_failed": "Could not delete the group '{group}': {error}", + "group_mailalias_add": "The email alias '{mail}' will be added to the group '{group}'", + "group_mailalias_remove": "The email alias '{mail}' will be removed from the group '{group}'", + "group_no_change": "Nothing to change for group '{group}'", "group_unknown": "The group '{group}' is unknown", + "group_update_aliases": "Updating aliases for group '{group}'", "group_update_failed": "Could not update the group '{group}': {error}", "group_updated": "Group '{group}' updated", + "group_user_add": "The user '{user}' will be added to the group '{group}'", "group_user_already_in_group": "User {user} is already in group {group}", "group_user_not_in_group": "User {user} is not in group {group}", + "group_user_remove": "The user '{user}' will be removed from the group '{group}'", "hook_exec_failed": "Could not run script: {path}", "hook_exec_not_terminated": "Script did not finish properly: {path}", "hook_json_return_error": "Could not read return from hook {path}. Error: {msg}. Raw content: {raw_content}", "hook_list_by_invalid": "This property can not be used to list hooks", "hook_name_unknown": "Unknown hook name '{name}'", "installation_complete": "Installation completed", + "invalid_credentials": "Invalid password or username", "invalid_number": "Must be a number", "invalid_number_max": "Must be lesser than {max}", "invalid_number_min": "Must be greater than {min}", - "invalid_password": "Invalid password", "invalid_regex": "Invalid regex:'{regex}'", + "invalid_shell": "Invalid shell: {shell}", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "ldap_attribute_already_exists": "LDAP attribute '{attribute}' already exists with value '{value}'", @@ -462,8 +527,11 @@ "log_permission_url": "Update URL related to permission '{}'", "log_regen_conf": "Regenerate system configurations '{}'", "log_remove_on_failed_install": "Remove '{}' after a failed installation", - "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", + "log_resource_snippet": "Provisioning/deprovisioning/updating a resource", "log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain", + "log_settings_reset": "Reset setting", + "log_settings_reset_all": "Reset all settings", + "log_settings_set": "Apply settings", "log_tools_migrations_migrate_forward": "Run migrations", "log_tools_postinstall": "Postinstall your YunoHost server", "log_tools_reboot": "Reboot your server", @@ -481,17 +549,16 @@ "mail_alias_remove_failed": "Could not remove e-mail alias '{mail}'", "mail_domain_unknown": "Invalid e-mail address for domain '{domain}'. Please, use a domain administrated by this server.", "mail_forward_remove_failed": "Could not remove e-mail forwarding '{mail}'", - "mail_unavailable": "This e-mail address is reserved and shall be automatically allocated to the very first user", + "mail_unavailable": "This e-mail address is reserved for the admins group", "mailbox_disabled": "E-mail turned off for user {user}", "mailbox_used_space_dovecot_down": "The Dovecot mailbox service needs to be up if you want to fetch used mailbox space", "main_domain_change_failed": "Unable to change the main domain", "main_domain_changed": "The main domain has been changed", "migration_0021_cleaning_up": "Cleaning up cache and packages not useful anymore...", - "migration_0021_venv_regen_failed": "The virtual environment '{venv}' failed to regenerate, you probably need to run the command `yunohost app upgrade --force`", "migration_0021_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.", "migration_0021_main_upgrade": "Starting main upgrade...", "migration_0021_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}", - "migration_0021_not_buster": "The current Debian distribution is not Buster!", + "migration_0021_not_buster2": "The current Debian distribution is not Buster! If you already ran the Buster->Bullseye migration, then this error is symptomatic of the fact that the migration procedure was not 100% succesful (otherwise YunoHost would have flagged it as completed). It is recommended to investigate what happened with the support team, who will need the **full** log of the `migration, which can be found in Tools > Logs in the webadmin.", "migration_0021_not_enough_free_space": "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.", "migration_0021_patch_yunohost_conflicts": "Applying patch to workaround conflict issue...", "migration_0021_patching_sources_list": "Patching the sources.lists...", @@ -504,15 +571,17 @@ "migration_0023_postgresql_11_not_installed": "PostgreSQL was not installed on your system. Nothing to do.", "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 is installed, but not PostgreSQL 13!? Something weird might have happened on your system :(...", "migration_0024_rebuild_python_venv_broken_app": "Skipping {app} because virtualenv can't easily be rebuilt for this app. Instead, you should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.", - "migration_0024_rebuild_python_venv_disclaimer_base": "Following the upgrade to Debian Bullseye, some Python applications needs to be partially rebuilt to get converted to the new Python version shipped in Debian (in technical terms: what's called the 'virtualenv' needs to be recreated). In the meantime, those Python applications may not work. YunoHost can attempt to rebuild the virtualenv for some of those, as detailed below. For other apps, or if the rebuild attempt fails, you will need to manually force an upgrade for those apps.", - "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Rebuilding the virtualenv will be attempted for the following apps (NB: the operation may take some time!): {rebuild_apps}", - "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs can't be rebuilt automatically for those apps. You need to force an upgrade for those, which can be done from the command line with: `yunohost app upgrade -f APP`: {ignored_apps}", - "migration_0024_rebuild_python_venv_in_progress": "Now attempting to rebuild python virtualenv for `{app}`", - "migration_0024_rebuild_python_venv_failed": "Failed to rebuild the python virtual env for {app}. The app may not work as long as this is not resolved. You should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_disclaimer_base": "Following the upgrade to Debian Bullseye, some Python applications needs to be partially rebuilt to get converted to the new Python version shipped in Debian (in technical terms: what's called the 'virtualenv' needs to be recreated). In the meantime, those Python applications may not work. YunoHost can attempt to rebuild the virtualenv for some of those, as detailed below. For other apps, or if the rebuild attempt fails, you will need to manually force an upgrade for those apps.", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs can't be rebuilt automatically for those apps. You need to force an upgrade for those, which can be done from the command line with: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Rebuilding the virtualenv will be attempted for the following apps (NB: the operation may take some time!): {rebuild_apps}", + "migration_0024_rebuild_python_venv_failed": "Failed to rebuild the Python virtualenv for {app}. The app may not work as long as this is not resolved. You should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_in_progress": "Now attempting to rebuild the Python virtualenv for `{app}`", "migration_description_0021_migrate_to_bullseye": "Upgrade the system to Debian Bullseye and YunoHost 11.x", "migration_description_0022_php73_to_php74_pools": "Migrate php7.3-fpm 'pool' conf files to php7.4", "migration_description_0023_postgresql_11_to_13": "Migrate databases from PostgreSQL 11 to 13", - "migration_description_0024_rebuild_python_venv": "Repair python app after bullseye migration", + "migration_description_0024_rebuild_python_venv": "Repair Python app after bullseye migration", + "migration_description_0025_global_settings_to_configpanel": "Migrate legacy global settings nomenclature to the new, modern nomenclature", + "migration_description_0026_new_admins_group": "Migrate to the new 'multiple admins' system", "migration_ldap_backup_before_migration": "Creating a backup of LDAP database and apps settings prior to the actual migration.", "migration_ldap_can_not_backup_before_migration": "The backup of the system could not be completed before the migration failed. Error: {error}", "migration_ldap_migration_failed_trying_to_rollback": "Could not migrate... trying to roll back the system.", @@ -528,8 +597,8 @@ "migrations_need_to_accept_disclaimer": "To run the migration {id}, your must accept the following disclaimer:\n---\n{disclaimer}\n---\nIf you accept to run the migration, please re-run the command with the option '--accept-disclaimer'.", "migrations_no_migrations_to_run": "No migrations to run", "migrations_no_such_migration": "There is no migration called '{id}'", - "migrations_not_pending_cant_skip": "Those migrations are not pending, so cannot be skipped: {ids}", - "migrations_pending_cant_rerun": "Those migrations are still pending, so cannot be run again: {ids}", + "migrations_not_pending_cant_skip": "These migrations are not pending, so cannot be skipped: {ids}", + "migrations_pending_cant_rerun": "These migrations are still pending, so cannot be run again: {ids}", "migrations_running_forward": "Running migration {id}...", "migrations_skip_migration": "Skipping migration {id}...", "migrations_success_forward": "Migration {id} completed", @@ -537,7 +606,9 @@ "not_enough_disk_space": "Not enough free space on '{path}'", "operation_interrupted": "The operation was manually interrupted?", "other_available_options": "... and {n} other available options not shown", + "password_confirmation_not_the_same": "The password and its confirmation do not match", "password_listed": "This password is among the most used passwords in the world. Please choose something more unique.", + "password_too_long": "Please choose a password shorter than 127 characters", "password_too_simple_1": "The password needs to be at least 8 characters long", "password_too_simple_2": "The password needs to be at least 8 characters long and contain a digit, upper and lower characters", "password_too_simple_3": "The password needs to be at least 8 characters long and contain a digit, upper, lower and special characters", @@ -546,8 +617,9 @@ "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", "pattern_email": "Must be a valid e-mail address, without '+' symbol (e.g. someone@example.com)", "pattern_email_forward": "Must be a valid e-mail address, '+' symbol accepted (e.g. someone+tag@example.com)", - "pattern_firstname": "Must be a valid first name", - "pattern_lastname": "Must be a valid last name", + "pattern_firstname": "Must be a valid first name (at least 3 chars)", + "pattern_fullname": "Must be a valid full name (at least 3 chars)", + "pattern_lastname": "Must be a valid last name (at least 3 chars)", "pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to not have a quota", "pattern_password": "Must be at least 3 characters long", "pattern_password_app": "Sorry, passwords can not contain the following characters: {forbidden_chars}", @@ -590,6 +662,7 @@ "regenconf_would_be_updated": "The configuration would have been updated for category '{category}'", "regex_incompatible_with_tile": "/!\\ Packagers! Permission '{permission}' has show_tile set to 'true' and you therefore cannot define a regex URL as the main URL", "regex_with_only_domain": "You can't use a regex for domain, only for path", + "registrar_infos": "Registrar infos", "restore_already_installed_app": "An app with the ID '{app}' is already installed", "restore_already_installed_apps": "The following apps can't be restored because they are already installed: {apps}", "restore_backup_too_old": "This backup archive can not be restored because it comes from a too-old YunoHost version.", @@ -606,8 +679,8 @@ "restore_running_app_script": "Restoring the app '{app}'...", "restore_running_hooks": "Running restoration hooks...", "restore_system_part_failed": "Could not restore the '{part}' system part", + "root_password_changed": "root's password was changed", "root_password_desynchronized": "The admin password was changed, but YunoHost could not propagate this to the root password!", - "root_password_replaced_by_admin_password": "Your root password have been replaced by your admin password.", "server_reboot": "The server will reboot", "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers}]", "server_shutdown": "The server will shut down", @@ -688,9 +761,10 @@ "user_unknown": "Unknown user: {user}", "user_update_failed": "Could not update user {user}: {error}", "user_updated": "User info changed", + "visitors": "Visitors", "yunohost_already_installed": "YunoHost is already installed", "yunohost_configured": "YunoHost is now configured", "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", - "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - adding a first user through the 'Users' section of the webadmin (or 'yunohost user create ' in command-line);\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." -} + "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." +} \ No newline at end of file diff --git a/locales/eo.json b/locales/eo.json index 8ac32d4ce..b0bdf280b 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -1,6 +1,4 @@ { - "admin_password_change_failed": "Ne povis ŝanĝi pasvorton", - "admin_password_changed": "La pasvorto de administrado estis ŝanĝita", "app_already_installed": "{app} estas jam instalita", "app_already_up_to_date": "{app} estas jam ĝisdata", "app_argument_required": "Parametro {name} estas bezonata", @@ -26,7 +24,6 @@ "service_disabled": "La servo '{service}' ne plu komenciĝos kiam sistemo ekos.", "action_invalid": "Nevalida ago « {action} »", "admin_password": "Pasvorto de la estro", - "admin_password_too_long": "Bonvolu elekti pasvorton pli mallonga ol 127 signoj", "already_up_to_date": "Nenio por fari. Ĉio estas jam ĝisdatigita.", "app_argument_choice_invalid": "Uzu unu el ĉi tiuj elektoj '{choices}' por la argumento '{name}' anstataŭ '{value}'", "app_argument_invalid": "Elektu validan valoron por la argumento '{name}': {error}", @@ -66,7 +63,6 @@ "app_upgrade_failed": "Ne povis ĝisdatigi {app}: {error}", "app_upgrade_several_apps": "La sekvaj apliko estos altgradigitaj: {apps}", "backup_archive_open_failed": "Ne povis malfermi la rezervan ar archiveivon", - "ask_lastname": "Familia nomo", "app_start_backup": "Kolekti dosierojn por esti subtenata por {app}...", "backup_archive_name_exists": "Rezerva arkivo kun ĉi tiu nomo jam ekzistas.", "backup_applying_method_tar": "Krei la rezervon TAR Arkivo...", @@ -86,8 +82,6 @@ "backup_applying_method_copy": "Kopii ĉiujn dosierojn por sekurigi...", "backup_couldnt_bind": "Ne povis ligi {src} al {dest}.", "ask_password": "Pasvorto", - "app_requirements_unmeet": "Postuloj ne estas renkontitaj por {app}, la pakaĵo {pkgname} ({version}) devas esti {spec}", - "ask_firstname": "Antaŭnomo", "backup_ask_for_copying_if_needed": "Ĉu vi volas realigi la sekurkopion uzante {size} MB provizore? (Ĉi tiu maniero estas uzata ĉar iuj dosieroj ne povus esti pretigitaj per pli efika metodo.)", "backup_mount_archive_for_restore": "Preparante arkivon por restarigo …", "backup_csv_creation_failed": "Ne povis krei la CSV-dosieron bezonatan por restarigo", @@ -105,7 +99,6 @@ "field_invalid": "Nevalida kampo '{}'", "log_app_makedefault": "Faru '{}' la defaŭlta apliko", "backup_system_part_failed": "Ne eblis sekurkopi la sistemon de '{part}'", - "global_settings_setting_security_postfix_compatibility": "Kongruo vs sekureca kompromiso por la Postfix-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", "group_unknown": "La grupo '{group}' estas nekonata", "mailbox_disabled": "Retpoŝto malŝaltita por uzanto {user}", "migrations_dependencies_not_satisfied": "Rulu ĉi tiujn migradojn: '{dependencies_id}', antaŭ migrado {id}.", @@ -169,10 +162,8 @@ "regenconf_file_manually_modified": "La agorddosiero '{conf}' estis modifita permane kaj ne estos ĝisdatigita", "regenconf_would_be_updated": "La agordo estus aktualigita por la kategorio '{category}'", "certmanager_cert_install_success_selfsigned": "Mem-subskribita atestilo nun instalita por la domajno '{domain}'", - "global_settings_unknown_setting_from_settings_file": "Nekonata ŝlosilo en agordoj: '{setting_key}', forĵetu ĝin kaj konservu ĝin en /etc/yunohost/settings-unknown.json", "regenconf_file_backed_up": "Agordodosiero '{conf}' estis rezervita al '{backup}'", "iptables_unavailable": "Vi ne povas ludi kun iptables ĉi tie. Vi estas en ujo aŭ via kerno ne subtenas ĝin", - "global_settings_cant_write_settings": "Ne eblis konservi agordojn, tial: {reason}", "service_added": "La servo '{service}' estis aldonita", "upnp_disabled": "UPnP malŝaltis", "service_started": "Servo '{service}' komenciĝis", @@ -196,12 +187,10 @@ "log_letsencrypt_cert_renew": "Renovigu '{}' Let's Encrypt atestilon", "backup_output_directory_required": "Vi devas provizi elirejan dosierujon por la sekurkopio", "log_link_to_log": "Plena ŝtipo de ĉi tiu operacio: '{desc} '", - "global_settings_cant_serialize_settings": "Ne eblis serialigi datumojn pri agordoj, motivo: {reason}", "backup_running_hooks": "Kurado de apogaj hokoj …", "unexpected_error": "Io neatendita iris malbone: {error}", "password_listed": "Ĉi tiu pasvorto estas inter la plej uzataj pasvortoj en la mondo. Bonvolu elekti ion pli unikan.", "ssowat_conf_generated": "SSOwat-agordo generita", - "log_remove_on_failed_restore": "Forigu '{}' post malsukcesa restarigo de rezerva ar archiveivo", "dpkg_is_broken": "Vi ne povas fari ĉi tion nun ĉar dpkg/APT (la administrantoj pri pakaĵaj sistemoj) ŝajnas esti rompita stato ... Vi povas provi solvi ĉi tiun problemon per konekto per SSH kaj funkcianta `sudo dpkg --configure -a`.", "certmanager_cert_signing_failed": "Ne povis subskribi la novan atestilon", "log_tools_upgrade": "Ĝisdatigu sistemajn pakaĵojn", @@ -211,7 +200,6 @@ "pattern_mailbox_quota": "Devas esti grandeco kun la sufikso b/k/M/G/T aŭ 0 por ne havi kvoton", "user_deletion_failed": "Ne povis forigi uzanton {user}: {error}", "backup_with_no_backup_script_for_app": "La app '{app}' ne havas sekretan skripton. Ignorante.", - "global_settings_key_doesnt_exists": "La ŝlosilo '{settings_key}' ne ekzistas en la tutmondaj agordoj, vi povas vidi ĉiujn disponeblajn klavojn per uzado de 'yunohost settings list'", "dyndns_no_domain_registered": "Neniu domajno registrita ĉe DynDNS", "dyndns_could_not_check_available": "Ne povis kontroli ĉu {domain} haveblas sur {provider}.", "hook_exec_not_terminated": "Skripto ne finiĝis ĝuste: {path}", @@ -236,9 +224,6 @@ "restore_nothings_done": "Nenio estis restarigita", "log_tools_postinstall": "Afiŝu vian servilon YunoHost", "dyndns_unavailable": "La domajno '{domain}' ne haveblas.", - "experimental_feature": "Averto: Ĉi tiu funkcio estas eksperimenta kaj ne konsiderata stabila, vi ne uzu ĝin krom se vi scias kion vi faras.", - "root_password_replaced_by_admin_password": "Via radika pasvorto estis anstataŭigita per via administra pasvorto.", - "global_settings_setting_security_password_user_strength": "Uzanto pasvorta forto", "restore_may_be_not_enough_disk_space": "Via sistemo ne ŝajnas havi sufiĉe da spaco (libera: {free_space} B, necesa spaco: {needed_space} B, sekureca marĝeno: {margin} B)", "log_corrupted_md_file": "La YAD-metadata dosiero asociita kun protokoloj estas damaĝita: '{md_file}\nEraro: {error} '", "downloading": "Elŝutante …", @@ -264,7 +249,6 @@ "log_user_delete": "Forigi uzanton '{}'", "dyndns_ip_updated": "Ĝisdatigis vian IP sur DynDNS", "regenconf_up_to_date": "La agordo jam estas ĝisdatigita por kategorio '{category}'", - "global_settings_setting_security_ssh_compatibility": "Kongruo vs sekureca kompromiso por la SSH-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", "migrations_need_to_accept_disclaimer": "Por funkciigi la migradon {id}, via devas akcepti la sekvan malakcepton:\n---\n{disclaimer}\n---\nSe vi akceptas funkcii la migradon, bonvolu rekonduki la komandon kun la opcio '--accept-disclaimer'.", "regenconf_file_remove_failed": "Ne povis forigi la agordodosieron '{conf}'", "not_enough_disk_space": "Ne sufiĉe libera spaco sur '{path}'", @@ -277,7 +261,6 @@ "user_unknown": "Nekonata uzanto: {user}", "migrations_to_be_ran_manually": "Migrado {id} devas funkcii permane. Bonvolu iri al Iloj → Migradoj en la retpaĝa paĝo, aŭ kuri `yunohost tools migrations run`.", "certmanager_cert_renew_success": "Ni Ĉifru atestilon renovigitan por la domajno '{domain}'", - "global_settings_reset_success": "Antaŭaj agordoj nun estas rezervitaj al {path}", "pattern_domain": "Devas esti valida domajna nomo (t.e. mia-domino.org)", "dyndns_key_generating": "Generi DNS-ŝlosilon ... Eble daŭros iom da tempo.", "restore_running_app_script": "Restarigi la programon '{app}'…", @@ -295,7 +278,6 @@ "log_backup_restore_system": "Restarigi sistemon de rezerva arkivo", "log_app_change_url": "Ŝanĝu la URL de la apliko '{}'", "service_already_started": "La servo '{service}' jam funkcias", - "global_settings_setting_security_password_admin_strength": "Admin pasvorta forto", "service_reload_or_restart_failed": "Ne povis reŝargi aŭ rekomenci la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}", "migrations_list_conflict_pending_done": "Vi ne povas uzi ambaŭ '--previous' kaj '--done' samtempe.", "server_shutdown_confirm": "La servilo haltos tuj, ĉu vi certas? [{answers}]", @@ -310,10 +292,8 @@ "password_too_simple_4": "La pasvorto bezonas almenaŭ 12 signojn kaj enhavas ciferon, majuskle, pli malaltan kaj specialajn signojn", "regenconf_file_updated": "Agordodosiero '{conf}' ĝisdatigita", "log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log show {name}'", - "global_settings_setting_security_nginx_compatibility": "Kongruo vs sekureca kompromiso por la TTT-servilo NGINX. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", "restore_complete": "Restarigita", "hook_exec_failed": "Ne povis funkcii skripto: {path}", - "global_settings_cant_open_settings": "Ne eblis malfermi agordojn, tial: {reason}", "user_created": "Uzanto kreita", "certmanager_attempt_to_replace_valid_cert": "Vi provas anstataŭigi bonan kaj validan atestilon por domajno {domain}! (Uzu --forte pretervidi)", "regenconf_updated": "Agordo ĝisdatigita por '{category}'", @@ -331,14 +311,12 @@ "log_selfsigned_cert_install": "Instalu mem-subskribitan atestilon sur '{}' domajno", "log_tools_reboot": "Reklamu vian servilon", "certmanager_cert_install_success": "Ni Ĉifru atestilon nun instalitan por la domajno '{domain}'", - "global_settings_bad_choice_for_enum": "Malbona elekto por agordo {setting}, ricevita '{choice}', sed disponeblaj elektoj estas: {available_choices}", "server_shutdown": "La servilo haltos", "log_tools_migrations_migrate_forward": "Kuru migradoj", "regenconf_now_managed_by_yunohost": "La agorda dosiero '{conf}' nun estas administrata de YunoHost (kategorio {category}).", "server_reboot_confirm": "Ĉu la servilo rekomencos tuj, ĉu vi certas? [{answers}]", "log_app_install": "Instalu la aplikon '{}'", "service_description_dnsmasq": "Traktas rezolucion de domajna nomo (DNS)", - "global_settings_unknown_type": "Neatendita situacio, la agordo {setting} ŝajnas havi la tipon {unknown_type} sed ĝi ne estas tipo subtenata de la sistemo.", "domain_hostname_failed": "Ne povis agordi novan gastigilon. Ĉi tio eble kaŭzos problemon poste (eble bone).", "server_reboot": "La servilo rekomenciĝos", "regenconf_failed": "Ne povis regeneri la agordon por kategorio(j): {categories}", @@ -352,7 +330,6 @@ "log_domain_remove": "Forigi domon '{}' de agordo de sistemo", "hook_list_by_invalid": "Ĉi tiu posedaĵo ne povas esti uzata por listigi hokojn", "confirm_app_install_thirdparty": "Danĝero! Ĉi tiu apliko ne estas parto de la aplika katalogo de Yunohost. Instali triajn aplikojn povas kompromiti la integrecon kaj sekurecon de via sistemo. Vi probable ne devas instali ĝin krom se vi scias kion vi faras. NENIU SUBTENO estos provizita se ĉi tiu app ne funkcias aŭ rompas vian sistemon ... Se vi pretas riski ĉiuokaze, tajpu '{answers}'", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permesu uzon de (malaktuala) DSA-hostkey por la agordo de daemon SSH", "dyndns_domain_not_provided": "Provizanto DynDNS {provider} ne povas provizi domajnon {domain}.", "backup_unable_to_organize_files": "Ne povis uzi la rapidan metodon por organizi dosierojn en la ar archiveivo", "password_too_simple_2": "La pasvorto bezonas almenaŭ 8 signojn kaj enhavas ciferon, majusklojn kaj minusklojn", @@ -365,7 +342,6 @@ "domain_cannot_remove_main": "Vi ne povas forigi '{domain}' ĉar ĝi estas la ĉefa domajno, vi bezonas unue agordi alian domajnon kiel la ĉefan domajnon per uzado de 'yunohost domain main-domain -n ', jen la listo de kandidataj domajnoj. : {other_domains}", "service_reloaded_or_restarted": "La servo '{service}' estis reŝarĝita aŭ rekomencita", "log_domain_add": "Aldonu '{}' domajnon en sisteman agordon", - "global_settings_bad_type_for_setting": "Malbona tipo por agordo {setting}, ricevita {received_type}, atendata {expected_type}", "unlimit": "Neniu kvoto", "system_username_exists": "Uzantnomo jam ekzistas en la listo de uzantoj de sistemo", "firewall_reloaded": "Fajroŝirmilo reŝarĝis", @@ -454,7 +430,6 @@ "diagnosis_found_errors_and_warnings": "Trovis {errors} signifaj problemo (j) (kaj {warnings} averto) rilataj al {category}!", "diagnosis_diskusage_low": "Stokado {mountpoint} (sur aparato {device}) nur restas {free} ({free_percent}%) spaco restanta (el {total}). Estu zorgema.", "diagnosis_diskusage_ok": "Stokado {mountpoint} (sur aparato {device}) ankoraŭ restas {free} ({free_percent}%) spaco (el {total})!", - "global_settings_setting_pop3_enabled": "Ebligu la protokolon POP3 por la poŝta servilo", "diagnosis_unknown_categories": "La jenaj kategorioj estas nekonataj: {categories}", "diagnosis_services_running": "Servo {service} funkcias!", "diagnosis_ports_unreachable": "Haveno {port} ne atingeblas de ekstere.", @@ -516,7 +491,6 @@ "diagnosis_http_partially_unreachable": "Domajno {domain} ŝajnas neatingebla per HTTP de ekster la loka reto en IPv {failed}, kvankam ĝi funkcias en IPv {passed}.", "diagnosis_http_nginx_conf_not_up_to_date": "La nginx-agordo de ĉi tiu domajno ŝajnas esti modifita permane, kaj malhelpas YunoHost diagnozi ĉu ĝi atingeblas per HTTP.", "diagnosis_http_nginx_conf_not_up_to_date_details": "Por solvi la situacion, inspektu la diferencon per la komandlinio per yunohost tools regen-conf nginx --dry-run --with-diff kaj se vi aranĝas, apliku la ŝanĝojn per yunohost tools regen-conf nginx --force.", - "global_settings_setting_smtp_allow_ipv6": "Permesu la uzon de IPv6 por ricevi kaj sendi poŝton", "backup_archive_corrupted": "I aspektas kiel la rezerva arkivo '{archive}' estas koruptita: {error}", "backup_archive_cant_retrieve_info_json": "Ne povis ŝarĝi infos por arkivo '{archive}' ... la info.json ne povas esti reprenita (aŭ ne estas valida JSON).", "ask_user_domain": "Domajno uzi por la retpoŝta adreso de la uzanto kaj XMPP-konto", @@ -530,5 +504,11 @@ "app_label_deprecated": "Ĉi tiu komando estas malrekomendita! Bonvolu uzi la novan komandon 'yunohost user permission update' por administri la app etikedo.", "app_argument_password_no_default": "Eraro dum analiza pasvorta argumento '{name}': pasvorta argumento ne povas havi defaŭltan valoron por sekureca kialo", "additional_urls_already_removed": "Plia URL '{url}' jam forigita en la aldona URL por permeso '{permission}'", - "additional_urls_already_added": "Plia URL '{url}' jam aldonita en la aldona URL por permeso '{permission}'" + "additional_urls_already_added": "Plia URL '{url}' jam aldonita en la aldona URL por permeso '{permission}'", + "global_settings_setting_nginx_compatibility_help": "Kongruo vs sekureca kompromiso por la TTT-servilo NGINX. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", + "global_settings_setting_admin_strength": "Admin pasvorta forto", + "global_settings_setting_user_strength": "Uzanto pasvorta forto", + "global_settings_setting_postfix_compatibility_help": "Kongruo vs sekureca kompromiso por la Postfix-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", + "global_settings_setting_ssh_compatibility_help": "Kongruo vs sekureca kompromiso por la SSH-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", + "global_settings_setting_smtp_allow_ipv6_help": "Permesu la uzon de IPv6 por ricevi kaj sendi poŝton" } \ No newline at end of file diff --git a/locales/es.json b/locales/es.json index aebb959a8..85d7b1f43 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,8 +1,6 @@ { - "action_invalid": "Acción no válida '{action} 1'", + "action_invalid": "Acción inválida '{action}'", "admin_password": "Contraseña administrativa", - "admin_password_change_failed": "No se pudo cambiar la contraseña", - "admin_password_changed": "La contraseña de administración fue cambiada", "app_already_installed": "{app} ya está instalada", "app_argument_choice_invalid": "Elija un valor válido para el argumento '{name}': '{value}' no se encuentra entre las opciones disponibles ({choices})", "app_argument_invalid": "Elija un valor válido para el argumento «{name}»: {error}", @@ -15,14 +13,11 @@ "app_not_properly_removed": "La {app} 0 no ha sido desinstalada correctamente", "app_removed": "{app} Desinstalado", "app_requirements_checking": "Comprobando los paquetes necesarios para {app}…", - "app_requirements_unmeet": "No se cumplen los requisitos para {app}, el paquete {pkgname} ({version}) debe ser {spec}", "app_sources_fetch_failed": "No se pudieron obtener los archivos con el código fuente, ¿es el URL correcto?", "app_unknown": "Aplicación desconocida", "app_unsupported_remote_type": "Tipo remoto no soportado por la aplicación", "app_upgrade_failed": "No se pudo actualizar {app}: {error}", "app_upgraded": "Actualizado {app}", - "ask_firstname": "Nombre", - "ask_lastname": "Apellido", "ask_main_domain": "Dominio principal", "ask_new_admin_password": "Nueva contraseña administrativa", "ask_password": "Contraseña", @@ -83,8 +78,8 @@ "pattern_backup_archive_name": "Debe ser un nombre de archivo válido con un máximo de 30 caracteres, solo se admiten caracteres alfanuméricos y los caracteres -_. (guiones y punto)", "pattern_domain": "El nombre de dominio debe ser válido (por ejemplo mi-dominio.org)", "pattern_email": "Debe ser una dirección de correo electrónico válida, sin el símbolo '+' (ej. alguien@ejemplo.com)", - "pattern_firstname": "Debe ser un nombre válido", - "pattern_lastname": "Debe ser un apellido válido", + "pattern_firstname": "Debe ser un nombre válido (al menos 3 caracteres)", + "pattern_lastname": "Debe ser un apellido válido (al menos 3 caracteres)", "pattern_mailbox_quota": "Debe ser un tamaño con el sufijo «b/k/M/G/T» o «0» para no tener una cuota", "pattern_password": "Debe contener al menos 3 caracteres", "pattern_port_or_range": "Debe ser un número de puerto válido (es decir entre 0-65535) o un intervalo de puertos (por ejemplo 100:200)", @@ -150,7 +145,7 @@ "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio «{domain}» no ha sido emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", "certmanager_attempt_to_renew_valid_cert": "¡El certificado para el dominio «{domain}» no está a punto de expirar! (Puede usar --force si sabe lo que está haciendo)", "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain} a través de HTTP. Por favor compruebe en los diagnósticos la categoría 'Web'para más información. (Si sabe lo que está haciendo, utilice '--no-checks' para no realizar estas comprobaciones.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio '{domain}' es diferente de la IP de este servidor. Por favor comprueba los 'registros DNS' (básicos) la categoría de diagnósticos para mayor información. Si recientemente modificó su registro 'A', espere a que se propague (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabe lo que está haciendo, use '--no-checks' para desactivar esos cheques)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Los registros DNS para el dominio '{domain}' son diferentes para la IP de este servidor. Por favor comprueba la categoría de los 'registros DNS' (básicos) en la página de diagnóstico para mayor información. Si has modificado recientemente tu registro 'A', espera a que se propague (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabes lo que estás haciendo, usa '--no-checks' para desactivar estos marcadores)", "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain} (archivo: {file}), razón: {reason}", "certmanager_cert_install_success_selfsigned": "Instalado correctamente un certificado autofirmado para el dominio «{domain}»", "certmanager_cert_install_success": "Instalado correctamente un certificado de Let's Encrypt para el dominio «{domain}»", @@ -163,7 +158,7 @@ "certmanager_unable_to_parse_self_CA_name": "No se pudo procesar el nombre de la autoridad de autofirma (archivo: {file})", "domains_available": "Dominios disponibles:", "backup_archive_broken_link": "No se pudo acceder al archivo de respaldo (enlace roto a {path})", - "certmanager_acme_not_configured_for_domain": "El reto ACME no ha podido ser realizado para {domain} porque en su configuración de nginx falta el código correcto... Por favor, asegúrate que la configuración de nginx es correcta ejecutando en el terminal `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "El reto ACME no ha podido ser realizado para {domain} porque en su configuración de nginx falta el código correcto... Por favor, asegúrate que la configuración de nginx es correcta ejecutando en la terminal `yunohost tools regen-conf nginx --dry-run --with-diff`.", "domain_hostname_failed": "No se pudo establecer un nuevo nombre de anfitrión («hostname»). Esto podría causar problemas más tarde (no es seguro... podría ir bien).", "app_already_installed_cant_change_url": "Esta aplicación ya está instalada. La URL no se puede cambiar solo con esta función. Marque `app changeurl` si está disponible.", "app_change_url_identical_domains": "El antiguo y nuevo dominio/url_path son idénticos ('{domain}{path}'), no se realizarán cambios.", @@ -192,8 +187,7 @@ "backup_with_no_backup_script_for_app": "La aplicación «{app}» no tiene un guión de respaldo. Omitiendo.", "backup_with_no_restore_script_for_app": "«{app}» no tiene un script de restauración, no podá restaurar automáticamente la copia de seguridad de esta aplicación.", "dyndns_domain_not_provided": "El proveedor de DynDNS {provider} no puede proporcionar el dominio {domain}.", - "experimental_feature": "Aviso : esta funcionalidad es experimental y no se considera estable, no debería usarla a menos que sepa lo que está haciendo.", - "good_practices_about_user_password": "Ahora está a punto de definir una nueva contraseña de usuario. La contraseña debe tener al menos 8 caracteres, aunque es una buena práctica usar una contraseña más larga (es decir, una frase de contraseña) y / o una variación de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).", + "good_practices_about_user_password": "Está a punto de establecer una nueva contraseña de usuario. La contraseña debería de ser de al menos 8 caracteres, aunque es una buena práctica usar una contraseña más larga (es decir, una frase de paso) y/o usar varias clases de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).", "password_listed": "Esta contraseña se encuentra entre las contraseñas más utilizadas del mundo. Por favor, elija algo menos común y más robusto.", "password_too_simple_1": "La contraseña debe tener al menos 8 caracteres de longitud", "password_too_simple_2": "La contraseña debe ser de al menos 8 caracteres de longitud e incluir un número y caracteres en mayúsculas y minúsculas", @@ -225,7 +219,6 @@ "server_reboot": "El servidor se reiniciará", "server_shutdown_confirm": "El servidor se apagará inmediatamente ¿está seguro? [{answers}]", "server_shutdown": "El servidor se apagará", - "root_password_replaced_by_admin_password": "Su contraseña de root ha sido sustituida por su contraseña de administración.", "root_password_desynchronized": "La contraseña de administración ha sido cambiada pero ¡YunoHost no pudo propagar esto a la contraseña de root!", "restore_system_part_failed": "No se pudo restaurar la parte del sistema «{part}»", "restore_removing_tmp_dir_failed": "No se pudo eliminar un directorio temporal antiguo", @@ -273,7 +266,7 @@ "migrations_failed_to_load_migration": "No se pudo cargar la migración {id}: {error}", "migrations_dependencies_not_satisfied": "Ejecutar estas migraciones: «{dependencies_id}» antes de migrar {id}.", "migrations_already_ran": "Esas migraciones ya se han realizado: {ids}", - "mail_unavailable": "Esta dirección de correo está reservada y será asignada automáticamente al primer usuario", + "mail_unavailable": "Esta dirección de correo electrónico está reservada para el grupo de administradores", "mailbox_disabled": "Correo desactivado para usuario {user}", "log_tools_reboot": "Reiniciar el servidor", "log_tools_shutdown": "Apagar el servidor", @@ -294,7 +287,6 @@ "log_domain_remove": "Eliminar el dominio «{}» de la configuración del sistema", "log_domain_add": "Añadir el dominio «{}» a la configuración del sistema", "log_remove_on_failed_install": "Eliminar «{}» después de una instalación fallida", - "log_remove_on_failed_restore": "Eliminar «{}» después de una restauración fallida desde un archivo de respaldo", "log_backup_restore_app": "Restaurar «{}» desde un archivo de respaldo", "log_backup_restore_system": "Restaurar sistema desde un archivo de respaldo", "log_available_on_yunopaste": "Este registro está ahora disponible vía {url}", @@ -319,26 +311,11 @@ "group_creation_failed": "No se pudo crear el grupo «{group}»: {error}", "group_created": "Creado el grupo «{group}»", "good_practices_about_admin_password": "Ahora está a punto de definir una nueva contraseña de usuario. La contraseña debe tener al menos 8 caracteres, aunque es una buena práctica usar una contraseña más larga (es decir, una frase de contraseña) y / o una variación de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).", - "global_settings_unknown_type": "Situación imprevista, la configuración {setting} parece tener el tipo {unknown_type} pero no es un tipo compatible con el sistema.", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permitir el uso de la llave (obsoleta) DSA para la configuración del demonio SSH", - "global_settings_unknown_setting_from_settings_file": "Clave desconocida en la configuración: «{setting_key}», desechada y guardada en /etc/yunohost/settings-unknown.json", - "global_settings_setting_security_postfix_compatibility": "Compromiso entre compatibilidad y seguridad para el servidor Postfix. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", - "global_settings_setting_security_ssh_compatibility": "Compromiso entre compatibilidad y seguridad para el servidor SSH. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", - "global_settings_setting_security_password_user_strength": "Seguridad de la contraseña de usuario", - "global_settings_setting_security_password_admin_strength": "Seguridad de la contraseña del administrador", - "global_settings_setting_security_nginx_compatibility": "Compromiso entre compatibilidad y seguridad para el servidor web NGINX. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", - "global_settings_reset_success": "Respaldada la configuración previa en {path}", - "global_settings_key_doesnt_exists": "La clave «{settings_key}» no existe en la configuración global, puede ver todas las claves disponibles ejecutando «yunohost settings list»", - "global_settings_cant_write_settings": "No se pudo guardar el archivo de configuración, motivo: {reason}", - "global_settings_cant_serialize_settings": "No se pudo seriar los datos de configuración, motivo: {reason}", - "global_settings_cant_open_settings": "No se pudo abrir el archivo de configuración, motivo: {reason}", - "global_settings_bad_type_for_setting": "Tipo erróneo para la configuración {setting}, obtuvo {received_type}, esperado {expected_type}", - "global_settings_bad_choice_for_enum": "Opción errónea para la configuración {setting}, obtuvo «{choice}» pero las opciones disponibles son: {available_choices}", "file_does_not_exist": "El archivo {path} no existe.", "dyndns_could_not_check_available": "No se pudo comprobar si {domain} está disponible en {provider}.", "domain_dns_conf_is_just_a_recommendation": "Este comando muestra la configuración *recomendada*. No configura las entradas DNS por ti. Es tu responsabilidad configurar la zona DNS en su registrador según esta recomendación.", "dpkg_lock_not_available": "Esta orden no se puede ejecutar en este momento ,parece que programa está usando el bloqueo de dpkg (el gestor de paquetes del sistema)", - "dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/APT (los gestores de paquetes del sistema) parecen estar mal configurados... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo apt install --fix-broken` y/o `sudo dpkg --configure -a`.", + "dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/APT (los gestores de paquetes del sistema) parecen estar mal configurados... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo apt install --fix-broken` y/o `sudo dpkg --configure -a` y/o `sudo dpkg --audit`.", "confirm_app_install_thirdparty": "¡PELIGRO! Esta aplicación no forma parte del catálogo de aplicaciones de YunoHost. La instalación de aplicaciones de terceros puede comprometer la integridad y seguridad de tu sistema. Probablemente NO deberías instalarla a menos que sepas lo que estás haciendo. NO se proporcionará NINGÚN SOPORTE si esta aplicación no funciona o rompe su sistema… Si de todos modos quieres correr ese riesgo, escribe '{answers}'", "confirm_app_install_danger": "¡PELIGRO! ¡Esta aplicación sigue siendo experimental (si no es expresamente no funcional)! Probablemente NO deberías instalarla a menos que sepas lo que estás haciendo. NO se proporcionará NINGÚN SOPORTE si esta aplicación no funciona o rompe tu sistema… Si de todos modos quieres correr ese riesgo, escribe '{answers}'", "confirm_app_install_warning": "Aviso: esta aplicación puede funcionar pero no está bien integrada en YunoHost. Algunas herramientas como la autentificación única y respaldo/restauración podrían no estar disponibles. ¿Instalar de todos modos? [{answers}] ", @@ -361,7 +338,6 @@ "app_not_upgraded": "La aplicación '{failed_app}' no se pudo actualizar y, como consecuencia, se cancelaron las actualizaciones de las siguientes aplicaciones: {apps}", "app_action_cannot_be_ran_because_required_services_down": "Estos servicios necesarios deberían estar funcionando para ejecutar esta acción: {services}. Pruebe a reiniciarlos para continuar (y posiblemente investigar por qué están caídos).", "already_up_to_date": "Nada que hacer. Todo está actualizado.", - "admin_password_too_long": "Elija una contraseña de menos de 127 caracteres", "aborting": "Cancelando.", "app_action_broke_system": "Esta acción parece que ha roto estos servicios importantes: {services}", "operation_interrupted": "¿La operación fue interrumpida manualmente?", @@ -415,7 +391,7 @@ "diagnosis_ip_no_ipv4": "El servidor no cuenta con ipv4 funcional.", "diagnosis_ip_not_connected_at_all": "¿¡Está conectado el servidor a internet!?", "diagnosis_ip_broken_resolvconf": "La resolución de nombres de dominio parece no funcionar en tu servidor, lo que parece estar relacionado con que /etc/resolv.conf no apunta a 127.0.0.1.", - "diagnosis_dns_missing_record": "Según la configuración DNS recomendada, deberías añadir un registro DNS\ntipo: {type}\nnombre: {name}\nvalor: {value}", + "diagnosis_dns_missing_record": "Según la configuración DNS recomendada, deberías añadir un registro DNS con las informaciones siguientes. Tipo: {type}
Nombre: {name}
Valor: {value}", "diagnosis_diskusage_low": "El almacenamiento {mountpoint} (en el dispositivo {device}) solo tiene {free} ({free_percent}%) de espacio disponible (de {total}). Ten cuidado.", "diagnosis_services_bad_status_tip": "Puedes intentar reiniciar el servicio, y si no funciona, echar un vistazo a los logs del serviciode la administración web (desde la línea de comandos puedes hacerlo con yunohost service restart {service} y yunohost service log {service}).", "diagnosis_ip_connected_ipv6": "¡El servidor está conectado a internet a través de IPv6!", @@ -434,17 +410,17 @@ "diagnosis_services_running": "¡El servicio {service} está en ejecución!", "diagnosis_failed": "Error al obtener el resultado del diagnóstico para la categoría '{category}': {error}", "diagnosis_ip_connected_ipv4": "¡El servidor está conectado a internet a través de IPv4!", - "diagnosis_security_vulnerable_to_meltdown_details": "Para corregir esto, debieras actualizar y reiniciar tu sistema para cargar el nuevo kernel de Linux (o contacta tu proveedor si esto no funciona). Mas información en https://meltdownattack.com/ .", - "diagnosis_ram_verylow": "Al sistema le queda solamente {available} ({available_percent}%) de RAM! (De un total de {total})", + "diagnosis_security_vulnerable_to_meltdown_details": "Para corregir esto, debieras actualizar y reiniciar tu sistema para cargar el nuevo kernel de Linux (o contacta tu proveedor si esto no funciona). Más información en https://meltdownattack.com/ .", + "diagnosis_ram_verylow": "¡Al sistema le queda solamente {available} ({available_percent}%) de RAM! (De un total de {total})", "diagnosis_ram_low": "Al sistema le queda {available} ({available_percent}%) de RAM de un total de {total}. Cuidado.", - "diagnosis_ram_ok": "El sistema aun tiene {available} ({available_percent}%) de RAM de un total de {total}.", + "diagnosis_ram_ok": "El sistema aún tiene {available} ({available_percent}%) de RAM de un total de {total}.", "diagnosis_swap_none": "El sistema no tiene mas espacio de intercambio. Considera agregar por lo menos {recommended} de espacio de intercambio para evitar que el sistema se quede sin memoria.", "diagnosis_swap_notsomuch": "Al sistema le queda solamente {total} de espacio de intercambio. Considera agregar al menos {recommended} para evitar que el sistema se quede sin memoria.", "diagnosis_mail_outgoing_port_25_blocked": "El puerto de salida 25 parece estar bloqueado. Intenta desbloquearlo con el panel de configuración de tu proveedor de servicios de Internet (o proveedor de halbergue). Mientras tanto, el servidor no podrá enviar correos electrónicos a otros servidores.", - "diagnosis_regenconf_allgood": "Todos los archivos de configuración están en linea con la configuración recomendada!", + "diagnosis_regenconf_allgood": "¡Todos los archivos de configuración están en línea con la configuración recomendada!", "diagnosis_regenconf_manually_modified": "El archivo de configuración {file} parece que ha sido modificado manualmente.", "diagnosis_regenconf_manually_modified_details": "¡Esto probablemente esta BIEN si sabes lo que estás haciendo! YunoHost dejará de actualizar este fichero automáticamente... Pero ten en cuenta que las actualizaciones de YunoHost pueden contener importantes cambios que están recomendados. Si quieres puedes comprobar las diferencias mediante yunohost tools regen-conf {category} --dry-run --with-diff o puedes forzar el volver a las opciones recomendadas mediante el comando yunohost tools regen-conf {category} --force", - "diagnosis_security_vulnerable_to_meltdown": "Pareces vulnerable a el colapso de vulnerabilidad critica de seguridad", + "diagnosis_security_vulnerable_to_meltdown": "Pareces vulnerable al colapso de vulnerabilidad crítica de seguridad", "diagnosis_description_basesystem": "Sistema de base", "diagnosis_description_ip": "Conectividad a Internet", "diagnosis_description_dnsrecords": "Registro DNS", @@ -464,9 +440,8 @@ "log_domain_main_domain": "Hacer de '{}' el dominio principal", "log_app_action_run": "Inicializa la acción de la aplicación '{}'", "group_already_exist_on_system_but_removing_it": "El grupo {group} ya existe en los grupos del sistema, pero YunoHost lo suprimirá …", - "global_settings_setting_pop3_enabled": "Habilita el protocolo POP3 para el servidor de correo electrónico", "domain_cannot_remove_main_add_new_one": "No se puede remover '{domain}' porque es su principal y único dominio. Primero debe agregar un nuevo dominio con la linea de comando 'yunohost domain add ', entonces configurarlo como dominio principal con 'yunohost domain main-domain -n ' y finalmente borrar el dominio '{domain}' con 'yunohost domain remove {domain}'.'", - "diagnosis_never_ran_yet": "Este servidor todavía no tiene reportes de diagnostico. Puede iniciar un diagnostico completo desde la interface administrador web o con la linea de comando 'yunohost diagnosis run'.", + "diagnosis_never_ran_yet": "Este servidor todavía no tiene reportes de diagnostico. Puede iniciar un diagnostico completo desde la interface administrador web o con la linea de comando 'yunohost diagnosis run'.", "diagnosis_unknown_categories": "Las siguientes categorías están desconocidas: {categories}", "diagnosis_http_unreachable": "El dominio {domain} esta fuera de alcance desde internet y a través de HTTP.", "diagnosis_http_bad_status_code": "Parece que otra máquina (quizás el router de conexión a internet) haya respondido en vez de tu servidor.
1. La causa más común es que el puerto 80 (y el 443) no hayan sido redirigidos a tu servidor.
2. En situaciones más complejas: asegurate de que ni el cortafuegos ni el proxy inverso están interfiriendo.", @@ -478,12 +453,12 @@ "diagnosis_ports_forwarding_tip": "Para solucionar este incidente, lo más seguro deberías configurar la redirección de los puertos en el router como se especifica en https://yunohost.org/isp_box_config", "certmanager_warning_subdomain_dns_record": "El subdominio '{subdomain}' no se resuelve en la misma dirección IP que '{domain}'. Algunas funciones no estarán disponibles hasta que solucione esto y regenere el certificado.", "domain_cannot_add_xmpp_upload": "No puede agregar dominios que comiencen con 'xmpp-upload'. Este tipo de nombre está reservado para la función de carga XMPP integrada en YunoHost.", - "yunohost_postinstall_end_tip": "¡La post-instalación completada! Para finalizar su configuración, por favor considere:\n - agregar un primer usuario a través de la sección 'Usuarios' del administrador web (o 'yunohost user create ' en la línea de comandos);\n - diagnosticar problemas potenciales a través de la sección 'Diagnóstico' del administrador web (o 'yunohost diagnosis run' en la línea de comandos);\n - leyendo las partes 'Finalizando su configuración' y 'Conociendo YunoHost' en la documentación del administrador: https://yunohost.org/admindoc.", - "diagnosis_dns_point_to_doc": "Por favor, consulta la documentación en https://yunohost.org/dns_config si necesitas ayuda para configurar los registros DNS.", + "yunohost_postinstall_end_tip": "¡La post-instalación completada! Para finalizar su configuración, por favor considere:\n - diagnosticar problemas potenciales a través de la sección 'Diagnóstico' del administrador web (o 'yunohost diagnosis run' en la línea de comandos);\n - leyendo las partes 'Finalizando su configuración' y 'Conociendo YunoHost' en la documentación del administrador: https://yunohost.org/admindoc.", + "diagnosis_dns_point_to_doc": "Por favor, consulta la documentación en https://yunohost.org/dns_config si necesitas ayuda para configurar los registros DNS.", "diagnosis_ip_global": "IP Global: {global}", "diagnosis_mail_outgoing_port_25_ok": "El servidor de email SMTP puede mandar emails (puerto saliente 25 no está bloqueado).", - "diagnosis_mail_outgoing_port_25_blocked_details": "Primeramente deberías intentar desbloquear el puerto de salida 25 en la interfaz de control de tu router o en la interfaz de tu provedor de hosting. (Algunos hosting pueden necesitar que les abras un ticket de soporte para esto).", - "diagnosis_swap_tip": "Por favor tenga cuidado y sepa que si el servidor contiene swap en una tarjeta SD o un disco duro de estado sólido, esto reducirá drásticamente la vida útil del dispositivo.", + "diagnosis_mail_outgoing_port_25_blocked_details": "Primeramente, deberías intentar desbloquear el puerto de salida 25 en la interfaz de control de tu router o en la interfaz de tu proveedor de hosting. (Algunos hostings pueden necesitar que les abras un ticket de soporte para esto).", + "diagnosis_swap_tip": "Por favor, tenga cuidado y sepa que si el servidor contiene swap en una tarjeta SD o un disco duro de estado sólido, esto reducirá drásticamente la vida útil del dispositivo.", "diagnosis_domain_expires_in": "{domain} expira en {days} días.", "diagnosis_domain_expiration_error": "¡Algunos dominios expirarán MUY PRONTO!", "diagnosis_domain_expiration_warning": "¡Algunos dominios expirarán pronto!", @@ -510,13 +485,10 @@ "app_label_deprecated": "Este comando está depreciado! Favor usar el nuevo comando 'yunohost user permission update' para administrar la etiqueta de app.", "app_argument_password_no_default": "Error al interpretar argumento de contraseña'{name}': El argumento de contraseña no puede tener un valor por defecto por razón de seguridad", "invalid_regex": "Regex no valido: «{regex}»", - "global_settings_setting_backup_compress_tar_archives": "Cuando se creen nuevas copias de respaldo, comprimir los archivos (.tar.gz) en lugar de descomprimir los archivos (.tar). N.B.: activar esta opción quiere decir que los archivos serán más pequeños pero que el proceso tardará más y utilizará más CPU.", "global_settings_setting_smtp_relay_password": "Clave de uso del SMTP", "global_settings_setting_smtp_relay_user": "Cuenta de uso de SMTP", "global_settings_setting_smtp_relay_port": "Puerto de envio / relay SMTP", - "global_settings_setting_smtp_relay_host": "El servidor relay de SMTP para enviar correo en lugar de esta instalación YunoHost. Útil si estás en una de estas situaciones: tu puerto 25 esta bloqueado por tu ISP o VPS, si estás en usado una IP marcada como residencial o DUHL, si no puedes configurar un DNS inverso o si el servidor no está directamente expuesto a internet y quieres utilizar otro servidor para enviar correos.", - "global_settings_setting_smtp_allow_ipv6": "Permitir el uso de IPv6 para enviar y recibir correo", - "diagnosis_processes_killed_by_oom_reaper": "Algunos procesos fueron terminados por el sistema recientemente porque se quedó sin memoria. Típicamente es sintoma de falta de memoria o de un proceso que se adjudicó demasiada memoria.
Resumen de los procesos terminados:
\n{kills_summary}", + "diagnosis_processes_killed_by_oom_reaper": "Algunos procesos fueron terminados por el sistema recientemente porque se quedó sin memoria. Típicamente, es síntoma de falta de memoria o de un proceso que se adjudicó demasiada memoria.
Resumen de los procesos terminados:
\n{kills_summary}", "diagnosis_http_nginx_conf_not_up_to_date_details": "Para arreglar este asunto, estudia las diferencias mediante el comando yunohost tools regen-conf nginx --dry-run --with-diff y si te parecen bien aplica los cambios mediante yunohost tools regen-conf nginx --force.", "diagnosis_http_nginx_conf_not_up_to_date": "Parece que la configuración nginx de este dominio haya sido modificada manualmente, esto no deja que YunoHost pueda diagnosticar si es accesible mediante HTTP.", "diagnosis_http_partially_unreachable": "El dominio {domain} parece que no es accesible mediante HTTP desde fuera de la red local mediante IPv{failed}, aunque si que funciona mediante IPv{passed}.", @@ -527,13 +499,13 @@ "diagnosis_mail_queue_unavailable_details": "Error: {error}", "diagnosis_mail_queue_unavailable": "No se ha podido consultar el número de correos electrónicos pendientes en la cola", "diagnosis_mail_queue_ok": "{nb_pending} correos esperando e la cola de correos electrónicos", - "diagnosis_mail_blacklist_website": "Cuando averigües y arregles el motivo por el que aprareces en la lista maligna, no dudes en solicitar que tu IP o dominio sea retirado de la {blacklist_website}", + "diagnosis_mail_blacklist_website": "Cuando averigües y arregles el motivo por el que apareces en la lista maligna, no dudes en solicitar que tu IP o dominio sea retirado de la {blacklist_website}", "diagnosis_mail_blacklist_reason": "El motivo de estar en la lista maligna es: {reason}", "diagnosis_mail_blacklist_listed_by": "Tu IP o dominio {item} está marcado como maligno en {blacklist_name}", "diagnosis_mail_blacklist_ok": "Las IP y los dominios utilizados en este servidor no parece que estén en ningún listado maligno (blacklist)", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "El DNS inverso actual es: {rdns_domain}
Valor esperado: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "La resolución de DNS inverso no está correctamente configurada mediante IPv{ipversion}. Algunos correos pueden fallar al ser enviados o pueden ser marcados como basura.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Algunos proveedores no permiten configurar el DNS inverso (o su funcionalidad puede estar rota...). Si tu DNS inverso está configurado correctamente para IPv4, puedes intentar deshabilitarlo para IPv6 cuando envies correos mediante el comando yunohost settings set smtp.allow_ipv6 -v off. Nota: esta solución quiere decir que no podrás enviar ni recibir correos con los pocos servidores que utilizan exclusivamente IPv6.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Algunos proveedores no permiten configurar el DNS inverso (o su funcionalidad puede estar rota...). Si tu DNS inverso está configurado correctamente para IPv4, puedes intentar deshabilitarlo para IPv6 cuando envies correos mediante el comando yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Nota: esta solución quiere decir que no podrás enviar ni recibir correos con los pocos servidores que utilizan exclusivamente IPv6.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Algunos proveedores no te permitirán que configures un DNS inverso (o puede que esta opción esté rota...). Si estás sufriendo problemas por este asunto, quizás te sirvan las siguientes soluciones:
- Algunos ISP proporcionan una alternativa mediante el uso de un relay de servidor de correo aunque esto implica que el relay podrá espiar tu tráfico de correo electrónico.
- Una solución amigable con la privacidad es utilizar una VPN con una *IP pública dedicada* para evitar este tipo de limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Quizás tu solución sea cambiar de proveedor de internet", "diagnosis_mail_fcrdns_nok_details": "Primero deberías intentar configurar el DNS inverso mediante {ehlo_domain} en la interfaz de internet de tu router o en la de tu proveedor de internet. (Algunos proveedores de internet en ocasiones necesitan que les solicites un ticket de soporte para ello).", "diagnosis_mail_fcrdns_dns_missing": "No hay definida ninguna DNS inversa mediante IPv{ipversion}. Algunos correos puede que fallen al enviarse o puede que se marquen como basura.", @@ -547,11 +519,11 @@ "diagnosis_mail_ehlo_unreachable_details": "No pudo abrirse la conexión en el puerto 25 de tu servidor mediante IPv{ipversion}. Parece que no se puede contactar.
1. La causa más común en estos casos suele ser que el puerto 25 no está correctamente redireccionado a tu servidor.
2. También deberías asegurarte que el servicio postfix está en marcha.
3. En casos más complejos: asegurate que no estén interfiriendo ni el firewall ni el reverse-proxy.", "diagnosis_mail_ehlo_unreachable": "El servidor de correo SMTP no puede contactarse desde el exterior mediante IPv{ipversion}. No puede recibir correos.", "diagnosis_mail_ehlo_ok": "¡El servidor de correo SMTP puede contactarse desde el exterior por lo que puede recibir correos!", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algunos proveedores de internet no le permitirán desbloquear el puerto 25 porque no les importa la Neutralidad de la Red.
- Algunos proporcionan una alternativa usando un relay como servidor de correo lo que implica que el relay podrá espiar tu trafico de correo.
- Una alternativa buena para la privacidad es utilizar una VPN *con una IP pública dedicada* para evitar estas limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Otra alternativa es cambiar de proveedor de inteernet a uno más amable con la Neutralidad de la Red", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algunos proveedores de internet no le permitirán desbloquear el puerto 25 porque no les importa la Neutralidad de la Red.
- Algunos proporcionan una alternativa usando un relay como servidor de correo lo que implica que el relay podrá espiar tu tráfico de correo.
- Una alternativa buena para la privacidad es utilizar una VPN *con una IP pública dedicada* para evitar estas limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Otra alternativa es cambiar de proveedor de internet a uno más amable con la Neutralidad de la Red", "diagnosis_backports_in_sources_list": "Parece que apt (el gestor de paquetes) está configurado para usar el repositorio backports. A menos que realmente sepas lo que estás haciendo, desaconsejamos absolutamente instalar paquetes desde backports, ya que pueden provocar comportamientos intestables o conflictos en el sistema.", "diagnosis_basesystem_hardware_model": "El modelo de servidor es {model}", - "additional_urls_already_removed": "La URL adicional «{url}» ya se ha eliminado para el permiso «{permission}»", - "additional_urls_already_added": "La URL adicional «{url}» ya se ha añadido para el permiso «{permission}»", + "additional_urls_already_removed": "La URL adicional '{url}' ya se ha eliminado para el permiso «{permission}»", + "additional_urls_already_added": "La URL adicional '{url}' ya se ha añadido para el permiso «{permission}»", "config_apply_failed": "Falló la aplicación de la nueva configuración: {error}", "app_restore_script_failed": "Ha ocurrido un error dentro del script de restauración de aplicaciones", "app_config_unable_to_apply": "No se pudieron aplicar los valores del panel configuración.", @@ -563,8 +535,8 @@ "domain_dns_push_partial_failure": "Entradas DNS actualizadas parcialmente: algunas advertencias/errores reportados.", "domain_unknown": "Dominio '{domain}' desconocido", "diagnosis_high_number_auth_failures": "Ultimamente ha habido un gran número de errores de autenticación. Asegúrate de que Fail2Ban está ejecutándose y correctamente configurado, o usa un puerto SSH personalizado como se explica en https://yunohost.org/security.", - "diagnosis_sshd_config_inconsistent": "Parece que el puerto SSH ha sido modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, hay un nuevo parámetro global 'security.ssh.port' disponible para evitar modificar manualmente la configuración.", - "diagnosis_sshd_config_inconsistent_details": "Por favor ejecuta yunohost settings set security.ssh.port -v TU_PUERTO_SSH para definir el puerto SSH, y comprueba yunohost tools regen-conf ssh --dry-run --with-diff y yunohost tools regen-conf ssh --force para resetear tu configuración a las recomendaciones de YunoHost.", + "diagnosis_sshd_config_inconsistent": "Parece que el puerto SSH ha sido modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, hay un nuevo parámetro global 'security.ssh.ssh_port' disponible para evitar modificar manualmente la configuración.", + "diagnosis_sshd_config_inconsistent_details": "Por favor ejecute yunohost settings set security.ssh.ssh_port -v TU_PUERTO_SSH para definir el puerto SSH, y compruebe las diferencias yunohost tools regen-conf ssh --dry-run --with-diff y yunohost tools regen-conf ssh --force para resetear tu configuración a las recomendaciones de YunoHost.", "config_forbidden_keyword": "'{keyword}' es una palabra reservada, no puedes crear ni usar un panel de configuración con una pregunta que use esta id.", "config_no_panel": "No se ha encontrado ningún panel de configuración.", "config_unknown_filter_key": "La clave de filtrado '{filter_key}' es incorrecta.", @@ -579,7 +551,6 @@ "config_validate_email": "Debe ser una dirección de correo correcta", "config_validate_time": "Debe ser una hora valida en formato HH:MM", "config_validate_url": "Debe ser una URL válida", - "config_version_not_supported": "Las versiones del panel de configuración '{version}' no están soportadas.", "domain_remove_confirm_apps_removal": "La supresión de este dominio también eliminará las siguientes aplicaciones:\n{apps}\n\n¿Seguro? [{answers}]", "domain_registrar_is_not_configured": "El registrador aún no ha configurado el dominio {domain}.", "diagnosis_apps_not_in_app_catalog": "Esta aplicación se encuentra ausente o ya no figura en el catálogo de aplicaciones de YunoHost. Deberías considerar desinstalarla ya que no recibirá actualizaciones y podría comprometer la integridad y seguridad de tu sistema.", @@ -589,7 +560,7 @@ "diagnosis_apps_bad_quality": "Esta aplicación está etiquetada como defectuosa en el catálogo de aplicaciones YunoHost. Podría ser un problema temporal mientras las personas responsables corrigen el asunto. Mientras tanto, la actualización de esta aplicación está desactivada.", "diagnosis_apps_broken": "Esta aplicación está etiquetada como defectuosa en el catálogo de aplicaciones YunoHost. Podría ser un problema temporal mientras las personas responsables corrigen el asunto. Mientras tanto, la actualización de esta aplicación está desactivada.", "diagnosis_apps_deprecated_practices": "La versión instalada de esta aplicación usa aún prácticas de empaquetado obsoletas. Deberías actualizarla.", - "diagnosis_apps_outdated_ynh_requirement": "La versión instalada de esta aplicación solo necesita YunoHost >= 2.x, lo que indica que no está al día con la buena praxis de ayudas y empaquetado recomendadas. Deberías actualizarla.", + "diagnosis_apps_outdated_ynh_requirement": "La versión instalada de esta aplicación solo necesita YunoHost >= 2.x o 3.x, lo que indica que no está al día con la buena praxis de ayudas y empaquetado recomendadas. Deberías actualizarla.", "domain_dns_conf_special_use_tld": "Este dominio se basa en un dominio de primer nivel (TLD) de usos especiales como .local o .test y no debería tener entradas DNS reales.", "diagnosis_sshd_config_insecure": "Parece que la configuración SSH ha sido modificada manualmente, y es insegura porque no tiene ninguna instrucción 'AllowGroups' o 'AllowUsers' para limitar el acceso a los usuarios autorizados.", "domain_dns_push_not_applicable": "La configuración automática de los registros DNS no puede realizarse en el dominio {domain}. Deberìas configurar manualmente los registros DNS siguiendo la documentación.", @@ -600,8 +571,7 @@ "domain_dns_push_success": "¡Registros DNS actualizados!", "domain_dns_push_failed_to_authenticate": "No se pudo autenticar en la API del registrador para el dominio '{domain}'. ¿Lo más probable es que las credenciales sean incorrectas? (Error: {error})", "domain_dns_registrar_experimental": "Hasta ahora, la comunidad de YunoHost no ha probado ni revisado correctamente la interfaz con la API de **{registrar}**. El soporte es **muy experimental**. ¡Ten cuidado!", - "domain_dns_push_record_failed": "No se pudo {acción} registrar {tipo}/{nombre}: {error}", - "domain_config_features_disclaimer": "Hasta ahora, habilitar/deshabilitar las funciones de correo o XMPP solo afecta la configuración de DNS recomendada y automática, ¡no las configuraciones del sistema!", + "domain_dns_push_record_failed": "No se pudo {action} registrar {type}/{name}: {error}", "domain_config_mail_in": "Correos entrantes", "domain_config_mail_out": "Correos salientes", "domain_config_xmpp": "Mensajería instantánea (XMPP)", @@ -618,26 +588,21 @@ "domain_dns_registrar_managed_in_parent_domain": "Este dominio es un subdominio de {parent_domain_link}. La configuración del registrador de DNS debe administrarse en el panel de configuración de {parent_domain}.", "domain_dns_registrar_yunohost": "Este dominio es un nohost.me / nohost.st / ynh.fr y, por lo tanto, YunoHost maneja automáticamente su configuración de DNS sin ninguna configuración adicional. (vea el comando 'yunohost dyndns update')", "domain_dns_registrar_not_supported": "YunoHost no pudo detectar automáticamente el registrador que maneja este dominio. Debe configurar manualmente sus registros DNS siguiendo la documentación en https://yunohost.org/dns.", - "global_settings_setting_security_nginx_redirect_to_https": "Redirija las solicitudes HTTP a HTTPs de forma predeterminada (¡NO LO DESACTIVE a menos que realmente sepa lo que está haciendo!)", - "global_settings_setting_security_webadmin_allowlist": "Direcciones IP permitidas para acceder al webadmin. Separado por comas.", "migration_ldap_backup_before_migration": "Creación de una copia de seguridad de la base de datos LDAP y la configuración de las aplicaciones antes de la migración real.", - "global_settings_setting_security_ssh_port": "Puerto SSH", "invalid_number": "Debe ser un miembro", "ldap_server_is_down_restart_it": "El servicio LDAP está inactivo, intente reiniciarlo...", - "invalid_password": "Contraseña inválida", "permission_cant_add_to_all_users": "El permiso {permission} no se puede agregar a todos los usuarios.", "log_domain_dns_push": "Enviar registros DNS para el dominio '{}'", "log_user_import": "Importar usuarios", "postinstall_low_rootfsspace": "El sistema de archivos raíz tiene un espacio total inferior a 10 GB, ¡lo cual es bastante preocupante! ¡Es probable que se quede sin espacio en disco muy rápidamente! Se recomienda tener al menos 16 GB para el sistema de archivos raíz. Si desea instalar YunoHost a pesar de esta advertencia, vuelva a ejecutar la instalación posterior con --force-diskspace", "migration_ldap_rollback_success": "Sistema revertido.", "permission_protected": "Permiso {permission} está protegido. No puede agregar o quitar el grupo de visitantes a/desde este permiso.", - "global_settings_setting_ssowat_panel_overlay_enabled": "Habilitar la superposición del panel SSOwat", + "global_settings_setting_ssowat_panel_overlay_enabled": "Habilitar el pequeño cuadrado de acceso directo al portal \"YunoHost\" en las aplicaciones", "migration_0021_start": "Iniciando migración a Bullseye", "migration_0021_patching_sources_list": "Parcheando los sources.lists...", "migration_0021_main_upgrade": "Iniciando actualización principal...", "migration_0021_still_on_buster_after_main_upgrade": "Algo salió mal durante la actualización principal, el sistema parece estar todavía en Debian Buster", "migration_0021_yunohost_upgrade": "Iniciando la actualización principal de YunoHost...", - "migration_0021_not_buster": "¡La distribución actual de Debian no es Buster!", "migration_0021_not_enough_free_space": "¡El espacio libre es bastante bajo en /var/! Debe tener al menos 1 GB libre para ejecutar esta migración.", "migration_0021_system_not_fully_up_to_date": "Su sistema no está completamente actualizado. Realice una actualización regular antes de ejecutar la migración a Bullseye.", "migration_0021_general_warning": "Tenga en cuenta que esta migración es una operación delicada. El equipo de YunoHost hizo todo lo posible para revisarlo y probarlo, pero la migración aún podría romper partes del sistema o sus aplicaciones.\n\nPor lo tanto, se recomienda:\n - Realice una copia de seguridad de cualquier dato o aplicación crítica. Más información en https://yunohost.org/backup;\n - Sea paciente después de iniciar la migración: dependiendo de su conexión a Internet y hardware, puede tomar algunas horas para que todo se actualice.", @@ -645,7 +610,6 @@ "migration_0021_modified_files": "Tenga en cuenta que se encontró que los siguientes archivos se modificaron manualmente y podrían sobrescribirse después de la actualización: {manually_modified_files}", "invalid_number_min": "Debe ser mayor que {min}", "pattern_email_forward": "Debe ser una dirección de correo electrónico válida, se acepta el símbolo '+' (por ejemplo, alguien+etiqueta@ejemplo.com)", - "global_settings_setting_security_ssh_password_authentication": "Permitir autenticación de contraseña para SSH", "invalid_number_max": "Debe ser menor que {max}", "ldap_attribute_already_exists": "El atributo LDAP '{attribute}' ya existe con el valor '{value}'", "log_app_config_set": "Aplicar configuración a la aplicación '{}'", @@ -657,8 +621,6 @@ "ldap_server_down": "No se puede conectar con el servidor LDAP", "log_backup_create": "Crear un archivo de copia de seguridad", "migration_ldap_can_not_backup_before_migration": "La copia de seguridad del sistema no se pudo completar antes de que fallara la migración. Error: {error}", - "global_settings_setting_security_experimental_enabled": "Habilite las funciones de seguridad experimentales (¡no habilite esto si no sabe lo que está haciendo!)", - "global_settings_setting_security_webadmin_allowlist_enabled": "Permita que solo algunas IP accedan al administrador web.", "migration_ldap_migration_failed_trying_to_rollback": "No se pudo migrar... intentando revertir el sistema.", "migration_0023_not_enough_space": "Deje suficiente espacio disponible en {path} para ejecutar la migración.", "migration_0023_postgresql_11_not_installed": "PostgreSQL no estaba instalado en su sistema. Nada que hacer.", @@ -684,5 +646,106 @@ "service_description_yunomdns": "Le permite llegar a su servidor usando 'yunohost.local' en su red local", "show_tile_cant_be_enabled_for_regex": "No puede habilitar 'show_tile' en este momento porque la URL para el permiso '{permission}' es una expresión regular", "show_tile_cant_be_enabled_for_url_not_defined": "No puede habilitar 'show_tile' en este momento, porque primero debe definir una URL para el permiso '{permission}'", - "regex_incompatible_with_tile": "/!\\ Empaquetadores! El permiso '{permission}' tiene show_tile establecido en 'true' y, por lo tanto, no puede definir una URL de expresión regular como la URL principal" -} + "regex_incompatible_with_tile": "/!\\ Empaquetadores! El permiso '{permission}' tiene show_tile establecido en 'true' y, por lo tanto, no puede definir una URL de expresión regular como la URL principal", + "global_settings_setting_backup_compress_tar_archives_help": "Cuando se creen nuevas copias de respaldo, comprimir los archivos (.tar.gz) en lugar de descomprimir los archivos (.tar). N.B.: activar esta opción quiere decir que los archivos serán más pequeños pero que el proceso tardará más y utilizará más CPU.", + "global_settings_setting_security_experimental_enabled_help": "Habilite las funciones de seguridad experimentales (¡no habilite esto si no sabe lo que está haciendo!)", + "global_settings_setting_nginx_compatibility_help": "Compromiso entre compatibilidad y seguridad para el servidor web NGINX. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", + "global_settings_setting_nginx_redirect_to_https_help": "Redirija las solicitudes HTTP a HTTPs de forma predeterminada (¡NO LO DESACTIVE a menos que realmente sepa lo que está haciendo!)", + "global_settings_setting_admin_strength": "Seguridad de la contraseña del administrador", + "global_settings_setting_user_strength": "Seguridad de la contraseña de usuario", + "global_settings_setting_postfix_compatibility_help": "Compromiso entre compatibilidad y seguridad para el servidor Postfix. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", + "global_settings_setting_ssh_compatibility_help": "Compromiso entre compatibilidad y seguridad para el servidor SSH. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", + "global_settings_setting_ssh_password_authentication_help": "Permitir autenticación de contraseña para SSH", + "global_settings_setting_ssh_port": "Puerto SSH", + "global_settings_setting_webadmin_allowlist_help": "Direcciones IP permitidas para acceder al webadmin. Separado por comas.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permita que solo algunas IP accedan al administrador web.", + "global_settings_setting_smtp_allow_ipv6_help": "Permitir el uso de IPv6 para enviar y recibir correo", + "global_settings_setting_smtp_relay_enabled_help": "El servidor relay de SMTP para enviar correo en lugar de esta instalación YunoHost. Útil si estás en una de estas situaciones: tu puerto 25 esta bloqueado por tu ISP o VPS, si estás en usado una IP marcada como residencial o DUHL, si no puedes configurar un DNS inverso o si el servidor no está directamente expuesto a internet y quieres utilizar otro servidor para enviar correos.", + "admins": "Administradores", + "all_users": "Todos los usuarios de YunoHost", + "app_action_failed": "No se ha podido ejecutar la acción {action} para la aplicación {app}", + "app_manifest_install_ask_init_admin_permission": "¿Quién debe tener acceso a las funciones de administración de esta aplicación? (Esto puede cambiarse posteriormente)", + "app_manifest_install_ask_init_main_permission": "¿Quién debería tener acceso a esta aplicación? (Esto puede cambiarse posteriormente)", + "ask_admin_fullname": "Nombre completo del administrador", + "ask_admin_username": "Nombre de usuario del administrador", + "ask_fullname": "Nombre completo", + "certmanager_cert_install_failed": "La instalación del certificado Let's Encrypt a fallado para {domains}", + "certmanager_cert_install_failed_selfsigned": "La instalación del certificado autofirmado ha fallado para {domains}", + "certmanager_cert_renew_failed": "La renovación del certificado Let's Encrypt ha fallado para {domains}", + "config_action_disabled": "No se ha podido ejecutar la acción '{action}' porque está desactivada, asegúrese de cumplir sus restricciones. ayuda: {help}", + "config_action_failed": "Error al ejecutar la acción '{action}': {error}", + "config_forbidden_readonly_type": "El tipo '{type}' no puede establecerse como solo lectura, utilice otro tipo para representar este valor (arg id relevante: '{id}').", + "diagnosis_using_stable_codename": "apt (el gestor de paquetes del sistema) está configurado actualmente para instalar paquetes de nombre en clave 'estable', en lugar del nombre en clave de la versión actual de Debian (bullseye).", + "diagnosis_using_stable_codename_details": "Esto suele deberse a una configuración incorrecta de su proveedor de alojamiento. Esto es peligroso, porque tan pronto como la siguiente versión de Debian se convierta en la nueva 'estable', apt querrá actualizar todos los paquetes del sistema sin pasar por un procedimiento de migración adecuado. Se recomienda arreglar esto editando la fuente de apt para el repositorio base de Debian, y reemplazar la palabra clave stable por bullseye. El fichero de configuración correspondiente debería ser /etc/apt/sources.list, o un fichero en /etc/apt/sources.list.d/.", + "domain_config_cert_install": "Instalar el certificado Let's Encrypt", + "domain_cannot_add_muc_upload": "No puedes añadir dominios que empiecen por 'muc.'. Este tipo de nombre está reservado para la función de chat XMPP multi-usuarios integrada en YunoHost.", + "domain_config_cert_renew_help": "El certificado se renovará automáticamente durante los últimos 15 días de validez. Si lo desea, puede renovarlo manualmente. (No recomendado).", + "domain_config_cert_summary_expired": "CRÍTICO: ¡El certificado actual no es válido! ¡HTTPS no funcionará en absoluto!", + "domain_config_cert_summary_letsencrypt": "¡Muy bien! Estás utilizando un certificado Let's Encrypt válido.", + "global_settings_setting_postfix_compatibility": "Compatibilidad con Postfix", + "global_settings_setting_root_password_confirm": "Nueva contraseña de root (confirmar)", + "global_settings_setting_webadmin_allowlist_enabled": "Activar la lista de IPs permitidas para Webadmin", + "migration_0024_rebuild_python_venv_broken_app": "Omitiendo {app} porque virtualenv no puede ser reconstruido fácilmente para esta app. En su lugar, deberías arreglar la situación forzando la actualización de esta app usando `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_in_progress": "Ahora intentando reconstruir el virtualenv de Python para `{app}`", + "confirm_app_insufficient_ram": "¡PELIGRO! Esta aplicación requiere {required} de RAM para ser instalada/actualizada, pero solo hay {current} disponible actualmente. Incluso si esta aplicación pudiera ejecutarse, su proceso de instalación/actualización requiere una gran cantidad de RAM, por lo que tu servidor puede congelarse y fallar miserablemente. Si estás dispuesto a asumir ese riesgo de todos modos, escribe '{answers}'", + "confirm_notifications_read": "ADVERTENCIA: Deberías revisar las notificaciones de la aplicación antes de continuar, puede haber información importante que debes conocer. [{answers}]", + "domain_config_cert_summary_selfsigned": "ADVERTENCIA: El certificado actual es autofirmado. ¡Los navegadores mostrarán una espeluznante advertencia a los nuevos visitantes!.", + "global_settings_setting_backup_compress_tar_archives": "Comprimir las copias de seguridad", + "global_settings_setting_root_access_explain": "En sistemas Linux, 'root' es el administrador absoluto. En el contexto de YunoHost, el acceso directo 'root' SSH está deshabilitado por defecto - excepto desde la red local del servidor. Los miembros del grupo 'admins' pueden usar el comando sudo para actuar como root desde la linea de comandos. Sin embargo, puede ser útil tener una contraseña de root (robusta) para depurar el sistema si por alguna razón los administradores regulares ya no pueden iniciar sesión.", + "migration_0021_not_buster2": "¡La distribución Debian actual no es Buster! Si ya ha ejecutado la migración Buster->Bullseye, entonces este error es sintomático del hecho de que el procedimiento de migración no fue 100% exitoso (de lo contrario YunoHost lo habría marcado como completado). Se recomienda investigar lo sucedido con el equipo de soporte, que necesitará el registro **completo** de la `migración, que se puede encontrar en Herramientas > Registros en el webadmin.", + "global_settings_reset_success": "Restablecer la configuración global", + "global_settings_setting_nginx_compatibility": "Compatibilidad con NGINX", + "global_settings_setting_nginx_redirect_to_https": "Forzar HTTPS", + "global_settings_setting_user_strength_help": "Estos requisitos sólo se aplican al inicializar o cambiar la contraseña", + "log_resource_snippet": "Aprovisionar/desaprovisionar/actualizar un recurso", + "global_settings_setting_pop3_enabled": "Habilitar POP3", + "global_settings_setting_smtp_allow_ipv6": "Permitir IPv6", + "global_settings_setting_security_experimental_enabled": "Funciones de seguridad experimentales", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs no puede reconstruirse automáticamente para esas aplicaciones. Necesitas forzar una actualización para ellas, lo que puede hacerse desde la línea de comandos con: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_0024_rebuild_python_venv_failed": "Error al reconstruir el virtualenv de Python para {app}. La aplicación puede no funcionar mientras esto no se resuelva. Deberías arreglar la situación forzando la actualización de esta app usando `yunohost app upgrade --force {app}`.", + "app_arch_not_supported": "Esta aplicación sólo puede instalarse en arquitecturas {required} pero la arquitectura de su servidor es {current}", + "app_resource_failed": "Falló la asignación, desasignación o actualización de recursos para {app}: {error}", + "app_not_enough_disk": "Esta aplicación requiere {required} espacio libre.", + "app_not_enough_ram": "Esta aplicación requiere {required} de RAM para ser instalada/actualizada, pero solo hay {current} disponible actualmente.", + "app_yunohost_version_not_supported": "Esta aplicación requiere YunoHost >= {required} pero la versión actualmente instalada es {current}", + "global_settings_setting_ssh_compatibility": "Compatibilidad con SSH", + "root_password_changed": "la contraseña de root fue cambiada", + "domain_config_acme_eligible_explain": "Este dominio no parece estar preparado para un certificado Let's Encrypt. Compruebe la configuración DNS y la accesibilidad del servidor HTTP. Las secciones \"Registros DNS\" y \"Web\" de la página de diagnóstico pueden ayudarte a entender qué está mal configurado.", + "domain_config_cert_no_checks": "Ignorar las comprobaciones de diagnóstico", + "domain_config_cert_renew": "Renovar el certificado Let's Encrypt", + "domain_config_cert_summary": "Estado del certificado", + "domain_config_cert_summary_abouttoexpire": "El certificado actual está a punto de caducar. Pronto debería renovarse automáticamente.", + "domain_config_cert_summary_ok": "Muy bien, ¡el certificado actual tiene buena pinta!", + "domain_config_cert_validity": "Validez", + "global_settings_setting_admin_strength_help": "Estos requisitos sólo se aplican al inicializar o cambiar la contraseña", + "global_settings_setting_pop3_enabled_help": "Habilitar el protocolo POP3 para el servidor de correo", + "log_settings_reset_all": "Restablecer todos los ajustes", + "log_settings_set": "Aplicar ajustes", + "pattern_fullname": "Debe ser un nombre completo válido (al menos 3 caracteres)", + "password_confirmation_not_the_same": "La contraseña y su confirmación no coinciden", + "password_too_long": "Elija una contraseña de menos de 127 caracteres", + "diagnosis_using_yunohost_testing": "apt (el gestor de paquetes del sistema) está actualmente configurado para instalar cualquier actualización de 'testing' para el núcleo de YunoHost.", + "diagnosis_using_yunohost_testing_details": "Esto probablemente esté bien si sabes lo que estás haciendo, ¡pero presta atención a las notas de la versión antes de instalar actualizaciones de YunoHost! Si quieres deshabilitar las actualizaciones de prueba, debes eliminar la palabra clave testing de /etc/apt/sources.list.d/yunohost.list.", + "global_settings_setting_passwordless_sudo": "Permitir a los administradores utilizar 'sudo' sin tener que volver a escribir sus contraseñas.", + "group_update_aliases": "Actualizando alias para el grupo '{group}'", + "group_no_change": "Nada que cambiar para el grupo '{group}'", + "global_settings_setting_portal_theme": "Tema del portal", + "global_settings_setting_portal_theme_help": "Más información sobre la creación de temas de portal personalizados en https://yunohost.org/theming", + "invalid_credentials": "Contraseña o nombre de usuario no válidos", + "global_settings_setting_root_password": "Nueva contraseña de root", + "global_settings_setting_webadmin_allowlist": "Lista de IPs permitidas para Webadmin", + "migration_0024_rebuild_python_venv_disclaimer_base": "Tras la actualización a Debian Bullseye, algunas aplicaciones Python necesitan ser parcialmente reconstruidas para ser convertidas a la nueva versión de Python distribuida en Debian (en términos técnicos: lo que se llama el 'virtualenv' necesita ser recreado). Mientras tanto, esas aplicaciones Python pueden no funcionar. YunoHost puede intentar reconstruir el virtualenv para algunas de ellas, como se detalla a continuación. Para otras aplicaciones, o si el intento de reconstrucción falla, necesitarás forzar manualmente una actualización para esas aplicaciones.", + "migration_description_0024_rebuild_python_venv": "Reparar la aplicación Python tras la migración a bullseye", + "global_settings_setting_smtp_relay_enabled": "Activar el relé SMTP", + "domain_config_acme_eligible": "Elegibilidad ACME", + "global_settings_setting_ssh_password_authentication": "Autenticación por contraseña", + "domain_config_cert_issuer": "Autoridad de certificación", + "invalid_shell": "Shell inválido: {shell}", + "log_settings_reset": "Restablecer ajuste", + "migration_description_0026_new_admins_group": "Migrar al nuevo sistema de 'varios administradores'", + "visitors": "Visitantes", + "global_settings_setting_smtp_relay_host": "Host de retransmisión SMTP", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Se intentará reconstruir el virtualenv para las siguientes apps (NB: ¡la operación puede llevar algún tiempo!): {rebuild_apps}", + "migration_description_0025_global_settings_to_configpanel": "Migración de la nomenclatura de ajustes globales heredada a la nomenclatura nueva y moderna", + "registrar_infos": "Información sobre el registrador" +} \ No newline at end of file diff --git a/locales/eu.json b/locales/eu.json index e0ce226d5..0267b3366 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -1,25 +1,22 @@ { - "password_too_simple_1": "Pasahitzak gutxienez zortzi karaktere izan behar ditu", + "password_too_simple_1": "Pasahitzak 8 karaktere izan behar ditu gutxienez", "action_invalid": "'{action}' eragiketa baliogabea da", "aborting": "Bertan behera uzten.", - "admin_password_changed": "Administrazio-pasahitza aldatu da", - "admin_password_change_failed": "Ezinezkoa izan da pasahitza aldatzea", "additional_urls_already_added": "'{url}' URL gehigarria '{permission}' baimenerako gehitu da dagoeneko", "additional_urls_already_removed": "'{url}' URL gehigarriari '{permission}' baimena kendu zaio dagoeneko", "admin_password": "Administrazio-pasahitza", "diagnosis_ip_global": "IP orokorra: {global}", - "app_argument_password_no_default": "Errorea egon da '{name}' pasahitzaren argumentua ikuskatzean: pasahitzak ezin du balio hori izan segurtasuna dela-eta", + "app_argument_password_no_default": "Errorea egon da '{name}' pasahitzaren argumentua ikuskatzean: pasahitzak ezin du balio hori izan segurtasun arrazoiengatik", "app_extraction_failed": "Ezinezkoa izan da instalazio fitxategiak ateratzea", - "app_requirements_unmeet": "{app}(e)k behar dituen baldintzak ez dira betetzen, {pkgname} ({version}) paketea {spec} izan behar da", - "backup_deleted": "Babeskopia ezabatuta", + "backup_deleted": "Babeskopia ezabatu da: {name}", "app_argument_required": "'{name}' argumentua ezinbestekoa da", - "certmanager_acme_not_configured_for_domain": "Ezinezkoa da ACME azterketa {domain} domeinurako burutzea une honetan nginx ezarpenek ez dutelako beharrezko kodea… Egiaztatu nginx ezarpenak egunean daudela 'yunohost tools regen-conf nginx --dry-run --with-diff' komandoa exekutatuz.", - "certmanager_domain_dns_ip_differs_from_public_ip": "'{domain}' domeinurako DNS balioak ez datoz bat zerbitzariaren IParekin. Mesedez, egiaztatu 'DNS balioak' (oinarrizkoa) kategoria diagnostikoen atalean. A balioak duela gutxi aldatu badituzu, itxaron hedatu daitezen (badaude DNSen hedapena ikusteko erramintak interneten). (Zertan ari zeren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", - "confirm_app_install_thirdparty": "KONTUZ! Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Kanpoko aplikazioek sistemaren integritate eta segurtasuna arriskuan jarri dezakete. Ziur asko EZ zenuke instalatu beharko zertan ari zaren ez badakizu. Aplikazio hau ez badabil edo sistema kaltetzen badu EZ DA LAGUNTZARIK EMANGO… aurrera jarraitu nahi duzu hala ere? Aukeratu '{answers}'", + "certmanager_acme_not_configured_for_domain": "Une honetan ezinezkoa da ACME azterketa {domain} domeinurako burutzea nginx ezarpenek ez dutelako beharrezko kodea… Egiaztatu nginx ezarpenak egunean daudela 'yunohost tools regen-conf nginx --dry-run --with-diff' komandoa exekutatuz.", + "certmanager_domain_dns_ip_differs_from_public_ip": "'{domain}' domeinurako DNS balioak ez datoz bat zerbitzariaren IParekin. Egiaztatu 'DNS balioak' (oinarrizkoa) kategoria diagnostikoen atalean. 'A' balioak duela gutxi aldatu badituzu, itxaron hedatu daitezen (badaude DNSen hedapena ikusteko erramintak interneten). (Zertan ari zeren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", + "confirm_app_install_thirdparty": "KONTUZ! Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Kanpoko aplikazioek sistemaren integritate eta segurtasuna arriskuan jar dezakete. Ziur asko EZ zenuke instalatu beharko zertan ari zaren ez badakizu. Aplikazio hau ez badabil edo sistema kaltetzen badu EZ DA LAGUNTZARIK EMANGO… aurrera jarraitu nahi duzu hala ere? Hautatu '{answers}'", "app_start_remove": "{app} ezabatzen…", "diagnosis_http_hairpinning_issue_details": "Litekeena da erantzulea zure kable-modem / routerra izatea. Honen eraginez, saretik kanpo daudenek zerbitzaria arazorik gabe erabili ahal izango dute, baina sare lokalean bertan daudenek (ziur asko zure kasua) ezingo dute kanpoko IPa edo domeinu izena erabili zerbitzarira konektatzeko. Egoera hobetu edo guztiz konpontzeko, irakurri dokumentazioa", "diagnosis_http_special_use_tld": "{domain} domeinua top-level domain (TLD) motakoa da .local edo .test bezala eta ez du sare lokaletik kanpo eskuragarri zertan egon.", - "diagnosis_ip_weird_resolvconf_details": "/etc/resolv.conf fitxategia symlink bat izan beharko litzateke 127.0.0.1ra adi dagoen /etc/resolvconf/run/resolv.conf fitxategira (dnsmasq). DNS ebazleak eskuz konfiguratu nahi badituzu, mesedez aldatu /etc/resolv.dnsmasq.conf fitxategia.", + "diagnosis_ip_weird_resolvconf_details": "/etc/resolv.conf fitxategia symlink bat izan beharko litzateke 127.0.0.1ra adi dagoen /etc/resolvconf/run/resolv.conf fitxategira (dnsmasq). DNS ebazleak eskuz konfiguratu nahi badituzu, aldatu /etc/resolv.dnsmasq.conf fitxategia.", "diagnosis_ip_connected_ipv4": "Zerbitzaria IPv4 bidez dago internetera konektatuta!", "diagnosis_basesystem_ynh_inconsistent_versions": "YunoHost paketeen bertsioak ez datoz bat… ziur asko noizbait eguneraketa batek kale egin edo erabat amaitu ez zuelako.", "diagnosis_high_number_auth_failures": "Azken aldian kale egin duten saio-hasiera saiakera ugari egon dira. Egiaztatu fail2ban martxan dabilela eta egoki konfiguratuta dagoela, edo erabili beste ataka bat SSHrako dokumentazioan azaldu bezala.", @@ -28,7 +25,7 @@ "app_install_files_invalid": "Ezin dira fitxategi hauek instalatu", "diagnosis_description_ip": "Internet konexioa", "diagnosis_description_dnsrecords": "DNS erregistroak", - "app_label_deprecated": "Komando hau zaharkitua dago! Mesedez, erabili 'yunohost user permission update' komando berria aplikazioaren etiketa kudeatzeko.", + "app_label_deprecated": "Komando hau zaharkitua dago! Erabili 'yunohost user permission update' komando berria aplikazioaren etiketa kudeatzeko.", "confirm_app_install_danger": "KONTUZ! Aplikazio hau esperimentala da (edo ez dabil)! Ez zenuke instalatu beharko zertan ari zaren ez badakizu. Aplikazio hau ez badabil edo sistema kaltetzen badu, EZ DA LAGUNTZARIK EMANGO… aurrera jarraitu nahi al duzu hala ere? Aukeratu '{answers}'", "diagnosis_description_systemresources": "Sistemaren baliabideak", "backup_csv_addition_failed": "Ezinezkoa izan da fitxategiak CSV fitxategira kopiatzea", @@ -44,7 +41,6 @@ "diagnosis_ip_not_connected_at_all": "Badirudi zerbitzaria ez dagoela internetera konektatuta!?", "app_already_up_to_date": "{app} egunean da dagoeneko", "app_change_url_success": "{app} aplikazioaren URLa {domain}{path} da orain", - "admin_password_too_long": "Mesedez, aukeratu 127 karaktere baino laburragoa den pasahitz bat", "app_action_broke_system": "Eragiketa honek {services} zerbitzu garrantzitsua(k) hondatu d(it)uela dirudi", "diagnosis_basesystem_hardware_model": "Zerbitzariaren modeloa {model} da", "already_up_to_date": "Ez dago egiteko ezer. Guztia dago egunean.", @@ -53,7 +49,6 @@ "config_validate_email": "Benetazko posta elektronikoa izan behar da", "config_validate_time": "OO:MM formatua duen ordu bat izan behar da", "config_validate_url": "Benetazko URL bat izan behar da", - "config_version_not_supported": "Ezinezkoa da konfigurazio-panelaren '{version}' bertsioa erabiltzea.", "app_restore_script_failed": "Errorea gertatu da aplikazioa lehengoratzeko aginduan", "app_upgrade_some_app_failed": "Ezinezkoa izan da aplikazio batzuk eguneratzea", "app_install_failed": "Ezinezkoa izan da {app} instalatzea: {error}", @@ -84,8 +79,6 @@ "app_upgrade_failed": "Ezinezkoa izan da {app} eguneratzea: {error}", "app_upgrade_app_name": "Orain {app} eguneratzen…", "app_upgraded": "{app} eguneratu da", - "ask_firstname": "Izena", - "ask_lastname": "Abizena", "ask_main_domain": "Domeinu nagusia", "config_forbidden_keyword": "'{keyword}' etiketa sistemak bakarrik erabil dezake; ezin da ID hau daukan baliorik sortu edo erabili.", "config_unknown_filter_key": "'{filter_key}' filtroaren kakoa ez da zuzena.", @@ -118,12 +111,12 @@ "apps_catalog_updating": "Aplikazioen katalogoa eguneratzen…", "certmanager_cert_signing_failed": "Ezinezkoa izan da ziurtagiri berria sinatzea", "certmanager_cert_renew_success": "Let's Encrypt ziurtagiria berriztu da '{domain}' domeinurako", - "app_requirements_checking": "{app}(e)k behar dituen paketeak ikuskatzen…", + "app_requirements_checking": "{app}(e)k behar dituen betekizunak egiaztatzen…", "certmanager_unable_to_parse_self_CA_name": "Ezinezkoa izan da norberak sinatutako ziurtagiriaren izena prozesatzea (fitxategia: {file})", "app_remove_after_failed_install": "Aplikazioa ezabatzen instalatzerakoan errorea dela-eta…", "diagnosis_basesystem_ynh_single_version": "{package} bertsioa: {version} ({repo})", "diagnosis_failed_for_category": "'{category}' ataleko diagnostikoak kale egin du: {error}", - "diagnosis_cache_still_valid": "(Cachea oraindik baliogarria da {category} ataleko diagnosirako. Ez da berrabiaraziko!)", + "diagnosis_cache_still_valid": "(Katxea oraindik baliogarria da {category} ataleko diagnosirako. Ez da berrabiaraziko!)", "diagnosis_found_errors": "{category} atalari dago(z)kion {errors} arazo aurkitu d(ir)a!", "diagnosis_found_warnings": "{category} atalari dagokion eta hobetu daite(z)keen {warnings} abisu aurkitu d(ir)a.", "diagnosis_ip_connected_ipv6": "Zerbitzaria IPv6 bidez dago internetera konektatuta!", @@ -141,12 +134,12 @@ "diagnosis_http_unreachable": "Badirudi {domain} domeinua ez dagoela eskuragarri HTTP bidez sare lokaletik kanpo.", "apps_catalog_failed_to_download": "Ezinezkoa izan da {apps_catalog} aplikazioen zerrenda eskuratzea: {error}", "apps_catalog_init_success": "Abiarazi da aplikazioen katalogo sistema!", - "apps_catalog_obsolete_cache": "Aplikazioen katalogoaren cachea hutsik edo zaharkituta dago.", + "apps_catalog_obsolete_cache": "Aplikazioen katalogoaren katxea hutsik edo zaharkituta dago.", "diagnosis_description_mail": "Posta elektronikoa", "diagnosis_http_connection_error": "Arazoa konexioan: ezin izan da domeinu horretara konektatu, litekeena da eskuragarri ez egotea.", "diagnosis_description_web": "Weba", "diagnosis_display_tip": "Aurkitu diren arazoak ikusteko joan administrazio-atariko Diagnostikoak atalera, edo exekutatu 'yunohost diagnosis show --issues --human-readable' komandoak nahiago badituzu.", - "diagnosis_dns_point_to_doc": "Mesedez, irakurri dokumentazioa DNS erregistroekin laguntza behar baduzu.", + "diagnosis_dns_point_to_doc": "Irakurri dokumentazioa DNS erregistroekin laguntza behar baduzu.", "diagnosis_mail_ehlo_unreachable": "SMTP posta zerbitzaria ez dago eskuragarri IPv{ipversion}ko sare lokaletik kanpo eta, beraz, ez da posta elektronikoa jasotzeko gai.", "diagnosis_mail_ehlo_bad_answer_details": "Litekeena da zure zerbitzaria ez den beste gailu batek erantzun izana.", "diagnosis_mail_blacklist_listed_by": "Zure domeinua edo {item} IPa {blacklist_name} zerrenda beltzean ageri da", @@ -154,10 +147,10 @@ "diagnosis_http_could_not_diagnose_details": "Errorea: {error}", "diagnosis_http_hairpinning_issue": "Dirudienez zure sareak ez du hairpinninga gaituta.", "diagnosis_http_partially_unreachable": "Badirudi {domain} domeinua ezin dela bisitatu HTTP bidez IPv{failed} sare lokaletik kanpo, bai ordea IPv{passed} erabiliz.", - "backup_archive_cant_retrieve_info_json": "Ezinezkoa izan da '{archive}' fitxategiko informazioa eskuratzea… info.json ezin izan da eskuratu (edo ez da baliozko jsona).", + "backup_archive_cant_retrieve_info_json": "Ezinezkoa izan da '{archive}' fitxategiko informazioa eskuratzea… info.json fitxategia ezin izan da eskuratu (edo ez da baliozko jsona).", "diagnosis_domain_expiration_not_found": "Ezinezkoa izan da domeinu batzuen iraungitze data egiaztatzea", "diagnosis_domain_expiration_not_found_details": "Badirudi {domain} domeinuari buruzko WHOIS informazioak ez duela zehazten noiz iraungiko den.", - "certmanager_domain_not_diagnosed_yet": "Oraindik ez dago {domain} domeinurako diagnostikorik. Mesedez, berrabiarazi diagnostikoak 'DNS balioak' eta 'Web' ataletarako diagnostikoen gunean Let's Encrypt ziurtagirirako prest ote dagoen egiaztatzeko. (Edo zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztatzea desgaitzeko.)", + "certmanager_domain_not_diagnosed_yet": "Oraindik ez dago {domain} domeinurako diagnostikorik. Berrabiarazi diagnostikoak 'DNS balioak' eta 'Web' ataletarako diagnostikoen gunean Let's Encrypt ziurtagirirako prest ote dagoen egiaztatzeko. (Edo zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztatzea desgaitzeko.)", "diagnosis_domain_expiration_warning": "Domeinu batzuk iraungitzear daude!", "app_packaging_format_not_supported": "Aplikazio hau ezin da instalatu YunoHostek ez duelako paketea ezagutzen. Sistema eguneratzea hausnartu beharko zenuke ziur asko.", "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost dyndns update --force erabiliz.", @@ -175,7 +168,7 @@ "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)!", + "diagnosis_diskusage_ok": "{mountpoint} fitxategi-sistemak ({device} euskarrian) edukieraren {free} (%{free_percent}a) ditu oraindik erabilgarri ({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", @@ -206,7 +199,7 @@ "backup_archive_writing_error": "Ezinezkoa izan da '{source}' ('{dest}' fitxategiak eskatu dituenak) fitxategia '{archive}' konprimatutako babeskopian sartzea", "backup_ask_for_copying_if_needed": "Behin-behinean {size}MB erabili nahi dituzu babeskopia gauzatu ahal izateko? (Horrela egiten da fitxategi batzuk ezin direlako modu eraginkorragoan prestatu.)", "backup_cant_mount_uncompress_archive": "Ezinezkoa izan da deskonprimatutako fitxategia muntatzea idazketa-babesa duelako", - "backup_created": "Babeskopia sortu da", + "backup_created": "Babeskopia sortu da: {name}", "backup_copying_to_organize_the_archive": "{size}MB kopiatzen fitxategia antolatzeko", "backup_couldnt_bind": "Ezin izan da {src} {dest}-ra lotu.", "backup_output_directory_forbidden": "Aukeratu beste katalogo bat emaitza gordetzeko. Babeskopiak ezin dira sortu /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var edo /home/yunohost.backup/archives azpi-katalogoetan", @@ -223,8 +216,8 @@ "certmanager_cert_install_success_selfsigned": "Norberak sinatutako ziurtagiria instalatu da '{domain}' domeinurako", "certmanager_domain_cert_not_selfsigned": "{domain} domeinurako ziurtagiria ez da norberak sinatutakoa. Ziur al zaude ordezkatu nahi duzula? (Erabili '--force' hori egiteko.)", "certmanager_certificate_fetching_or_enabling_failed": "{domain} domeinurako ziurtagiri berriak kale egin du…", - "certmanager_domain_http_not_working": "Ez dirudi {domain} domeinua HTTP bidez ikusgai dagoenik. Mesedez, egiaztatu 'Weba' atala diagnosien gunean informazio gehiagorako. (Zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", - "certmanager_hit_rate_limit": "{domain} domeinu-multzorako ziurtagiri gehiegi jaulki dira dagoeneko. Mesedez, saia saitez geroago. Ikus https://letsencrypt.org/docs/rate-limits/ xehetasun gehiagorako", + "certmanager_domain_http_not_working": "Ez dirudi {domain} domeinua HTTP bidez ikusgai dagoenik. Egiaztatu 'Weba' atala diagnosien gunean informazio gehiagorako. (Zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", + "certmanager_hit_rate_limit": "{domain} domeinu-multzorako ziurtagiri gehiegi jaulki dira dagoeneko. Saia saitez geroago. Ikus https://letsencrypt.org/docs/rate-limits/ xehetasun gehiagorako", "certmanager_no_cert_file": "Ezinezkoa izan da {domain} domeinurako ziurtagiri fitxategia irakurrtzea (fitxategia: {file})", "certmanager_self_ca_conf_file_not_found": "Ezinezkoa izan da konfigurazio-fitxategia aurkitzea norberak sinatutako ziurtagirirako (fitxategia: {file})", "confirm_app_install_warning": "Adi: litekeena da aplikazio hau ibiltzea baina ez dago YunoHostera egina. Ezaugarri batzuk, SSO edo babeskopia/lehengoratzea esaterako, desgaituta egon daitezke. Instalatu hala ere? [{answers}] ", @@ -237,13 +230,13 @@ "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_allgood": "Instalatutako aplikazioak bat datoz oinarrizko pakete-jarraibideekin", "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_apps_not_in_app_catalog": "Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Iraganean egon bazen eta orain ez badago, desinstalatzea litzateke onena, ez baitu eguneraketarik jasoko eta sistemaren integritate eta segurtasuna arriskuan jar lezakeelako.", + "diagnosis_apps_outdated_ynh_requirement": "Instalatutako aplikazio honen bertsioak yunohost >= 2.x edo 3.x baino ez du behar, eta horrek eguneratua izan ez dela eta egungo pakete-jardunbideekin bat ez datorrela iradokitzen du. Eguneratzen saiatu beharko zinateke.", "diagnosis_description_apps": "Aplikazioak", "domain_dns_conf_special_use_tld": "Domeinu hau top-level domain (TLD) erabilera bereziko motakoa da .local edo .test bezala eta ez du DNS ezarpenik behar.", "log_permission_create": "Sortu '{}' baimena", @@ -253,10 +246,7 @@ "group_user_already_in_group": "{user} erabiltzailea {group} taldean dago dagoeneko", "firewall_reloaded": "Suebakia birkargatu da", "domain_unknown": "'{domain}' domeinua ezezaguna da", - "global_settings_cant_serialize_settings": "Ezinezkoa izan da konfikurazio-datuak serializatzea, zergatia: {reason}", - "global_settings_setting_security_nginx_redirect_to_https": "Birbideratu HTTP eskaerak HTTPSra (EZ ITZALI hau ez badakizu zertan ari zaren!)", "group_deleted": "'{group}' taldea ezabatu da", - "invalid_password": "Pasahitza ez da zuzena", "log_domain_main_domain": "Lehenetsi '{}' domeinua", "log_user_group_update": "Moldatu '{}' taldea", "dyndns_could_not_check_available": "Ezinezkoa izan da {domain} {provider}(e)n eskuragarri dagoen egiaztatzea.", @@ -280,22 +270,15 @@ "extracting": "Ateratzen…", "diagnosis_ports_unreachable": "{port}. ataka ez dago eskuragarri kanpotik.", "diagnosis_regenconf_manually_modified_details": "Ez dago arazorik zertan ari zaren baldin badakizu! YunoHostek fitxategi hau automatikoki eguneratzeari utziko dio… Baina kontuan izan YunoHosten eguneraketek aldaketa garrantzitsuak izan ditzaketela. Nahi izatekotan, desberdintasunak aztertu ditzakezu yunohost tools regen-conf {category} --dry-run --with-diff komandoa exekutatuz, eta gomendatutako konfiguraziora bueltatu yunohost tools regen-conf {category} --force erabiliz", - "experimental_feature": "Adi: Funtzio hau esperimentala eta ezegonkorra da, ez zenuke erabili beharko ez badakizu zertan ari zaren.", - "global_settings_cant_write_settings": "Ezinezkoa izan da konfigurazio fitxategia gordetzea, zergatia: {reason}", "dyndns_domain_not_provided": "{provider} DynDNS enpresak ezin du {domain} domeinua eskaini.", "firewall_reload_failed": "Ezinezkoa izan da suebakia birkargatzea", - "global_settings_setting_security_password_admin_strength": "Administrazio-pasahitzaren segurtasuna", "hook_name_unknown": "'{name}' 'hook' izen ezezaguna", "domain_deletion_failed": "Ezinezkoa izan da {domain} ezabatzea: {error}", - "global_settings_setting_security_nginx_compatibility": "Bateragarritasun eta segurtasun arteko gatazka NGINX web zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", "log_regen_conf": "Berregin '{}' sistemaren konfigurazioa", "dpkg_lock_not_available": "Ezin da komando hau une honetan exekutatu beste aplikazio batek dpkg (sistemaren paketeen kudeatzailea) blokeatuta duelako, erabiltzen ari baita", "group_created": "'{group}' taldea sortu da", - "global_settings_setting_security_password_user_strength": "Erabiltzaile-pasahitzaren segurtasuna", - "global_settings_setting_security_experimental_enabled": "Gaitu segurtasun funtzio esperimentalak (ez ezazu egin ez badakizu zertan ari zaren!)", - "good_practices_about_admin_password": "Administrazio-pasahitz berria ezartzear zaude. Pasahitzak zortzi karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", - "log_help_to_get_failed_log": "Ezin izan da '{desc}' eragiketa exekutatu. Mesedez, laguntza nahi baduzu partekatu eragiketa honen erregistro osoa 'yunohost log share {name}' komandoa erabiliz", - "global_settings_setting_security_webadmin_allowlist_enabled": "Baimendu IP zehatz batzuk bakarrik administrazio-atarian.", + "good_practices_about_admin_password": "Administrazio-pasahitz berria ezartzear zaude. Pasahitzak 8 karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", + "log_help_to_get_failed_log": "Ezin izan da '{desc}' eragiketa exekutatu. Laguntza nahi baduzu partekatu eragiketa honen erregistro osoa 'yunohost log share {name}' komandoa erabiliz", "group_unknown": "'{group}' taldea ezezaguna da", "group_updated": "'{group}' taldea eguneratu da", "group_update_failed": "Ezinezkoa izan da '{group}' taldea eguneratzea: {error}", @@ -304,10 +287,10 @@ "log_permission_delete": "Ezabatu '{}' baimena", "group_already_exist": "{group} taldea existitzen da dagoeneko", "group_user_not_in_group": "{user} erabiltzailea ez dago {group} taldean", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Operadore batzuek ez dute alderantzizko DNSa konfiguratzen uzten (edo funtzioa ez dabil…). IPv4rako alderantzizko DNSa zuzen konfiguratuta badago, IPv6 desgaitzen saia zaitezke posta elektronikoa bidaltzeko, yunohost settings set smtp.allow_ipv6 -v off exekutatuz. Adi: honek esan nahi du ez zarela gai izango IPv6 bakarrik darabilten zerbitzari apurren posta elektronikoa jasotzeko edo beraiei bidaltzeko.", - "diagnosis_sshd_config_inconsistent": "Dirudienez SSH ataka eskuz aldatu da /etc/ssh/sshd_config fitxategian. YunoHost 4.2tik aurrera 'security.ssh.port' izeneko ezarpen orokor bat dago konfigurazioa eskuz aldatzea ekiditeko.", - "diagnosis_sshd_config_inconsistent_details": "Mesedez, exekutatu yunohost settings set security.ssh.port -v YOUR_SSH_PORT SSH ataka zehazteko, egiaztatu yunohost tools regen-conf ssh --dry-run --with-diff erabiliz eta yunohost tools regen-conf ssh --force exekutatu gomendatutako konfiguraziora bueltatu nahi baduzu.", - "domain_dns_push_failed_to_authenticate": "Ezinezkoa izan da '{domain}' domeinurako APIa erabiliz erregistro-enpresan saioa hastea. Zuzenak al dira datuak? (Errorea: {error})", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Operadore batzuek ez dute alderantzizko DNSa konfiguratzen uzten (edo funtzioa ez dabil…). IPv4rako alderantzizko DNSa zuzen konfiguratuta badago, IPv6 desgaitzen saia zaitezke posta elektronikoa bidaltzeko, yunohost settings set email.smtp.smtp_allow_ipv6 -v off exekutatuz. Adi: honek esan nahi du ez zarela gai izango IPv6 bakarrik darabilten zerbitzari apurren posta elektronikoa jasotzeko edo beraiei bidaltzeko.", + "diagnosis_sshd_config_inconsistent": "Dirudienez SSH ataka eskuz aldatu da /etc/ssh/sshd_config fitxategian. YunoHost 4.2tik aurrera 'security.ssh.ssh_port' izeneko ezarpen orokor bat dago konfigurazioa eskuz aldatzea ekiditeko.", + "diagnosis_sshd_config_inconsistent_details": "Exekutatu yunohost settings set security.ssh.ssh_port -v SSH_ATAKA SSH ataka zehazteko, egiaztatu yunohost tools regen-conf ssh --dry-run --with-diff erabiliz eta yunohost tools regen-conf ssh --force exekutatu gomendatutako konfiguraziora bueltatu nahi baduzu.", + "domain_dns_push_failed_to_authenticate": "Ezinezkoa izan da '{domain}' domeinuko erregistro-enpresan APIa erabiliz saioa hastea. Ziurrenik datuak ez dira zuzenak. (Errorea: {error})", "domain_dns_pushing": "DNS ezarpenak bidaltzen…", "diagnosis_sshd_config_insecure": "Badirudi SSH konfigurazioa eskuz aldatu dela eta ez da segurua ez duelako 'AllowGroups' edo 'AllowUsers' baldintzarik jartzen fitxategien atzitzea oztopatzeko.", "disk_space_not_sufficient_update": "Ez dago aplikazio hau eguneratzeko nahikoa espaziorik", @@ -317,14 +300,13 @@ "domain_dns_push_success": "DNS ezarpenak eguneratu dira!", "domain_dns_push_failed": "DNS ezarpenen eguneratzeak kale egin du.", "domain_dns_push_partial_failure": "DNS ezarpenak erdipurdi eguneratu dira: jakinarazpen/errore batzuk egon dira.", - "global_settings_setting_smtp_relay_host": "YunoHosten ordez posta elektronikoa bidaltzeko SMTP relay helbidea. Erabilgarri izan daiteke egoera hauetan: operadore edo VPS enpresak 25. ataka blokeatzen badu, DUHLen zure etxeko IPa ageri bada, ezin baduzu alderantzizko DNSa ezarri edo zerbitzari hau ez badago zuzenean internetera konektatuta baina posta elektronikoa bidali nahi baduzu.", "group_deletion_failed": "Ezinezkoa izan da '{group}' taldea ezabatzea: {error}", "invalid_number_min": "{min} baino handiagoa izan behar da", "invalid_number_max": "{max} baino txikiagoa izan behar da", "diagnosis_services_bad_status": "{service} zerbitzua {status} dago :(", "diagnosis_ports_needed_by": "{category} funtzioetarako ezinbestekoa da ataka hau eskuragarri egotea ({service} zerbitzua)", "diagnosis_package_installed_from_sury": "Sistemaren pakete batzuen lehenagoko bertsioak beharko lirateke", - "global_settings_setting_smtp_relay_password": "SMTP relay helbideko pasahitza", + "global_settings_setting_smtp_relay_password": "SMTP relay pasahitza", "global_settings_setting_smtp_relay_port": "SMTP relay ataka", "domain_deleted": "Domeinua ezabatu da", "domain_dyndns_root_unknown": "Ez da ezagutzen DynDNSaren root domeinua", @@ -332,7 +314,7 @@ "domain_registrar_is_not_configured": "Oraindik ez da {domain} domeinurako erregistro-enpresa ezarri.", "domain_dns_push_not_applicable": "Ezin da {domain} domeinurako DNS konfigurazio automatiko funtzioa erabili. DNS erregistroak eskuz ezarri beharko zenituzke gidaorriei erreparatuz: https://yunohost.org/dns_config.", "domain_dns_push_managed_in_parent_domain": "DNS ezarpenak automatikoki konfiguratzeko funtzioa {parent_domain} domeinu nagusian kudeatzen da.", - "domain_dns_registrar_managed_in_parent_domain": "Domeinu hau {parent_domain_link} (r)en azpidomeinua da. DNS ezarpenak {parent_domain}(r)en konfigurazio atalean kudeatu behar dira.", + "domain_dns_registrar_managed_in_parent_domain": "Domeinu hau {parent_domain_link}(r)en azpidomeinua da. DNS ezarpenak {parent_domain}(r)en konfigurazio atalean kudeatu behar dira.", "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost 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!", @@ -345,7 +327,6 @@ "domain_config_auth_application_key": "Aplikazioaren gakoa", "domain_config_auth_application_secret": "Aplikazioaren gako sekretua", "domain_config_auth_consumer_key": "Erabiltzailearen gakoa", - "global_settings_setting_smtp_allow_ipv6": "Baimendu IPv6 posta elektronikoa jaso eta bidaltzeko", "group_cannot_be_deleted": "{group} taldea ezin da eskuz ezabatu.", "log_domain_config_set": "Aldatu '{}' domeinuko ezarpenak", "log_domain_dns_push": "Bidali '{}' domeinuaren DNS ezarpenak", @@ -355,13 +336,9 @@ "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).", + "good_practices_about_user_password": "Erabiltzaile-pasahitz berria ezartzear zaude. Pasahitzak 8 karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", "group_cannot_edit_all_users": "'all_users' taldea ezin da eskuz moldatu. YunoHosten izena emanda dauden erabiltzaile guztiak barne dituen talde berezia da", "invalid_number": "Zenbaki bat izan behar da", "ldap_attribute_already_exists": "'{attribute}' LDAP funtzioa existitzen da dagoeneko eta '{value}' balioa dauka", @@ -378,9 +355,9 @@ "diagnosis_mail_queue_unavailable": "Ezinezkoa da ilaran zenbat posta elektroniko dauden kontsultatzea", "log_user_create": "Gehitu '{}' erabiltzailea", "group_cannot_edit_visitors": "'bisitariak' taldea ezin da eskuz moldatu. Saiorik hasi gabeko bisitariak barne hartzen dituen talde berezia da", - "diagnosis_ram_verylow": "RAM memoriaren {available} baino ez ditu erabilgarri sistemak; memoria guztiaren ({total}) %{available_percent}a bakarrik!", - "diagnosis_ram_low": "RAM memoriaren {available} ditu erabilgarri sistemak; memoria guztiaren ({total}) %{available_percent}a. Adi ibili.", - "diagnosis_ram_ok": "RAM memoriaren {available} ditu oraindik erabilgarri sistemak; memoria guztiaren ({total}) %{available_percent}a.", + "diagnosis_ram_verylow": "Sistemak RAM memoriaren {available} baino ez ditu erabilgarri; memoria guztiaren ({total}) %{available_percent}a bakarrik!", + "diagnosis_ram_low": "Sistemak RAM memoriaren {available} ditu erabilgarri; memoria guztiaren ({total}) %{available_percent}a. Adi ibili.", + "diagnosis_ram_ok": "Sistemak RAM memoriaren {available} ditu oraindik erabilgarri; memoria guztiaren ({total}) %{available_percent}a.", "diagnosis_swap_none": "Sistemak ez du swap-ik. Gutxienez {recommended} izaten saiatu beharko zinateke, sistema memoriarik gabe gera ez dadin.", "diagnosis_swap_ok": "Sistemak {total} swap dauzka!", "diagnosis_regenconf_allgood": "Konfigurazio-fitxategi guztiak bat datoz gomendatutako ezarpenekin!", @@ -399,7 +376,7 @@ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Oraingo alderantzizko DNSa: {rdns_domain}
Esperotako balioa: {ehlo_domain}", "diagnosis_mail_queue_too_big": "Mezu gehiegi posta elektronikoaren ilaran: ({nb_pending} mezu)", "diagnosis_ports_could_not_diagnose_details": "Errorea: {error}", - "diagnosis_swap_tip": "Mesedez, kontuan hartu zerbitzari honen swap memoria SD edo SSD euskarri batean gordetzeak euskarri horren bizi-iraupena izugarri laburtu dezakeela.", + "diagnosis_swap_tip": "Kontuan hartu zerbitzari honen swap memoria SD edo SSD euskarri batean gordetzeak euskarri horren bizi-iraupena izugarri laburtu dezakeela.", "invalid_regex": "'Regexa' ez da zuzena: '{regex}'", "group_creation_failed": "Ezinezkoa izan da '{group}' taldea sortzea: {error}", "log_user_permission_reset": "Berrezarri '{}' baimena", @@ -416,31 +393,23 @@ "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'.", + "dpkg_is_broken": "Une honetan ezinezkoa da sistemaren dpkg/APT pakateen kudeatzaileek hondatutako itxura dutelako… Arazoa konpontzeko SSH bidez konektatzen saia zaitezke eta ondoren exekutatu 'sudo apt install --fix-broken' edota 'sudo dpkg --configure -a' edota 'sudo dpkg --audit'.", "domain_cannot_remove_main": "Ezin duzu '{domain}' ezabatu domeinu nagusia delako. Beste domeinu bat ezarri beharko duzu nagusi bezala 'yunohost domain main-domain -n ' erabiliz; honako hauek dituzu aukeran: {other_domains}", "domain_created": "Sortu da domeinua", "domain_dyndns_already_subscribed": "Dagoeneko izena eman duzu DynDNS domeinu batean", "domain_hostname_failed": "Ezinezkoa izan da hostname berria ezartzea. Honek arazoak ekar litzake etorkizunean (litekeena da ondo egotea).", - "domain_uninstall_app_first": "Honako aplikazio hauek domeinuan instalatuta daude:\n{apps}\n\nMesedez, desinstalatu 'yunohost app remove the_app_id' exekutatuz edo alda itzazu beste domeinu batera 'yunohost app change-url the_app_id' erabiliz domeinua ezabatu baino lehen", + "domain_uninstall_app_first": "Honako aplikazio hauek domeinuan instalatuta daude:\n{apps}\n\nDesinstalatu 'yunohost app remove the_app_id' exekutatuz edo alda itzazu beste domeinu batera 'yunohost app change-url the_app_id' erabiliz domeinua ezabatu baino lehen", "file_does_not_exist": "{path} fitxategia ez da existitzen.", "firewall_rules_cmd_failed": "Suebakiko arau batzuen exekuzioak huts egin du. Informazio gehiago erregistroetan.", "log_app_remove": "Ezabatu '{}' aplikazioa", - "global_settings_cant_open_settings": "Ezinezkoa izan da konfigurazio fitxategia irekitzea, zergatia: {reason}", - "global_settings_reset_success": "Lehengo ezarpenak {path}-n gorde dira", - "global_settings_unknown_setting_from_settings_file": "Gako ezezaguna ezarpenetan: '{setting_key}', baztertu eta gorde ezazu hemen: /etc/yunohost/settings-unknown.json", "domain_remove_confirm_apps_removal": "Domeinu hau ezabatzean aplikazio hauek desinstalatuko dira:\n{apps}\n\nZiur al zaude? [{answers}]", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Baimendu DSA gakoa (zaharkitua) SSH zerbitzuaren konfiguraziorako", "hook_list_by_invalid": "Aukera hau ezin da 'hook'ak zerrendatzeko erabili", "installation_complete": "Instalazioa amaitu da", "hook_exec_failed": "Ezinezkoa izan da agindua exekutatzea: {path}", @@ -452,7 +421,7 @@ "log_remove_on_failed_install": "Ezabatu '{}' instalazioak huts egin ondoren", "log_domain_add": "Gehitu '{}' domeinua sistemaren konfiguraziora", "log_dyndns_subscribe": "Eman izena YunoHosten '{}' azpidomeinuan", - "diagnosis_no_cache": "Oraindik ez dago '{category}' atalerako diagnostikoaren cacherik", + "diagnosis_no_cache": "Oraindik ez dago '{category}' atalerako diagnostikoaren katxerik", "diagnosis_mail_queue_ok": "Posta elektronikoaren ilaran zain dauden mezuak: {nb_pending}", "global_settings_setting_smtp_relay_user": "SMTP relay erabiltzailea", "domain_cert_gen_failed": "Ezinezkoa izan da ziurtagiria sortzea", @@ -465,21 +434,17 @@ "log_user_permission_update": "Eguneratu '{}' baimenerako sarbideak", "log_user_update": "Eguneratu '{}' erabiltzailearen informazioa", "dyndns_no_domain_registered": "Ez dago DynDNSrekin izena emandako domeinurik", - "global_settings_bad_type_for_setting": "{setting} ezarpenerako mota okerra. {received_type} ezarri da, {expected_type} espero zen", "diagnosis_mail_fcrdns_dns_missing": "Ez da alderantzizko DNSrik ezarri IPv{ipversion}rako. Litekeena da hartzaileak posta elektroniko batzuk jaso ezin izatea edo mezuok spam modura etiketatuak izatea.", "log_backup_create": "Sortu babeskopia fitxategia", - "global_settings_setting_backup_compress_tar_archives": "Babeskopia berriak sortzean, konprimitu fitxategiak (.tar.gz) konprimitu gabeko fitxategien (.tar) ordez. Aukera hau gaitzean babeskopiek espazio gutxiago beharko dute, baina hasierako prozesua luzeagoa izango da eta CPUari lan handiagoa eragingo dio.", - "global_settings_setting_security_webadmin_allowlist": "Administrazio-ataria bisita dezaketen IP helbideak, koma bidez bereiziak.", - "global_settings_key_doesnt_exists": "'{settings_key}' gakoa ez da existitzen konfigurazio orokorrean; erabilgarri dauden gakoak ikus ditzakezu 'yunohost settings list' exekutatuz", - "global_settings_setting_ssowat_panel_overlay_enabled": "Gaitu SSOwat paneleko \"overlay\"a", + "global_settings_setting_ssowat_panel_overlay_enabled": "Gaitu YunoHosten atarira daraman lasterbidea aplikazioetan", "log_backup_restore_system": "Lehengoratu sistema babeskopia fitxategi batetik", "log_domain_remove": "Ezabatu '{}' domeinua sistemaren ezarpenetatik", - "log_link_to_failed_log": "Ezinezkoa izan da '{desc}' eragiketa exekutatzea. Mesedez, laguntza nahi izanez gero, partekatu erakigeta honen erregistro osoa hemen sakatuz", + "log_link_to_failed_log": "Ezinezkoa izan da '{desc}' eragiketa exekutatzea. Laguntza nahi izanez gero, partekatu erakigeta honen erregistro osoa hemen sakatuz", "log_permission_url": "Eguneratu '{}' baimenari lotutako URLa", "log_user_group_create": "Sortu '{}' taldea", "permission_creation_failed": "Ezinezkoa izan da '{permission}' baimena sortzea: {error}", "permission_not_found": "Ez da '{permission}' baimena aurkitu", - "pattern_lastname": "Abizen horrek ez du balio", + "pattern_lastname": "Abizen horrek ez du balio (gutxienez hiru karaktere behar ditu)", "permission_deleted": "'{permission}' baimena ezabatu da", "service_disabled": "'{service}' zerbitzua ez da etorkizunean zerbitzaria abiaraztearekin batera exekutatuko.", "unexpected_error": "Ezusteko zerbaitek huts egin du: {error}", @@ -500,7 +465,7 @@ "user_import_success": "Erabiltzaileak arazorik gabe inportatu dira", "yunohost_already_installed": "YunoHost instalatuta dago dagoeneko", "migrations_success_forward": "{id} migrazioak amaitu du", - "migrations_to_be_ran_manually": "{id} migrazioa eskuz abiarazi behar da. Mesedez, joan Erramintak → Migrazioak atalera administrazio-atarian edo bestela exekutatu 'yunohost tools migrations run'.", + "migrations_to_be_ran_manually": "{id} migrazioa eskuz abiarazi behar da. Joan Tresnak → Migrazioak atalera administrazio-atarian edo bestela exekutatu 'yunohost tools migrations run'.", "permission_currently_allowed_for_all_users": "Baimen hau erabiltzaile guztiei esleitzen zaie eta baita beste talde batzuei ere. Litekeena da 'all users' baimena edo esleituta duten taldeei baimena kendu nahi izatea.", "permission_require_account": "'{permission}' baimena zerbitzarian kontua duten erabiltzaileentzat da eta, beraz, ezin da gaitu bisitarientzat.", "postinstall_low_rootfsspace": "'root' fitxategi-sistemak 10 GB edo espazio gutxiago dauka, kezkatzekoa dena! Litekeena da espaziorik gabe geratzea aurki! Gomendagarria da 'root' fitxategi-sistemak gutxienez 16 GB libre izatea. Jakinarazpen honen ondoren YunoHost instalatzen jarraitu nahi baduzu, berrabiarazi agindua '--force-diskspace' gehituz", @@ -517,8 +482,8 @@ "service_restart_failed": "Ezin izan da '{service}' zerbitzua berrabiarazi\n\nZerbitzuen azken erregistroak: {logs}", "service_restarted": "'{service}' zerbitzua berrabiarazi da", "service_start_failed": "Ezin izan da '{service}' zerbitzua abiarazi\n\nZerbitzuen azken erregistroak: {logs}", - "update_apt_cache_failed": "Ezin da APT Debian-en pakete kudeatzailearen cachea eguneratu. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", - "update_apt_cache_warning": "Zerbaitek huts egin du APT Debian-en pakete kudeatzailearen cachea eguneratzean. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", + "update_apt_cache_failed": "Ezin da APT Debian-en pakete kudeatzailearen katxea eguneratu. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", + "update_apt_cache_warning": "Zerbaitek huts egin du APT Debian-en pakete kudeatzailearen katxea eguneratzean. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", "user_created": "Erabiltzailea sortu da", "user_deletion_failed": "Ezin izan da '{user}' ezabatu: {error}", "permission_updated": "'{permission}' baimena moldatu da", @@ -532,7 +497,7 @@ "yunohost_installing": "YunoHost instalatzen…", "migrations_failed_to_load_migration": "Ezinezkoa izan da {id} migrazioa kargatzea: {error}", "migrations_must_provide_explicit_targets": "'--skip' edo '--force-rerun' aukerak erabiltzean jomuga zehatzak zehaztu behar dituzu", - "migrations_pending_cant_rerun": "Migrazio hauek exekutatzeke daude eta, beraz, ezin dira berriro abiarazi: {ids}", + "migrations_pending_cant_rerun": "Migrazio hauek oraindik ez dira exekutatu eta, beraz, ezin dira berriro abiarazi: {ids}", "regenconf_file_kept_back": "'{conf}' konfigurazio fitxategia regen-conf-ek ({category} atala) ezabatzekoa zen baina mantendu egin da.", "regenconf_file_removed": "'{conf}' konfigurazio fitxategia ezabatu da", "permission_already_allowed": "'{group} taldeak badauka dagoeneko '{permission}' baimena", @@ -560,7 +525,7 @@ "regenconf_file_manually_removed": "'{conf}' konfigurazio fitxategia eskuz ezabatu da eta ez da berriro sortuko", "regenconf_up_to_date": "Konfigurazioa egunean dago dagoeneko '{category}' atalerako", "migrations_no_such_migration": "Ez dago '{id}' izeneko migraziorik", - "migrations_not_pending_cant_skip": "Migrazio hauek ez daude exekutatzeke eta, beraz, ezin dira saihestu: {ids}", + "migrations_not_pending_cant_skip": "Migrazio hauek ez daude exekutatzeke eta, beraz, ez dago saihesteko aukerarik: {ids}", "regex_with_only_domain": "Ezin duzu regex domeinuetarako erabili; bideetarako bakarrik", "port_already_closed": "{port}. ataka itxita dago dagoeneko {ip_version} konexioetarako", "regenconf_file_copy_failed": "Ezinezkoa izan da '{new}' konfigurazio fitxategi berria '{conf}'-(e)n kopiatzea", @@ -571,18 +536,17 @@ "service_enable_failed": "Ezin izan da '{service}' zerbitzua sistema abiaraztearekin batera exekutatzea lortu.\n\nZerbitzuen erregistro berrienak: {logs}", "system_username_exists": "Erabiltzaile izena existitzen da dagoeneko sistemaren erabiltzaileen zerrendan", "user_already_exists": "'{user}' erabiltzailea existitzen da dagoeneko", - "mail_domain_unknown": "Ezinezkoa da posta elektroniko hori '{domain}' domeinurako erabiltzea. Mesedez, erabili zerbitzari honek kudeatzen duen domeinu bat.", + "mail_domain_unknown": "Ezinezkoa da posta elektroniko hori '{domain}' domeinurako erabiltzea. Erabili zerbitzari honek kudeatzen duen domeinu bat.", "migrations_list_conflict_pending_done": "Ezin dituzu '--previous' eta '--done' aldi berean erabili.", "migrations_loading_migration": "{id} migrazioa kargatzen…", "migrations_no_migrations_to_run": "Ez dago exekutatzeko migraziorik", - "password_listed": "Pasahitz hau munduan erabilienetarikoa da. Mesedez, aukeratu bereziagoa den beste bat.", - "password_too_simple_2": "Pasahitzak zortzi karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat eta txikiren bat izan behar ditu", - "pattern_firstname": "Izen horrek ez du balio", + "password_listed": "Pasahitz hau munduan erabilienetarikoa da. Aukeratu bereziagoa den beste bat.", + "password_too_simple_2": "Pasahitzak 8 karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat eta txikiren bat izan behar ditu", + "pattern_firstname": "Izen horrek ez du balio (gutxienez hiru karaktere behar ditu)", "pattern_password": "Gutxienez hiru karaktere izan behar ditu", "restore_failed": "Ezin izan da sistema lehengoratu", "restore_removing_tmp_dir_failed": "Ezinezkoa izan da behin-behineko direktorio zaharra ezabatzea", "restore_running_app_script": "'{app}' aplikazioa lehengoratzen…", - "root_password_replaced_by_admin_password": "Administrazio-pasahitzak root pasahitza ordezkatu du.", "service_description_fail2ban": "Internetetik datozen bortxaz egindako saiakerak eta bestelako erasoak ekiditen ditu", "service_description_ssh": "Zerbitzarira sare lokaletik kanpo konektatzea ahalbidetzen du (SSH protokoloa)", "service_description_yunohost-firewall": "Zerbitzuen konexiorako atakak ireki eta ixteko kudeatzailea da", @@ -600,13 +564,13 @@ "yunohost_configured": "YunoHost konfiguratuta dago", "service_description_yunomdns": "Sare lokalean zerbitzarira 'yunohost.local' erabiliz konektatzea ahalbidetzen du", "mail_alias_remove_failed": "Ezin izan da '{mail}' e-mail ezizena ezabatu", - "mail_unavailable": "Helbide elektroniko hau lehenengo erabiltzailearentzat gorde da eta hari ezarri zaio automatikoki", + "mail_unavailable": "Helbide elektroniko hau administratzaileen taldearentzat gorde da", "migration_ldap_backup_before_migration": "Sortu LDAP datubase eta aplikazioen ezarpenen babeskopia migrazioa abiarazi baino lehen.", "migration_ldap_can_not_backup_before_migration": "Sistemaren babeskopiak ez du amaitu migrazioak huts egin baino lehen. Errorea: {error}", "migrations_migration_has_failed": "{id} migrazioak ez du amaitu, geldiarazten. Errorea: {exception}", - "migrations_need_to_accept_disclaimer": "{id} migrazioa abiarazteko, ondorengo baldintzak onartu behar dituzu:\n---\n{disclaimer}\n---\nMigrazioa onartzen baduzu, mesedez berrabiarazi prozesua komandoan '--accept-disclaimer' aukera gehituz.", + "migrations_need_to_accept_disclaimer": "{id} migrazioa abiarazteko, ondorengo baldintzak onartu behar dituzu:\n---\n{disclaimer}\n---\nMigrazioa onartzen baduzu, berrabiarazi prozesua komandoan '--accept-disclaimer' aukera gehituz.", "not_enough_disk_space": "Ez dago nahikoa espazio librerik '{path}'-n", - "password_too_simple_3": "Pasahitzak zortzi karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat, txikiren bat eta karaktere bereziren bat izan behar ditu", + "password_too_simple_3": "Pasahitzak 8 karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat, txikiren bat eta karaktere bereziren bat izan behar ditu", "pattern_backup_archive_name": "Fitxategiaren izenak 30 karaktere izan ditzake gehienez, alfanumerikoak eta ._- baino ez", "pattern_domain": "Domeinu izen baliagarri bat izan behar da (adibidez: nire-domeinua.eus)", "pattern_mailbox_quota": "Tamainak b/k/M/G/T zehaztu behar du edo 0 mugarik ezarri nahi ez bada", @@ -619,8 +583,8 @@ "port_already_opened": "{port}. ataka dagoeneko irekita dago {ip_version} konexioetarako", "user_home_creation_failed": "Ezin izan da erabiltzailearentzat '{home}' direktorioa sortu", "user_unknown": "Erabiltzaile ezezaguna: {user}", - "yunohost_postinstall_end_tip": "Instalazio ondorengo prozesua amaitu da! Sistemaren konfigurazioa bukatzeko:\n- gehitu erabiltzaile bat administrazio-atariko 'Erabiltzaileak' atalean (edo 'yunohost user create ' komandoa erabiliz);\n- erabili 'Diagnostikoak' atala ohiko arazoei aurre hartzeko. Administrazio-atarian abiarazi edo 'yunohost diagnosis run' exekutatu;\n- irakurri 'Finalizing your setup' eta 'Getting to know YunoHost' atalak. Dokumentazioan aurki ditzakezu: https://yunohost.org/admindoc.", - "yunohost_not_installed": "YunoHost ez da zuzen instalatu. Mesedez, exekutatu 'yunohost tools postinstall'", + "yunohost_postinstall_end_tip": "Instalazio ondorengo prozesua amaitu da! Sistemaren konfigurazioa bukatzeko:\n- erabili 'Diagnostikoak' atala ohiko arazoei aurre hartzeko. Administrazio-atarian abiarazi edo 'yunohost diagnosis run' exekutatu;\n- irakurri 'Finalizing your setup' eta 'Getting to know YunoHost' atalak. Dokumentazioan aurki ditzakezu: https://yunohost.org/admindoc.", + "yunohost_not_installed": "YunoHost ez da zuzen instalatu. Exekutatu 'yunohost tools postinstall'", "unlimit": "Mugarik ez", "restore_already_installed_apps": "Ondorengo aplikazioak ezin dira lehengoratu dagoeneko instalatuta daudelako: {apps}", "password_too_simple_4": "Pasahitzak 12 karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat, txikiren bat eta karaktere bereziren bat izan behar ditu", @@ -670,19 +634,137 @@ "migration_0021_main_upgrade": "Eguneraketa nagusia abiarazten…", "migration_0021_still_on_buster_after_main_upgrade": "Zerbaitek huts egin du eguneraketa nagusian, badirudi sistemak oraindik darabilela Debian Buster", "migration_0021_yunohost_upgrade": "YunoHosten muineko eguneraketa abiarazten…", - "migration_0021_not_buster": "Uneko Debian ez da Buster!", "migration_0021_not_enough_free_space": "/var/-enerabilgarri dagoen espazioa oso txikia da! Guxtienez GB 1 izan beharko zenuke erabilgarri migrazioari ekiteko.", - "migration_0021_system_not_fully_up_to_date": "Sistema ez dago erabat egunean. Mesedez, egizu eguneraketa arrunt bat Bullseye-(e)rako migrazioa abiarazi baino lehen.", - "migration_0021_general_warning": "Mesedez, kontuan hartu migrazio hau konplexua dela. YunoHost taldeak ahalegin handia egin du probatzeko, baina hala ere migrazioak sistemaren zatiren bat edo aplikazioak apurt litzake.\n\nHorregatik, gomendagarria da:\n\t- Datu edo aplikazio garrantzitsuen babeskopia egitea. Informazio gehiago: https://yunohost.org/backup;\n\t- Ez izan presarik migrazioa abiaraztean: zure internet eta hardwarearen arabera ordu batzuk ere iraun lezake eguneraketa prozesuak.", - "migration_0021_modified_files": "Mesedez, kontuan hartu ondorengo fitxategiak eskuz moldatu omen direla eta eguneraketak berridatziko dituela: {manually_modified_files}", - "migration_0021_cleaning_up": "Cachea eta erabilgarriak ez diren paketeak garbitzen…", + "migration_0021_system_not_fully_up_to_date": "Sistema ez dago erabat egunean. Egizu eguneraketa arrunt bat Bullseye-(e)rako migrazioa abiarazi baino lehen.", + "migration_0021_general_warning": "Kontuan hartu migrazio hau konplexua dela. YunoHost taldeak ahalegin handia egin du probatzeko, baina hala ere migrazioak sistemaren zatiren bat edo aplikazioak apurt litzake.\n\nHorregatik, gomendagarria da:\n\t- Datu edo aplikazio garrantzitsuen babeskopia egitea. Informazio gehiago: https://yunohost.org/backup;\n\t- Ez izan presarik migrazioa abiaraztean: zure internet eta hardwarearen arabera ordu batzuk ere iraun lezake eguneraketa prozesuak.", + "migration_0021_modified_files": "Kontuan hartu ondorengo fitxategiak eskuz moldatu omen direla eta eguneraketak berridatziko dituela: {manually_modified_files}", + "migration_0021_cleaning_up": "Katxea eta erabilgarriak ez diren paketeak garbitzen…", "migration_0021_patch_yunohost_conflicts": "Arazo gatazkatsu bati adabakia jartzen…", "migration_description_0021_migrate_to_bullseye": "Eguneratu sistema Debian Bullseye eta Yunohost 11.x-ra", - "global_settings_setting_security_ssh_password_authentication": "Baimendu pasahitz bidezko autentikazioa SSHrako", - "migration_0021_problematic_apps_warning": "Mesedez, kontuan izan ziur asko gatazkatsuak izango diren odorengo aplikazioak aurkitu direla. Badirudi ez zirela YunoHost aplikazioen katalogotik instalatu, edo ez daude 'badabiltza' bezala etiketatuak. Ondorioz, ezin da bermatu eguneratu ondoren funtzionatzen jarraituko dutenik: {problematic_apps}", + "migration_0021_problematic_apps_warning": "Kontuan izan ziur asko gatazkatsuak izango diren odorengo aplikazioak aurkitu direla. Badirudi ez zirela YunoHost aplikazioen katalogotik instalatu, edo ez daude 'badabiltza' bezala etiketatuak. Ondorioz, ezin da bermatu eguneratu ondoren funtzionatzen jarraituko dutenik: {problematic_apps}", "migration_0023_not_enough_space": "{path}-en ez dago toki nahikorik migrazioa abiarazteko.", "migration_0023_postgresql_11_not_installed": "PostgreSQL ez zegoen zure isteman instalatuta. Ez dago egitekorik.", "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 dago instalatuta baina PostgreSQL 13 ez!? Zerbait arraroa gertatu omen zaio zure sistemari :( …", "migration_description_0022_php73_to_php74_pools": "Migratu php7.3-fpm 'pool' ezarpen-fitxategiak php7.4ra", - "migration_description_0023_postgresql_11_to_13": "Migratu datubaseak PostgreSQL 11tik 13ra" -} \ No newline at end of file + "migration_description_0023_postgresql_11_to_13": "Migratu datubaseak PostgreSQL 11tik 13ra", + "global_settings_setting_backup_compress_tar_archives_help": "Babeskopia berriak sortzean, konprimitu fitxategiak (.tar.gz) konprimitu gabeko fitxategien (.tar) ordez. Aukera hau gaitzean babeskopiek espazio gutxiago beharko dute, baina hasierako prozesua luzeagoa izango da eta CPUari lan handiagoa eragingo dio.", + "global_settings_setting_security_experimental_enabled_help": "Gaitu segurtasun funtzio esperimentalak (ez ezazu egin ez badakizu zertan ari zaren!)", + "global_settings_setting_nginx_compatibility_help": "Bateragarritasun eta segurtasun arteko gatazka NGINX web zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", + "global_settings_setting_nginx_redirect_to_https_help": "Birbideratu HTTP eskaerak HTTPSra (EZ ITZALI hau ez badakizu zertan ari zaren!)", + "global_settings_setting_admin_strength": "Administrazio-pasahitzaren segurtasuna", + "global_settings_setting_user_strength": "Erabiltzaile-pasahitzaren segurtasuna", + "global_settings_setting_postfix_compatibility_help": "Bateragarritasun eta segurtasun arteko gatazka Postfix zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", + "global_settings_setting_ssh_compatibility_help": "Bateragarritasun eta segurtasun arteko gatazka SSH zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", + "global_settings_setting_ssh_password_authentication_help": "Baimendu pasahitz bidezko autentikazioa SSHrako", + "global_settings_setting_ssh_port": "SSH ataka", + "global_settings_setting_webadmin_allowlist_help": "Administrazio-ataria bisita dezaketen IP helbideak, koma bidez bereiziak.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Baimendu IP zehatz batzuk bakarrik administrazio-atarian.", + "global_settings_setting_smtp_allow_ipv6_help": "Baimendu IPv6 posta elektronikoa jaso eta bidaltzeko", + "global_settings_setting_smtp_relay_enabled_help": "YunoHosten ordez posta elektronikoa bidaltzeko SMTP relay helbidea. Erabilgarri izan daiteke egoera hauetan: operadore edo VPS enpresak 25. ataka blokeatzen badu, DUHLen zure etxeko IPa ageri bada, ezin baduzu alderantzizko DNSa ezarri edo zerbitzari hau ez badago zuzenean internetera konektatuta baina posta elektronikoa bidali nahi baduzu.", + "migration_0024_rebuild_python_venv_broken_app": "{app} aplikazioari ez ikusiarena egin zaio ezin delako ingurune birtuala modu errazean birsortu. Horren ordez, aplikazioaren eguneraketa behartzen saia zaitezke `yunohost app upgrade --force {app}` arazoa konpontzeko.", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Ondorengo aplikazioen virtualenv-a birsortzeko saiakera egingo da (eragiketak luze jo dezake!): {rebuild_apps}", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenv-ak ezin dira birsortu aplikazio horientzat. Eguneraketa behartu behar duzu horientzat, ondorengo komandoa exekutatuz egin daiteke: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_0024_rebuild_python_venv_in_progress": "`{app}` aplikazioaren Python virtualenv-a birsortzeko lanetan", + "migration_0024_rebuild_python_venv_failed": "Kale egin du {app} aplikazioaren Python virtualenv-aren birsorkuntza saiakerak. Litekeena da aplikazioak ez funtzionatzea arazoa konpondu arte. Aplikazioaren eguneraketa behartu beharko zenuke ondorengo komandoarekin: `yunohost app upgrade --force {app}`.", + "migration_description_0024_rebuild_python_venv": "Konpondu Python aplikazioa Bullseye eguneraketa eta gero", + "migration_0024_rebuild_python_venv_disclaimer_base": "Debian Bullseye eguneraketa dela-eta, Python aplikazio batzuk birsortu behar dira Debianekin datorren Pythonen bertsiora egokitzeko (teknikoki 'virtualenv' deritzaiona birsortu behar da). Egin artean, litekeena da Python aplikazio horiek ez funtzionatzea. YunoHost saia daiteke beherago ageri diren aplikazioen virtualenv edo ingurune birtualak birsortzen. Beste aplikazio batzuen kasuan, edo birsortze saiakerak kale egingo balu, aplikazio horien eguneraketa behartu beharko duzu.", + "migration_0021_not_buster2": "Zerbitzariak darabilen Debian bertsioa ez da Buster! Dagoeneko Buster -> Bullseye migrazioa exekutatu baduzu, errore honek migrazioa erabat arrakastatsua izan ez zela esan nahi du (bestela YunoHostek amaitutzat markatuko luke). Komenigarria izango litzateke, laguntza taldearekin batera, zer gertatu zen aztertzea. Horretarako `migrazioaren erregistro **osoa** beharko duzue, Tresnak > Erregistroak atalean eskuragarri dagoena.", + "admins": "Administratzaileek", + "app_action_failed": "{app} aplikaziorako {action} eragiketak huts egin du", + "config_action_disabled": "Ezin izan da '{action}' eragiketa exekutatu ezgaituta dagoelako, egiaztatu bere mugak betetzen dituzula. Laguntza: {help}", + "all_users": "YunoHosten erabiltzaile guztiek", + "app_manifest_install_ask_init_admin_permission": "Nork izan beharko luke aplikazio honetako administrazio aukeretara sarbidea? (Aldatzea dago)", + "app_manifest_install_ask_init_main_permission": "Nork izan beharko luke aplikazio honetara sarbidea? (Aldatzea dago)", + "ask_admin_fullname": "Administratzailearen izen osoa", + "ask_admin_username": "Administratzailearen erabiltzaile-izena", + "ask_fullname": "Izen osoa", + "certmanager_cert_install_failed": "Let's Encrypt zirutagiriaren instalazioak huts egin du honako domeinu(eta)rako: {domains}", + "certmanager_cert_install_failed_selfsigned": "Norberak sinatutako zirutagiriaren instalazioak huts egin du honako domeinu(eta)rako: {domains}", + "certmanager_cert_renew_failed": "Let's Encrypt zirutagiriaren berrizteak huts egin du honako domeinu(eta)rako: {domains}", + "config_action_failed": "Ezin izan da '{action}' eragiketa exekutatu: {error}", + "domain_config_cert_summary_expired": "LARRIA: Uneko ziurtagiria ez da baliozkoa! HTTPS ezin da erabili!", + "domain_config_cert_summary_selfsigned": "ADI: Uneko zirutagiria norberak sinatutakoa da. Web-nabigatzaileek bisitariak izutuko dituen mezu bat erakutsiko dute!", + "global_settings_setting_postfix_compatibility": "Postfixekin bateragarritasuna", + "global_settings_setting_root_access_explain": "Linux sistemetan 'root' administratzaile gorena da. YunoHosten testuinguruan, zuzeneko 'root' SSH saioa ezgaituta dago defektuz, zerbitzariaren sare lokaletik ez bada. 'administrariak' taldeko kideek sudo komandoa erabili dezakete root bailitzan jarduteko terminalaren bidez. Hala ere lagungarri izan liteke root pasahitz (sendo) bat izatea sistema arazteko egoeraren batean administratzaile arruntek saiorik hasi ezin balute.", + "log_settings_reset": "Berrezarri ezarpenak", + "log_settings_reset_all": "Berrezarri ezarpen guztiak", + "root_password_changed": "root pasahitza aldatu da", + "visitors": "Bisitariek", + "global_settings_setting_security_experimental_enabled": "Segurtasun ezaugarri esperimentalak", + "registrar_infos": "Erregistro-enpresaren informazioa", + "global_settings_setting_pop3_enabled": "Gaitu POP3", + "global_settings_reset_success": "Berrezarri ezarpen globalak", + "global_settings_setting_backup_compress_tar_archives": "Konprimatu babeskopiak", + "config_forbidden_readonly_type": "'{type}' mota ezin da ezarri readonly bezala; beste mota bat erabili balio hau emateko (argudioaren ida: '{id}').", + "diagnosis_using_stable_codename": "apt (sistemaren pakete kudeatzailea) 'stable' (egonkorra) izen kodea duten paketeak instalatzeko ezarrita dago une honetan, eta ez uneko Debianen bertsioaren (bullseye) izen kodea.", + "diagnosis_using_yunohost_testing": "apt (sistemaren pakete kudeatzailea) YunoHosten muinerako 'testing' (proba) izen kodea duten paketeak instalatzeko ezarrita dago une honetan.", + "diagnosis_using_yunohost_testing_details": "Ez dago arazorik zertan ari zaren baldin badakizu, baina arretaz irakurri oharrak YunoHosten eguneraketak instalatu baino lehen! 'testing' (proba) bertsioak ezgaitu nahi badituzu, kendu testing gakoa /etc/apt/sources.list.d/yunohost.list fitxategitik.", + "global_settings_setting_smtp_allow_ipv6": "Baimendu IPv6", + "global_settings_setting_smtp_relay_host": "SMTP relay ostatatzailea", + "domain_config_acme_eligible": "ACME egokitasuna", + "domain_config_acme_eligible_explain": "Ez dirudi domeinu hau Let's Encrypt ziurtagirirako prest dagoenik. Egiaztatu DNS ezarpenak eta zerbitzariaren HTTP irisgarritasuna. Diagnostikoen orrialdeko 'DNS erregistroak' eta 'Web' atalek zer dagoen gaizki ulertzen lagun zaitzakete.", + "domain_config_cert_install": "Instalatu Let's Encrypt ziurtagiria", + "domain_config_cert_issuer": "Ziurtagiriaren jaulkitzailea", + "domain_config_cert_no_checks": "Muzin egin diagnostikoaren egiaztapenei", + "domain_config_cert_renew": "Berritu Let's Encrypt ziurtagiria", + "domain_config_cert_renew_help": "Ziurtagiria automatikoki berrituko da baliozkoa den azken 15 egunetan. Eskuz berritu dezakezu hala nahi baduzu. (Ez da gomendagarria).", + "domain_config_cert_summary": "Ziurtagiriaren egoera", + "domain_config_cert_summary_abouttoexpire": "Uneko ziurtagiria iraungitzear dago. Aurki berritu beharko litzateke automatikoki.", + "domain_config_cert_summary_letsencrypt": "Primeran! Baliozko Let's Encrypt zirutagiria erabiltzen ari zara!", + "domain_config_cert_summary_ok": "Ados, uneko ziurtagiriak itxura ona du!", + "domain_config_cert_validity": "Balizokotasuna", + "global_settings_setting_admin_strength_help": "Betekizun hauek pasahitza lehenbizikoz sortzerakoan edo aldatzerakoan baino ez dira bete behar", + "global_settings_setting_nginx_compatibility": "NGINXekin bateragarritasuna", + "global_settings_setting_nginx_redirect_to_https": "Behartu HTTPS", + "global_settings_setting_pop3_enabled_help": "Gaitu POP3 protokoloa eposta zerbitzarirako", + "global_settings_setting_root_password": "root pasahitz berria", + "global_settings_setting_root_password_confirm": "root pasahitz berria (egiaztatu)", + "global_settings_setting_smtp_relay_enabled": "Gaitu SMTP relay", + "global_settings_setting_ssh_compatibility": "SSH bateragarritasuna", + "global_settings_setting_ssh_password_authentication": "Pasahitz bidezko autentifikazioa", + "global_settings_setting_user_strength_help": "Betekizun hauek lehenbizikoz sortzerakoan edo pasahitza aldatzerakoan bete behar dira soilik", + "global_settings_setting_webadmin_allowlist": "Administrazio-atarira sartzeko baimendutako IPak", + "global_settings_setting_webadmin_allowlist_enabled": "Gaitu administrazio-ataria sartzeko baimendutako IPak", + "invalid_credentials": "Pasahitz edo erabiltzaile-izen baliogabea", + "log_resource_snippet": "Baliabide baten eguneraketa / eskuragarritasuna / eskuragarritasun eza", + "log_settings_set": "Aplikatu ezarpenak", + "migration_description_0025_global_settings_to_configpanel": "Migratu ezarpen globalen nomenklatura zaharra izendegi berri eta modernora", + "migration_description_0026_new_admins_group": "Migratu 'administrari bat baino gehiago' sistema berrira", + "password_confirmation_not_the_same": "Pasahitzak ez datoz bat", + "password_too_long": "Aukeratu 127 karaktere baino laburragoa den pasahitz bat", + "diagnosis_using_stable_codename_details": "Ostatatzaileak zerbait oker ezarri duenean gertatu ohi da hau. Arriskutsua da, Debianen datorren bertsioa 'estable' (egonkorra) bilakatzen denean, apt-ek sistemaren pakete guztiak bertsio-berritzen saiatuko da, beharrezko migrazio-prozedurarik burutu gabe. Debianen repositorioan apt iturria editatzen konpontzea da gomendioa, stable gakoa bullseye gakoarekin ordezkatuz. Ezarpen-fitxategia /etc/apt/sources.list izan beharko litzateke, edo /etc/apt/sources.list.d/ direktorioko fitxategiren bat.", + "group_update_aliases": "'{group}' taldearen aliasak eguneratzen", + "group_no_change": "Ez da ezer aldatu behar '{group}' talderako", + "app_not_enough_ram": "Aplikazio honek {required} RAM behar ditu instalatu edo bertsio-berritzeko, baina {current} bakarrik daude erabilgarri une honetan.", + "domain_cannot_add_muc_upload": "Ezin duzu 'muc.'-ekin hasten den domeinurik gehitu. Mota honetako izenak YunoHosten integratuta dagoen XMPP taldeko txatek erabil ditzaten gordeta daude.", + "confirm_app_insufficient_ram": "KONTUZ! Aplikazio honek {required} RAM behar ditu instalatu edo bertsio-berritzeko baina unean soilik {current} daude erabilgarri. Aplikazioa ibiliko balitz ere, instalazioak edo bertsio-berritzeak RAM kopuru handia behar du eta zure zerbitzariak erantzuteari utzi eta huts egin lezake. Hala ere arriskatu nahi baduzu idatzi '{answers}'", + "confirm_notifications_read": "ADI: ikuskatu aplikazioaren jakinarazpenak jarraitu baino lehen, baliteke jakin beharreko zerbait esatea. [{answers}]", + "app_arch_not_supported": "Aplikazio hau {required} arkitekturan instala daiteke bakarrik, baina zure zerbitzariaren arkitektura {current} da", + "app_resource_failed": "Huts egin du {app} aplikaziorako baliabideen eguneraketak / prestaketak / askapenak: {error}", + "app_not_enough_disk": "Aplikazio honek {required} espazio libre behar ditu.", + "app_yunohost_version_not_supported": "Aplikazio honek YunoHost >= {required} behar du baina unean instalatutako bertsioa {current} da", + "global_settings_setting_passwordless_sudo": "Baimendu administrariek 'sudo' erabiltzea pasahitzak berriro idatzi beharrik gabe", + "global_settings_setting_portal_theme": "Atariko gaia", + "global_settings_setting_portal_theme_help": "Atariko gai propioak sortzeari buruzko informazio gehiago: https://yunohost.org/theming", + "invalid_shell": "Shell baliogabea: {shell}", + "domain_config_default_app_help": "Jendea automatikoki birbideratuko da aplikazio honetara domeinu hau bisitatzerakoan. Aplikaziorik ezarri ezean, jendea saioa hasteko erabiltzaileen atarira birbideratuko da.", + "domain_config_xmpp_help": "Ohart ongi: XMPP ezaugarri batzuk gaitzeko DNS erregistroak eguneratu eta Lets Encrypt ziurtagiria birsortu beharko dira", + "global_settings_setting_dns_exposure": "DNS ezarpenetan eta diagnostikoan kontuan hartzeko IP bertsioak", + "global_settings_setting_dns_exposure_help": "Ohart ongi: honek gomendatutako DNS ezarpenei eta diagnostikoari eragiten die soilik. Ez du eraginik sistemaren ezarpenetan.", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 automatikoki ezarri ohi du sistemak edo hornitzaileak erabilgarri baldin badago. Bestela eskuz ezarri beharko dituzu aukera batzuk ondorengo dokumentazioan azaldu bezala: https://yunohost.org/#/ipv6.", + "pattern_fullname": "Baliozko izen oso bat izan behar da (gutxienez hiru karaktere)", + "app_change_url_failed": "Ezin izan da {app} aplikazioaren URLa aldatu: {error}", + "app_change_url_require_full_domain": "Ezin da {app} aplikazioa URL berri honetara aldatu domeinu oso bat behar duelako (i.e. with path = /)", + "app_change_url_script_failed": "Errorea gertatu da URLa aldatzeko aginduaren barnean", + "app_corrupt_source": "YunoHostek deskargatu du {app} aplikaziorako '{source_id}' ({url}) baliabidea baina ez dator bat espero zen 'checksum'arekin. Agian zerbitzariak interneteko konexioa galdu du tarte batez, EDO baliabidea nolabait moldatua izan da arduradunaren aldetik (edo partehartzaile maltzur batetik?) eta YunoHosten arduradunek egoera aztertu eta aplikazioaren manifestua eguneratu behar dute aldaketa hau kontuan hartzeko.\n Espero zen sha256 checksuma: {expected_sha256}\n Deskargatutakoaren sha256 checksuma: {computed_sha256}\n Deskargatutako fitxategiaren tamaina: {size}", + "app_failed_to_upgrade_but_continue": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du, jarraitu hurrengo bertsio-berritzeetara eskatu bezala. Exekutatu 'yunohost log show {operation_logger_name}' errorearen erregistroa ikusteko", + "app_not_upgraded_broken_system": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du eta sistema hondatu du, beraz, ondorengo aplikazioen bertsio-berritzeak ezeztatu dira: {apps}", + "app_not_upgraded_broken_system_continue": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du eta sistema hondatu du (beraz, --continue-on-failure aukerari muzin egin zaio) eta ondorengo aplikazioen bertsio-berritzeak ezeztatu dira: {apps}", + "app_failed_to_download_asset": "{app} aplikaziorako '{source_id}' ({url}) baliabidea deskargatzeak huts egin du: {out}", + "apps_failed_to_upgrade": "Aplikazio hauen bertsio-berritzeak huts egin du: {apps}", + "apps_failed_to_upgrade_line": "\n * {app_id} (dagokion erregistroa ikusteko, exekutatu 'yunohost log show {operation_logger_name}')", + "group_mailalias_add": "'{mail}' ePosta aliasa jarri zaio '{group}' taldeari", + "group_mailalias_remove": "'{mail}' ePosta aliasa kendu zaio '{group}' taldeari", + "group_user_remove": "'{user}' erabiltzailea '{group}' taldetik kenduko da", + "group_user_add": "'{user}' erabiltzailea '{group}' taldera gehituko da" +} diff --git a/locales/fa.json b/locales/fa.json index 599ab1ea7..fe6310c5d 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -11,9 +11,6 @@ "app_action_broke_system": "این اقدام به نظر می رسد سرویس های مهمی را خراب کرده است: {services}", "app_action_cannot_be_ran_because_required_services_down": "برای اجرای این عملیات سرویس هایی که مورد نیازاند و باید اجرا شوند: {services}. سعی کنید آنها را مجدداً راه اندازی کنید (و علت خرابی احتمالی آنها را بررسی کنید).", "already_up_to_date": "کاری برای انجام دادن نیست. همه چیز در حال حاضر به روز است.", - "admin_password_too_long": "لطفاً گذرواژه ای کوتاهتر از 127 کاراکتر انتخاب کنید", - "admin_password_changed": "رمز مدیریت تغییر کرد", - "admin_password_change_failed": "تغییر رمز امکان پذیر نیست", "admin_password": "رمز عبور مدیریت", "additional_urls_already_removed": "نشانی اینترنتی اضافی '{url}' قبلاً در نشانی اینترنتی اضافی برای اجازه '{permission}'حذف شده است", "additional_urls_already_added": "نشانی اینترنتی اضافی '{url}' قبلاً در نشانی اینترنتی اضافی برای اجازه '{permission}' اضافه شده است", @@ -145,8 +142,6 @@ "ask_new_domain": "دامنه جدید", "ask_new_admin_password": "رمز جدید مدیریت", "ask_main_domain": "دامنه اصلی", - "ask_lastname": "نام خانوادگی", - "ask_firstname": "نام کوچک", "ask_user_domain": "دامنه ای که برای آدرس ایمیل کاربر و حساب XMPP استفاده می شود", "apps_catalog_update_success": "کاتالوگ برنامه به روز شد!", "apps_catalog_obsolete_cache": "حافظه پنهان کاتالوگ برنامه خالی یا منسوخ شده است.", @@ -171,7 +166,6 @@ "app_restore_script_failed": "خطایی در داخل اسکریپت بازیابی برنامه رخ داده است", "app_restore_failed": "{app} بازیابی نشد: {error}", "app_remove_after_failed_install": "حذف برنامه در پی شکست نصب...", - "app_requirements_unmeet": "شرایط مورد نیاز برای {app} برآورده نمی شود ، بسته {pkgname} ({version}) باید {spec} باشد", "app_requirements_checking": "در حال بررسی بسته های مورد نیاز برای {app}...", "app_removed": "{app} حذف نصب شد", "app_not_properly_removed": "{app} به درستی حذف نشده است", @@ -301,39 +295,15 @@ "group_already_exist": "گروه {group} از قبل وجود دارد", "good_practices_about_user_password": "گذرواژه باید حداقل 8 کاراکتر باشد - اگرچه استفاده از گذرواژه طولانی تر تمرین خوبی است (به عنوان مثال عبارت عبور) و/یا استفاده از تنوع کاراکترها (بزرگ ، کوچک ، رقم و کاراکتر های خاص).", "good_practices_about_admin_password": "اکنون می خواهید گذرواژه جدیدی برای مدیریت تعریف کنید. گذرواژه باید حداقل 8 کاراکتر باشد - اگرچه استفاده از گذرواژه طولانی تر تمرین خوبی است (به عنوان مثال عبارت عبور) و/یا استفاده از تنوع کاراکترها (بزرگ ، کوچک ، رقم و کاراکتر های خاص).", - "global_settings_unknown_type": "وضعیت غیرمنتظره ، به نظر می رسد که تنظیمات {setting} دارای نوع {unknown_type} است اما از نوع پشتیبانی شده توسط سیستم نیست.", - "global_settings_setting_backup_compress_tar_archives": "هنگام ایجاد پشتیبان جدید ، بایگانی های فشرده (.tar.gz) را به جای بایگانی های فشرده نشده (.tar) انتخاب کنید. N.B. : فعال کردن این گزینه به معنای ایجاد آرشیوهای پشتیبان سبک تر است ، اما روش پشتیبان گیری اولیه به طور قابل توجهی طولانی تر و سنگین تر بر روی CPU خواهد بود.", - "global_settings_setting_security_experimental_enabled": "فعال کردن ویژگی های امنیتی آزمایشی (اگر نمی دانید در حال انجام چه کاری هستید این کار را انجام ندهید!)", - "global_settings_setting_security_webadmin_allowlist": "آدرس های IP که مجاز به دسترسی مدیر وب هستند. جدا شده با ویرگول.", - "global_settings_setting_security_webadmin_allowlist_enabled": "فقط به برخی از IP ها اجازه دسترسی به مدیریت وب را بدهید.", "global_settings_setting_smtp_relay_password": "رمز عبور میزبان رله SMTP", "global_settings_setting_smtp_relay_user": "حساب کاربری رله SMTP", "global_settings_setting_smtp_relay_port": "پورت رله SMTP", - "global_settings_setting_smtp_relay_host": "میزبان رله SMTP برای ارسال نامه به جای این نمونه yunohost استفاده می شود. اگر در یکی از این شرایط قرار دارید مفید است: پورت 25 شما توسط ارائه دهنده ISP یا VPS شما مسدود شده است، شما یک IP مسکونی دارید که در DUHL ذکر شده است، نمی توانید DNS معکوس را پیکربندی کنید یا این سرور مستقیماً در اینترنت نمایش داده نمی شود و می خواهید از یکی دیگر برای ارسال ایمیل استفاده کنید.", - "global_settings_setting_smtp_allow_ipv6": "اجازه دهید از IPv6 برای دریافت و ارسال نامه استفاده شود", "global_settings_setting_ssowat_panel_overlay_enabled": "همپوشانی پانل SSOwat را فعال کنید", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "اجازه دهید از کلید میزبان DSA (منسوخ شده) برای پیکربندی SH daemon استفاده شود", - "global_settings_unknown_setting_from_settings_file": "کلید ناشناخته در تنظیمات: '{setting_key}'، آن را کنار گذاشته و در /etc/yunohost/settings-unknown.json ذخیره کنید", - "global_settings_setting_security_ssh_port": "درگاه SSH", - "global_settings_setting_security_postfix_compatibility": "سازگاری در مقابل مبادله امنیتی برای سرور Postfix. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", - "global_settings_setting_security_ssh_compatibility": "سازگاری در مقابل مبادله امنیتی برای سرور SSH. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", - "global_settings_setting_security_password_user_strength": "قدرت رمز عبور کاربر", - "global_settings_setting_security_password_admin_strength": "قدرت رمز عبور مدیر", - "global_settings_setting_security_nginx_compatibility": "سازگاری در مقابل مبادله امنیتی برای وب سرور NGINX. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", - "global_settings_setting_pop3_enabled": "پروتکل POP3 را برای سرور ایمیل فعال کنید", - "global_settings_reset_success": "تنظیمات قبلی اکنون در {path} پشتیبان گیری شده است", - "global_settings_key_doesnt_exists": "کلید '{settings_key}' در تنظیمات جهانی وجود ندارد ، با اجرای 'لیست تنظیمات yunohost' می توانید همه کلیدهای موجود را مشاهده کنید", - "global_settings_cant_write_settings": "فایل تنظیمات ذخیره نشد، به دلیل: {reason}", - "global_settings_cant_serialize_settings": "سریال سازی داده های تنظیمات انجام نشد، به دلیل: {reason}", - "global_settings_cant_open_settings": "فایل تنظیمات باز نشد ، به دلیل: {reason}", - "global_settings_bad_type_for_setting": "نوع نادرست برای تنظیم {setting} ، دریافت شده {received_type}، مورد انتظار {expected_type}", - "global_settings_bad_choice_for_enum": "انتخاب نادرست برای تنظیم {setting} ، '{choice}' دریافت شد ، اما گزینه های موجود عبارتند از: {available_choices}", "firewall_rules_cmd_failed": "برخی از دستورات قانون فایروال شکست خورده است. اطلاعات بیشتر در گزارش.", "firewall_reloaded": "فایروال بارگیری مجدد شد", "firewall_reload_failed": "بارگیری مجدد فایروال امکان پذیر نیست", "file_does_not_exist": "فایل {path} وجود ندارد.", "field_invalid": "فیلد نامعتبر '{}'", - "experimental_feature": "هشدار: این ویژگی آزمایشی است و پایدار تلقی نمی شود ، نباید از آن استفاده کنید مگر اینکه بدانید در حال انجام چه کاری هستید.", "extracting": "استخراج...", "dyndns_unavailable": "دامنه '{domain}' در دسترس نیست.", "dyndns_domain_not_provided": "ارائه دهنده DynDNS {provider} نمی تواند دامنه {domain} را ارائه دهد.", @@ -387,7 +357,6 @@ "password_too_simple_1": "رمز عبور باید حداقل 8 کاراکتر باشد", "password_listed": "این رمز در بین پر استفاده ترین رمزهای عبور در جهان قرار دارد. لطفاً چیزی منحصر به فرد تر انتخاب کنید.", "operation_interrupted": "عملیات به صورت دستی قطع شد؟", - "invalid_password": "رمز عبور نامعتبر", "invalid_number": "باید یک عدد باشد", "not_enough_disk_space": "فضای آزاد کافی در '{path}' وجود ندارد", "migrations_to_be_ran_manually": "مهاجرت {id} باید به صورت دستی اجرا شود. لطفاً به صفحه Tools → Migrations در صفحه webadmin بروید، یا `yunohost tools migrations run` را اجرا کنید.", @@ -445,7 +414,6 @@ "log_domain_remove": "دامنه '{}' را از پیکربندی سیستم حذف کنید", "log_domain_add": "دامنه '{}' را به پیکربندی سیستم اضافه کنید", "log_remove_on_failed_install": "پس از نصب ناموفق '{}' را حذف کنید", - "log_remove_on_failed_restore": "پس از بازیابی ناموفق از بایگانی پشتیبان، '{}' را حذف کنید", "log_backup_restore_app": "بازیابی '{}' از بایگانی پشتیبان", "log_backup_restore_system": "بازیابی سیستم بوسیله آرشیو پشتیبان", "log_backup_create": "بایگانی پشتیبان ایجاد کنید", @@ -542,7 +510,6 @@ "server_reboot": "سرور راه اندازی مجدد می شود", "server_shutdown_confirm": "آیا مطمئن هستید که سرور بلافاصله خاموش می شود؟ [{answers}]", "server_shutdown": "سرور خاموش می شود", - "root_password_replaced_by_admin_password": "گذرواژه ریشه شما با رمز مدیریت جایگزین شده است.", "root_password_desynchronized": "گذرواژه مدیریت تغییر کرد ، اما YunoHost نتوانست این را به رمز عبور ریشه منتقل کند!", "restore_system_part_failed": "بخش سیستم '{part}' بازیابی و ترمیم نشد", "restore_running_hooks": "در حال اجرای قلاب های ترمیم و بازیابی...", @@ -589,5 +556,17 @@ "permission_deletion_failed": "اجازه '{permission}' حذف نشد: {error}", "permission_deleted": "مجوز '{permission}' حذف شد", "permission_cant_add_to_all_users": "مجوز {permission} را نمی توان به همه کاربران اضافه کرد.", - "permission_currently_allowed_for_all_users": "این مجوز در حال حاضر به همه کاربران علاوه بر آن گروه های دیگر نیز اعطا شده. احتمالاً بخواهید مجوز 'all_users' را حذف کنید یا سایر گروه هایی را که در حال حاضر مجوز به آنها اعطا شده است را هم حذف کنید." + "permission_currently_allowed_for_all_users": "این مجوز در حال حاضر به همه کاربران علاوه بر آن گروه های دیگر نیز اعطا شده. احتمالاً بخواهید مجوز 'all_users' را حذف کنید یا سایر گروه هایی را که در حال حاضر مجوز به آنها اعطا شده است را هم حذف کنید.", + "global_settings_setting_backup_compress_tar_archives_help": "هنگام ایجاد پشتیبان جدید ، بایگانی های فشرده (.tar.gz) را به جای بایگانی های فشرده نشده (.tar) انتخاب کنید. N.B. : فعال کردن این گزینه به معنای ایجاد آرشیوهای پشتیبان سبک تر است ، اما روش پشتیبان گیری اولیه به طور قابل توجهی طولانی تر و سنگین تر بر روی CPU خواهد بود.", + "global_settings_setting_security_experimental_enabled_help": "فعال کردن ویژگی های امنیتی آزمایشی (اگر نمی دانید در حال انجام چه کاری هستید این کار را انجام ندهید!)", + "global_settings_setting_nginx_compatibility_help": "سازگاری در مقابل مبادله امنیتی برای وب سرور NGINX. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", + "global_settings_setting_admin_strength": "قدرت رمز عبور مدیر", + "global_settings_setting_user_strength": "قدرت رمز عبور کاربر", + "global_settings_setting_postfix_compatibility_help": "سازگاری در مقابل مبادله امنیتی برای سرور Postfix. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", + "global_settings_setting_ssh_compatibility_help": "سازگاری در مقابل مبادله امنیتی برای سرور SSH. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", + "global_settings_setting_ssh_port": "درگاه SSH", + "global_settings_setting_webadmin_allowlist_help": "آدرس های IP که مجاز به دسترسی مدیر وب هستند. جدا شده با ویرگول.", + "global_settings_setting_webadmin_allowlist_enabled_help": "فقط به برخی از IP ها اجازه دسترسی به مدیریت وب را بدهید.", + "global_settings_setting_smtp_allow_ipv6_help": "اجازه دهید از IPv6 برای دریافت و ارسال نامه استفاده شود", + "global_settings_setting_smtp_relay_enabled_help": "میزبان رله SMTP برای ارسال نامه به جای این نمونه yunohost استفاده می شود. اگر در یکی از این شرایط قرار دارید مفید است: پورت 25 شما توسط ارائه دهنده ISP یا VPS شما مسدود شده است، شما یک IP مسکونی دارید که در DUHL ذکر شده است، نمی توانید DNS معکوس را پیکربندی کنید یا این سرور مستقیماً در اینترنت نمایش داده نمی شود و می خواهید از یکی دیگر برای ارسال ایمیل استفاده کنید." } \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index 77f1ca9d1..f98470c99 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,8 +1,6 @@ { "action_invalid": "Action '{action}' incorrecte", "admin_password": "Mot de passe d'administration", - "admin_password_change_failed": "Impossible de changer le mot de passe", - "admin_password_changed": "Le mot de passe d'administration a été modifié", "app_already_installed": "{app} est déjà installé", "app_argument_choice_invalid": "Choisissez une valeur valide pour l'argument '{name}' : '{value}' ne fait pas partie des choix disponibles ({choices})", "app_argument_invalid": "Valeur invalide pour le paramètre '{name}' : {error}", @@ -14,15 +12,12 @@ "app_not_installed": "Nous n'avons pas trouvé {app} dans la liste des applications installées : {all_apps}", "app_not_properly_removed": "{app} n'a pas été supprimé correctement", "app_removed": "{app} désinstallé", - "app_requirements_checking": "Vérification des paquets requis pour {app}...", - "app_requirements_unmeet": "Les pré-requis de {app} ne sont pas satisfaits, le paquet {pkgname} ({version}) doit être {spec}", + "app_requirements_checking": "Vérification des prérequis pour {app} ...", "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources, l'URL est-elle correcte ?", "app_unknown": "Application inconnue", "app_unsupported_remote_type": "Ce type de commande à distance utilisé pour cette application n'est pas supporté", "app_upgrade_failed": "Impossible de mettre à jour {app} : {error}", "app_upgraded": "{app} mis à jour", - "ask_firstname": "Prénom", - "ask_lastname": "Nom", "ask_main_domain": "Domaine principal", "ask_new_admin_password": "Nouveau mot de passe d'administration", "ask_password": "Mot de passe", @@ -32,10 +27,10 @@ "backup_archive_name_unknown": "L'archive locale de sauvegarde nommée '{name}' est inconnue", "backup_archive_open_failed": "Impossible d'ouvrir l'archive de la sauvegarde", "backup_cleaning_failed": "Impossible de nettoyer le dossier temporaire de sauvegarde", - "backup_created": "Sauvegarde terminée", + "backup_created": "Sauvegarde créée : {name}", "backup_creation_failed": "Impossible de créer l'archive de la sauvegarde", "backup_delete_error": "Impossible de supprimer '{path}'", - "backup_deleted": "La sauvegarde a été supprimée", + "backup_deleted": "Sauvegarde supprimée : {name}", "backup_hook_unknown": "Script de sauvegarde '{hook}' inconnu", "backup_nothings_done": "Il n'y a rien à sauvegarder", "backup_output_directory_forbidden": "Choisissez un répertoire de destination différent. Les sauvegardes ne peuvent pas être créées dans les sous-dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", @@ -85,8 +80,8 @@ "pattern_backup_archive_name": "Doit être un nom de fichier valide avec un maximum de 30 caractères, et composé de caractères alphanumériques et -_. uniquement", "pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.fr)", "pattern_email": "Il faut une adresse électronique valide, sans le symbole '+' (par exemple johndoe@exemple.com)", - "pattern_firstname": "Doit être un prénom valide", - "pattern_lastname": "Doit être un nom valide", + "pattern_firstname": "Doit être un prénom valide (au moins 3 caractères)", + "pattern_lastname": "Doit être un nom de famille valide (au moins 3 caractères)", "pattern_mailbox_quota": "Doit avoir une taille suffixée avec b/k/M/G/T ou 0 pour désactiver le quota", "pattern_password": "Doit être composé d'au moins 3 caractères", "pattern_port_or_range": "Doit être un numéro de port valide compris entre 0 et 65535, ou une gamme de ports (exemple : 100:200)", @@ -151,7 +146,7 @@ "certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain} n'est pas émis par Let's Encrypt. Impossible de le renouveler automatiquement !", "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain} n'est pas sur le point d'expirer ! (Vous pouvez utiliser --force si vous savez ce que vous faites)", "certmanager_domain_http_not_working": "Le domaine {domain} ne semble pas être accessible via HTTP. Merci de vérifier la catégorie 'Web' dans le diagnostic pour plus d'informations. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "Les enregistrements DNS du domaine '{domain}' sont différents de l'adresse IP de ce serveur. Pour plus d'informations, veuillez consulter la catégorie \"Enregistrements DNS\" dans la section Diagnostic. Si vous avez récemment modifié votre enregistrement A, veuillez attendre sa propagation (des vérificateurs de propagation DNS sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver ces contrôles)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Les enregistrements DNS du domaine '{domain}' sont différents de l'adresse IP de ce serveur. Pour plus d'informations, veuillez consulter la catégorie \"Enregistrements DNS\" dans la section Diagnostic. Si vous avez récemment modifié votre enregistrement A, veuillez attendre sa propagation (des vérificateurs de propagation DNS sont disponibles en ligne). Si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver ces contrôles.", "certmanager_cannot_read_cert": "Quelque chose s'est mal passé lors de la tentative d'ouverture du certificat actuel pour le domaine {domain} (fichier : {file}), la cause est : {reason}", "certmanager_cert_install_success_selfsigned": "Le certificat auto-signé est maintenant installé pour le domaine '{domain}'", "certmanager_cert_install_success": "Le certificat Let's Encrypt est maintenant installé pour le domaine '{domain}'", @@ -165,7 +160,7 @@ "mailbox_used_space_dovecot_down": "Le service Dovecot doit être démarré si vous souhaitez voir l'espace disque occupé par la messagerie", "domains_available": "Domaines disponibles :", "backup_archive_broken_link": "Impossible d'accéder à l'archive de sauvegarde (lien invalide vers {path})", - "certmanager_acme_not_configured_for_domain": "Le challenge ACME n'a pas pu être validé pour le domaine {domain} pour le moment car le code de la configuration NGINX est manquant... Merci de vérifier que votre configuration NGINX est à jour avec la commande : `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "Pour le moment le protocole de communication ACME n'a pas pu être validé pour le domaine {domain} car le code correspondant de la configuration NGINX est manquant ... Merci de vérifier que votre configuration NGINX est à jour avec la commande : `yunohost tools regen-conf nginx --dry-run --with-diff`.", "domain_hostname_failed": "Échec de l'utilisation d'un nouveau nom d'hôte. Cela pourrait causer des soucis plus tard (cela n'en causera peut-être pas).", "app_already_installed_cant_change_url": "Cette application est déjà installée. L'URL ne peut pas être changé simplement par cette fonction. Vérifiez si cela est disponible avec `app changeurl`.", "app_change_url_identical_domains": "L'ancien et le nouveau couple domaine/chemin_de_l'URL sont identiques pour ('{domain}{path}'), rien à faire.", @@ -173,14 +168,6 @@ "app_change_url_success": "L'URL de l'application {app} a été changée en {domain}{path}", "app_location_unavailable": "Cette URL n'est pas disponible ou est en conflit avec une application existante :\n{apps}", "app_already_up_to_date": "{app} est déjà à jour", - "global_settings_bad_choice_for_enum": "Valeur du paramètre {setting} incorrecte. Reçu : {choice}, mais les valeurs possibles sont : {available_choices}", - "global_settings_bad_type_for_setting": "Le type du paramètre {setting} est incorrect. Reçu {received_type} alors que {expected_type} était attendu", - "global_settings_cant_open_settings": "Échec de l'ouverture du ficher de configurations car : {reason}", - "global_settings_cant_write_settings": "Échec d'écriture du fichier de configurations car : {reason}", - "global_settings_key_doesnt_exists": "La clef '{settings_key}' n'existe pas dans les configurations générales, vous pouvez voir toutes les clefs disponibles en saisissant 'yunohost settings list'", - "global_settings_reset_success": "Vos configurations précédentes ont été sauvegardées dans {path}", - "global_settings_unknown_type": "Situation inattendue : la configuration {setting} semble avoir le type {unknown_type} mais celui-ci n'est pas pris en charge par le système.", - "global_settings_unknown_setting_from_settings_file": "Clé inconnue dans les paramètres : '{setting_key}', rejet de cette clé et sauvegarde de celle-ci dans /etc/yunohost/unkown_settings.json", "backup_abstract_method": "Cette méthode de sauvegarde reste à implémenter", "backup_applying_method_tar": "Création de l'archive TAR de la sauvegarde ...", "backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder ...", @@ -202,7 +189,6 @@ "backup_unable_to_organize_files": "Impossible d'utiliser la méthode rapide pour organiser les fichiers dans l'archive", "backup_with_no_backup_script_for_app": "L'application {app} n'a pas de script de sauvegarde. Ignorer.", "backup_with_no_restore_script_for_app": "{app} n'a pas de script de restauration, vous ne pourrez pas restaurer automatiquement la sauvegarde de cette application.", - "global_settings_cant_serialize_settings": "Échec de la sérialisation des données de paramétrage car : {reason}", "restore_removing_tmp_dir_failed": "Impossible de sauvegarder un ancien dossier temporaire", "restore_extracting": "Extraction des fichiers nécessaires depuis l'archive...", "restore_may_be_not_enough_disk_space": "Votre système ne semble pas avoir suffisamment d'espace (libre : {free_space} B, espace nécessaire : {needed_space} B, marge de sécurité : {margin} B)", @@ -240,7 +226,6 @@ "service_description_ssh": "Vous permet de vous connecter à distance à votre serveur via un terminal (protocole SSH)", "service_description_yunohost-api": "Permet les interactions entre l'interface web de YunoHost et le système", "service_description_yunohost-firewall": "Gère l'ouverture et la fermeture des ports de connexion aux services", - "experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l'utiliser à moins que vous ne sachiez ce que vous faites.", "log_corrupted_md_file": "Le fichier YAML de métadonnées associé aux logs est corrompu : '{md_file}'\nErreur : {error}", "log_link_to_log": "Journal complet de cette opération : ' {desc} '", "log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log show {name}'", @@ -256,7 +241,6 @@ "log_available_on_yunopaste": "Le journal est désormais disponible via {url}", "log_backup_restore_system": "Restaurer le système depuis une archive de sauvegarde", "log_backup_restore_app": "Restaurer '{}' depuis une sauvegarde", - "log_remove_on_failed_restore": "Retirer '{}' après un échec de restauration depuis une archive de sauvegarde", "log_remove_on_failed_install": "Enlever '{}' après une installation échouée", "log_domain_add": "Ajouter le domaine '{}' dans la configuration du système", "log_domain_remove": "Enlever le domaine '{}' de la configuration du système", @@ -274,7 +258,7 @@ "log_tools_upgrade": "Mettre à jour les paquets du système", "log_tools_shutdown": "Éteindre votre serveur", "log_tools_reboot": "Redémarrer votre serveur", - "mail_unavailable": "Cette adresse d'email est réservée et doit être automatiquement attribuée au tout premier utilisateur", + "mail_unavailable": "Cette adresse email est réservée au groupe des administrateurs", "good_practices_about_admin_password": "Vous êtes sur le point de définir un nouveau mot de passe administrateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou d'utiliser une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "good_practices_about_user_password": "Vous êtes sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "password_listed": "Ce mot de passe fait partie des mots de passe les plus utilisés dans le monde. Veuillez en choisir un autre moins commun et plus robuste.", @@ -294,18 +278,14 @@ "ask_new_path": "Nouveau chemin", "backup_actually_backuping": "Création d'une archive de sauvegarde à partir des fichiers collectés ...", "backup_mount_archive_for_restore": "Préparation de l'archive pour restauration...", - "confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais n'est pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que l'authentification unique et la sauvegarde/restauration peuvent ne pas être disponibles. L'installer quand même ? [{answers}] ", + "confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais n'est pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que l'authentification unique SSO et la sauvegarde/restauration peuvent ne pas être disponibles. L'installer quand même ? [{answers}] ", "confirm_app_install_danger": "DANGER ! Cette application est connue pour être encore expérimentale (si elle ne fonctionne pas explicitement) ! Vous ne devriez probablement PAS l'installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système... Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers}'", "confirm_app_install_thirdparty": "DANGER ! Cette application ne fait pas partie du catalogue d'applications de YunoHost. L'installation d'applications tierces peut compromettre l'intégrité et la sécurité de votre système. Vous ne devriez probablement PAS l'installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système... Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers}'", - "dpkg_is_broken": "Vous ne pouvez pas faire ça maintenant car dpkg/apt (le gestionnaire de paquets du système) semble avoir laissé des choses non configurées. Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo apt install --fix-broken` et/ou `sudo dpkg --configure -a'.", + "dpkg_is_broken": "Vous ne pouvez pas faire ça maintenant car dpkg/apt (le gestionnaire de paquets du système) semble avoir laissé des choses non configurées. Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo apt install --fix-broken` et/ou `sudo dpkg --configure -a` et/ou `sudo dpkg --audit`.", "dyndns_could_not_check_available": "Impossible de vérifier si {domain} est disponible chez {provider}.", "file_does_not_exist": "Le fichier dont le chemin est {path} n'existe pas.", - "global_settings_setting_security_password_admin_strength": "Qualité du mot de passe administrateur", - "global_settings_setting_security_password_user_strength": "Qualité du mot de passe de l'utilisateur", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Autoriser l'utilisation de la clé hôte DSA (obsolète) pour la configuration du service SSH", "hook_json_return_error": "Échec de la lecture au retour du script {path}. Erreur : {msg}. Contenu brut : {raw_content}", "pattern_password_app": "Désolé, les mots de passe ne peuvent pas contenir les caractères suivants : {forbidden_chars}", - "root_password_replaced_by_admin_password": "Votre mot de passe root a été remplacé par votre mot de passe administrateur.", "service_reload_failed": "Impossible de recharger le service '{service}'.\n\nJournaux historisés récents de ce service : {logs}", "service_reloaded": "Le service '{service}' a été rechargé", "service_restart_failed": "Impossible de redémarrer le service '{service}'\n\nJournaux historisés récents de ce service : {logs}", @@ -314,7 +294,6 @@ "service_reloaded_or_restarted": "Le service '{service}' a été rechargé ou redémarré", "this_action_broke_dpkg": "Cette action a laissé des paquets non configurés par dpkg/apt (les gestionnaires de paquets du système) ... Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo apt install --fix-broken` et/ou `sudo dpkg --configure -a`.", "app_action_cannot_be_ran_because_required_services_down": "Ces services requis doivent être en cours d'exécution pour exécuter cette action : {services}. Essayez de les redémarrer pour continuer (et éventuellement rechercher pourquoi ils sont en panne).", - "admin_password_too_long": "Veuillez choisir un mot de passe comportant moins de 127 caractères", "log_regen_conf": "Régénérer les configurations du système '{}'", "regenconf_file_backed_up": "Le fichier de configuration '{conf}' a été sauvegardé sous '{backup}'", "regenconf_file_copy_failed": "Impossible de copier le nouveau fichier de configuration '{new}' vers '{conf}'", @@ -326,9 +305,6 @@ "regenconf_now_managed_by_yunohost": "Le fichier de configuration '{conf}' est maintenant géré par YunoHost (catégorie {category}).", "regenconf_up_to_date": "La configuration est déjà à jour pour la catégorie '{category}'", "already_up_to_date": "Il n'y a rien à faire. Tout est déjà à jour.", - "global_settings_setting_security_nginx_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur web Nginx. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)", - "global_settings_setting_security_ssh_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur SSH. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)", - "global_settings_setting_security_postfix_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur Postfix. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)", "regenconf_file_kept_back": "Le fichier de configuration '{conf}' devait être supprimé par 'regen-conf' (catégorie {category}) mais a été conservé.", "regenconf_updated": "La configuration a été mise à jour pour '{category}'", "regenconf_would_be_updated": "La configuration aurait dû être mise à jour pour la catégorie '{category}'", @@ -395,7 +371,7 @@ "app_install_failed": "Impossible d'installer {app} : {error}", "app_install_script_failed": "Une erreur est survenue dans le script d'installation de l'application", "permission_require_account": "Permission {permission} n'a de sens que pour les utilisateurs ayant un compte et ne peut donc pas être activé pour les visiteurs.", - "app_remove_after_failed_install": "Supprimer l'application après l'échec de l'installation...", + "app_remove_after_failed_install": "Suppression de l'application après l'échec de l'installation ...", "diagnosis_cant_run_because_of_dep": "Impossible d'exécuter le diagnostic pour {category} alors qu'il existe des problèmes importants liés à {dep}.", "diagnosis_found_errors": "Trouvé {errors} problème(s) significatif(s) lié(s) à {category} !", "diagnosis_found_errors_and_warnings": "Trouvé {errors} problème(s) significatif(s) (et {warnings} (avertissement(s)) en relation avec {category} !", @@ -405,14 +381,14 @@ "diagnosis_dns_missing_record": "Selon la configuration DNS recommandée, vous devez ajouter un enregistrement DNS
Type : {type}
Nom : {name}
Valeur : {value}", "diagnosis_diskusage_ok": "L'espace de stockage {mountpoint} (sur le périphérique {device}) a encore {free} ({free_percent}%) d'espace restant (sur {total}) !", "diagnosis_ram_ok": "Le système dispose encore de {available} ({available_percent}%) de RAM sur {total}.", - "diagnosis_regenconf_allgood": "Tous les fichiers de configuration sont conformes à la configuration recommandée !", - "diagnosis_security_vulnerable_to_meltdown": "Vous semblez vulnérable à la vulnérabilité de sécurité critique de Meltdown", + "diagnosis_regenconf_allgood": "Tous les fichiers de configuration sont conformes aux préconisations !", + "diagnosis_security_vulnerable_to_meltdown": "Vous semblez vulnérable à la faille de sécurité majeure qu'est Meltdown", "diagnosis_basesystem_host": "Le serveur utilise Debian {debian_version}", "diagnosis_basesystem_kernel": "Le serveur utilise le noyau Linux {kernel_version}", "diagnosis_basesystem_ynh_single_version": "{package} version : {version} ({repo})", "diagnosis_basesystem_ynh_main_version": "Le serveur utilise YunoHost {main_version} ({repo})", "diagnosis_basesystem_ynh_inconsistent_versions": "Vous exécutez des versions incohérentes des packages YunoHost ... très probablement en raison d'une mise à niveau échouée ou partielle.", - "diagnosis_failed_for_category": "Échec du diagnostic pour la catégorie '{category}': {error}", + "diagnosis_failed_for_category": "Échec du diagnostic pour la catégorie '{category}' : {error}", "diagnosis_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment !)", "diagnosis_ignored_issues": "(+ {nb_ignored} problème(s) ignoré(s))", "diagnosis_found_warnings": "Trouvé {warnings} objet(s) pouvant être amélioré(s) pour {category}.", @@ -423,7 +399,7 @@ "diagnosis_ip_connected_ipv6": "Le serveur est connecté à Internet en IPv6 !", "diagnosis_ip_no_ipv6": "Le serveur ne dispose pas d'une adresse IPv6.", "diagnosis_ip_dnsresolution_working": "La résolution de nom de domaine fonctionne !", - "diagnosis_ip_broken_dnsresolution": "La résolution du nom de domaine semble interrompue pour une raison quelconque ... Un pare-feu bloque-t-il les requêtes DNS ?", + "diagnosis_ip_broken_dnsresolution": "La résolution du nom de domaine semble bloquée ou interrompue pour une raison quelconque ... Un pare-feu bloque-t-il les requêtes DNS ?", "diagnosis_ip_broken_resolvconf": "La résolution du nom de domaine semble être cassée sur votre serveur, ce qui semble lié au fait que /etc/resolv.conf ne pointe pas vers 127.0.0.1.", "diagnosis_dns_good_conf": "Les enregistrements DNS sont correctement configurés pour le domaine {domain} (catégorie {category})", "diagnosis_dns_bad_conf": "Certains enregistrements DNS sont manquants ou incorrects pour le domaine {domain} (catégorie {category})", @@ -442,7 +418,7 @@ "apps_catalog_failed_to_download": "Impossible de télécharger le catalogue des applications {apps_catalog} : {error}", "diagnosis_mail_outgoing_port_25_blocked": "Le port sortant 25 semble être bloqué. Vous devriez essayer de le débloquer dans le panneau de configuration de votre fournisseur de services Internet (ou hébergeur). En attendant, le serveur ne pourra pas envoyer des emails à d'autres serveurs.", "domain_cannot_remove_main_add_new_one": "Vous ne pouvez pas supprimer '{domain}' car il s'agit du domaine principal et de votre seul domaine. Vous devez d'abord ajouter un autre domaine à l'aide de 'yunohost domain add ', puis définir comme domaine principal à l'aide de 'yunohost domain main-domain -n ' et vous pouvez ensuite supprimer le domaine '{domain}' à l'aide de 'yunohost domain remove {domain}'.'", - "diagnosis_security_vulnerable_to_meltdown_details": "Pour résoudre ce problème, vous devez mettre à niveau votre système et redémarrer pour charger le nouveau noyau Linux (ou contacter votre fournisseur de serveur si cela ne fonctionne pas). Voir https://meltdownattack.com/ pour plus d'informations.", + "diagnosis_security_vulnerable_to_meltdown_details": "Pour résoudre ce problème, vous devez mettre à jour votre système et le redémarrer pour charger le nouveau noyau linux (ou contacter votre fournisseur de serveur si cela ne fonctionne pas). Voir https://meltdownattack.com/ pour plus d'informations.", "diagnosis_description_basesystem": "Système de base", "diagnosis_description_ip": "Connectivité Internet", "diagnosis_description_dnsrecords": "Enregistrements DNS", @@ -456,8 +432,8 @@ "apps_catalog_obsolete_cache": "Le cache du catalogue d'applications est vide ou obsolète.", "apps_catalog_update_success": "Le catalogue des applications a été mis à jour !", "diagnosis_description_mail": "Email", - "diagnosis_ports_unreachable": "Le port {port} n'est pas accessible de l'extérieur.", - "diagnosis_ports_ok": "Le port {port} est accessible de l'extérieur.", + "diagnosis_ports_unreachable": "Le port {port} n'est pas accessible depuis l'extérieur.", + "diagnosis_ports_ok": "Le port {port} est accessible depuis l'extérieur.", "diagnosis_http_could_not_diagnose": "Impossible de diagnostiquer si le domaine est accessible de l'extérieur.", "diagnosis_http_could_not_diagnose_details": "Erreur : {error}", "diagnosis_http_ok": "Le domaine {domain} est accessible en HTTP depuis l'extérieur.", @@ -470,56 +446,55 @@ "diagnosis_ports_forwarding_tip": "Pour résoudre ce problème, vous devez probablement configurer la redirection de port sur votre routeur Internet comme décrit dans https://yunohost.org/isp_box_config", "diagnosis_http_connection_error": "Erreur de connexion : impossible de se connecter au domaine demandé, il est probablement injoignable.", "diagnosis_no_cache": "Pas encore de cache de diagnostique pour la catégorie '{category}'", - "yunohost_postinstall_end_tip": "La post-installation est terminée ! Pour finaliser votre configuration, il est recommandé de :\n- ajouter un premier utilisateur depuis la section \"Utilisateurs\" de l'interface web (ou 'yunohost user create ' en ligne de commande) ;\n- diagnostiquer les potentiels problèmes dans la section \"Diagnostic\" de l'interface web (ou 'yunohost diagnosis run' en ligne de commande) ;\n- lire les parties 'Finalisation de votre configuration' et 'Découverte de YunoHost' dans le guide de l'administrateur : https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "La post-installation est terminée ! Pour finaliser votre installation, il est recommandé de :\n - diagnostiquer les potentiels problèmes dans la section 'Diagnostic' de l'interface web (ou 'yunohost diagnosis run' en ligne de commande) ;\n - lire les parties 'Lancer la configuration initiale' et 'Découvrez l'auto-hébergement, comment installer et utiliser YunoHost' dans le guide d'administration : https://yunohost.org/admindoc.", "diagnosis_services_bad_status_tip": "Vous pouvez essayer de redémarrer le service, et si cela ne fonctionne pas, consultez les journaux de service dans le webadmin (à partir de la ligne de commande, vous pouvez le faire avec yunohost service restart {service} et yunohost service log {service} ).", "diagnosis_http_bad_status_code": "Le système de diagnostique n'a pas réussi à contacter votre serveur. Il se peut qu'une autre machine réponde à la place de votre serveur. Vérifiez que le port 80 est correctement redirigé, que votre configuration Nginx est à jour et qu'un reverse-proxy n'interfère pas.", - "diagnosis_http_timeout": "Expiration du délai en essayant de contacter votre serveur de l'extérieur. Il semble être inaccessible. Vérifiez que vous transférez correctement le port 80, que Nginx est en cours d'exécution et qu'un pare-feu n'interfère pas.", - "global_settings_setting_pop3_enabled": "Activer le protocole POP3 pour le serveur de messagerie", + "diagnosis_http_timeout": "Expiration du délai en essayant de contacter votre serveur depuis l'extérieur. Il semble être inaccessible.
1. La cause la plus fréquente pour ce problème est que les ports 80 et 443 ne sont pas correctement redirigés vers votre serveur.
2. Vous devriez également vérifier que le service NGINX est en cours d'exécution
3. Pour les installations plus complexes, assurez-vous qu'aucun pare-feu ou reverse-proxy n'interfère.", + "global_settings_setting_pop3_enabled_help": "Activer le protocole POP3 pour le serveur de messagerie", "log_app_action_run": "Lancer l'action de l'application '{}'", "diagnosis_never_ran_yet": "Il apparaît que le serveur a été installé récemment et qu'il n'y a pas encore eu de diagnostic. Vous devriez en lancer un depuis la webadmin ou en utilisant 'yunohost diagnosis run' depuis la ligne de commande.", "diagnosis_description_web": "Web", "diagnosis_basesystem_hardware": "L'architecture du serveur est {virt} {arch}", "group_already_exist_on_system_but_removing_it": "Le groupe {group} est déjà présent dans les groupes du système, mais YunoHost va le supprimer...", "certmanager_warning_subdomain_dns_record": "Le sous-domaine '{subdomain}' ne résout pas vers la même adresse IP que '{domain}'. Certaines fonctionnalités seront indisponibles tant que vous n'aurez pas corrigé cela et regénéré le certificat.", - "domain_cannot_add_xmpp_upload": "Vous ne pouvez pas ajouter de domaine commençant par 'xmpp-upload.'. Ce type de nom est réservé à la fonctionnalité d'upload XMPP intégrée dans YunoHost.", + "domain_cannot_add_xmpp_upload": "Vous ne pouvez pas ajouter de domaine commençant par 'xmpp-upload.'. Ce type de nom est réservé à la fonctionnalité XMPP éponyme intégrée dans YunoHost.", "diagnosis_mail_outgoing_port_25_ok": "Le serveur de messagerie SMTP peut envoyer des emails (le port sortant 25 n'est pas bloqué).", - "diagnosis_mail_outgoing_port_25_blocked_details": "Vous devez d'abord essayer de débloquer le port sortant 25 dans votre interface de routeur Internet ou votre interface d'hébergement. (Certains hébergeurs peuvent vous demander de leur envoyer un ticket de support pour cela).", + "diagnosis_mail_outgoing_port_25_blocked_details": "Vous devriez d'abord essayer d'ouvrir le port 25 dans l'interface de votre routeur, box Internet ou interface d'hébergement. (Certains hébergeurs peuvent vous demander d'ouvrir un ticket sur leur support d'assistance pour cela).", "diagnosis_mail_ehlo_bad_answer": "Un service non SMTP a répondu sur le port 25 en IPv{ipversion}", - "diagnosis_mail_ehlo_bad_answer_details": "Cela peut être dû à une autre machine qui répond au lieu de votre serveur.", + "diagnosis_mail_ehlo_bad_answer_details": "Cela peut être dû à une autre machine qui répond à la place de votre serveur.", "diagnosis_mail_ehlo_wrong": "Un autre serveur de messagerie SMTP répond sur IPv{ipversion}. Votre serveur ne sera probablement pas en mesure de recevoir des email.", "diagnosis_mail_ehlo_could_not_diagnose": "Impossible de diagnostiquer si le serveur de messagerie postfix est accessible de l'extérieur en IPv{ipversion}.", "diagnosis_mail_ehlo_could_not_diagnose_details": "Erreur : {error}", - "diagnosis_mail_fcrdns_dns_missing": "Aucun DNS inverse n'est défini pour IPv{ipversion}. Certains emails seront peut-être refusés ou considérés comme des spam.", + "diagnosis_mail_fcrdns_dns_missing": "Aucun reverse-DNS n'est défini pour IPv{ipversion}. Il se peut que certains emails ne soient pas acheminés ou soient considérés comme du spam.", "diagnosis_mail_fcrdns_ok": "Votre DNS inverse est correctement configuré !", - "diagnosis_mail_fcrdns_nok_details": "Vous devez d'abord essayer de configurer le DNS inverse avec {ehlo_domain} dans votre interface de routeur Internet ou votre interface d'hébergement. (Certains hébergeurs peuvent vous demander de leur envoyer un ticket de support pour cela).", - "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Le DNS inverse n'est pas correctement configuré en IPv{ipversion}. Certains emails seront peut-être refusés ou considérés comme des spam.", + "diagnosis_mail_fcrdns_nok_details": "Vous devez d'abord essayer de configurer le reverse-DNS avec {ehlo_domain} dans l'interface de votre routeur, box Internet ou votre interface d'hébergement. (Certains hébergeurs peuvent vous demander d'ouvrir un ticket sur leur support d'assistance pour cela).", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Le reverse-DNS n'est pas correctement configuré en IPv{ipversion}. Il se peut que certains emails ne soient pas acheminés ou soient considérés comme du spam.", "diagnosis_mail_blacklist_ok": "Les adresses IP et les domaines utilisés par ce serveur ne semblent pas être sur liste noire", "diagnosis_mail_blacklist_reason": "La raison de la liste noire est : {reason}", - "diagnosis_mail_blacklist_website": "Après avoir identifié la raison pour laquelle vous êtes répertorié et l'avoir corrigé, n'hésitez pas à demander le retrait de votre IP ou domaine sur {blacklist_website}", + "diagnosis_mail_blacklist_website": "Après avoir identifié la raison pour laquelle vous êtes répertorié sur cette liste et l'avoir corrigée, n'hésitez pas à demander le retrait de votre IP ou de votre domaine sur {blacklist_website}", "diagnosis_mail_queue_ok": "{nb_pending} emails en attente dans les files d'attente de messagerie", "diagnosis_mail_queue_unavailable_details": "Erreur : {error}", "diagnosis_mail_queue_too_big": "Trop d'emails en attente dans la file d'attente ({nb_pending} emails)", - "global_settings_setting_smtp_allow_ipv6": "Autoriser l'utilisation d'IPv6 pour recevoir et envoyer du courrier", "diagnosis_display_tip": "Pour voir les problèmes détectés, vous pouvez accéder à la section Diagnostic du webadmin ou exécuter 'yunohost diagnosis show --issues --human-readable' à partir de la ligne de commande.", "diagnosis_ip_global": "IP globale : {global}", "diagnosis_ip_local": "IP locale : {local}", - "diagnosis_dns_point_to_doc": "Veuillez consulter la documentation sur https://yunohost.org/dns_config si vous avez besoin d'aide pour configurer les enregistrements DNS.", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Certains fournisseurs ne vous laisseront pas débloquer le port sortant 25 parce qu'ils ne se soucient pas de la neutralité du Net.
- Certains d'entre eux offrent l'alternative d'utiliser un serveur de messagerie relai bien que cela implique que le relai sera en mesure d'espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Vous pouvez également envisager de passer à un fournisseur plus respectueux de la neutralité du net", + "diagnosis_dns_point_to_doc": "Veuillez consulter la documentation disponible ici https://yunohost.org/dns_config si vous avez besoin d'aide pour configurer les enregistrements DNS.", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Certains opérateurs ne vous laisseront pas débloquer le port 25 parce qu'ils ne se soucient pas de la neutralité du Net.
- Certains d'entre eux offrent la possibilité d'utiliser un serveur de messagerie relai bien que cela implique que celui-ci sera en mesure d'espionner le trafic de votre messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Vous pouvez également envisager de passer à un fournisseur plus respectueux de la neutralité du net", "diagnosis_mail_ehlo_ok": "Le serveur de messagerie SMTP est accessible de l'extérieur et peut donc recevoir des emails !", "diagnosis_mail_ehlo_unreachable": "Le serveur de messagerie SMTP est inaccessible de l'extérieur en IPv{ipversion}. Il ne pourra pas recevoir des emails.", - "diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible.
1. La cause la plus courante de ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur.
2. Vous devez également vous assurer que le service postfix est en cours d'exécution.
3. Sur les configurations plus complexes: assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.", + "diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible.
1. La cause la plus courante de ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur.
2. Vous devez également vous assurer que le service postfix est en cours d'exécution.
3. Sur les configurations plus complexes : assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.", "diagnosis_mail_ehlo_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur.
EHLO reçu : {wrong_ehlo}
Attendu : {right_ehlo}
La cause la plus courante à ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur. Vous pouvez également vous assurer qu'aucun pare-feu ou reverse-proxy n'interfère.", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes :
- Certains FAI fournissent l'alternative de à l'aide d'un relais de serveur de messagerie bien que cela implique que le relais pourra espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Enfin, il est également possible de changer de fournisseur", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si votre DNS inversé est correctement configuré en IPv4, vous pouvez essayer de désactiver l'utilisation d'IPv6 lors de l'envoi d'emails en exécutant yunohost settings set smtp.allow_ipv6 -v off. Remarque : cette dernière solution signifie que vous ne pourrez pas envoyer ou recevoir de emails avec les quelques serveurs qui ont uniquement de l'IPv6.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains opérateurs ne vous laisseront pas configurer votre reverse-DNS (ou leur fonctionnalité pourrait être cassée ...). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes :
- Certains FAI offre cette possibilité à l'aide d'un relais de serveur de messagerie bien que cela implique que le relais pourra espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Enfin, il est également possible de changer d'opérateur", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si votre DNS inversé est correctement configuré en IPv4, vous pouvez essayer de désactiver l'utilisation d'IPv6 lors de l'envoi d'emails en exécutant yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Remarque : cette dernière solution signifie que vous ne pourrez pas envoyer ou recevoir d'emails avec les quelques serveurs qui ont uniquement de l'IPv6.", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS inverse actuel : {rdns_domain}
Valeur attendue : {ehlo_domain}", "diagnosis_mail_blacklist_listed_by": "Votre IP ou domaine {item} est sur liste noire sur {blacklist_name}", "diagnosis_mail_queue_unavailable": "Impossible de consulter le nombre d'emails en attente dans la file d'attente", - "diagnosis_ports_partially_unreachable": "Le port {port} n'est pas accessible de l'extérieur en IPv{failed}.", + "diagnosis_ports_partially_unreachable": "Le port {port} n'est pas accessible depuis l'extérieur en IPv{failed}.", "diagnosis_http_hairpinning_issue": "Votre réseau local ne semble pas supporter l'hairpinning.", "diagnosis_http_hairpinning_issue_details": "C'est probablement à cause de la box/routeur de votre fournisseur d'accès internet. Par conséquent, les personnes extérieures à votre réseau local pourront accéder à votre serveur comme prévu, mais pas les personnes internes au réseau local (comme vous, probablement ?) si elles utilisent le nom de domaine ou l'IP globale. Vous pourrez peut-être améliorer la situation en consultant https://yunohost.org/dns_local_network", "diagnosis_http_partially_unreachable": "Le domaine {domain} semble inaccessible en HTTP depuis l'extérieur du réseau local en IPv{failed}, bien qu'il fonctionne en IPv{passed}.", "diagnosis_http_nginx_conf_not_up_to_date": "La configuration Nginx de ce domaine semble avoir été modifiée manuellement et empêche YunoHost de diagnostiquer si elle est accessible en HTTP.", - "diagnosis_http_nginx_conf_not_up_to_date_details": "Pour corriger la situation, inspectez la différence avec la ligne de commande en utilisant les outils yunohost tools regen-conf nginx --dry-run --with-diff et si vous êtes d'accord, appliquez les modifications avec yunohost tools regen-conf nginx --force.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Pour corriger la situation, vérifier les différences avec la ligne de commande en utilisant les outils yunohost tools regen-conf nginx --dry-run --with-diff et si vous êtes d'accord avec le résultat, appliquez les modifications avec yunohost tools regen-conf nginx --force.", "backup_archive_cant_retrieve_info_json": "Impossible d'avoir des informations sur l'archive '{archive}'... Le fichier info.json ne peut pas être trouvé (ou n'est pas un fichier json valide).", "backup_archive_corrupted": "Il semble que l'archive de la sauvegarde '{archive}' est corrompue : {error}", "diagnosis_ip_no_ipv6_tip": "L'utilisation de IPv6 n'est pas obligatoire pour le fonctionnement de votre serveur, mais cela contribue à la santé d'Internet dans son ensemble. IPv6 généralement configuré automatiquement par votre système ou votre FAI s'il est disponible. Autrement, vous devrez prendre quelque minutes pour le configurer manuellement à l'aide de cette documentation : https://yunohost.org/#/ipv6. Si vous ne pouvez pas activer IPv6 ou si c'est trop technique pour vous, vous pouvez aussi ignorer cet avertissement sans que cela pose problème.", @@ -530,27 +505,27 @@ "diagnosis_domain_expiration_warning": "Certains domaines vont expirer prochainement !", "diagnosis_domain_expiration_error": "Certains domaines vont expirer TRÈS PROCHAINEMENT !", "diagnosis_domain_expires_in": "{domain} expire dans {days} jours.", - "certmanager_domain_not_diagnosed_yet": "Il n'y a pas encore de résultat de diagnostic pour le domaine {domain}. Merci de relancer un diagnostic pour les catégories 'Enregistrements DNS' et 'Web' dans la section Diagnostique pour vérifier si le domaine est prêt pour Let's Encrypt. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)", - "diagnosis_swap_tip": "Merci d'être prudent et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire drastiquement l'espérance de vie du périphérique.", + "certmanager_domain_not_diagnosed_yet": "Il n'y a pas encore de résultat de diagnostic pour le domaine {domain}. Merci de relancer un diagnostic pour les catégories 'Enregistrements DNS' et 'Web' dans la section Diagnostic pour vérifier si le domaine est prêt pour Let's Encrypt. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)", + "diagnosis_swap_tip": "Soyez averti et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire considérablement l'espérance de vie de celui-ci.", "restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}", "regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.", "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost dyndns update --force.", "app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de YunoHost. Vous devriez probablement envisager de mettre à jour votre système.", - "global_settings_setting_backup_compress_tar_archives": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar). N.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.", - "diagnosis_processes_killed_by_oom_reaper": "Certains processus ont été arrêtés récemment par le système car il manquait de mémoire. Cela apparaît généralement quand le système manque de mémoire ou qu'un processus consomme trop de mémoire. Liste des processus tués :\n{kills_summary}", + "global_settings_setting_backup_compress_tar_archives": "Compresser les archives de backup", + "diagnosis_processes_killed_by_oom_reaper": "Certains processus ont été récemment arrêtés par le système car il manquait de mémoire. Ceci est typiquement symptomatique d'un manque de mémoire sur le système ou d'un processus consommant trop de mémoire. Liste des processus arrêtés :\n{kills_summary}", "ask_user_domain": "Domaine à utiliser pour l'adresse email de l'utilisateur et le compte XMPP", "app_manifest_install_ask_is_public": "Cette application devrait-elle être visible par les visiteurs anonymes ?", "app_manifest_install_ask_admin": "Choisissez un administrateur pour cette application", "app_manifest_install_ask_password": "Choisissez un mot de passe administrateur pour cette application", "app_manifest_install_ask_path": "Choisissez le chemin d'URL (après le domaine) où cette application doit être installée", "app_manifest_install_ask_domain": "Choisissez le domaine sur lequel vous souhaitez installer cette application", - "global_settings_setting_smtp_relay_user": "Compte utilisateur du relais SMTP", + "global_settings_setting_smtp_relay_host": "Adresse du relais SMTP", + "global_settings_setting_smtp_relay_user": "Utilisateur du relais SMTP", "global_settings_setting_smtp_relay_port": "Port du relais SMTP", - "global_settings_setting_smtp_relay_host": "Un relais SMTP permet d'envoyer du courrier à la place de cette instance YunoHost. Cela est utile si vous êtes dans l'une de ces situations : le port 25 est bloqué par votre FAI ou par votre fournisseur VPS ; vous avez une IP résidentielle répertoriée sur DUHL ; vous ne pouvez pas configurer le DNS inversé ; ou le serveur n'est pas directement accessible depuis Internet et vous voulez en utiliser un autre pour envoyer des mails.", "diagnosis_package_installed_from_sury_details": "Certains paquets ont été installés par inadvertance à partir d'un dépôt tiers appelé Sury. L'équipe YunoHost a amélioré la stratégie de gestion de ces paquets, mais on s'attend à ce que certaines configurations qui ont installé des applications PHP7.3 tout en étant toujours sur Stretch présentent des incohérences. Pour résoudre cette situation, vous devez essayer d'exécuter la commande suivante : {cmd_to_fix}", - "app_argument_password_no_default": "Erreur lors de l'analyse de l'argument de mot de passe '{name}' : l'argument de mot de passe ne peut pas avoir de valeur par défaut pour des raisons de sécurité", + "app_argument_password_no_default": "Erreur lors de l'analyse syntaxique du mot de passe '{name}' : le mot de passe ne peut pas avoir de valeur par défaut pour des raisons de sécurité", "pattern_email_forward": "L'adresse électronique doit être valide, le symbole '+' étant accepté (par exemple : johndoe+yunohost@exemple.com)", - "global_settings_setting_smtp_relay_password": "Mot de passe du relais de l'hôte SMTP", + "global_settings_setting_smtp_relay_password": "Mot de passe du relais SMTP", "diagnosis_package_installed_from_sury": "Des paquets du système devraient être rétrogradé de version", "additional_urls_already_added": "URL supplémentaire '{url}' déjà ajoutée pour la permission '{permission}'", "unknown_main_domain_path": "Domaine ou chemin inconnu pour '{app}'. Vous devez spécifier un domaine et un chemin pour pouvoir spécifier une URL pour l'autorisation.", @@ -564,7 +539,7 @@ "additional_urls_already_removed": "URL supplémentaire '{url}' déjà supprimées pour la permission '{permission}'", "invalid_number": "Doit être un nombre", "diagnosis_basesystem_hardware_model": "Le modèle/architecture du serveur est {model}", - "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système.", + "diagnosis_backports_in_sources_list": "Il semble que le gestionnaire de paquet APT soit configuré pour utiliser le dépôt des rétro-portages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant du dépôt 'backports', car cela risque de créer des instabilités ou des conflits sur votre système.", "postinstall_low_rootfsspace": "Le système de fichiers a une taille totale inférieure à 10 Go, ce qui est préoccupant et devrait attirer votre attention ! Vous allez certainement arriver à court d'espace disque (très) rapidement ! Il est recommandé d'avoir au moins 16 Go à la racine pour ce système de fichiers. Si vous voulez installer YunoHost malgré cet avertissement, relancez la post-installation avec --force-diskspace", "domain_remove_confirm_apps_removal": "Le retrait de ce domaine retirera aussi ces applications :\n{apps}\n\nÊtes vous sûr de vouloir cela ? [{answers}]", "diagnosis_rootfstotalspace_critical": "Le système de fichiers racine ne fait que {space} ! Vous allez certainement le remplir très rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.", @@ -572,29 +547,24 @@ "app_restore_script_failed": "Une erreur s'est produite dans le script de restauration de l'application", "restore_backup_too_old": "Cette sauvegarde ne peut pas être restaurée car elle provient d'une version de YunoHost trop ancienne.", "log_backup_create": "Créer une archive de sauvegarde", - "global_settings_setting_ssowat_panel_overlay_enabled": "Activer la superposition de la vignette SSOwat", + "global_settings_setting_ssowat_panel_overlay_enabled": "Activer la vignette 'YunoHost' (raccourci vers le portail) sur les apps", "migration_ldap_rollback_success": "Système rétabli dans son état initial.", "permission_cant_add_to_all_users": "L'autorisation {permission} ne peut pas être ajoutée à tous les utilisateurs.", "migration_ldap_migration_failed_trying_to_rollback": "Impossible de migrer... tentative de restauration du système.", "migration_ldap_can_not_backup_before_migration": "La sauvegarde du système n'a pas pu être terminée avant l'échec de la migration. Erreur : {error }", "migration_ldap_backup_before_migration": "Création d'une sauvegarde de la base de données LDAP et des paramètres des applications avant la migration proprement dite.", - "global_settings_setting_security_ssh_port": "Port SSH", - "diagnosis_sshd_config_inconsistent_details": "Veuillez exécuter yunohost settings set security.ssh.port -v VOTRE_PORT_SSH pour définir le port SSH, et vérifiez yunohost tools regen-conf ssh --dry-run --with-diff et yunohost tools regen-conf ssh --force pour réinitialiser votre configuration aux recommandations YunoHost.", - "diagnosis_sshd_config_inconsistent": "Il semble que le port SSH a été modifié manuellement dans /etc/ssh/sshd_config. Depuis YunoHost 4.2, un nouveau paramètre global 'security.ssh.port' est disponible pour éviter de modifier manuellement la configuration.", + "diagnosis_sshd_config_inconsistent_details": "Veuillez exécuter yunohost settings set security.ssh.ssh_port -v VOTRE_PORT_SSH pour définir le port SSH, et vérifiez yunohost tools regen-conf ssh --dry-run --with-diff et yunohost tools regen-conf ssh --force pour réinitialiser votre configuration aux recommandations YunoHost.", + "diagnosis_sshd_config_inconsistent": "Il semble que le port SSH ait été modifié manuellement dans /etc/ssh/sshd_config. Depuis YunoHost 4.2, un nouveau paramètre global 'security.ssh.ssh_port' est disponible pour éviter de modifier manuellement la configuration.", "diagnosis_sshd_config_insecure": "La configuration SSH semble avoir été modifiée manuellement et n'est pas sécurisée car elle ne contient aucune directive 'AllowGroups' ou 'AllowUsers' pour limiter l'accès aux utilisateurs autorisés.", "backup_create_size_estimation": "L'archive contiendra environ {size} de données.", - "global_settings_setting_security_webadmin_allowlist": "Adresses IP autorisées à accéder à la webadmin. Elles doivent être séparées par une virgule.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Autoriser seulement certaines IP à accéder à la webadmin.", "diagnosis_dns_specialusedomain": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial comme .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.", - "invalid_password": "Mot de passe incorrect", "ldap_server_is_down_restart_it": "Le service LDAP est en panne, essayez de le redémarrer...", "ldap_server_down": "Impossible d'atteindre le serveur LDAP", - "global_settings_setting_security_experimental_enabled": "Activer les fonctionnalités de sécurité expérimentales (ne l'activez pas si vous ne savez pas ce que vous faites !)", - "diagnosis_apps_deprecated_practices": "La version installée de cette application utilise toujours certaines pratiques de packaging obsolètes. Vous devriez vraiment envisager de mettre l'application à jour.", - "diagnosis_apps_outdated_ynh_requirement": "La version installée de cette application nécessite uniquement YunoHost >= 2.x, cela indique que l'application n'est pas à jour avec les bonnes pratiques de packaging et les helpers recommandées. Vous devriez vraiment envisager de mettre l'application à jour.", + "diagnosis_apps_deprecated_practices": "La version installée de cette application utilise encore de très anciennes pratiques de packaging obsolètes et dépassées. Vous devriez vraiment envisager de mettre à jour cette application.", + "diagnosis_apps_outdated_ynh_requirement": "La version installée de cette application nécessite uniquement YunoHost >= 2.x ou 3.x, ce qui tend à indiquer qu'elle n'est pas à jour avec les pratiques recommandées de packaging et des helpers . Vous devriez vraiment envisager de la mettre à jour.", "diagnosis_apps_bad_quality": "Cette application est actuellement signalée comme cassée dans le catalogue d'applications de YunoHost. Cela peut être un problème temporaire. En attendant que les mainteneurs tentent de résoudre le problème, la mise à jour de cette application est désactivée.", "diagnosis_apps_broken": "Cette application est actuellement signalée comme cassée dans le catalogue d'applications de YunoHost. Cela peut être un problème temporaire. En attendant que les mainteneurs tentent de résoudre le problème, la mise à jour de cette application est désactivée.", - "diagnosis_apps_not_in_app_catalog": "Cette application est absente ou ne figure plus dans le catalogue d'applications de YunoHost. Vous devriez envisager de la désinstaller car elle ne recevra pas de mise à jour et pourrait compromettre l'intégrité et la sécurité de votre système.", + "diagnosis_apps_not_in_app_catalog": "Cette application ne figure pas dans le catalogue de YunoHost. Si elle l'était dans le passé et a été supprimée, vous devriez envisager de désinstaller cette application car elle ne recevra pas de mises à jour et peut compromettre l'intégrité et la sécurité de votre système.", "diagnosis_apps_issue": "Un problème a été détecté pour l'application {app}", "diagnosis_apps_allgood": "Toutes les applications installées respectent les pratiques de packaging de base", "diagnosis_description_apps": "Applications", @@ -607,7 +577,6 @@ "user_import_bad_line": "Ligne incorrecte {line} : {details}", "log_user_import": "Importer des utilisateurs", "diagnosis_high_number_auth_failures": "Il y a eu récemment un grand nombre d'échecs d'authentification. Assurez-vous que Fail2Ban est en cours d'exécution et est correctement configuré, ou utilisez un port personnalisé pour SSH comme expliqué dans https://yunohost.org/security.", - "global_settings_setting_security_nginx_redirect_to_https": "Rediriger les requêtes HTTP vers HTTPS par défaut (NE PAS DÉSACTIVER à moins de savoir vraiment ce que vous faites !)", "config_validate_color": "Doit être une couleur hexadécimale RVB valide", "app_config_unable_to_apply": "Échec de l'application des valeurs du panneau de configuration.", "app_config_unable_to_read": "Échec de la lecture des valeurs du panneau de configuration.", @@ -620,7 +589,6 @@ "config_validate_email": "Doit être un email valide", "config_validate_time": "Doit être une heure valide comme HH:MM", "config_validate_url": "Doit être une URL Web valide", - "config_version_not_supported": "Les versions du panneau de configuration '{version}' ne sont pas prises en charge.", "danger": "Danger :", "invalid_number_min": "Doit être supérieur à {min}", "invalid_number_max": "Doit être inférieur à {max}", @@ -630,7 +598,6 @@ "domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.", "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost dyndns update')", "domain_dns_registrar_supported": "YunoHost a détecté automatiquement que ce domaine est géré par le registrar **{registrar}**. Si vous le souhaitez, YunoHost configurera automatiquement cette zone DNS, si vous lui fournissez les identifiants API appropriés. Vous pouvez trouver de la documentation sur la façon d'obtenir vos identifiants API sur cette page : https://yunohost.org/registar_api_{registrar}. (Vous pouvez également configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns )", - "domain_config_features_disclaimer": "Jusqu'à présent, l'activation/désactivation des fonctionnalités de messagerie ou XMPP n'a d'impact que sur la configuration DNS recommandée et automatique, et non sur les configurations système !", "domain_dns_push_managed_in_parent_domain": "La fonctionnalité de configuration DNS automatique est gérée dans le domaine parent {parent_domain}.", "domain_dns_registrar_managed_in_parent_domain": "Ce domaine est un sous-domaine de {parent_domain_link}. La configuration du registrar DNS doit être gérée dans le panneau de configuration de {parent_domain}.", "domain_dns_registrar_not_supported": "YunoHost n'a pas pu détecter automatiquement le bureau d'enregistrement gérant ce domaine. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns.", @@ -659,7 +626,7 @@ "diagnosis_http_special_use_tld": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et n'est donc pas censé être exposé en dehors du réseau local.", "domain_dns_conf_special_use_tld": "Ce domaine est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.", "other_available_options": "... et {n} autres options disponibles non affichées", - "domain_config_auth_consumer_key": "Consumer key", + "domain_config_auth_consumer_key": "Clé utilisateur", "domain_unknown": "Domaine '{domain}' inconnu", "migration_0021_start": "Démarrage de la migration vers Bullseye", "migration_0021_patching_sources_list": "Mise à jour du fichier sources.lists...", @@ -673,9 +640,8 @@ "migration_0021_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés à la suite de la mise à niveau : {manually_modified_files}", "migration_0021_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus nécessaires...", "migration_0021_patch_yunohost_conflicts": "Application du correctif pour contourner le problème de conflit...", - "migration_0021_not_buster": "La distribution Debian actuelle n'est pas Buster !", + "migration_0021_not_buster2": "La distribution Debian actuelle n'est pas Buster ! Si vous avez déjà effectué la migration Buster->Bullseye, alors cette erreur est symptomatique du fait que la migration n'a pas été terminée correctement à 100% (sinon YunoHost aurait marqué la migration comme terminée). Il est recommandé d'étudier ce qu'il s'est passé avec l'équipe de support, qui aura besoin du log **complet** de la migration, qui peut être retrouvé dans Outils > Journaux dans la webadmin.", "migration_description_0021_migrate_to_bullseye": "Mise à niveau du système vers Debian Bullseye et YunoHost 11.x", - "global_settings_setting_security_ssh_password_authentication": "Autoriser l'authentification par mot de passe pour SSH", "domain_config_default_app": "Application par défaut", "migration_description_0022_php73_to_php74_pools": "Migration des fichiers de configuration php7.3-fpm 'pool' vers php7.4", "migration_description_0023_postgresql_11_to_13": "Migration des bases de données de PostgreSQL 11 vers 13", @@ -684,5 +650,121 @@ "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 est installé, mais pas PostgreSQL 13 ! ? Quelque chose d'anormal s'est peut-être produit sur votre système :(...", "tools_upgrade_failed": "Impossible de mettre à jour les paquets : {packages_list}", "migration_0023_not_enough_space": "Prévoyez suffisamment d'espace disponible dans {path} pour exécuter la migration.", - "migration_0023_postgresql_11_not_installed": "PostgreSQL n'a pas été installé sur votre système. Il n'y a rien à faire." + "migration_0023_postgresql_11_not_installed": "PostgreSQL n'a pas été installé sur votre système. Il n'y a rien à faire.", + "global_settings_setting_backup_compress_tar_archives_help": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar).\nN.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.", + "global_settings_setting_security_experimental_enabled": "Fonctionnalités de sécurité expérimentales", + "global_settings_setting_security_experimental_enabled_help": "Activer les fonctionnalités de sécurité expérimentales (ne l'activez pas si vous ne savez pas ce que vous faites !)", + "global_settings_setting_nginx_compatibility_help": "Compromis 'compatibilité versus sécurité' pour le serveur web NGINX. Affecte les cryptogrammes utilisés (et d'autres aspects liés à la sécurité)", + "global_settings_setting_nginx_redirect_to_https_help": "Rediriger les requêtes HTTP vers HTTPS par défaut (NE PAS DÉSACTIVER à moins de savoir vraiment ce que vous faites !)", + "global_settings_setting_admin_strength": "Critères pour les mots de passe administrateur", + "global_settings_setting_user_strength": "Critères pour les mots de passe utilisateurs", + "global_settings_setting_postfix_compatibility_help": "Compromis 'compatibilité versus sécurité' pour le serveur Postfix. Affecte les cryptogrammes utilisés (et d'autres aspects liés à la sécurité)", + "global_settings_setting_ssh_compatibility_help": "Compromis 'compatibilité versus sécurité' pour le serveur SSH. Affecte les cryptogrammes utilisés (et d'autres aspects liés à la sécurité).", + "global_settings_setting_ssh_password_authentication_help": "Autoriser l'authentification par mot de passe pour SSH", + "global_settings_setting_ssh_port": "Port SSH", + "global_settings_setting_webadmin_allowlist_help": "Adresses IP autorisées à accéder à la webadmin. Elles doivent être séparées par une virgule.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Autoriser seulement certaines IP à accéder à la webadmin.", + "global_settings_setting_smtp_allow_ipv6_help": "Autoriser l'utilisation d'IPv6 pour recevoir et envoyer du courrier", + "global_settings_setting_smtp_relay_enabled_help": "Un relais SMTP permet d'envoyer du courrier à la place de cette instance YunoHost. Cela est utile si vous êtes dans l'une de ces situations : le port 25 est bloqué par votre FAI ou par votre fournisseur VPS ; vous avez une IP résidentielle répertoriée sur DUHL ; vous ne pouvez pas configurer le DNS inversé ; ou le serveur n'est pas directement accessible depuis Internet et vous voulez en utiliser un autre pour envoyer des mails.", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "La reconstruction du virtualenv sera tentée pour les applications suivantes (NB : l'opération peut prendre un certain temps !) : {rebuild_apps}", + "migration_0024_rebuild_python_venv_in_progress": "Tentative de reconstruction du virtualenv Python pour `{app}`", + "migration_0024_rebuild_python_venv_failed": "Échec de la reconstruction de l'environnement virtuel Python pour {app}. L'application peut ne pas fonctionner tant que ce problème n'est pas résolu. Vous devriez corriger la situation en forçant la mise à jour de cette application en utilisant `yunohost app upgrade --force {app}`.", + "migration_description_0024_rebuild_python_venv": "Réparer l'application Python après la migration Bullseye", + "migration_0024_rebuild_python_venv_broken_app": "Ignorer {app} car virtualenv ne peut pas être facilement reconstruit pour cette application. Au lieu de cela, vous devriez corriger la situation en forçant la mise à jour de cette application en utilisant `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_disclaimer_base": "Suite à la mise à niveau vers Debian Bullseye, certaines applications Python doivent être partiellement reconstruites pour être converties vers la nouvelle version Python livrée dans Debian (en termes techniques : ce qu'on appelle le \"virtualenv\" doit être recréé). En attendant, ces applications Python peuvent ne pas fonctionner. YunoHost peut tenter de reconstruire le virtualenv pour certains d'entre eux, comme détaillé ci-dessous. Pour les autres applications, ou si la tentative de reconstruction échoue, vous devrez forcer manuellement une mise à niveau pour ces applications.", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Les virtualenvs ne peuvent pas être reconstruits automatiquement pour ces applications. Vous devez forcer une mise à jour pour ceux-ci, ce qui peut être fait à partir de la ligne de commande : `yunohost app upgrade --force APP` : {ignored_apps}", + "admins": "Administrateurs", + "all_users": "Tous les utilisateurs de YunoHost", + "app_action_failed": "Échec de la commande {action} de l'application {app}", + "app_manifest_install_ask_init_admin_permission": "Qui doit avoir accès aux fonctions d'administration de cette application ? (Ceci peut être modifié ultérieurement)", + "app_manifest_install_ask_init_main_permission": "Qui doit avoir accès à cette application ? (Ceci peut être modifié ultérieurement)", + "ask_admin_fullname": "Nom complet de l'administrateur", + "ask_admin_username": "Nom d'utilisateur de l'administrateur", + "ask_fullname": "Nom complet (Nom et Prénom)", + "certmanager_cert_install_failed": "L'installation du certificat Let's Encrypt a échoué pour {domains}", + "certmanager_cert_install_failed_selfsigned": "L'installation du certificat auto-signé a échoué pour {domains}", + "certmanager_cert_renew_failed": "Le renouvellement du certificat Let's Encrypt a échoué pour {domains}", + "diagnosis_using_stable_codename": "apt (le gestionnaire de paquets du système) est actuellement configuré pour installer les paquets du nom de code 'stable', et cela au lieu du nom de code de la version actuelle de Debian (bullseye).", + "diagnosis_using_stable_codename_details": "C'est généralement dû à une configuration incorrecte de votre fournisseur d'hébergement. C'est dangereux, car dès que la prochaine version de Debian deviendra la nouvelle 'stable', apt voudra mettre à jour tous les paquets système sans passer par une procédure de migration appropriée propre à YunoHost. Il est recommandé de corriger cela en éditant le source apt pour le dépôt Debian de base, et de remplacer le mot clé stable par bullseye. Le fichier de configuration correspondant doit être /etc/apt/sources.list, ou un fichier dans /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing_details": "C'est probablement normal si vous savez ce que vous faites, toutefois faites attention aux notes de version avant d'installer les mises à niveau de YunoHost ! Si vous voulez désactiver les mises à jour 'testing', vous devez supprimer le mot-clé testing de /etc/apt/sources.list.d/yunohost.list.", + "global_settings_setting_nginx_redirect_to_https": "Forcer HTTPS", + "global_settings_setting_postfix_compatibility": "Compatibilité Postfix", + "global_settings_setting_root_access_explain": "Sur les systèmes Linux, 'root' est l'administrateur absolu du système : il a tous les droits. Dans le contexte de YunoHost, la connexion SSH directe de 'root' est désactivée par défaut - sauf depuis le réseau local du serveur. Les membres du groupe 'admins' peuvent utiliser la commande sudo pour agir en tant que root à partir de la ligne de commande. Cependant, il peut être utile de disposer d'un mot de passe root (robuste) pour déboguer le système si, pour une raison quelconque, les administrateurs réguliers ne peuvent plus se connecter.", + "global_settings_setting_root_password_confirm": "Nouveau mot de passe root (confirmer)", + "global_settings_setting_smtp_relay_enabled": "Activer le relais SMTP", + "global_settings_setting_ssh_compatibility": "Compatibilité SSH", + "global_settings_setting_user_strength_help": "Ces paramètres ne seront appliqués que lors de l'initialisation ou de la modification du mot de passe", + "migration_description_0025_global_settings_to_configpanel": "Migrer l'ancienne terminologie des paramètres globaux vers la nouvelle terminologie modernisée", + "migration_description_0026_new_admins_group": "Migrer vers le nouveau système de gestion 'multi-administrateurs' (plusieurs utilisateurs pourront être présents dans le groupe 'Admins' avec des tous les droits d'administration sur toute l'instance YunoHost)", + "password_confirmation_not_the_same": "Le mot de passe et la confirmation de ce dernier ne correspondent pas", + "pattern_fullname": "Doit être un nom complet valide (au moins 3 caractères)", + "config_action_disabled": "Impossible d'exécuter l'action '{action}' car elle est désactivée, assurez-vous de respecter ses paramètres et contraintes. Aide : {help}", + "config_action_failed": "Échec de l'exécution de l'action '{action}' : {error}", + "config_forbidden_readonly_type": "Le type '{type}' ne peut pas être défini comme étant en lecture seule, utilisez un autre type pour obtenir cette valeur (identifiant de l'argument : '{id}').", + "global_settings_setting_pop3_enabled": "Activer POP3", + "registrar_infos": "Infos du Registrar (fournisseur du nom de domaine)", + "root_password_changed": "Le mot de passe de root a été changé", + "visitors": "Visiteurs", + "global_settings_reset_success": "Réinitialisation des paramètres généraux", + "domain_config_acme_eligible": "Éligibilité au protocole ACME (Automatic Certificate Management Environment, littéralement : environnement de gestion automatique de certificat)", + "domain_config_acme_eligible_explain": "Ce domaine ne semble pas prêt pour installer un certificat Let's Encrypt. Veuillez vérifier votre configuration DNS mais aussi que votre serveur est bien joignable en HTTP. Les sections 'Enregistrements DNS' et 'Web' de la page Diagnostic peuvent vous aider à comprendre ce qui est mal configuré.", + "domain_config_cert_install": "Installer un certificat Let's Encrypt", + "domain_config_cert_issuer": "Autorité de certification", + "domain_config_cert_no_checks": "Ignorer les tests et autres vérifications du diagnostic", + "domain_config_cert_renew": "Renouvellement du certificat Let's Encrypt", + "domain_config_cert_renew_help": "Le certificat sera automatiquement renouvelé dans les 15 derniers jours précédant sa fin de validité. Vous pouvez le renouveler manuellement si vous le souhaitez (non recommandé).", + "domain_config_cert_summary": "État du certificat", + "domain_config_cert_summary_abouttoexpire": "Le certificat actuel est sur le point d'expirer. Il devrait bientôt être renouvelé automatiquement.", + "domain_config_cert_summary_expired": "ATTENTION : Le certificat actuel n'est pas valide ! HTTPS ne fonctionnera pas du tout !", + "domain_config_cert_summary_letsencrypt": "Bravo ! Vous utilisez un certificat Let's Encrypt valide !", + "domain_config_cert_summary_ok": "Bien, le certificat actuel semble bon !", + "domain_config_cert_summary_selfsigned": "AVERTISSEMENT : Le certificat actuel est auto-signé. Les navigateurs afficheront un avertissement effrayant aux nouveaux visiteurs !", + "domain_config_cert_validity": "Validité", + "global_settings_setting_admin_strength_help": "Ces paramètres ne seront appliqués que lors de l'initialisation ou de la modification du mot de passe", + "global_settings_setting_nginx_compatibility": "Compatibilité NGINX", + "global_settings_setting_root_password": "Nouveau mot de passe root", + "global_settings_setting_ssh_password_authentication": "Authentification par mot de passe", + "global_settings_setting_webadmin_allowlist": "Liste des IP autorisées pour l'administration Web", + "global_settings_setting_webadmin_allowlist_enabled": "Activer la liste des IP autorisées pour l'administration Web", + "invalid_credentials": "Mot de passe ou nom d'utilisateur incorrect", + "log_resource_snippet": "Allocation/retrait/mise à jour d'une ressource", + "log_settings_reset": "Réinitialisation des paramètres", + "log_settings_reset_all": "Réinitialisation de tous les paramètres", + "log_settings_set": "Application des paramètres", + "diagnosis_using_yunohost_testing": "apt (le gestionnaire de paquets du système) est actuellement configuré pour installer toutes les mises à niveau dites 'testing' de votre instance YunoHost.", + "global_settings_setting_smtp_allow_ipv6": "Autoriser l'IPv6", + "password_too_long": "Veuillez choisir un mot de passe de moins de 127 caractères", + "domain_cannot_add_muc_upload": "Vous ne pouvez pas ajouter de domaines commençant par 'muc.'. Ce type de nom est réservé à la fonction de chat XMPP multi-utilisateurs intégrée à YunoHost.", + "group_update_aliases": "Mise à jour des alias du groupe '{group}'", + "group_no_change": "Rien à mettre à jour pour le groupe '{group}'", + "global_settings_setting_portal_theme": "Thème du portail", + "global_settings_setting_portal_theme_help": "Pour plus d'informations sur la création de thèmes de portail personnalisés, voir https://yunohost.org/theming", + "global_settings_setting_passwordless_sudo": "Permettre aux administrateurs d'utiliser 'sudo' sans retaper leur mot de passe", + "app_arch_not_supported": "Cette application ne peut être installée que sur les architectures {required}. L'architecture de votre serveur est {current}", + "app_resource_failed": "L'allocation automatique des ressources (provisioning), la suppression d'accès à ces ressources (déprovisioning) ou la mise à jour des ressources pour {app} a échoué : {error}", + "confirm_app_insufficient_ram": "ATTENTION ! Cette application requiert {required} de RAM pour l'installation/mise à niveau mais il n'y a que {current} de disponible actuellement. Même si cette application pouvait fonctionner, son processus d'installation/mise à niveau nécessite une grande quantité de RAM. Votre serveur pourrait donc geler et planter lamentablement. Si vous êtes prêt à prendre ce risque, tapez '{answers}'", + "app_not_enough_disk": "Cette application nécessite {required} d'espace libre.", + "app_not_enough_ram": "Cette application nécessite {required} de mémoire vive (RAM) pour être installée/mise à niveau mais seule {current} de mémoire est disponible actuellement.", + "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}", + "confirm_notifications_read": "AVERTISSEMENT : Vous devriez vérifier les notifications de l'application susmentionnée avant de continuer, il pourrait y avoir des informations importantes à connaître. [{answers}]", + "invalid_shell": "Shell invalide : {shell}", + "global_settings_setting_dns_exposure": "Suites d'IP à prendre en compte pour la configuration et le diagnostic du DNS", + "global_settings_setting_dns_exposure_help": "NB : Ceci n'affecte que la configuration DNS recommandée et les vérifications de diagnostic. Cela n'affecte pas les configurations du système.", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 devrait généralement être configuré automatiquement par le système ou par votre fournisseur d'accès à internet (FAI) s'il est disponible. Sinon, vous devrez peut-être configurer quelques éléments manuellement, comme expliqué dans la documentation ici : https://yunohost.org/#/ipv6.", + "domain_config_default_app_help": "Les personnes seront automatiquement redirigées vers cette application lorsqu'elles ouvriront ce domaine. Si aucune application n'est spécifiée, les personnes sont redirigées vers le formulaire de connexion du portail utilisateur.", + "domain_config_xmpp_help": "NB : certaines fonctions XMPP nécessiteront la mise à jour de vos enregistrements DNS et la régénération de votre certificat Lets Encrypt pour être activées", + "app_change_url_failed": "Impossible de modifier l'url de {app} : {error}", + "app_change_url_require_full_domain": "{app} ne peut pas être déplacée vers cette nouvelle URL car elle nécessite un domaine complet (c'est-à-dire avec un chemin = /)", + "app_change_url_script_failed": "Une erreur s'est produite dans le script de modification de l'url", + "app_failed_to_upgrade_but_continue": "La mise à jour de l'application {failed_app} a échoué, mais YunoHost va continuer avec les mises à jour suivantes comme demandé. Lancez 'yunohost log show {operation_logger_name}' pour voir le journal des échecs", + "app_not_upgraded_broken_system_continue": "L'application '{failed_app}' n'a pas réussi à se mettre à jour et a mis le système dans un état alternatif car quelque chose est au moins momentanément \"cassé\" (le paramètre --continue-on-failure est donc ignoré). La conséquence est que les mises à jour des applications suivantes ont été annulées : {apps}", + "app_not_upgraded_broken_system": "L'application '{failed_app}' n'a pas réussi à se mettre à jour et a mis le système dans un état de panne. Par conséquent, les mises à niveau des applications suivantes ont été annulées : {apps}", + "apps_failed_to_upgrade": "Ces applications n'ont pas pu être mises à jour : {apps}", + "apps_failed_to_upgrade_line": "\n * {app_id} (pour voir le journal correspondant, faites un 'yunohost log show {operation_logger_name}')", + "app_failed_to_download_asset": "Échec du téléchargement de la ressource '{source_id}' ({url}) pour {app} : {out}", + "app_corrupt_source": "YunoHost a pu télécharger la ressource '{source_id}' ({url}) pour {app}, malheureusement celle-ci ne correspond pas à la somme de contrôle attendue. Cela peut signifier qu'une défaillance temporaire du réseau s'est produite sur votre serveur, OU que la ressource a été modifiée par le mainteneur de l'application en amont (ou un acteur malveillant ?) et que les responsables du paquet de cette application pour YunoHost doivent investiguer et mettre à jour le manifeste de l'application pour refléter ce changement.\n Somme de contrôle sha256 attendue : {expected_sha256}\n Somme de contrôle sha256 téléchargée : {computed_sha256}\n Taille du fichier téléchargé : {size}", + "group_mailalias_add": "L'alias de courrier électronique '{mail}' sera ajouté au groupe '{group}'", + "group_user_add": "L'utilisateur '{user}' sera ajouté au groupe '{group}'", + "group_user_remove": "L'utilisateur '{user}' sera retiré du groupe '{group}'", + "group_mailalias_remove": "L'alias de courrier électronique '{mail}' sera supprimé du groupe '{group}'" } diff --git a/locales/gl.json b/locales/gl.json index 4a77645d6..3aaacd9c9 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -7,9 +7,6 @@ "app_action_broke_system": "Esta acción semella que estragou estos servizos importantes: {services}", "app_action_cannot_be_ran_because_required_services_down": "Estos servizos requeridos deberían estar en execución para realizar esta acción: {services}. Intenta reinicialos para continuar (e tamén intenta saber por que están apagados).", "already_up_to_date": "Nada que facer. Todo está ao día.", - "admin_password_too_long": "Elixe un contrasinal menor de 127 caracteres", - "admin_password_changed": "Realizado o cambio de contrasinal de administración", - "admin_password_change_failed": "Non se puido cambiar o contrasinal", "admin_password": "Contrasinal de administración", "additional_urls_already_removed": "URL adicional '{url}' xa foi eliminada das URL adicionais para o permiso '{permission}'", "additional_urls_already_added": "URL adicional '{url}' xa fora engadida ás URL adicionais para o permiso '{permission}'", @@ -21,7 +18,7 @@ "backup_archive_writing_error": "Non se puideron engadir os ficheiros '{source}' (chamados no arquivo '{dest}' para ser copiados dentro do arquivo comprimido '{archive}'", "backup_archive_system_part_not_available": "A parte do sistema '{part}' non está dispoñible nesta copia", "backup_archive_corrupted": "Semella que o arquivo de copia '{archive}' está estragado : {error}", - "backup_archive_cant_retrieve_info_json": "Non se puido cargar a info desde arquivo '{archive}'... O info.json non s puido obter (ou é un json non válido).", + "backup_archive_cant_retrieve_info_json": "Non se puido cargar a info do arquivo '{archive}'... Non se obtivo o ficheiro info.json (ou é un json non válido).", "backup_archive_open_failed": "Non se puido abrir o arquivo de copia de apoio", "backup_archive_name_unknown": "Arquivo local de copia de apoio descoñecido con nome '{name}'", "backup_archive_name_exists": "Xa existe un arquivo de copia con este nome.", @@ -38,8 +35,6 @@ "ask_new_domain": "Novo dominio", "ask_new_admin_password": "Novo contrasinal de administración", "ask_main_domain": "Dominio principal", - "ask_lastname": "Apelido", - "ask_firstname": "Nome", "ask_user_domain": "Dominio a utilizar como enderezo de email e conta XMPP da usuaria", "apps_catalog_update_success": "O catálogo de aplicacións foi actualizado!", "apps_catalog_obsolete_cache": "A caché do catálogo de apps está baleiro ou obsoleto.", @@ -64,8 +59,7 @@ "app_restore_script_failed": "Houbo un erro interno do script de restablecemento da app", "app_restore_failed": "Non se puido restablecer {app}: {error}", "app_remove_after_failed_install": "Eliminando a app debido ao fallo na instalación...", - "app_requirements_unmeet": "Non se cumpren os requerimentos de {app}, o paquete {pkgname} ({version}) debe ser {spec}", - "app_requirements_checking": "Comprobando os paquetes requeridos por {app}...", + "app_requirements_checking": "Comprobando os requisitos de {app}...", "app_removed": "{app} desinstalada", "app_not_properly_removed": "{app} non se eliminou de xeito correcto", "app_not_installed": "Non se puido atopar {app} na lista de apps instaladas: {all_apps}", @@ -88,7 +82,7 @@ "app_change_url_success": "A URL de {app} agora é {domain}{path}", "app_change_url_no_script": "A app '{app_name}' non soporta o cambio de URL. Pode que debas actualizala.", "app_change_url_identical_domains": "O antigo e o novo dominio/url_path son idénticos ('{domain}{path}'), nada que facer.", - "backup_deleted": "Copia de apoio eliminada", + "backup_deleted": "Copia eliminada: {name}", "backup_delete_error": "Non se eliminou '{path}'", "backup_custom_mount_error": "O método personalizado de copia non superou o paso 'mount'", "backup_custom_backup_error": "O método personalizado da copia non superou o paso 'backup'", @@ -96,7 +90,7 @@ "backup_csv_addition_failed": "Non se engadiron os ficheiros a copiar ao ficheiro CSV", "backup_creation_failed": "Non se puido crear o arquivo de copia de apoio", "backup_create_size_estimation": "O arquivo vai conter arredor de {size} de datos.", - "backup_created": "Copia de apoio creada", + "backup_created": "Copia creada: {name}", "backup_couldnt_bind": "Non se puido ligar {src} a {dest}.", "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo", "backup_cleaning_failed": "Non se puido baleirar o cartafol temporal para a copia", @@ -167,7 +161,7 @@ "diagnosis_ip_weird_resolvconf_details": "O ficheiro /etc/resolv.conf debería ser unha ligazón simbólica a /etc/resolvconf/run/resolv.conf apuntando el mesmo a 127.0.0.1 (dnsmasq). Se queres configurar manualmente a resolución DNS, por favor edita /etc/resolv.dnsmasq.conf.", "diagnosis_ip_weird_resolvconf": "A resolución DNS semella funcionar, mais parecese que estás a utilizar un /etc/resolv.conf personalizado.", "diagnosis_ip_broken_resolvconf": "A resolución de nomes de dominio semella non funcionar no teu servidor, que parece ter relación con que /etc/resolv.conf non sinala a 127.0.0.1.", - "diagnosis_ip_broken_dnsresolution": "A resolución de nomes de dominio semella que por algunha razón non funciona... Pode estar o cortalumes bloqueando as peticións DNS?", + "diagnosis_ip_broken_dnsresolution": "A resolución de nomes de dominio semella que non funciona... Está o cortalumes bloqueando as peticións DNS?", "diagnosis_ip_dnsresolution_working": "A resolución de nomes de dominio está a funcionar!", "diagnosis_ip_not_connected_at_all": "O servidor semella non ter ningún tipo de conexión a internet!?", "diagnosis_ip_local": "IP local: {local}", @@ -214,7 +208,7 @@ "diagnosis_mail_ehlo_bad_answer": "Un servizo non-SMTP respondeu no porto 25 en IPv{ipversion}", "diagnosis_mail_ehlo_unreachable_details": "Non se puido abrir unha conexión no porto 25 do teu servidor en IPv{ipversion}. Non semella accesible.
1. A causa máis habitual é que o porto 25 non está correctamente redirixido no servidor.
2. Asegúrate tamén de que o servizo postfix está a funcionar.
3. En configuracións máis complexas: asegúrate de que o cortalumes ou reverse-proxy non están interferindo.", "diagnosis_mail_fcrdns_nok_details": "Deberías intentar configurar o DNS inverso con {ehlo_domain} na interface do teu rúter de internet ou na interface do teu provedor de hospedaxe. (Algúns provedores de hospedaxe poderían pedirche que lle fagas unha solicitude por escrito para isto).", - "diagnosis_mail_fcrdns_dns_missing": "Non hai DNS inverso definido en IPv{ipversion}. Algúns emails poderían non ser entregrado ou ser marcados como spam.", + "diagnosis_mail_fcrdns_dns_missing": "Non hai DNS inverso definido en IPv{ipversion}. Algúns emails poderían non ser entregados ou ser marcados como spam.", "diagnosis_mail_fcrdns_ok": "O DNS inverso está correctamente configurado!", "diagnosis_mail_ehlo_could_not_diagnose_details": "Erro: {error}", "diagnosis_mail_ehlo_could_not_diagnose": "Non se puido determinar se o servidor de email postfix é accesible desde o exterior en IPv{ipversion}.", @@ -232,7 +226,7 @@ "diagnosis_mail_blacklist_ok": "Os IPs e dominios utilizados neste servidor non parecen estar en listas de bloqueo", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS inverso actual: {rdns_domain}
Valor agardado: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "O DNS inverso non está correctamente configurado para IPv{ipversion}. É posible que non se entreguen algúns emails ou sexan marcados como spam.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Algúns provedores non che permiten configurar DNS inverso (ou podería non funcionar...). Se o teu DNS inverso está correctamente configurado para IPv4, podes intentar desactivar o uso de IPv6 ao enviar os emails executando yunohost settings set smtp.allow_ipv6 -v off. Nota: esta última solución significa que non poderás enviar ou recibir emails desde os poucos servidores que só usan IPv6 que teñen esta limitación.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Algúns provedores non che permiten configurar DNS inverso (ou podería non funcionar...). Se o teu DNS inverso está correctamente configurado para IPv4, podes intentar desactivar o uso de IPv6 ao enviar os emails executando yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Nota: esta última solución significa que non poderás enviar ou recibir emails desde os poucos servidores que só usan IPv6 que teñen esta limitación.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Algúns provedores non che permiten configurar o teu DNS inverso (ou podería non ser funcional...). Se tes problemas debido a isto, considera as seguintes solucións:
- Algúns ISP proporcionan alternativas como usar un repetidor de servidor de correo pero implica que o repetidor pode ver todo o teu tráfico de email.
-Unha alternativa respetuosa coa privacidade é utilizar un VPN *cun IP público dedicado* para evitar estas limitacións. Le https://yunohost.org/#/vpn_advantage
- Ou tamén podes cambiar a un provedor diferente", "diagnosis_http_ok": "O dominio {domain} é accesible a través de HTTP desde o exterior da rede local.", "diagnosis_http_could_not_diagnose_details": "Erro: {error}", @@ -260,8 +254,8 @@ "diagnosis_rootfstotalspace_critical": "O sistema de ficheiros root só ten un total de {space} e podería ser preocupante! Probablemente esgotes o espazo no disco moi pronto! Recomendamos ter un sistema de ficheiros root de polo menos 16 GB.", "diagnosis_rootfstotalspace_warning": "O sistema de ficheiros root só ten un total de {space}. Podería ser suficiente, mais pon tino porque poderías esgotar o espazo no disco rápidamente... Recoméndase ter polo meno 16 GB para o sistema de ficheiros root.", "domain_cannot_remove_main": "Non podes eliminar '{domain}' porque é o dominio principal, primeiro tes que establecer outro dominio como principal usando 'yunohost domain main-domain -n '; aquí tes a lista dos dominios posibles: {other_domains}", - "diagnosis_sshd_config_inconsistent_details": "Executa yunohost settings set security.ssh.port -v O_TEU_PORTO_SSH para definir o porto SSH, comproba con yunohost tools regen-conf ssh --dry-run --with-diff e restablece a configuración con yunohost tools regen-conf ssh --force a configuración recomendada de YunoHost.", - "diagnosis_sshd_config_inconsistent": "Semella que o porto SSH foi modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, un novo axuste global 'security.ssh.port' está dispoñible para evitar a edición manual da configuración.", + "diagnosis_sshd_config_inconsistent_details": "Executa yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT para definir o porto SSH, comproba con yunohost tools regen-conf ssh --dry-run --with-diff e restablece a configuración con yunohost tools regen-conf ssh --force a configuración recomendada de YunoHost.", + "diagnosis_sshd_config_inconsistent": "Semella que o porto SSH foi modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, un novo axuste global 'security.ssh.ssh_port' está dispoñible para evitar a edición manual da configuración.", "diagnosis_sshd_config_insecure": "Semella que a configuración SSH modificouse manualmente, e é insegura porque non contén unha directiva 'AllowGroups' ou 'AllowUsers' para limitar o acceso ás usuarias autorizadas.", "diagnosis_processes_killed_by_oom_reaper": "Algúns procesos foron apagados recentemente polo sistema porque quedou sen memoria dispoñible. Isto acontece normalmente porque o sistema quedou sen memoria ou un proceso consumía demasiada. Resumo cos procesos apagados:\n{kills_summary}", "diagnosis_never_ran_yet": "Semella que o servidor foi configurado recentemente e aínda non hai informes diagnósticos. Deberías iniciar un diagnóstico completo, ben desde a administración web ou usando 'yunohost diagnosis run' desde a liña de comandos.", @@ -274,7 +268,6 @@ "diagnosis_http_connection_error": "Erro de conexión: non se puido conectar co dominio solicitado, moi probablemente non sexa accesible.", "diagnosis_http_timeout": "Caducou a conexión mentras se intentaba contactar o servidor desde o exterior. Non semella accesible.
1. A razón máis habitual é que o porto 80 (e 443) non están correctamente redirixidos ao teu servidor.
2. Deberías comprobar tamén que o servizo nginx está a funcionar
3. En configuracións máis avanzadas: revisa que nin o cortalumes nin o proxy-inverso están interferindo.", "field_invalid": "Campo non válido '{}'", - "experimental_feature": "Aviso: esta característica é experimental e non se considera estable, non deberías utilizala a menos que saibas o que estás a facer.", "extracting": "Extraendo...", "dyndns_unavailable": "O dominio '{domain}' non está dispoñible.", "dyndns_domain_not_provided": "O provedor DynDNS {provider} non pode proporcionar o dominio {domain}.", @@ -288,7 +281,7 @@ "dyndns_ip_update_failed": "Non se actualizou o enderezo IP en DynDNS", "dyndns_could_not_check_available": "Non se comprobou se {domain} está dispoñible en {provider}.", "dpkg_lock_not_available": "Non se pode executar agora mesmo este comando porque semella que outro programa está a utilizar dpkg (o xestos de paquetes do sistema)", - "dpkg_is_broken": "Non podes facer isto agora mesmo porque dpkg/APT (o xestor de paquetes do sistema) semella que non está a funcionar... Podes intentar solucionalo conectándote a través de SSH e executando `sudo apt install --fix-broken`e/ou `sudo dpkg --configure -a`.", + "dpkg_is_broken": "Non podes facer isto agora mesmo porque dpkg/APT (o xestor de paquetes do sistema) semella que non está a funcionar... Podes intentar solucionalo conectándote a través de SSH e executando `sudo apt install --fix-broken`e/ou `sudo dpkg --configure -a` e/ou `sudo dpkg --audit`.", "downloading": "Descargando...", "done": "Feito", "domains_available": "Dominios dispoñibles:", @@ -308,24 +301,7 @@ "domain_cannot_add_xmpp_upload": "Non podes engadir dominios que comecen con 'xmpp-upload.'. Este tipo de nome está reservado para a función se subida de XMPP integrada en YunoHost.", "file_does_not_exist": "O ficheiro {path} non existe.", "firewall_reload_failed": "Non se puido recargar o cortalumes", - "global_settings_setting_smtp_allow_ipv6": "Permitir o uso de IPv6 para recibir e enviar emais", - "global_settings_setting_ssowat_panel_overlay_enabled": "Activar as capas no panel SSOwat", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permitir o uso de DSA hostkey (en desuso) para a configuración do demoño SSH", - "global_settings_unknown_setting_from_settings_file": "Chave descoñecida nos axustes: '{setting_key}', descártaa e gárdaa en /etc/yunohost/settings-unknown.json", - "global_settings_setting_security_ssh_port": "Porto SSH", - "global_settings_setting_security_postfix_compatibility": "Compromiso entre compatibilidade e seguridade para o servidor Postfix. Aféctalle ao cifrado (e outros aspectos da seguridade)", - "global_settings_setting_security_ssh_compatibility": "Compromiso entre compatibilidade e seguridade para o servidor SSH. Aféctalle ao cifrado (e outros aspectos da seguridade)", - "global_settings_setting_security_password_user_strength": "Fortaleza do contrasinal da usuaria", - "global_settings_setting_security_password_admin_strength": "Fortaleza do contrasinal de Admin", - "global_settings_setting_security_nginx_compatibility": "Compromiso entre compatiblidade e seguridade para o servidor NGINX. Afecta ao cifrado (e outros aspectos relacionados coa seguridade)", - "global_settings_setting_pop3_enabled": "Activar protocolo POP3 no servidor de email", - "global_settings_reset_success": "Fíxose copia de apoio dos axustes en {path}", - "global_settings_key_doesnt_exists": "O axuste '{settings_key}' non existe nos axustes globais, podes ver os valores dispoñibles executando 'yunohost settings list'", - "global_settings_cant_write_settings": "Non se gardou o ficheiro de configuración, razón: {reason}", - "global_settings_cant_serialize_settings": "Non se serializaron os datos da configuración, razón: {reason}", - "global_settings_cant_open_settings": "Non se puido abrir o ficheiro de axustes, razón: {reason}", - "global_settings_bad_type_for_setting": "Tipo incorrecto do axuste {setting}, recibido {received_type}, agardábase {expected_type}", - "global_settings_bad_choice_for_enum": "Elección incorrecta para o axuste {setting}, recibido '{choice}', mais as opcións dispoñibles son: {available_choices}", + "global_settings_setting_ssowat_panel_overlay_enabled": "Activar o pequeno atallo cadrado ao portal 'YunoHost' nas apps", "firewall_rules_cmd_failed": "Fallou algún comando das regras do cortalumes. Máis info no rexistro.", "firewall_reloaded": "Recargouse o cortalumes", "group_creation_failed": "Non se puido crear o grupo '{group}': {error}", @@ -335,12 +311,9 @@ "group_already_exist": "Xa existe o grupo {group}", "good_practices_about_user_password": "Vas definir o novo contrasinal de usuaria. O contrasinal debe ter 8 caracteres como mínimo—aínda que se recomenda utilizar un máis longo (ex. unha frase de paso) e/ou utilizar caracteres variados (maiúsculas, minúsculas, números e caracteres especiais).", "good_practices_about_admin_password": "Vas definir o novo contrasinal de administración. O contrasinal debe ter 8 caracteres como mínimo—aínda que se recomenda utilizar un máis longo (ex. unha frase de paso) e/ou utilizar caracteres variados (maiúsculas, minúsculas, números e caracteres especiais).", - "global_settings_unknown_type": "Situación non agardada, o axuste {setting} semella ter o tipo {unknown_type} pero non é un valor soportado polo sistema.", - "global_settings_setting_backup_compress_tar_archives": "Ao crear novas copias de apoio, comprime os arquivos (.tar.gz) en lugar de non facelo (.tar). Nota: activando esta opción creas arquivos máis lixeiros, mais o procedemento da primeira copia será significativamente máis longo e esixente coa CPU.", - "global_settings_setting_smtp_relay_password": "Contrasinal no repetidor SMTP", - "global_settings_setting_smtp_relay_user": "Conta de usuaria no repetidor SMTP", + "global_settings_setting_smtp_relay_password": "Contrasinal do repetidor SMTP", + "global_settings_setting_smtp_relay_user": "Usuaria no repetidor SMTP", "global_settings_setting_smtp_relay_port": "Porto do repetidor SMTP", - "global_settings_setting_smtp_relay_host": "Servidor repetidor SMTP para enviar emails no lugar da túa instancia yunohost. É útil se estás nunha destas situacións: o teu porto 25 está bloqueado polo teu provedor ISP u VPN, se tes unha IP residencial nunha lista DUHL, se non podes configurar DNS inversa ou se este servidor non ten conexión directa a internet e queres utilizar outro para enviar os emails.", "group_updated": "Grupo '{group}' actualizado", "group_unknown": "Grupo descoñecido '{group}'", "group_deletion_failed": "Non se eliminou o grupo '{group}': {error}", @@ -349,8 +322,6 @@ "group_cannot_edit_primary_group": "O grupo '{group}' non se pode editar manualmente. É o grupo primario que contén só a unha usuaria concreta.", "group_cannot_edit_visitors": "O grupo 'visitors' non se pode editar manualmente. É un grupo especial que representa a tódas visitantes anónimas", "group_cannot_edit_all_users": "O grupo 'all_users' non se pode editar manualmente. É un grupo especial que contén tódalas usuarias rexistradas en YunoHost", - "global_settings_setting_security_webadmin_allowlist": "Enderezos IP con permiso para acceder á webadmin. Separados por vírgulas.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Permitir que só algúns IPs accedan á webadmin.", "disk_space_not_sufficient_update": "Non hai espazo suficiente no disco para actualizar esta aplicación", "disk_space_not_sufficient_install": "Non queda espazo suficiente no disco para instalar esta aplicación", "log_help_to_get_log": "Para ver o rexistro completo da operación '{desc}', usa o comando 'yunohost log show {name}'", @@ -376,7 +347,6 @@ "log_domain_remove": "Eliminar o dominio '{}' da configuración do sistema", "log_domain_add": "Engadir dominio '{}' á configuración do sistema", "log_remove_on_failed_install": "Eliminar '{}' tras unha instalación fallida", - "log_remove_on_failed_restore": "Eliminar '{}' tras un intento fallido de restablecemento desde copia", "log_backup_restore_app": "Restablecer '{}' desde unha copia de apoio", "log_backup_restore_system": "Restablecer o sistema desde unha copia de apoio", "log_backup_create": "Crear copia de apoio", @@ -397,7 +367,7 @@ "migration_ldap_backup_before_migration": "Crear copia de apoio da base de datos LDAP e axustes de apps antes de realizar a migración.", "main_domain_changed": "Foi cambiado o dominio principal", "main_domain_change_failed": "Non se pode cambiar o dominio principal", - "mail_unavailable": "Este enderezo de email está reservado e debería adxudicarse automáticamente á primeira usuaria", + "mail_unavailable": "Este enderezo de email está reservado para o grupo de admins", "mailbox_used_space_dovecot_down": "O servizo de caixa de correo Dovecot ten que estar activo se queres obter o espazo utilizado polo correo", "mailbox_disabled": "Desactivado email para usuaria {user}", "mail_forward_remove_failed": "Non se eliminou o reenvío de email '{mail}'", @@ -437,8 +407,8 @@ "pattern_port_or_range": "Debe ser un número válido de porto (entre 0-65535) ou rango de portos (ex. 100:200)", "pattern_password": "Ten que ter polo menos 3 caracteres", "pattern_mailbox_quota": "Ten que ser un tamaño co sufixo b/k/M/G/T ou 0 para non ter unha cota", - "pattern_lastname": "Ten que ser un apelido válido", - "pattern_firstname": "Ten que ser un nome válido", + "pattern_lastname": "Ten que ser un apelido válido (min. 3 caract.)", + "pattern_firstname": "Ten que ser un nome válido (min. 3 caract.)", "pattern_email": "Ten que ser un enderezo de email válido, sen o símbolo '+' (ex. persoa@exemplo.com)", "pattern_email_forward": "Ten que ser un enderezo de email válido, está aceptado o símbolo '+' (ex. persoa+etiqueta@exemplo.com)", "pattern_domain": "Ten que ser un nome de dominio válido (ex. dominiopropio.org)", @@ -454,8 +424,8 @@ "migrations_success_forward": "Migración {id} completada", "migrations_skip_migration": "Omitindo migración {id}...", "migrations_running_forward": "Realizando migración {id}...", - "migrations_pending_cant_rerun": "Esas migracións están pendentes, polo que non ser executadas outra vez: {ids}", - "migrations_not_pending_cant_skip": "Esas migracións non están pendentes, polo que non poden ser omitidas: {ids}", + "migrations_pending_cant_rerun": "Estas migracións están pendentes, polo que non ser realizadas outra vez: {ids}", + "migrations_not_pending_cant_skip": "Estas migracións non están pendentes, polo que non poden ser omitidas: {ids}", "migrations_no_such_migration": "Non hai migración co nome '{id}'", "migrations_no_migrations_to_run": "Sen migracións a executar", "migrations_need_to_accept_disclaimer": "Para executar a migración {id}, tes que aceptar o seguinte aviso:\n---\n{disclaimer}\n---\nSe aceptas executar a migración, por favor volve a executar o comando coa opción '--accept-disclaimer'.", @@ -530,7 +500,6 @@ "server_reboot": "Vaise reiniciar o servidor", "server_shutdown_confirm": "Queres apagar o servidor inmediatamente? [{answers}]", "server_shutdown": "Vaise apagar o servidor", - "root_password_replaced_by_admin_password": "O contrasinal root foi substituído polo teu contrasinal de administración.", "root_password_desynchronized": "Mudou o contrasinal de administración, pero YunoHost non puido transferir este cambio ao contrasinal root!", "restore_system_part_failed": "Non se restableceu a parte do sistema '{part}'", "restore_running_hooks": "Executando os ganchos do restablecemento...", @@ -540,11 +509,9 @@ "restore_not_enough_disk_space": "Non hai espazo abondo (espazo: {free_space.d} B, espazo necesario: {needed_space} B, marxe de seguridade: {margin} B)", "restore_may_be_not_enough_disk_space": "O teu sistema semella que non ten espazo abondo (libre: {free_space} B, espazo necesario: {needed_space} B, marxe de seguridade {margin} B)", "restore_hook_unavailable": "O script de restablecemento para '{part}' non está dispoñible no teu sistema nin no arquivo", - "invalid_password": "Contrasinal non válido", "ldap_server_is_down_restart_it": "O servidor LDAP está caído, intenta reinicialo...", "ldap_server_down": "Non se chegou ao servidor LDAP", - "global_settings_setting_security_experimental_enabled": "Activar características de seguridade experimentais (non actives isto se non sabes o que estás a facer!)", - "yunohost_postinstall_end_tip": "Post-install completada! Para rematar a configuración considera:\n- engadir unha primeira usuaria na sección 'Usuarias' na webadmin (ou 'yunohost user create ' na liña de comandos);\n- diagnosticar potenciais problemas na sección 'Diagnóstico' na webadmin (ou 'yunohost diagnosis run' na liña de comandos);\n- ler 'Rematando a configuración' e 'Coñece YunoHost' na documentación da administración: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "Post-install completada! Para rematar a configuración considera:\n- diagnosticar potenciais problemas na sección 'Diagnóstico' na webadmin (ou 'yunohost diagnosis run' na liña de comandos);\n- ler 'Rematando a configuración' e 'Coñece YunoHost' na documentación da administración: https://yunohost.org/admindoc.", "yunohost_not_installed": "YunoHost non está instalado correctamente. Executa 'yunohost tools postinstall'", "yunohost_installing": "Instalando YunoHost...", "yunohost_configured": "YunoHost está configurado", @@ -592,14 +559,13 @@ "service_enabled": "O servizo '{service}' vai ser iniciado automáticamente no inicio do sistema.", "diagnosis_apps_allgood": "Tódalas apps instaladas respectan as prácticas básicas de empaquetado", "diagnosis_apps_bad_quality": "Esta aplicación está actualmente marcada como estragada no catálogo de aplicacións de YunoHost. Podería ser un problema temporal mentras as mantedoras intentan arranxar o problema. Ata ese momento a actualización desta app está desactivada.", - "global_settings_setting_security_nginx_redirect_to_https": "Redirixir peticións HTTP a HTTPs por defecto (NON DESACTIVAR ISTO a non ser que realmente saibas o que fas!)", "log_user_import": "Importar usuarias", "user_import_failed": "A operación de importación de usuarias fracasou", "user_import_missing_columns": "Faltan as seguintes columnas: {columns}", "user_import_nothing_to_do": "Ningunha usuaria precisa ser importada", "user_import_partial_failed": "A operación de importación de usuarias fallou parcialmente", "diagnosis_apps_deprecated_practices": "A versión instalada desta app aínda utiliza algunha das antigas prácticas de empaquetado xa abandonadas. Deberías considerar actualizala.", - "diagnosis_apps_outdated_ynh_requirement": "A versión instalada desta app só require yunohost >= 2.x, que normalmente indica que non está ao día coas prácticas recomendadas de empaquetado e asistentes. Deberías considerar actualizala.", + "diagnosis_apps_outdated_ynh_requirement": "A versión instalada desta app só require yunohost >= 2.x ou 3.x, esto normalmente indica que non está ao día coas prácticas recomendadas de empaquetado e asistentes. Deberías considerar actualizala.", "user_import_success": "Usuarias importadas correctamente", "diagnosis_high_number_auth_failures": "Hai un alto número sospeitoso de intentos fallidos de autenticación. Deberías comprobar que fail2ban está a executarse e que está correctamente configurado, ou utiliza un porto personalizado para SSH tal como se explica en https://yunohost.org/security.", "user_import_bad_file": "O ficheiro CSV non ten o formato correcto e será ignorado para evitar unha potencial perda de datos", @@ -623,7 +589,6 @@ "log_app_config_set": "Aplicar a configuración á app '{}'", "app_config_unable_to_apply": "Fallou a aplicación dos valores de configuración.", "config_cant_set_value_on_section": "Non podes establecer un valor único na sección completa de configuración.", - "config_version_not_supported": "A versión do panel de configuración '{version}' non está soportada.", "invalid_number_max": "Ten que ser menor de {max}", "service_not_reloading_because_conf_broken": "Non se recargou/reiniciou o servizo '{name}' porque a súa configuración está estragada: {errors}", "diagnosis_http_special_use_tld": "O dominio {domain} baséase nun dominio de alto-nivel (TLD) especial como .local ou .test e por isto non é de agardar que esté exposto fóra da rede local.", @@ -637,7 +602,6 @@ "domain_dns_push_record_failed": "Fallou {action} do rexistro {type}/{name}: {error}", "domain_dns_push_success": "Rexistros DNS actualizados!", "domain_dns_push_failed": "Fallou completamente a actualización dos rexistros DNS.", - "domain_config_features_disclaimer": "Ata o momento, activar/desactivar as funcións de email ou XMPP só ten impacto na configuración automática da configuración DNS, non na configuración do sistema!", "domain_config_mail_in": "Emails entrantes", "domain_config_mail_out": "Emails saíntes", "domain_config_xmpp": "Mensaxería instantánea (XMPP)", @@ -666,7 +630,6 @@ "migration_0021_main_upgrade": "Iniciando a actualización principal...", "migration_0021_still_on_buster_after_main_upgrade": "Algo fallou durante a actualización principal, o sistema semlla que aínda está en Debian Buster", "migration_0021_yunohost_upgrade": "Iniciando actualización compoñente core de YunoHost...", - "migration_0021_not_buster": "A distribución Debian actual non é Buster!", "migration_0021_not_enough_free_space": "Queda pouco espazo en /var/! Deberías ter polo menos 1GB libre para facer a migración.", "migration_0021_problematic_apps_warning": "Detectamos que están instaladas estas app que poderían ser problemáticas. Semella que non foron instaladas desde o catálogo YunoHost, ou non están marcadas como que 'funcionan'. Así, non podemos garantir que seguiran funcionando ben tras a migración: {problematic_apps}", "migration_0021_modified_files": "Ten en conta que os seguintes ficheiros semella que foron editados manualmente e poderían ser sobrescritos durante a migración: {manually_modified_files}", @@ -675,7 +638,6 @@ "migration_description_0021_migrate_to_bullseye": "Actualizar o sistema a Debian Bullseye e YunoHost 11.x", "migration_0021_system_not_fully_up_to_date": "O teu sistema non está completamente actualizado. Fai unha actualización normal antes de executar a migración a Bullseye.", "migration_0021_general_warning": "Ten en conta que a migración é unha operación delicada. O equipo de YunoHost fixo todo o que puido para revisalo e probalo, pero aínda así poderían acontecer fallos no sistema ou apps.\n\nAsí as cousas, é recomendable:\n - Facer unha copia de apoio dos datos e apps importantes. Máis info en https://yunohost.org/backup;\n - Ter paciencia unha vez inicias a migración: dependendo da túa conexión a internet e hardware, podería levarlle varias horas completar o proceso.", - "global_settings_setting_security_ssh_password_authentication": "Permitir autenticación con contrasinal para SSH", "tools_upgrade_failed": "Non se actualizaron os paquetes: {packages_list}", "migration_0023_not_enough_space": "Crear espazo suficiente en {path} para realizar a migración.", "migration_0023_postgresql_11_not_installed": "PostgreSQL non estaba instalado no sistema. Nada que facer.", @@ -684,5 +646,125 @@ "migration_description_0023_postgresql_11_to_13": "Migrar bases de datos de PostgreSQL 11 a 13", "service_description_postgresql": "Almacena datos da app (Base datos SQL)", "tools_upgrade": "Actualizando paquetes do sistema", - "domain_config_default_app": "App por defecto" -} \ No newline at end of file + "domain_config_default_app": "App por defecto", + "global_settings_setting_backup_compress_tar_archives_help": "Ao crear novas copias de apoio, comprime os arquivos (.tar.gz) en lugar de non facelo (.tar). Nota: activando esta opción creas arquivos máis lixeiros, mais o procedemento da primeira copia será significativamente máis longo e esixente coa CPU.", + "global_settings_setting_security_experimental_enabled_help": "Activar características de seguridade experimentais (non actives isto se non sabes o que estás a facer!)", + "global_settings_setting_nginx_compatibility_help": "Compromiso entre compatiblidade e seguridade para o servidor NGINX. Afecta ao cifrado (e outros aspectos relacionados coa seguridade)", + "global_settings_setting_nginx_redirect_to_https_help": "Redirixir peticións HTTP a HTTPs por defecto (NON DESACTIVAR ISTO a non ser que realmente saibas o que fas!)", + "global_settings_setting_admin_strength": "Fortaleza do contrasinal de Admin", + "global_settings_setting_user_strength": "Fortaleza do contrasinal da usuaria", + "global_settings_setting_postfix_compatibility_help": "Compromiso entre compatibilidade e seguridade para o servidor Postfix. Aféctalle ao cifrado (e outros aspectos da seguridade)", + "global_settings_setting_ssh_compatibility_help": "Compromiso entre compatibilidade e seguridade para o servidor SSH. Aféctalle ao cifrado (e outros aspectos da seguridade)", + "global_settings_setting_ssh_password_authentication_help": "Permitir autenticación con contrasinal para SSH", + "global_settings_setting_ssh_port": "Porto SSH", + "global_settings_setting_webadmin_allowlist_help": "Enderezos IP con permiso para acceder á webadmin. Separados por vírgulas.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permitir que só algúns IPs accedan á webadmin.", + "global_settings_setting_smtp_allow_ipv6_help": "Permitir o uso de IPv6 para recibir e enviar emais", + "global_settings_setting_smtp_relay_enabled_help": "Servidor repetidor SMTP para enviar emails no lugar da túa instancia yunohost. É útil se estás nunha destas situacións: o teu porto 25 está bloqueado polo teu provedor ISP u VPN, se tes unha IP residencial nunha lista DUHL, se non podes configurar DNS inversa ou se este servidor non ten conexión directa a internet e queres utilizar outro para enviar os emails.", + "migration_0024_rebuild_python_venv_broken_app": "Omitimos a app {app} porque virtualenv non se pode reconstruir para esta app. Deberías intentar resolver o problema forzando a actualización da app usando `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_disclaimer_base": "Após a actualización a Debian Bullseye, algunhas aplicacións de Python precisan ser reconstruídas para usar a nova versión de Python que inclúe Debian (técnicamente: recrear o `virtualenv`). Mentras tanto, algunhas aplicacións de Python poderían non funcionar. YunoHost pode intentar reconstruir o virtualenv para algunhas, como se indica abaixo. Para outras, ou se falla a reconstrución, pode que teñas que forzar a actualización desas apps.", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Vaise intentar a reconstrución de virtualenv para as seguintes apps (Nota: a operación podería tomar algún tempo!): {rebuild_apps}", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Non se puido reconstruir virtualenv para estas apps. Precisas forzar a súa actualización, pódelo facer desde a liña de comandos con: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_0024_rebuild_python_venv_in_progress": "Intentando reconstruir o Python virtualenv para `{app}`", + "migration_description_0024_rebuild_python_venv": "Reparar app Python após a migración a bullseye", + "migration_0024_rebuild_python_venv_failed": "Fallou a reconstrución de Python virtualenv para {app}. A app podería non funcionar mentras non se resolve. Deberías intentar arranxar a situación forzando a actualización desta app usando `yunohost app upgrade --force {app}`.", + "migration_0021_not_buster2": "A distribución actual Debian non é Buster! Se xa realizaches a migración Buster->Bullseye entón este erro indica que o proceso de migración non se realizou de xeito correcto ao 100% (se non YunoHost debería telo marcado como completado). É recomendable comprobar xunto co equipo de axuda o que aconteceu, necesitarán o rexistro **completo** da `migración`, que podes atopar na webadmin en Ferramentas > Rexistros.", + "global_settings_setting_admin_strength_help": "Estos requerimentos só se esixen ao inicializar ou cambiar o contrasinal", + "global_settings_setting_root_access_explain": "En sistemas Linux, 'root' é a administradora absoluta. No contexto YunoHost, o acceso SSH de 'root' está desactivado por defecto - excepto na rede local do servidor. Os compoñentes do grupo 'admins' poden utilizar o comando sudo para actuar como root desde a liña de comandos. É conveniente ter un contrasinal (forte) para root para xestionar o sistema por se as persoas administradoras perden o acceso por algún motivo.", + "migration_description_0025_global_settings_to_configpanel": "Migrar o nome antigo dos axustes globais aos novos nomes modernos", + "global_settings_reset_success": "Restablecer axustes globais", + "domain_config_acme_eligible": "Elixibilidade ACME", + "domain_config_acme_eligible_explain": "Este dominio non semella estar preparado para un certificado Let's Encrypt. Comproba a configuración DNS e que é accesible por HTTP. A sección 'Rexistros DNS' e 'Web' na páxina de diagnóstico pode axudarche a entender o que está a fallar.", + "domain_config_cert_install": "Instalar certificado Let's Encrypt", + "domain_config_cert_issuer": "Autoridade certificadora", + "domain_config_cert_no_checks": "Ignorar comprobacións de diagnóstico", + "domain_config_cert_renew": "Anovar certificado Let's Encrypt", + "domain_config_cert_renew_help": "O certificado anovarase automáticamente nos últimos 15 días de validez. Podes anovalo automáticamente se queres. (Non é recomendable).", + "domain_config_cert_summary": "Estado do certificado", + "domain_config_cert_summary_abouttoexpire": "O certificado actual vai caducar. Debería anovarse automáticamente..", + "domain_config_cert_summary_expired": "CRÍTICO: O certificado actual non é válido! HTTPS non funcionará!!", + "domain_config_cert_summary_letsencrypt": "Ben! Estás a usar un certificado Let's Encrypt válido!", + "domain_config_cert_summary_ok": "Correcto, o certificado ten boa pinta!", + "domain_config_cert_summary_selfsigned": "AVISO: O certificado actual está auto-asinado. Os navegadores van mostrar un aviso que mete medo a quen te visite!", + "domain_config_cert_validity": "Validez", + "global_settings_setting_ssh_password_authentication": "Autenticación con contrasinal", + "global_settings_setting_user_strength_help": "Estos requerimentos só se esixen ao inicializar ou cambiar o contrasinal", + "global_settings_setting_webadmin_allowlist": "Lista IP autorizados para Webadmin", + "global_settings_setting_webadmin_allowlist_enabled": "Activar a lista de IP autorizados", + "invalid_credentials": "Credenciais non válidas", + "log_settings_reset": "Restablecer axuste", + "log_settings_reset_all": "Restablecer tódolos axustes", + "log_settings_set": "Aplicar axustes", + "admins": "Admins", + "all_users": "Tódalas usuarias de YunoHost", + "app_action_failed": "Fallou a execución da acción {action} da app {app}", + "app_manifest_install_ask_init_admin_permission": "Quen debería ter acceso de administración a esta app? (Pode cambiarse despois)", + "app_manifest_install_ask_init_main_permission": "Quen debería ter acceso a esta app? (Pode cambiarse despois)", + "ask_admin_fullname": "Nome completo de Admin", + "ask_admin_username": "Identificador da Admin", + "ask_fullname": "Nome completo", + "certmanager_cert_install_failed": "Fallou a instalación do certificado Let's Encrypt para {domains}", + "certmanager_cert_install_failed_selfsigned": "Fallou a instalación do certificado auto-asinado para {domains}", + "certmanager_cert_renew_failed": "Fallou a renovación do certificado Let's Encrypt para {domains}", + "password_confirmation_not_the_same": "Non concordan os contrasinais escritos", + "password_too_long": "Elixe un contrasinal menor de 127 caracteres", + "pattern_fullname": "Ten que ser un nome completo válido (min. 3 caract.)", + "registrar_infos": "Info da rexistradora", + "root_password_changed": "cambiouse o contrasinal de root", + "visitors": "Visitantes", + "global_settings_setting_security_experimental_enabled": "Ferramentas experimentais de seguridade", + "diagnosis_using_stable_codename": "apt (o xestor de paquetes do sistema) está configurado para instalar paquetes co nome de código 'stable', no lugar do nome de código da versión actual de Debian (bullseye).", + "diagnosis_using_stable_codename_details": "Normalmente esto é debido a unha configuración incorrecta do teu provedor de hospedaxe. Esto é perigoso, porque tan pronto como a nova versión de Debian se convirta en 'stable', apt vai querer actualizar tódolos paquetes do sistema se realizar o procedemento de migración requerido. É recomendable arranxar isto editando a fonte de apt ao repositorio base de Debian, e substituir a palabra stable por bullseye. O ficheiro de configuración correspondente debería ser /etc/sources.list, ou ficheiro dentro de /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing": "apt (o xestor de paquetes do sistema) está configurado actualmente para instalar calquera actualización 'testing' para o núcleo YunoHost.", + "diagnosis_using_yunohost_testing_details": "Isto probablemente sexa correcto se sabes o que estás a facer, pero pon coidado e le as notas de publicación antes de realizar actualizacións de YunoHost! Se queres desactivar as actualizacións 'testing', deberías eliminar a palabra testing de /etc/apt/sources.list.d/yunohost.list.", + "global_settings_setting_backup_compress_tar_archives": "Comprimir copias de apoio", + "global_settings_setting_pop3_enabled": "Activar POP3", + "global_settings_setting_smtp_allow_ipv6": "Permitir IPv6", + "global_settings_setting_smtp_relay_host": "Sevidor repetidor SMTP", + "config_action_disabled": "Non se executou a accción '{action}' porque está desactivada, comproba os seus requerimentos. Axuda: {help}", + "config_action_failed": "Fallou a execución da acción '{action}': {error}", + "config_forbidden_readonly_type": "O tipo '{type}' non pode establecerse como só lectura, usa outro tipo para mostrar este valor (id relevante: '{id}').", + "global_settings_setting_nginx_compatibility": "Compatibilidade NGINX", + "global_settings_setting_nginx_redirect_to_https": "Forzar HTTPS", + "global_settings_setting_pop3_enabled_help": "Activar o protocolo POP3 no servidor de email", + "global_settings_setting_postfix_compatibility": "Compatibilidade Postfix", + "global_settings_setting_root_password": "Novo contrasinal root", + "global_settings_setting_root_password_confirm": "Novo contrasinal root (confirmar)", + "global_settings_setting_smtp_relay_enabled": "Activar repetidor SMTP", + "global_settings_setting_ssh_compatibility": "Compatibilidade SSH", + "migration_description_0026_new_admins_group": "Migrar ao novo sistema de 'admins múltiples'", + "group_update_aliases": "Actualizando os alias do grupo '{group}'", + "group_no_change": "Nada que cambiar para o grupo '{group}'", + "domain_cannot_add_muc_upload": "Non podes engadir dominios que comecen por 'muc.'. Este tipo de dominio está reservado para as salas de conversa de XMPP integradas en YunoHost.", + "global_settings_setting_passwordless_sudo": "Permitir a Admins usar 'sudo' sen ter que volver a escribir o contrasinal", + "global_settings_setting_portal_theme": "Decorado do Portal", + "global_settings_setting_portal_theme_help": "Tes máis info acerca da creación de decorados para o portal de acceso en https://yunohost.org/theming", + "app_arch_not_supported": "Esta app só pode ser instalada e arquitecturas {required} pero a arquitectura do teu servidor é {current}", + "app_not_enough_disk": "Esta app precisa {required} de espazo libre.", + "app_yunohost_version_not_supported": "Esta app require YunoHost >= {required} pero a versión actual instalada é {current}", + "confirm_app_insufficient_ram": "PERIGO! Esta app precisa {required} de RAM para instalar/actualizar pero só hai {current} dispoñibles. Incluso se a app funcionase, o seu proceso de instalación/actualización require gran cantidade de RAM e o teu servidor podería colgarse e fallar. Se queres asumir o risco, escribe '{answers}'", + "confirm_notifications_read": "AVISO: Deberías comprobar as notificacións da app antes de continuar, poderías ter información importante que revisar. [{answers}]", + "app_not_enough_ram": "Esta app require {required} de RAM para instalar/actualizar pero só hai {current} dispoñible.", + "global_settings_setting_dns_exposure": "Versións de IP a ter en conta para a configuración DNS e diagnóstico", + "global_settings_setting_dns_exposure_help": "Nota: Esto só lle afecta á configuración DNS recomendada e diagnóstico do sistema. Non lle afecta aos axustes do sistema.", + "diagnosis_ip_no_ipv6_tip_important": "Se está dispoñible, IPv6 debería estar automáticamente configurado polo sistema ou o teu provedor. Se non, pode que teñas que facer algúns axustes manualmente tal como se explica na documentación: https://yunohost.org/#/ipv6.", + "domain_config_default_app_help": "As persoas serán automáticamente redirixidas a esta app ao abrir o dominio. Se non se indica ningunha, serán redirixidas ao formulario de acceso no portal de usuarias.", + "domain_config_xmpp_help": "Nota: algunhas características de XMPP para ser utilizadas precisan que teñas ao día os rexistros DNS e rexeneres os certificados Lets Encrypt", + "app_change_url_failed": "Non se cambiou o url para {app}: {error}", + "app_change_url_require_full_domain": "{app} non se pode mover a este novo URL porque require un dominio completo propio (ex. con ruta = /)", + "app_change_url_script_failed": "Algo fallou ao executar o script de cambio de url", + "apps_failed_to_upgrade_line": "\n * {app_id} (para ver o rexistro correspondente executa 'yunohost log show {operation_logger_name}')", + "app_failed_to_upgrade_but_continue": "Fallou a actualización de {failed_app}, seguimos coas demáis actualizacións. Executa 'yunohost log show {operation_logger_name}' para ver o rexistro do fallo", + "app_not_upgraded_broken_system": "Fallou a actualización de '{failed_app}' e estragou o sistema, como consecuencia cancelouse a actualización das seguintes apps: {apps}", + "app_not_upgraded_broken_system_continue": "Fallou a actualización de '{failed_app}' e estragou o sistema (polo que ignórase --continue-on-failure), como consecuencia cancelouse a actualización das seguintes apps: {apps}", + "apps_failed_to_upgrade": "Fallou a actualización das seguintes aplicacións:{apps}", + "invalid_shell": "Intérprete de ordes non válido: {shell}", + "log_resource_snippet": "Aprovisionamento/desaprovisionamento/actualización dun recurso", + "app_resource_failed": "Fallou o aprovisionamento, desaprovisionamento ou actualización de recursos para {app}: {error}", + "app_failed_to_download_asset": "Fallou a descarga do recurso '{source_id}' ({url}) para {app}: {out}", + "app_corrupt_source": "YunoHost foi quen de descargar o recurso '{source_id}' ({url}) para {app}, pero a suma de comprobación para o recurso non concorda. Pode significar que houbo un fallo temporal na conexión do servidor á rede, OU que o recurso sufreu, dalgún xeito, cambios desde que os desenvolvedores orixinais (ou unha terceira parte maliciosa?), o equipo de YunoHost ten que investigar e actualizar o manifesto da app para mostrar este cambio.\n Suma sha256 agardada: {expected_sha256} \n Suma sha256 do descargado: {computed_sha256}\n Tamaño do ficheiro: {size}", + "group_mailalias_add": "Vaise engadir o alias de correo '{mail}' ao grupo '{group}'", + "group_mailalias_remove": "Vaise quitar o alias de email '{mail}' do grupo '{group}'", + "group_user_add": "Vaise engadir a '{user}' ao grupo '{grupo}'", + "group_user_remove": "Vaise quitar a '{user}' do grupo '{grupo}'" +} diff --git a/locales/he.json b/locales/he.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/locales/he.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/locales/hi.json b/locales/hi.json index 1eed9faa4..6f40ad1ae 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -1,8 +1,6 @@ { "action_invalid": "अवैध कार्रवाई '{action}'", "admin_password": "व्यवस्थापक पासवर्ड", - "admin_password_change_failed": "पासवर्ड बदलने में असमर्थ", - "admin_password_changed": "व्यवस्थापक पासवर्ड बदल दिया गया है", "app_already_installed": "'{app}' पहले से ही इंस्टाल्ड है", "app_argument_choice_invalid": "गलत तर्क का चयन किया गया '{name}' , तर्क इन विकल्पों में से होने चाहिए {choices}", "app_argument_invalid": "तर्क के लिए अमान्य मान '{name}': {error}", @@ -15,14 +13,11 @@ "app_not_properly_removed": "{app} ठीक ढंग से नहीं अनइन्सटॉल की गई", "app_removed": "{app} को अनइन्सटॉल कर दिया गया", "app_requirements_checking": "जरूरी पैकेजेज़ की जाँच हो रही है ....", - "app_requirements_unmeet": "आवश्यकताए पूरी नहीं हो सकी, पैकेज {pkgname}({version})यह होना चाहिए {spec}", "app_sources_fetch_failed": "सोर्स फाइल्स प्राप्त करने में असमर्थ", "app_unknown": "अनजान एप्लीकेशन", "app_unsupported_remote_type": "एप्लीकेशन के लिए उन्सुपपोर्टेड रिमोट टाइप इस्तेमाल किया गया", "app_upgrade_failed": "{app} अपडेट करने में असमर्थ", "app_upgraded": "{app} अपडेट हो गयी हैं", - "ask_firstname": "नाम", - "ask_lastname": "अंतिम नाम", "ask_main_domain": "मुख्य डोमेन", "ask_new_admin_password": "नया व्यवस्थापक पासवर्ड", "ask_password": "पासवर्ड", diff --git a/locales/hu.json b/locales/hu.json index 9c482a370..d45a95dc3 100644 --- a/locales/hu.json +++ b/locales/hu.json @@ -2,8 +2,6 @@ "aborting": "Megszakítás.", "action_invalid": "Érvénytelen művelet '{action}'", "admin_password": "Adminisztrátori jelszó", - "admin_password_change_failed": "Nem lehet a jelszót megváltoztatni", - "admin_password_changed": "Az adminisztrátori jelszó megváltozott", "app_already_installed": "{app} már telepítve van", "app_already_installed_cant_change_url": "Ez az app már telepítve van. Ezzel a funkcióval az url nem változtatható. Javaslat 'app url változtatás' ha lehetséges.", "app_already_up_to_date": "{app} napra kész", diff --git a/locales/id.json b/locales/id.json index 47aff8b2e..c6b023102 100644 --- a/locales/id.json +++ b/locales/id.json @@ -1,34 +1,29 @@ { "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_identical_domains": "Domain/url_path yang lama dan baru identik ('{domain}{path}'), tak ada yang perlu dilakukan.", "app_change_url_no_script": "Aplikasi '{app_name}' belum mendukung pengubahan URL. Mungkin Anda harus memperbaruinya.", "app_change_url_success": "URL {app} sekarang adalah {domain}{path}", - "app_id_invalid": "ID aplikasi tidak valid", + "app_id_invalid": "ID aplikasi tidak sah", "app_install_failed": "Tidak dapat memasang {app}: {error}", - "app_install_files_invalid": "Berkas-berkas ini tidak dapat dipasang", - "app_install_script_failed": "Sebuah kesalahan terjadi pada script pemasangan aplikasi", + "app_install_files_invalid": "Berkas ini tidak dapat dipasang", + "app_install_script_failed": "Sebuah kesalahan terjadi pada skrip 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_not_properly_removed": "{app} belum dilepas dengan benar", + "app_remove_after_failed_install": "Melepas aplikasi setelah kegagalan pemasangan...", + "app_removed": "{app} dilepas", "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", @@ -40,19 +35,361 @@ "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_start_remove": "Melepas {app}...", "app_manifest_install_ask_password": "Pilih kata sandi administrasi untuk aplikasi ini", - "app_upgrade_several_apps": "Aplikasi-aplikasi berikut akan diperbarui: {apps}", + "app_upgrade_several_apps": "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_created": "Cadangan dibuat: {name}", "backup_creation_failed": "Tidak dapat membuat arsip cadangan", "backup_delete_error": "Tidak dapat menghapus '{path}'", - "backup_deleted": "Cadangan dihapus", + "backup_deleted": "Cadangan dihapus: {name}", "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_method_tar_finished": "Arsip TAR cadangan 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 + "backup_mount_archive_for_restore": "Menyiapkan arsip untuk pemulihan...", + "aborting": "Membatalkan.", + "action_invalid": "Tindakan tidak valid '{action}'", + "app_action_cannot_be_ran_because_required_services_down": "Layanan yang dibutuhkan ini harus aktif untuk menjalankan tindakan ini: {services}. Coba memulai ulang layanan tersebut untuk melanjutkan (dan mungkin melakukan penyelidikan mengapa layanan tersebut nonaktif).", + "app_argument_choice_invalid": "Pilih yang valid untuk argumen '{name}': '{value}' tidak termasuk pada pilihan yang tersedia ({choices})", + "app_argument_invalid": "Pilih yang valid untuk argumen '{name}': {error}", + "app_extraction_failed": "Tidak dapat mengekstrak berkas pemasangan", + "app_full_domain_unavailable": "Maaf, aplikasi ini harus dipasang pada domain sendiri, namun aplikasi lain sudah terpasang pada domain '{domain}'. Anda dapat menggunakan subdomain hanya untuk aplikasi ini.", + "app_location_unavailable": "URL ini mungkin tidak tersedia atau terjadi konflik dengan aplikasi yang telah terpasang:\n{apps}", + "app_not_upgraded": "Aplikasi '{failed_app}' gagal diperbarui, oleh karena itu pembaruan aplikasi berikut juga dibatalkan: {apps}", + "app_config_unable_to_apply": "Gagal menerapkan nilai-nilai panel konfigurasi.", + "app_config_unable_to_read": "Gagal membaca nilai-nilai panel konfigurasi.", + "permission_cannot_remove_main": "Menghapus izin utama tidak diperbolehkan", + "service_description_postgresql": "Menyimpan data aplikasi (basis data SQL)", + "restore_already_installed_app": "Aplikasi dengan ID '{app}' telah terpasang", + "app_change_url_require_full_domain": "{app} tidak dapat dipindah ke URL baru ini karena ini memerlukan domain penuh (tanpa jalur = /)", + "app_change_url_script_failed": "Galat terjadi di skrip pengubahan URL", + "app_not_enough_disk": "Aplikasi ini memerlukan {required} ruang kosong.", + "app_not_enough_ram": "Aplikasi ini memerlukan {required} RAM untuk pemasangan/pembaruan, tapi sekarang hanya tersedia {current} saja.", + "app_packaging_format_not_supported": "Aplikasi ini tidak dapat dipasang karena format pengemasan tidak didukung oleh YunoHost versi Anda. Anda sebaiknya memperbarui sistem Anda.", + "ask_admin_username": "Nama pengguna admin", + "backup_archive_broken_link": "Tidak dapat mengakses arsip cadangan (tautan rusak untuk {path})", + "backup_archive_open_failed": "Tidak dapat membuka arsip cadangan", + "certmanager_cert_install_success_selfsigned": "Sertifikat ditandai sendiri sekarang terpasang untuk '{domain}'", + "certmanager_cert_renew_failed": "Pembaruan ulang sertifikat Let's Encrypt gagal untuk {domains}", + "certmanager_cert_renew_success": "Sertifikat Let's Encrypt diperbarui untuk domain '{domain}'", + "diagnosis_apps_allgood": "Semua aplikasi yang dipasang mengikuti panduan penyusunan yang baik", + "diagnosis_basesystem_kernel": "Peladen memakai kernel Linux {kernel_version}", + "diagnosis_cache_still_valid": "(Tembolok masih valid untuk diagnosis {category}. Belum akan didiagnosis ulang!)", + "diagnosis_description_dnsrecords": "Rekaman DNS", + "diagnosis_description_ip": "Konektivitas internet", + "diagnosis_description_web": "Web", + "diagnosis_domain_expiration_error": "Beberapa domain akan kedaluwarsa SEGERA!", + "diagnosis_domain_expiration_not_found_details": "Informasi WHOIS untuk domain {domain} sepertinya tidak mengandung informasi tentang tanggal kedaluwarsa?", + "diagnosis_domain_expiration_warning": "Beberapa domain akan kedaluwarsa!", + "diagnosis_domain_expires_in": "{domain} kedaluwarsa dalam {days} hari.", + "diagnosis_everything_ok": "Sepertinya semuanya bagus untuk {category}!", + "diagnosis_ip_no_ipv6_tip": "Memiliki IPv6 tidaklah wajib agar sistem Anda bekerja, tapi itu akan membuat internet lebih sehat. IPv6 biasanya secara otomatis akan dikonfigurasikan oleh sistem atau penyedia peladen Anda jika tersedia. Jika belum dikonfigurasi, Anda mungkin harus mengonfigurasi beberapa hal secara manual seperti yang dijelaskan di dokumentasi di sini: https://yunohost.org/#/ipv6. Jika Anda tidak dapat mengaktifkan IPv6 atau terlalu rumit buat Anda, Anda bisa mengabaikan peringatan ini.", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 biasanya secara otomatis akan dikonfigurasikan oleh sistem atau penyedia peladen Anda jika tersedia. Jika belum dikonfigurasi, Anda mungkin harus mengonfigurasi beberapa hal secara manual seperti yang dijelaskan di dokumentasi di sini: https://yunohost.org/#/ipv6.", + "diagnosis_ip_not_connected_at_all": "Peladen ini sepertinya tidak terhubung dengan internet sama sekali?", + "diagnosis_mail_queue_unavailable_details": "Galat: {error}", + "global_settings_setting_root_password_confirm": "Kata sandi root baru (konfirmasi)", + "global_settings_setting_smtp_allow_ipv6": "Perbolehkan IPv6", + "global_settings_setting_ssh_port": "Porta SSH", + "log_app_change_url": "Mengubah URL untuk aplikasi '{}'", + "log_app_config_set": "Menerapkan konfigurasi untuk aplikasi '{}'", + "log_app_install": "Memasang aplikasi '{}'", + "log_app_makedefault": "Membuat '{}' sebagai aplikasi baku", + "log_app_remove": "Melepas aplikasi '{}'", + "log_app_upgrade": "Memperbarui aplikasi '{}'", + "log_available_on_yunopaste": "Log ini sekarang sudah tersedia di {url}", + "log_backup_create": "Membuat arsip cadangan", + "log_backup_restore_app": "Memulihkan '{}' dari arsip cadangan", + "log_backup_restore_system": "Memulihkan sistem dari arsip cadangan", + "log_corrupted_md_file": "Berkas metadata YAML yang terkait dengan log rusak: '{md_file}\nGalat: {error}'", + "log_domain_config_set": "Memperbarui konfigurasi untuk domain '{}'", + "log_domain_main_domain": "Atur '{}' sebagai domain utama", + "log_domain_remove": "Hapus domain '{}' dari konfigurasi sistem", + "log_link_to_log": "Log penuh untuk tindakan ini: '{desc}'", + "log_settings_reset": "Atur ulang pengaturan", + "log_tools_migrations_migrate_forward": "Menjalankan migrasi", + "log_tools_reboot": "Mulai ulang peladen Anda", + "log_tools_shutdown": "Matikan peladen Anda", + "log_tools_upgrade": "Perbarui paket sistem", + "migration_0021_main_upgrade": "Memulai pembaruan utama...", + "migration_0021_start": "Memulai migrasi ke Bullseye", + "migration_0021_yunohost_upgrade": "Memulai pembaruan YunoHost Core...", + "permission_updated": "Izin '{permission}' diperbarui", + "registrar_infos": "Info registrar", + "restore_already_installed_apps": "Aplikasi berikut tidak dapat dipulihkan karena mereka sudah terpasang: {apps}", + "restore_backup_too_old": "Arsip cadangan ini tidak dapat dipulihkan karena ini dihasilkan dari YunoHost dengan versi yang terlalu tua.", + "restore_failed": "Tidak dapat memulihkan sistem", + "restore_nothings_done": "Tidak ada yang dipulihkan", + "restore_running_app_script": "Memulihkan aplikasi {app}...", + "root_password_changed": "kata sandi root telah diubah", + "root_password_desynchronized": "Kata sandi administrasi telah diubah tapi YunoHost tidak dapat mengubahnya menjadi kata sandi root!", + "server_reboot_confirm": "Peladen akan dimulai ulang segera, apakan Anda yakin [{answers}]", + "server_shutdown": "Peladen akan dimatikan", + "server_shutdown_confirm": "Peladen akan dimatikan segera, apakah Anda yakin? [{answers}]", + "service_add_failed": "Tidak dapat menambahkan layanan '{service}'", + "service_added": "Layanan '{service}' ditambahkan", + "service_already_stopped": "Layanan '{service}' telah dihentikan", + "service_cmd_exec_failed": "Tidak dapat menjalankan perintah '{command}'", + "service_description_dnsmasq": "Mengurus DNS", + "service_description_dovecot": "Digunakan untuk memperbolehkan klien surel mengakses surel (via IMAP dan POP3)", + "service_description_metronome": "Mengelola akun XMPP", + "service_description_postfix": "Digunakan untuk mengirim dan menerima surel", + "service_description_slapd": "Menyimpan info terkait pengguna, domain, dan sejenisnya", + "service_description_ssh": "Memperbolehkan Anda untuk terhubung secara jarak jauh dengan peladen Anda via terminal (protokol SSH)", + "service_description_yunohost-firewall": "Mengelola pembukaan dan penutupan porta koneksi ke layanan", + "unbackup_app": "{app} tidak akan disimpan", + "user_deleted": "Pengguna dihapus", + "user_deletion_failed": "Tidak dapat menghapus pengguna {user}: {error}", + "user_import_bad_file": "Berkas CSV Anda tidak secara benar diformat, akan diabaikan untuk menghindari potensi data hilang", + "yunohost_postinstall_end_tip": "Proses pasca-pemasangan sudah selesai! Untuk menyelesaikan pengaturan Anda, pertimbangkan:\n - diagnosis masalah yang mungkin lewat bagian 'Diagnosis' di webadmin (atau 'yunohost diagnosis run' di cmd);\n - baca bagian 'Finalizing your setup' dan 'Getting to know YunoHost' di dokumentasi admin: https://yunohost.org/admindoc.", + "app_already_installed_cant_change_url": "Aplikasi ini sudah terpasang. URL tidak dapat diubah hanya dengan ini. Periksa `app changeurl` jika tersedia.", + "app_requirements_checking": "Memeriksa persyaratan untuk {app}...", + "backup_create_size_estimation": "Arsip ini akan mengandung data dengan ukuran {size}.", + "certmanager_certificate_fetching_or_enabling_failed": "Mencoba untuk menggunakan sertifikat baru untuk {domain} tidak bisa...", + "certmanager_no_cert_file": "Tidak dapat membuka berkas sertifikat untuk domain {domain} (berkas: {file})", + "diagnosis_basesystem_hardware": "Arsitektur perangkat keras peladen adalah {virt} {arch}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Anda menjalankan versi paket YunoHost yang tidak konsisten... sepertinya karena pembaruan yang gagal.", + "diagnosis_basesystem_ynh_single_version": "versi {package}: {version} ({repo})", + "diagnosis_description_services": "Status layanan", + "diagnosis_description_systemresources": "Sumber daya sistem", + "diagnosis_domain_not_found_details": "Domain {domain} tidak ada di basis data WHOIS atau sudah kedaluwarsa!", + "diagnosis_http_ok": "Domain {domain} bisa dicapai dengan HTTP dari luar jaringan lokal.", + "diagnosis_ip_connected_ipv4": "Peladen ini terhubung ke internet lewat IPv4!", + "diagnosis_ip_no_ipv6": "Peladen ini sepertinya tidak memiliki IPv6.", + "domain_cert_gen_failed": "Tidak dapat membuat sertifikat", + "done": "Selesai", + "log_domain_add": "Menambahkan domain '{}' ke konfigurasi sistem", + "main_domain_changed": "Domain utama telah diubah", + "service_already_started": "Layanan '{service}' telah berjalan", + "service_description_fail2ban": "Melindungi dari berbagai macam serangan dari Internet", + "service_description_yunohost-api": "Mengelola interaksi antara antarmuka web YunoHost dengan sistem", + "this_action_broke_dpkg": "Tindakan ini merusak dpkg/APT (pengelola paket sistem)... Anda bisa mencoba menyelesaikan masalah ini dengan masuk lewat SSH dan menjalankan `sudo apt install --fix-broken` dan/atau `sudo dpkg --configure -a`.", + "app_manifest_install_ask_init_admin_permission": "Siapa yang boleh mengakses fitur admin untuk aplikasi ini? (Ini bisa diubah nanti)", + "admins": "Admin", + "all_users": "Semua pengguna YunoHost", + "app_action_failed": "Gagal menjalankan tindakan {action} untuk aplikasi {app}", + "unrestore_app": "{app} akan dipulihkan", + "user_already_exists": "Pengguna '{user}' telah ada", + "user_created": "Pengguna dibuat", + "user_creation_failed": "Tidak dapat membuat pengguna {user}: {error}", + "user_home_creation_failed": "Tidak dapat membuat folder home '{home}' untuk pengguna", + "app_manifest_install_ask_init_main_permission": "Siapa yang boleh mengakses aplikasi ini? (Ini bisa diubah nanti)", + "ask_admin_fullname": "Nama lengkap admin", + "ask_fullname": "Nama lengkap", + "backup_abstract_method": "Metode pencadangan ini belum diimplementasikan", + "backup_csv_addition_failed": "Tidak dapat menambahkan berkas ke cadangan dengan berkas CSV", + "config_action_failed": "Gagal menjalankan tindakan '{action}': {error}", + "config_validate_color": "Harus warna heksadesimal RGB yang valid", + "danger": "Peringatan:", + "diagnosis_basesystem_host": "Peladen memakai Debian {debian_version}", + "diagnosis_domain_expiration_not_found": "Tidak dapat memeriksa tanggal kedaluwarsa untuk beberapa domain", + "diagnosis_http_could_not_diagnose_details": "Galat: {error}", + "app_manifest_install_ask_path": "Pilih jalur URL (setelah domain) dimana aplikasi ini akan dipasang", + "certmanager_cert_signing_failed": "Tidak dapat memverifikasi sertifikat baru", + "config_validate_url": "Harus URL web yang valid", + "diagnosis_description_ports": "Penyingkapan porta", + "diagnosis_failed_for_category": "Diagnosis gagal untuk kategori '{category}': {error}", + "mail_unavailable": "Alamat surel ini hanya untuk kelompok admin", + "main_domain_change_failed": "Tidak dapat mengubah domain utama", + "diagnosis_ip_global": "IP Global: {global}", + "diagnosis_ip_dnsresolution_working": "Resolusi nama domain bekerja!", + "diagnosis_ip_local": "IP Lokal: {local}", + "diagnosis_ip_no_ipv4": "Peladen ini sepertinya tidak memiliki IPv4.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Galat: {error}", + "global_settings_setting_ssh_password_authentication_help": "Izinkan autentikasi kata sandi untuk SSH", + "password_listed": "Kata sandi ini termasuk dalam daftar kata sandi yang sering digunakan di dunia. Mohon untuk memilih yang lebih unik.", + "permission_not_found": "Izin '{permission}' tidak ditemukan", + "restore_not_enough_disk_space": "Ruang tidak cukup (ruang: {free_space} B, ruang yang dibutuhkan: {needed_space} B, margin aman: {margin} B)", + "server_reboot": "Peladen akan dimulai ulang", + "service_description_nginx": "Menyediakan akses untuk semua situs yang dihos di peladen Anda", + "service_description_rspamd": "Filter spam dan fitur terkait surel lainnya", + "service_remove_failed": "Tidak dapat menghapus layanan '{service}'", + "user_unknown": "Pengguna tidak diketahui: {user}", + "user_update_failed": "Tidak dapat memperbarui pengguna {user}: {error}", + "yunohost_configured": "YunoHost sudah terkonfigurasi", + "global_settings_setting_pop3_enabled": "Aktifkan POP3", + "log_user_import": "Mengimpor pengguna", + "app_start_backup": "Mengumpulkan berkas untuk dicadangkan untuk {app}...", + "app_upgrade_script_failed": "Galat terjadi di skrip pembaruan aplikasi", + "backup_csv_creation_failed": "Tidak dapat membuat berkas CSV yang dibutuhkan untuk pemulihan", + "certmanager_attempt_to_renew_valid_cert": "Sertifikat untuk domain '{domain}' belum akan kedaluwarsa! (Anda bisa menggunakan --force jika Anda tahu apa yang Anda lakukan)", + "extracting": "Mengekstrak...", + "system_username_exists": "Nama pengguna telah ada di daftar pengguna sistem", + "upgrade_complete": "Pembaruan selesai", + "upgrading_packages": "Memperbarui paket...", + "diagnosis_description_apps": "Aplikasi", + "diagnosis_description_basesystem": "Basis sistem", + "global_settings_setting_pop3_enabled_help": "Aktifkan protokol POP3 untuk peladen surel", + "password_confirmation_not_the_same": "Kata sandi dan untuk konfirmasinya tidak sama", + "restore_complete": "Pemulihan selesai", + "user_import_success": "Pengguna berhasil diimpor", + "user_updated": "Informasi pengguna diubah", + "visitors": "Pengunjung", + "yunohost_already_installed": "YunoHost sudah terpasang", + "yunohost_installing": "Memasang YunoHost...", + "yunohost_not_installed": "YunoHost tidak terpasang dengan benar. Jalankan 'yunohost tools postinstall'", + "restore_removing_tmp_dir_failed": "Tidak dapat menghapus direktori sementara yang dulu", + "app_sources_fetch_failed": "Tidak dapat mengambil berkas sumber, apakah URL-nya benar?", + "installation_complete": "Pemasangan selesai", + "app_arch_not_supported": "Aplikasi ini hanya bisa dipasang pada arsitektur {required}, tapi arsitektur peladen Anda adalah {current}", + "diagnosis_basesystem_hardware_model": "Model peladen adalah {model}", + "app_yunohost_version_not_supported": "Aplikasi ini memerlukan YunoHost >= {required}, tapi versi yang terpasang adalah {current}", + "ask_new_path": "Jalur baru", + "backup_cleaning_failed": "Tidak dapat menghapus folder cadangan sementara", + "diagnosis_description_mail": "Surel", + "diagnosis_description_regenconf": "Konfigurasi sistem", + "diagnosis_display_tip": "Untuk melihat masalah yang ditemukan, Anda bisa ke bagian Diagnosis di administrasi web atau jalankan 'yunohost diagnosis show --issues --human-readable'.", + "diagnosis_domain_expiration_success": "Domain Anda sudah terdaftar dan belum akan kedaluwarsa dalam waktu dekat.", + "diagnosis_failed": "Gagal mengambil hasil diagnosis untuk kategori '{category}': {error}", + "global_settings_setting_portal_theme": "Tema portal", + "global_settings_setting_portal_theme_help": "Informasi lebih lanjut tentang tema portal kustom ada di https://yunohost.org/theming", + "global_settings_setting_ssh_password_authentication": "Autentikasi kata sandi", + "certmanager_attempt_to_renew_nonLE_cert": "Sertifikat untuk domain '{domain}' tidak diterbitkan oleh Let's Encrypt. Tidak dapat memperbarui secara otomatis!", + "certmanager_cert_install_failed": "Pemasangan sertifikat Let's Encrypt gagal untuk {domains}", + "certmanager_cert_install_failed_selfsigned": "Pemasangan sertifikat ditandai sendiri (self-signed) gagal untuk {domains}", + "config_validate_email": "Harus surel yang valid", + "config_apply_failed": "Gagal menerapkan konfigurasi baru: {error}", + "diagnosis_basesystem_ynh_main_version": "Peladen memakai YunoHost {main_version} ({repo})", + "diagnosis_cant_run_because_of_dep": "Tidak dapat menjalankan diagnosis untuk {category} ketika ada masalah utama yang terkait dengan {dep}.", + "diagnosis_services_conf_broken": "Konfigurasi rusak untuk layanan {service}!", + "diagnosis_services_running": "Layanan {service} berjalan!", + "diagnosis_swap_ok": "Sistem ini memiliki {total} swap!", + "downloading": "Mengunduh...", + "pattern_password": "Harus paling tidak 3 karakter", + "pattern_password_app": "Maaf, kata sandi tidak dapat mengandung karakter berikut: {forbidden_chars}", + "pattern_port_or_range": "Harus angka porta yang valid (cth. 0-65535) atau jangkauan porta (cth. 100:200)", + "permission_already_exist": "Izin '{permission}' sudah ada", + "permission_cant_add_to_all_users": "Izin '{permission}' tidak dapat ditambahkan ke semua pengguna.", + "permission_created": "Izin '{permission}' dibuat", + "permission_creation_failed": "Tidak dapat membuat izin '{permission}': {error}", + "permission_deleted": "Izin '{permission}' dihapus", + "service_description_mysql": "Menyimpan data aplikasi (basis data SQL)", + "mailbox_disabled": "Surel dimatikan untuk pengguna {user}", + "log_user_update": "Memperbarui informasi untuk pengguna '{}'", + "apps_catalog_obsolete_cache": "Tembolok katalog aplikasi kosong atau sudah tua.", + "backup_actually_backuping": "Membuat arsip cadangan dari berkas yang dikumpulkan...", + "backup_applying_method_copy": "Menyalin semua berkas ke cadangan...", + "backup_archive_app_not_found": "Tidak dapat menemukan {app} di arsip cadangan", + "config_validate_date": "Harus tanggal yang valid seperti format YYYY-MM-DD", + "config_validate_time": "Harus waktu yang valid seperti HH:MM", + "diagnosis_ip_connected_ipv6": "Peladen ini terhubung ke internet lewat IPv6!", + "diagnosis_services_bad_status": "Layanan {service} {status} :(", + "global_settings_setting_root_password": "Kata sandi root baru", + "log_app_action_run": "Menjalankan tindakan dari aplikasi '{}'", + "log_settings_reset_all": "Atur ulang semua pengaturan", + "log_settings_set": "Terapkan pengaturan", + "service_removed": "Layanan '{service}' dihapus", + "service_restart_failed": "Tidak dapat memulai ulang layanan '{service}'\n\nLog layanan baru-baru ini:{logs}", + "ssowat_conf_generated": "Konfigurasi SSOwat diperbarui", + "system_upgraded": "Sistem diperbarui", + "tools_upgrade": "Memperbarui paket sistem", + "upnp_dev_not_found": "Tidak ada perangkat UPnP yang ditemukan", + "upnp_enabled": "UPnP dinyalakan", + "upnp_port_open_failed": "Tidak dapat membuka porta lewat UPnP", + "app_change_url_failed": "Tidak dapat mengubah URL untuk {app}: {error}", + "app_restore_script_failed": "Galat terjadi di skrip pemulihan aplikasi", + "app_label_deprecated": "Perintah ini sudah usang! Silakan untuk menggunakan perintah baru 'yunohost user permission update' untuk mengelola label aplikasi.", + "app_make_default_location_already_used": "Tidak dapat membuat '{app}' menjadi aplikasi baku untuk domain, '{domain}' telah dipakai oleh '{other_app}'", + "app_manifest_install_ask_is_public": "Bolehkan aplikasi ini dibuka untuk pengunjung awanama?", + "upnp_disabled": "UPnP dimatikan", + "global_settings_setting_smtp_allow_ipv6_help": "Perbolehkan penggunaan IPv6 untuk menerima dan mengirim surel", + "domain_config_default_app": "Aplikasi baku", + "diagnosis_diskusage_verylow": "Penyimpanan {mountpoint} (di perangkat {device}) hanya tinggal memiliki {free} ({free_percent}%) ruang kosong yang tersedia (dari {total}). Direkomendasikan untuk membersihkan ruang penyimpanan!", + "domain_config_api_protocol": "Protokol API", + "domain_config_cert_summary_letsencrypt": "Bagus! Anda menggunakan sertifikat Let's Encrypt yang valid!", + "domain_config_mail_out": "Surel keluar", + "domain_deletion_failed": "Tidak dapat menghapus domain {domain}: {error}", + "backup_copying_to_organize_the_archive": "Menyalin {size}MB untuk menyusun arsip", + "backup_method_copy_finished": "Salinan cadangan telah selesai", + "certmanager_domain_cert_not_selfsigned": "Sertifikat untuk domain {domain} bukan disertifikasi sendiri. Apakah Anda yakin ingin mengubahnya? (Gunakan '--force' jika iya)", + "diagnosis_diskusage_ok": "Penyimpanan {mountpoint} (di perangkat {device}) masih memiliki {free} ({free_percent}%) ruang kosong yang tersedia (dari {total})!", + "diagnosis_http_nginx_conf_not_up_to_date": "Konfigurasi nginx domain ini sepertinya diubah secara manual, itu mencegah YunoHost untuk mendiagnosis apakah domain ini terhubung ke HTTP.", + "domain_created": "Domain dibuat", + "migrations_running_forward": "Menjalankan migrasi {id}...", + "permission_deletion_failed": "Tidak dapat menghapus izin '{permission}': {error}", + "domain_config_cert_no_checks": "Abaikan pemeriksaan diagnosis", + "domain_config_cert_renew": "Perbarui sertifikat Let's Encrypt", + "domain_config_cert_summary": "Status sertifikat", + "domain_config_cert_summary_expired": "PENTING: Sertifikat saat ini tidak valid! HTTPS tidak akan bekerja sama sekali!", + "port_already_opened": "Porta {port} telah dibuka untuk koneksi {ip_version}", + "migrations_success_forward": "Migrasi {id} selesai", + "not_enough_disk_space": "Ruang kosong tidak cukup di '{path}'", + "password_too_long": "Pilih kata sandi yang lebih pendek dari 127 karakter", + "regenconf_file_backed_up": "Berkas konfigurasi '{conf}' dicadangkan ke '{backup}'", + "domain_creation_failed": "Tidak dapat membuat domain {domain}: {error}", + "domain_deleted": "Domain dihapus", + "regex_with_only_domain": "Anda tidak dapat menggunakan regex untuk domain, hanya untuk jalur", + "diagnosis_diskusage_low": "Penyimpanan {mountpoint} (di perangkat {device}) hanya tinggal memiliki {free} ({free_percent}%) ruang kosong yang tersedia (dari {total}).", + "domain_config_cert_summary_ok": "Oke, sertifikat saat ini terlihat bagus!", + "app_failed_to_upgrade_but_continue": "Gagal memperbarui aplikasi {failed_app}, melanjutkan pembaruan berikutnya seperti yang diminta. Jalankan 'yunohost log show {operation_logger_name}' untuk melihat log kegagalan", + "certmanager_attempt_to_replace_valid_cert": "Anda sedang mencoba untuk menimpa sertifikat yang valid untuk domain {domain}! (Gunakan --force untuk melewati ini)", + "permission_protected": "Izin {permission} dilindungi. Anda tidak dapat menambahkan atau menghapus kelompok pengunjung ke/dari izin ini.", + "permission_require_account": "Izin {permission} hanya masuk akal untuk pengguna yang memiliki akun, maka ini tidak dapat diaktifkan untuk pengunjung.", + "permission_update_failed": "Tidak dapat memperbarui izin '{permission}': {error}", + "apps_failed_to_upgrade": "Aplikasi berikut gagal untuk diperbarui:{apps}", + "backup_archive_name_unknown": "Arsip cadangan lokal tidak diketahui yang bernama '{name}'", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Untuk memperbaiki ini, periksa perbedaannya dari CLI menggunakan yunohost tools regen-conf nginx --dry-run --with-diff dan jika Anda sudah, terapkan perubahannya menggunakan yunohost tools regen-conf nginx --force.", + "domain_config_auth_token": "Token autentikasi", + "domain_config_cert_install": "Pasang sertifikat Let's Encrypt", + "domain_config_cert_summary_abouttoexpire": "Sertifikat saat ini akan kedaluwarsa. Akan secara otomatis diperbarui secepatnya.", + "domain_config_mail_in": "Surel datang", + "password_too_simple_1": "Panjang kata sandi harus paling tidak 8 karakter", + "password_too_simple_2": "Panjang kata sandi harus paling tidak 8 karakter dan mengandung digit, huruf kapital, dan huruf kecil", + "password_too_simple_3": "Panjang kata sandi harus paling tidak 8 karakter dan mengandung digit, huruf kapital, huruf kecil, dan karakter khusus", + "password_too_simple_4": "Panjang kata sandi harus paling tidak 12 karakter dan mengandung digit, huruf kapital, huruf kecil, dan karakter khusus", + "port_already_closed": "Porta {port} telah ditutup untuk koneksi {ip_version}", + "service_description_yunomdns": "Membuat Anda bisa menemukan peladen Anda menggunakan 'yunohost.local' di jaringan lokal Anda", + "regenconf_file_copy_failed": "Tidak dapat menyalin berkas konfigurasi baru '{new}' ke '{conf}'", + "regenconf_file_kept_back": "Berkas konfigurasi '{conf}' seharusnya dihapus oleh regen-conf (kategori {category}) tapi tidak jadi.", + "regenconf_file_manually_modified": "Berkas konfigurasi '{conf}' telah diubah secara manual dan tidak akan diperbarui", + "regenconf_file_manually_removed": "Berkas konfigurasi '{conf}' telah dihapus secara manual dan tidak akan dibikin", + "regenconf_file_remove_failed": "Tidak dapat menghapus berkas konfigurasi '{conf}'", + "regenconf_file_removed": "Berkas konfigurasi '{conf}' dihapus", + "regenconf_file_updated": "Berkas konfigurasi '{conf}' diperbarui", + "regenconf_now_managed_by_yunohost": "Berkas konfigurasi '{conf}' sekarang dikelola oleh YunoHost (kategori {category})", + "regenconf_updated": "Konfigurasi diperbarui untuk '{category}'", + "log_user_group_delete": "Menghapus kelompok '{}'", + "backup_archive_cant_retrieve_info_json": "Tidak dapat memuat info untuk arsip '{archive}'... Berkas info.json tidak dapat didapakan (atau bukan json yang valid).", + "diagnosis_mail_blacklist_reason": "Alasan pendaftarhitaman adalah: {reason}", + "diagnosis_ports_unreachable": "Porta {port} tidak tercapai dari luar.", + "diagnosis_ram_verylow": "Sistem hanya memiliki {available} ({available_percent}%) RAM yang tersedia! (dari {total})", + "diagnosis_regenconf_allgood": "Semua berkas konfigurasi sesuai dengan rekomendasi konfigurasi!", + "diagnosis_security_vulnerable_to_meltdown": "Sepertinya sistem Anda rentan terhadap kerentanan keamanan Meltdown", + "diagnosis_security_vulnerable_to_meltdown_details": "Untuk memperbaiki ini, sebaiknya perbarui sistem Anda dan mulai ulang untuk memuat kernel linux yang baru (atau hubungi penyedia peladen Anda jika itu tidak bekerja). Kunjungi https://meltdownattack.com/ untuk informasi lebih lanjut.", + "domain_exists": "Domain telah ada", + "domain_uninstall_app_first": "Aplikasi berikut masih terpasang di domain Anda:\n{apps}\n\nSilakan lepas mereka menggunakan 'yunohost app remove id_aplikasi' atau pindahkan ke domain lain menggunakan 'yunohost app change-url id_aplikasi' sebelum melanjutkan ke penghapusan domain", + "group_creation_failed": "Tidak dapat membuat kelompok '{group}': {error}", + "group_deleted": "Kelompok '{group}' dihapus", + "log_letsencrypt_cert_install": "Memasang sertifikat Let's Encrypt di domain '{}'", + "log_permission_create": "Membuat izin '{}'", + "log_permission_delete": "Menghapus izin '{}'", + "backup_with_no_backup_script_for_app": "Aplikasi '{app}' tidak memiliki skrip pencadangan. Mengabaikan.", + "backup_system_part_failed": "Tidak dapat mencadangkan bagian '{part}' sistem", + "log_user_create": "Menambahkan pengguna '{}'", + "log_user_delete": "Menghapus pengguna '{}'", + "log_user_group_create": "Membuat kelompok '{}'", + "log_user_group_update": "Memperbarui kelompok '{}'", + "log_user_permission_update": "Memperbarui akses untuk izin '{}'", + "mail_alias_remove_failed": "Tidak dapat menghapus alias surel '{mail}'", + "diagnosis_mail_blacklist_ok": "IP dan domain yang digunakan oleh peladen ini sepertinya tidak didaftarhitamkan", + "diagnosis_dns_point_to_doc": "Silakan periksa dokumentasi di https://yunohost.org/dns_config jika Anda masih membutuhkan bantuan untuk mengatur rekaman DNS.", + "diagnosis_regenconf_manually_modified": "Berkas konfigurasi {file} sepertinya telah diubah manual.", + "backup_with_no_restore_script_for_app": "{app} tidak memiliki skrip pemulihan, Anda tidak akan bisa secara otomatis memulihkan cadangan aplikasi ini.", + "config_no_panel": "Tidak dapat menemukan panel konfigurasi.", + "confirm_app_install_warning": "Peringatan: Aplikasi ini mungkin masih bisa bekerja, tapi tidak terintegrasi dengan baik dengan YunoHost. Beberapa fitur seperti SSO dan pencadangan mungkin tidak tersedia. Tetap pasang? [{answers}] ", + "diagnosis_ports_ok": "Porta {port} tercapai dari luar.", + "diagnosis_ports_partially_unreachable": "Porta {port} tidak tercapai dari luar lewat IPv{failed}.", + "domain_remove_confirm_apps_removal": "Menghapus domain ini akan melepas aplikasi berikut:\n{apps}\n\nApakah Anda yakin? [{answers}]", + "domains_available": "Domain yang tersedia:", + "global_settings_reset_success": "Atur ulang pengaturan global", + "group_created": "Kelompok '{group}' dibuat", + "group_deletion_failed": "Tidak dapat menghapus kelompok '{group}': {error}", + "group_updated": "Kelompok '{group}' diperbarui", + "invalid_credentials": "Nama pengguna atau kata sandi salah", + "log_letsencrypt_cert_renew": "Memperbarui sertifikat Let's Encrypt '{}'", + "log_selfsigned_cert_install": "Memasang sertifikat ditandai sendiri pada domain '{}'", + "log_user_permission_reset": "Mengatur ulang izin '{}'", + "domain_config_xmpp": "Pesan Langsung (XMPP)" +} diff --git a/locales/it.json b/locales/it.json index 844b756ea..21fb52367 100644 --- a/locales/it.json +++ b/locales/it.json @@ -23,8 +23,6 @@ "upgrading_packages": "Aggiornamento dei pacchetti...", "user_deleted": "Utente cancellato", "admin_password": "Password dell'amministrazione", - "admin_password_change_failed": "Impossibile cambiare la password", - "admin_password_changed": "La password d'amministrazione è stata cambiata", "app_install_files_invalid": "Questi file non possono essere installati", "app_not_correctly_installed": "{app} sembra di non essere installata correttamente", "app_not_properly_removed": "{app} non è stata correttamente rimossa", @@ -34,9 +32,6 @@ "app_upgrade_failed": "Impossibile aggiornare {app}: {error}", "app_upgraded": "{app} aggiornata", "app_requirements_checking": "Controllo i pacchetti richiesti per {app}...", - "app_requirements_unmeet": "Requisiti non soddisfatti per {app}, il pacchetto {pkgname} ({version}) deve essere {spec}", - "ask_firstname": "Nome", - "ask_lastname": "Cognome", "ask_main_domain": "Dominio principale", "ask_new_admin_password": "Nuova password dell'amministrazione", "backup_app_failed": "Non è possibile fare il backup {app}", @@ -187,7 +182,6 @@ "certmanager_cannot_read_cert": "Qualcosa è andato storto nel tentativo di aprire il certificato attuale per il dominio {domain} (file: {file}), motivo: {reason}", "certmanager_cert_install_success": "Certificato Let's Encrypt per il dominio {domain} installato", "aborting": "Annullamento.", - "admin_password_too_long": "Per favore scegli una password più corta di 127 caratteri", "app_not_upgraded": "Impossibile aggiornare le applicazioni '{failed_app}' e di conseguenza l'aggiornamento delle seguenti applicazione è stato cancellato: {apps}", "app_start_install": "Installando '{app}'...", "app_start_remove": "Rimozione di {app}...", @@ -222,28 +216,12 @@ "domain_dns_conf_is_just_a_recommendation": "Questo comando ti mostra la configurazione *raccomandata*. Non ti imposta la configurazione DNS al tuo posto. È tua responsabilità configurare la tua zona DNS nel tuo registrar in accordo con queste raccomandazioni.", "dyndns_could_not_check_available": "Impossibile controllare se {domain} è disponibile su {provider}.", "dyndns_domain_not_provided": "Il fornitore DynDNS {provider} non può fornire il dominio {domain}.", - "experimental_feature": "Attenzione: Questa funzionalità è sperimentale e non è considerata stabile, non dovresti utilizzarla a meno che tu non sappia cosa stai facendo.", "file_does_not_exist": "Il file {path} non esiste.", - "global_settings_bad_choice_for_enum": "Scelta sbagliata per l'impostazione {setting}, ricevuta '{choice}', ma le scelte disponibili sono: {available_choices}", - "global_settings_bad_type_for_setting": "Tipo errato per l'impostazione {setting}, ricevuto {received_type}, atteso {expected_type}", - "global_settings_cant_open_settings": "Apertura del file delle impostazioni non riuscita, motivo: {reason}", - "global_settings_cant_serialize_settings": "Serializzazione dei dati delle impostazioni non riuscita, motivo: {reason}", - "global_settings_cant_write_settings": "Scrittura del file delle impostazioni non riuscita, motivo: {reason}", - "global_settings_key_doesnt_exists": "La chiave '{settings_key}' non esiste nelle impostazioni globali, puoi vedere tutte le chiavi disponibili eseguendo 'yunohost settings list'", - "global_settings_reset_success": "Le impostazioni precedenti sono state salvate in {path}", "already_up_to_date": "Niente da fare. Tutto è già aggiornato.", - "global_settings_setting_security_nginx_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server web NGIX. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", - "global_settings_setting_security_password_admin_strength": "Complessità della password di amministratore", - "global_settings_setting_security_password_user_strength": "Complessità della password utente", - "global_settings_setting_security_ssh_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server SSH. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", - "global_settings_unknown_setting_from_settings_file": "Chiave sconosciuta nelle impostazioni: '{setting_key}', scartata e salvata in /etc/yunohost/settings-unknown.json", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Consenti l'uso del hostkey DSA (deprecato) per la configurazione del demone SSH", - "global_settings_unknown_type": "Situazione inaspettata, l'impostazione {setting} sembra essere di tipo {unknown_type} ma non è un tipo supportato dal sistema.", "good_practices_about_admin_password": "Stai per impostare una nuova password di amministratore. La password deve essere almeno di 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una frase, una serie di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).", "log_corrupted_md_file": "Il file dei metadati YAML associato con i registri è danneggiato: '{md_file}'\nErrore: {error}", "log_link_to_log": "Registro completo di questa operazione: '{desc}'", "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log show {name}'", - "global_settings_setting_security_postfix_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", "log_link_to_failed_log": "Impossibile completare l'operazione '{desc}'! Per ricevere aiuto, per favore fornisci il registro completo dell'operazione cliccando qui", "log_help_to_get_failed_log": "L'operazione '{desc}' non può essere completata. Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log share {name}'", "log_does_exists": "Non esiste nessun registro delle operazioni chiamato '{log}', usa 'yunohost log list' per vedere tutti i registri delle operazioni disponibili", @@ -255,7 +233,6 @@ "log_available_on_yunopaste": "Questo registro è ora disponibile via {url}", "log_backup_restore_system": "Ripristina sistema da un archivio di backup", "log_backup_restore_app": "Ripristina '{}' da un archivio di backup", - "log_remove_on_failed_restore": "Rimuovi '{}' dopo un ripristino fallito da un archivio di backup", "log_remove_on_failed_install": "Rimuovi '{}' dopo un'installazione fallita", "log_domain_add": "Aggiungi il dominio '{}' nella configurazione di sistema", "log_domain_remove": "Rimuovi il dominio '{}' dalla configurazione di sistema", @@ -414,7 +391,6 @@ "server_reboot": "Il server si riavvierà", "server_shutdown_confirm": "Il server si spegnerà immediatamente, sei sicuro? [{answers}]", "server_shutdown": "Il server si spegnerà", - "root_password_replaced_by_admin_password": "La tua password di root è stata sostituita dalla tua password d'amministratore.", "root_password_desynchronized": "La password d'amministratore è stata cambiata, ma YunoHost non ha potuto propagarla alla password di root!", "restore_system_part_failed": "Impossibile ripristinare la sezione di sistema '{part}'", "restore_removing_tmp_dir_failed": "Impossibile rimuovere una vecchia directory temporanea", @@ -506,13 +482,9 @@ "group_already_exist_on_system_but_removing_it": "Il gruppo {group} esiste già tra i gruppi di sistema, ma YunoHost lo cancellerà...", "group_already_exist_on_system": "Il gruppo {group} esiste già tra i gruppi di sistema", "group_already_exist": "Il gruppo {group} esiste già", - "global_settings_setting_backup_compress_tar_archives": "Quando creo nuovi backup, usa un archivio (.tar.gz) al posto di un archivio non compresso (.tar). NB: abilitare quest'opzione significa create backup più leggeri, ma la procedura durerà di più e il carico CPU sarà maggiore.", "global_settings_setting_smtp_relay_password": "Password del relay SMTP", "global_settings_setting_smtp_relay_user": "User account del relay SMTP", "global_settings_setting_smtp_relay_port": "Porta del relay SMTP", - "global_settings_setting_smtp_relay_host": "Utilizza SMTP relay per inviare mail al posto di questa instanza yunohost. Utile se sei in una di queste situazioni: la tua porta 25 è bloccata dal tuo provider ISP o VPS; hai un IP residenziale listato su DUHL; non sei puoi configurare il DNS inverso; oppure questo server non è direttamente esposto a Internet e vuoi usarne un'altro per spedire email.", - "global_settings_setting_smtp_allow_ipv6": "Permetti l'utilizzo di IPv6 per ricevere e inviare mail", - "global_settings_setting_pop3_enabled": "Abilita il protocollo POP3 per il server mail", "dyndns_provider_unreachable": "Incapace di raggiungere il provider DynDNS {provider}: o il tuo YunoHost non è connesso ad internet o il server dynette è down.", "dpkg_lock_not_available": "Impossibile eseguire il comando in questo momento perché un altro programma sta bloccando dpkg (il package manager di sistema)", "domain_cannot_remove_main_add_new_one": "Non puoi rimuovere '{domain}' visto che è il dominio principale nonché il tuo unico dominio, devi prima aggiungere un altro dominio eseguendo 'yunohost domain add ', impostarlo come dominio principale con 'yunohost domain main-domain n ', e solo allora potrai rimuovere il dominio '{domain}' eseguendo 'yunohost domain remove {domain}'.'", @@ -574,20 +546,16 @@ "migration_ldap_backup_before_migration": "Sto generando il backup del database LDAP e delle impostazioni delle app prima di effettuare la migrazione.", "log_backup_create": "Crea un archivio backup", "global_settings_setting_ssowat_panel_overlay_enabled": "Abilita il pannello sovrapposto SSOwat", - "global_settings_setting_security_ssh_port": "Porta SSH", "diagnosis_sshd_config_inconsistent_details": "Esegui yunohost settings set security.ssh.port -v PORTA_SSH per definire la porta SSH, e controlla con yunohost tools regen-conf ssh --dry-run --with-diff, poi yunohost tools regen-conf ssh --force per resettare la tua configurazione con le raccomandazioni YunoHost.", "diagnosis_sshd_config_inconsistent": "Sembra che la porta SSH sia stata modificata manualmente in /etc/ssh/sshd_config: A partire da YunoHost 4.2, una nuova configurazione globale 'security.ssh.port' è disponibile per evitare di modificare manualmente la configurazione.", "diagnosis_sshd_config_insecure": "Sembra che la configurazione SSH sia stata modificata manualmente, ed non è sicuro dato che non contiene le direttive 'AllowGroups' o 'Allowusers' che limitano l'accesso agli utenti autorizzati.", "backup_create_size_estimation": "L'archivio conterrà circa {size} di dati.", "app_restore_script_failed": "C'è stato un errore all'interno dello script di recupero", - "global_settings_setting_security_webadmin_allowlist": "Indirizzi IP con il permesso di accedere al webadmin, separati da virgola.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Permetti solo ad alcuni IP di accedere al webadmin.", "disk_space_not_sufficient_update": "Non c'è abbastanza spazio libero per aggiornare questa applicazione", "disk_space_not_sufficient_install": "Non c'è abbastanza spazio libero per installare questa applicazione", "app_config_unable_to_apply": "Applicazione dei valori nel pannello di configurazione non riuscita.", "app_config_unable_to_read": "Lettura dei valori nel pannello di configurazione non riuscita.", "diagnosis_apps_issue": "È stato rilevato un errore per l’app {app}", - "global_settings_setting_security_nginx_redirect_to_https": "Reindirizza richieste HTTP a HTTPs di default (NON DISABILITARE a meno che tu non sappia veramente bene cosa stai facendo!)", "diagnosis_http_special_use_tld": "Il dominio {domain} è basato su un dominio di primo livello (TLD) dall’uso speciale, come .local o .test, perciò non è previsto che sia esposto al di fuori della rete locale.", "domain_dns_conf_special_use_tld": "Questo dominio è basato su un dominio di primo livello (TLD) dall’uso speciale, come .local o .test, perciò non è previsto abbia reali record DNS.", "domain_dns_push_not_applicable": "La configurazione automatica del DNS non è applicabile al dominio {domain}. Dovresti configurare i tuoi record DNS manualmente, seguendo la documentazione su https://yunohost.org/dns_config.", @@ -610,12 +578,10 @@ "user_import_partial_failed": "L’importazione degli utenti è parzialmente fallita", "domain_unknown": "Il dominio '{domain}' è sconosciuto", "log_user_import": "Importa utenti", - "invalid_password": "Password non valida", "diagnosis_high_number_auth_failures": "Recentemente c’è stato un numero insolitamente alto di autenticazioni fallite. Potresti assicurarti che fail2ban stia funzionando e che sia configurato correttamente, oppure usare una differente porta SSH, come spiegato in https://yunohost.org/security.", "diagnosis_apps_allgood": "Tutte le applicazioni installate rispettano le pratiche di packaging di base", "config_apply_failed": "L’applicazione della nuova configurazione è fallita: {error}", "diagnosis_apps_outdated_ynh_requirement": "La versione installata di quest’app richiede esclusivamente YunoHost >= 2.x, che tendenzialmente significa che non è aggiornata secondo le pratiche di packaging raccomandate. Dovresti proprio considerare di aggiornarla.", - "global_settings_setting_security_experimental_enabled": "Abilita funzionalità di sicurezza sperimentali (non abilitare se non sai cosa stai facendo!)", "invalid_number_min": "Deve essere più grande di {min}", "invalid_number_max": "Deve essere meno di {max}", "log_app_config_set": "Applica la configurazione all’app '{}'", @@ -645,19 +611,32 @@ "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…" + "ldap_server_is_down_restart_it": "Il servizio LDAP è down, prova a riavviarlo…", + "global_settings_setting_backup_compress_tar_archives_help": "Quando creo nuovi backup, usa un archivio (.tar.gz) al posto di un archivio non compresso (.tar). NB: abilitare quest'opzione significa create backup più leggeri, ma la procedura durerà di più e il carico CPU sarà maggiore.", + "global_settings_setting_security_experimental_enabled_help": "Abilita funzionalità di sicurezza sperimentali (non abilitare se non sai cosa stai facendo!)", + "global_settings_setting_nginx_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server web NGIX. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", + "global_settings_setting_nginx_redirect_to_https_help": "Reindirizza richieste HTTP a HTTPs di default (NON DISABILITARE a meno che tu non sappia veramente bene cosa stai facendo!)", + "global_settings_setting_admin_strength": "Complessità della password di amministratore", + "global_settings_setting_user_strength": "Complessità della password utente", + "global_settings_setting_postfix_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", + "global_settings_setting_ssh_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server SSH. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", + "global_settings_setting_ssh_port": "Porta SSH", + "global_settings_setting_webadmin_allowlist_help": "Indirizzi IP con il permesso di accedere al webadmin, separati da virgola.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permetti solo ad alcuni IP di accedere al webadmin.", + "global_settings_setting_smtp_allow_ipv6_help": "Permetti l'utilizzo di IPv6 per ricevere e inviare mail", + "global_settings_setting_smtp_relay_enabled_help": "Utilizza SMTP relay per inviare mail al posto di questa instanza yunohost. Utile se sei in una di queste situazioni: la tua porta 25 è bloccata dal tuo provider ISP o VPS; hai un IP residenziale listato su DUHL; non sei puoi configurare il DNS inverso; oppure questo server non è direttamente esposto a Internet e vuoi usarne un'altro per spedire email.", + "domain_config_default_app": "Applicazione di default", + "app_change_url_failed": "Non è possibile cambiare l'URL per {app}:{error}" } \ No newline at end of file diff --git a/locales/ja.json b/locales/ja.json new file mode 100644 index 000000000..90645193b --- /dev/null +++ b/locales/ja.json @@ -0,0 +1,770 @@ +{ + "password_too_simple_1": "パスワードは少なくとも8文字必要です", + "aborting": "中止します。", + "action_invalid": "不正なアクション ’ {action}’", + "additional_urls_already_added": "アクセス許可 '{permission}' に対する追加URLには ‘{url}’ が既に追加されています", + "admin_password": "管理者パスワード", + "app_action_cannot_be_ran_because_required_services_down": "このアクションを実行するには、次の必要なサービスが実行されている必要があります: {services} 。続行するには再起動してみてください (そして何故ダウンしているのか調査してください)。", + "app_action_failed": "‘{name}’ アプリのアクション ’{action}' に失敗しました", + "app_argument_invalid": "引数 '{name}' の有効な値を選択してください: {error}", + "app_argument_password_no_default": "パスワード引数 '{name}' の解析中にエラーが発生しました: セキュリティ上の理由から、パスワード引数にデフォルト値を設定することはできません", + "app_argument_required": "‘{name}’ は必要です。", + "app_change_url_failed": "{app}のURLを変更できませんでした:{error}", + "app_change_url_identical_domains": "古いドメインと新しいドメイン/url_pathは同一であり( '{domain}{path}')、何もしません。", + "app_change_url_script_failed": "URL 変更スクリプト内でエラーが発生しました", + "app_failed_to_upgrade_but_continue": "アプリの{failed_app}アップグレードに失敗しました。要求に応じて次のアップグレードに進みます。「yunohostログショー{operation_logger_name}」を実行して失敗ログを表示します", + "app_full_domain_unavailable": "申し訳ありませんが、このアプリは独自のドメインにインストールする必要がありますが、他のアプリは既にドメイン '{domain}' にインストールされています。代わりに、このアプリ専用のサブドメインを使用できます。", + "app_id_invalid": "不正なアプリID", + "app_install_failed": "インストールできません {app}:{error}", + "app_manifest_install_ask_password": "このアプリの管理パスワードを選択してください", + "app_manifest_install_ask_path": "このアプリをインストールするURLパス(ドメインの後)を選択します", + "app_not_properly_removed": "{app}が正しく削除されていません", + "app_not_upgraded": "アプリ「{failed_app}」のアップグレードに失敗したため、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_start_remove": "‘{app}’ を削除しています…", + "app_start_restore": "‘{app}’ をリストアしています…", + "ask_main_domain": "メインドメイン", + "ask_new_admin_password": "新しい管理者パスワード", + "ask_new_domain": "新しいドメイン", + "ask_new_path": "新しいパス", + "ask_password": "パスワード", + "ask_user_domain": "ユーザーのメールアドレスと XMPP アカウントに使用するドメイン", + "backup_abstract_method": "このバックアップ方法はまだ実装されていません", + "backup_actually_backuping": "収集したファイルからバックアップアーカイブを作成しています...", + "backup_archive_corrupted": "バックアップアーカイブ ’{archive}’ は破損しているようです: {error}", + "backup_archive_name_exists": "この名前のバックアップアーカイブはすでに存在します。", + "backup_archive_name_unknown": "「{name}」という名前の不明なローカルバックアップアーカイブ", + "backup_archive_open_failed": "バックアップアーカイブを開けませんでした", + "backup_archive_system_part_not_available": "このバックアップでは、システム部分 '{part}' を使用できません", + "backup_method_custom_finished": "カスタム バックアップ方法 '{method}' が完了しました", + "certmanager_attempt_to_replace_valid_cert": "ドメイン {domain} の適切で有効な証明書を上書きしようとしています。(—force でバイパスする)", + "certmanager_cannot_read_cert": "ドメイン {domain} (ファイル: {file}) の現在の証明書を開こうとしたときに問題が発生しました。理由: {reason}", + "certmanager_cert_install_failed": "{domains}のLet’s Encrypt 証明書のインストールに失敗しました", + "certmanager_cert_install_failed_selfsigned": "{domains} ドメインの自己署名証明書のインストールに失敗しました", + "certmanager_cert_install_success": "Let’s Encrypt 証明書が ‘{domain}’ にインストールされました", + "certmanager_cert_install_success_selfsigned": "ドメイン「{domain}」に自己署名証明書がインストールされました", + "certmanager_domain_dns_ip_differs_from_public_ip": "ドメイン '{domain}' の DNS レコードは、このサーバーの IP とは異なります。詳細については、診断の「DNSレコード」(基本)カテゴリを確認してください。最近 A レコードを変更した場合は、反映されるまでお待ちください (一部の DNS 伝達チェッカーはオンラインで入手できます)。(何をしているかがわかっている場合は、 '--no-checks'を使用してこれらのチェックをオフにします。", + "certmanager_domain_http_not_working": "ドメイン{domain}はHTTP経由でアクセスできないようです。詳細については、診断の「Web」カテゴリを確認してください。(何をしているかがわかっている場合は、 '--no-checks'を使用してこれらのチェックをオフにします。", + "certmanager_unable_to_parse_self_CA_name": "自己署名機関の名前を解析できませんでした (ファイル: {file})", + "certmanager_domain_not_diagnosed_yet": "ドメイン{domain}の診断結果はまだありません。診断セクションのカテゴリ「DNSレコード」と「Web」の診断を再実行して、ドメインが暗号化の準備ができているかどうかを確認してください。(または、何をしているかがわかっている場合は、「--no-checks」を使用してこれらのチェックをオフにします。", + "confirm_app_insufficient_ram": "危険!このアプリのインストール/アップグレードには{required}RAMが必要ですが、現在利用可能なのは{current}つだけです。このアプリを実行できたとしても、そのインストール/アップグレードプロセスには大量のRAMが必要なため、サーバーがフリーズして惨めに失敗する可能性があります。とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", + "confirm_notifications_read": "警告:続行する前に上記のアプリ通知を確認する必要があります、知っておくべき重要なことがあるかもしれません。[{answers}]", + "custom_app_url_required": "カスタム App をアップグレードするには URL を指定する必要があります{app}", + "danger": "危険:", + "diagnosis_cant_run_because_of_dep": "{dep}に関連する重要な問題がある間、{category}診断を実行できません。", + "diagnosis_description_apps": "アプリケーション", + "diagnosis_description_basesystem": "システム", + "diagnosis_description_dnsrecords": "DNS レコード", + "diagnosis_description_ip": "インターネット接続", + "diagnosis_description_mail": "メールアドレス", + "diagnosis_description_ports": "ポート開放", + "diagnosis_high_number_auth_failures": "最近、疑わしいほど多くの認証失敗が発生しています。fail2banが実行されていて正しく構成されていることを確認するか、https://yunohost.org/security で説明されているようにSSHにカスタムポートを使用することをお勧めします。", + "diagnosis_http_bad_status_code": "サーバーの代わりに別のマシン(おそらくインターネットルーター)が応答したようです。
1.この問題の最も一般的な原因は、ポート80(および443)が サーバーに正しく転送されていないことです。
2.より複雑なセットアップでは、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_http_hairpinning_issue_details": "これはおそらくISPボックス/ルーターが原因です。その結果、ローカルネットワークの外部の人々は期待どおりにサーバーにアクセスできますが、ドメイン名またはグローバルIPを使用する場合、ローカルネットワーク内の人々(おそらくあなたのような人)はアクセスできません。https://yunohost.org/dns_local_network を見ることによって状況を改善できるかもしれません", + "diagnosis_ignored_issues": "(+{nb_ignored}無視された問題)", + "diagnosis_ip_dnsresolution_working": "ドメイン名前解決は機能しています!", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 は通常、システムまたはプロバイダー (使用可能な場合) によって自動的に構成されます。それ以外の場合は、こちらのドキュメントで説明されているように、いくつかのことを手動で構成する必要があります: https://yunohost.org/#/ipv6。", + "diagnosis_ip_not_connected_at_all": "サーバーがインターネットに接続されていないようですね!?", + "diagnosis_ip_weird_resolvconf": "DNS名前解決は機能しているようですが、カスタムされた/etc/resolv.confを使用しているようです。", + "diagnosis_ip_weird_resolvconf_details": "ファイルは/etc/resolv.conf、(dnsmasq)を指す127.0.0.1それ自体への/etc/resolvconf/run/resolv.confシンボリックリンクである必要があります。DNSリゾルバーを手動で設定する場合は、編集/etc/resolv.dnsmasq.confしてください。", + "diagnosis_mail_blacklist_listed_by": "あなたのIPまたはドメイン {item} はブラックリスト {blacklist_name} に登録されています", + "diagnosis_mail_blacklist_ok": "このサーバーが使用するIPとドメインはブラックリストに登録されていないようです", + "diagnosis_mail_ehlo_could_not_diagnose_details": "エラー: {error}", + "diagnosis_mail_fcrdns_ok": "逆引きDNSが正しく構成されています!", + "diagnosis_mail_fcrdns_nok_alternatives_4": "一部のプロバイダーでは、逆引きDNSを構成できません(または機能が壊れている可能性があります…)。そのせいで問題が発生している場合は、次の解決策を検討してください。
- 一部のISPが提供するメールサーバーリレーを使用する ことで代替できますが、ISPが電子メールトラフィックを盗み見る可能性があることを意味します。
- プライバシーに配慮した代替手段は、この種の制限を回避するために*専用のパブリックIP*を持つVPNを使用することです。https://yunohost.org/#/vpn_advantage を見る
-または、別のプロバイダーに切り替えることが可能です", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "一部のプロバイダーは、ネット中立性を気にしないため、送信ポート25のブロックを解除することを許可しません。
-それらのいくつかは 、メールサーバーリレーを使用する 代替手段を提供しますが、リレーが電子メールトラフィックをスパイできることを意味します。
- プライバシーに配慮した代替手段は、*専用のパブリックIP*を持つVPNを使用して、これらの種類の制限を回避することです。https://yunohost.org/#/vpn_advantage を見る
-よりネット中立性に優しいプロバイダーへの切り替えを検討することもできます", + "diagnosis_mail_outgoing_port_25_ok": "SMTP メール サーバーは電子メールを送信できます (送信ポート 25 はブロックされません)。", + "diagnosis_mail_queue_ok": "メールキュー内の保留中のメール{nb_pending}", + "diagnosis_mail_queue_too_big": "メールキュー内の保留中のメールが多すぎます({nb_pending}メール)", + "diagnosis_mail_queue_unavailable": "キュー内の保留中の電子メールの数を調べることはできません", + "diagnosis_mail_queue_unavailable_details": "エラー: {error}", + "diagnosis_no_cache": "カテゴリ '{category}' の診断キャッシュがまだありません", + "diagnosis_ports_forwarding_tip": "この問題を解決するには、ほとんどの場合、https://yunohost.org/isp_box_config で説明されているように、インターネットルーターでポート転送を構成する必要があります", + "diagnosis_ports_needed_by": "このポートの公開は、{category}機能 (サービス {service}) に必要です。", + "diagnosis_ports_ok": "ポート {port} は外部から到達可能です。", + "diagnosis_ports_partially_unreachable": "ポート {port} は、IPv{failed} では外部から到達できません。", + "diagnosis_security_vulnerable_to_meltdown": "Meltdown(重大なセキュリティの脆弱性)に対して脆弱に見えます", + "diagnosis_services_conf_broken": "サービス{service}の構成が壊れています!", + "diagnosis_services_running": "サービス{service}が実行されています!", + "diagnosis_sshd_config_inconsistent": "SSHポートが/ etc / ssh / sshd_configで手動で変更されたようです。YunoHost 4.2以降、手動で構成を編集する必要がないように、新しいグローバル設定「security.ssh.ssh_port」を使用できます。", + "diagnosis_swap_none": "システムにスワップがまったくない。システムのメモリ不足の状況を回避するために、少なくとも {recommended} つのスワップを追加することを検討する必要があります。", + "diagnosis_swap_notsomuch": "システムにはスワップが {total} しかありません。システムのメモリ不足の状況を回避するために、少なくとも {recommended} のスワップを用意することを検討してください。", + "diagnosis_swap_ok": "システムには {total} のスワップがあります!", + "domain_cert_gen_failed": "証明書を生成できませんでした", + "domain_config_acme_eligible": "ACMEの資格", + "domain_config_cert_summary": "証明書の状態", + "domain_config_cert_summary_abouttoexpire": "現在の証明書の有効期限が近づいています。すぐに自動的に更新されるはずです。", + "domain_config_cert_summary_expired": "クリティカル: 現在の証明書が無効です!HTTPSはまったく機能しません!", + "domain_config_cert_validity": "データの入力規則", + "domain_config_xmpp": "インスタント メッセージング (XMPP)", + "domain_dns_conf_is_just_a_recommendation": "このコマンドは、*推奨*構成を表示します。実際にはDNS構成は設定されません。この推奨事項に従って、レジストラーで DNS ゾーンを構成するのはユーザーの責任です。", + "domain_dns_conf_special_use_tld": "このドメインは、.local や .test などの特殊な用途のトップレベル ドメイン (TLD) に基づいているため、実際の DNS レコードを持つことは想定されていません。", + "domain_dns_push_already_up_to_date": "レコードはすでに最新であり、何もする必要はありません。", + "domain_dns_push_failed": "DNS レコードの更新が失敗しました。", + "domain_dyndns_already_subscribed": "すでに DynDNS ドメインにサブスクライブしている", + "dyndns_key_generating": "DNS キーを生成しています...しばらく時間がかかる場合があります。", + "dyndns_key_not_found": "ドメインの DNS キーが見つかりません", + "firewall_reload_failed": "バックアップアーカイブを開けませんでした", + "global_settings_setting_postfix_compatibility_help": "Postfix サーバーの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します", + "global_settings_setting_root_password": "新しい管理者パスワード", + "global_settings_setting_root_password_confirm": "新しい管理者パスワード", + "global_settings_setting_smtp_allow_ipv6": "IPv6 を許可する", + "global_settings_setting_user_strength_help": "これらの要件は、パスワードを初期化または変更する場合にのみ適用されます", + "group_cannot_be_deleted": "グループ{group}を手動で削除することはできません。", + "group_created": "グループ '{group}' が作成されました", + "group_mailalias_add": "メール エイリアス '{mail}' がグループ '{group}' に追加されます。", + "group_mailalias_remove": "メール エイリアス '{mail}' がグループ '{group}' から削除されます。", + "group_no_change": "グループ '{group}' に対して変更はありません", + "group_unknown": "グループ '{group}' は不明です", + "group_user_already_in_group": "ユーザー {user} は既にグループ {group} に所属しています", + "group_user_not_in_group": "ユーザー {user}がグループ {group} にない", + "group_user_remove": "ユーザー '{user}' はグループ '{group}' から削除されます。", + "hook_exec_failed": "スクリプトを実行できませんでした: {path}", + "hook_exec_not_terminated": "スクリプトが正しく終了しませんでした: {path}", + "log_app_install": "‘{}’ アプリをインストールする", + "log_user_permission_update": "アクセス許可 '{}' のアクセスを更新する", + "log_user_update": "ユーザー '{}' の情報を更新する", + "mail_alias_remove_failed": "電子メール エイリアス '{mail}' を削除できませんでした", + "mail_domain_unknown": "ドメイン '{domain}' の電子メール アドレスが無効です。このサーバーによって管理されているドメインを使用してください。", + "mail_forward_remove_failed": "電子メール転送 '{mail}' を削除できませんでした", + "mail_unavailable": "この電子メール アドレスは、管理者グループ用に予約されています", + "migration_0021_start": "Bullseyeへの移行開始", + "migration_0021_yunohost_upgrade": "YunoHostコアのアップグレードを開始しています...", + "migration_description_0026_new_admins_group": "新しい「複数の管理者」システムに移行する", + "migration_ldap_backup_before_migration": "実際の移行の前に、LDAP データベースとアプリ設定のバックアップを作成します。", + "migration_ldap_can_not_backup_before_migration": "移行が失敗する前に、システムのバックアップを完了できませんでした。エラー: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "移行できませんでした...システムをロールバックしようとしています。", + "permission_updated": "アクセス許可 '{permission}' が更新されました", + "restore_confirm_yunohost_installed": "すでにインストールされているシステムを復元しますか?[{answers}]", + "restore_extracting": "アーカイブから必要なファイルを抽出しています...", + "restore_failed": "バックアップを復元する ‘{name}’", + "restore_hook_unavailable": "「{part}」の復元スクリプトは、システムで使用できず、アーカイブでも利用できません", + "restore_not_enough_disk_space": "十分なスペースがありません(スペース:{free_space} B、必要なスペース:{needed_space} B、セキュリティマージン:{margin} B)", + "restore_nothings_done": "何も復元されませんでした", + "restore_removing_tmp_dir_failed": "古い一時ディレクトリを削除できませんでした", + "restore_running_app_script": "アプリ「{app}」を復元しています...", + "restore_running_hooks": "復元フックを実行しています...", + "restore_system_part_failed": "「{part}」システム部分を復元できませんでした", + "root_password_changed": "パスワード確認", + "server_reboot": "サーバーが再起動します", + "server_shutdown_confirm": "サーバーはすぐにシャットダウンしますが、よろしいですか?[{answers}]", + "service_add_failed": "サービス '{service}' を追加できませんでした", + "service_added": "サービス '{service}' が追加されました", + "service_already_started": "サービス '{service}' は既に実行されています", + "service_description_dnsmasq": "ドメイン名解決 (DNS) を処理します。", + "service_description_dovecot": "電子メールクライアントが電子メールにアクセス/フェッチすることを許可します(IMAPおよびPOP3経由)", + "service_description_fail2ban": "インターネットからのブルートフォース攻撃やその他の種類の攻撃から保護します", + "service_description_metronome": "XMPP インスタント メッセージング アカウントを管理する", + "service_description_mysql": "アプリ データの格納 (SQL データベース)", + "service_description_postfix": "電子メールの送受信に使用", + "service_description_postgresql": "アプリ データの格納 (SQL データベース)", + "service_enable_failed": "起動時にサービス '{service}' を自動的に開始できませんでした。\n\n最近のサービスログ:{logs}", + "service_enabled": "サービス '{service}' は、システムの起動時に自動的に開始されるようになりました。", + "service_reloaded": "サービス '{service}' がリロードされました", + "service_not_reloading_because_conf_broken": "構成が壊れているため、サービス「{name}」をリロード/再起動しません:{errors}", + "show_tile_cant_be_enabled_for_regex": "権限 '{permission}' の URL は正規表現であるため、現在 'show_tile' を有効にすることはできません。", + "show_tile_cant_be_enabled_for_url_not_defined": "最初にアクセス許可 '{permission}' の URL を定義する必要があるため、現在 'show_tile' を有効にすることはできません。", + "ssowat_conf_generated": "SSOワット構成の再生成", + "system_upgraded": "システムのアップグレード", + "unlimit": "クォータなし", + "update_apt_cache_failed": "APT (Debian のパッケージマネージャ) のキャッシュを更新できません。問題のある行を特定するのに役立つ可能性のあるsources.list行のダンプを次に示します。\n{sourceslist}", + "update_apt_cache_warning": "APT(Debianのパッケージマネージャー)のキャッシュを更新中に問題が発生しました。問題のある行を特定するのに役立つ可能性のあるsources.list行のダンプを次に示します。\n{sourceslist}", + "admins": "管理者", + "all_users": "YunoHostの全ユーザー", + "already_up_to_date": "何もすることはありません。すべて最新です。", + "app_action_broke_system": "このアクションは、これらの重要なサービスを壊したようです: {services}", + "app_already_installed": "アプリ '{app}' は既にインストール済み", + "app_already_installed_cant_change_url": "このアプリは既にインストールされています。この機能だけではURLを変更することはできません。利用可能な場合は、`app changeurl`を確認してください。", + "app_already_up_to_date": "{app} アプリは既に最新です", + "app_arch_not_supported": "このアプリはアーキテクチャ {required} にのみインストールできますが、サーバーのアーキテクチャは{current} です", + "app_argument_choice_invalid": "引数 '{name}' に有効な値を選択してください: '{value}' は使用可能な選択肢に含まれていません ({choices})", + "app_change_url_no_script": "アプリ「{app_name}」はまだURLの変更をサポートしていません。多分あなたはそれをアップグレードする必要があります。", + "app_change_url_require_full_domain": "{app}は完全なドメイン(つまり、path = /)を必要とするため、この新しいURLに移動できません。", + "app_change_url_success": "{app} URL が{domain}{path}されました", + "app_config_unable_to_apply": "設定パネルの値を適用できませんでした。", + "app_config_unable_to_read": "設定パネルの値の読み取りに失敗しました。", + "app_corrupt_source": "YunoHost はアセット '{source_id}' ({url}) を {app} 用にダウンロードできましたが、アセットのチェックサムが期待されるものと一致しません。これは、あなたのサーバーで一時的なネットワーク障害が発生したか、もしくはアセットがアップストリームメンテナ(または悪意のあるアクター?)によって何らかの形で変更され、YunoHostパッケージャーがアプリマニフェストを調査/更新する必要があることを意味する可能性があります。\n 期待される sha256 チェックサム: {expected_sha256}\n ダウンロードしたsha256チェックサム: {computed_sha256}\n ダウンロードしたファイルサイズ: {size}", + "app_extraction_failed": "インストール ファイルを抽出できませんでした", + "app_failed_to_download_asset": "{app}のアセット「{source_id}」({url})をダウンロードできませんでした:{out}", + "app_install_files_invalid": "これらのファイルはインストールできません", + "app_install_script_failed": "アプリのインストールスクリプト内部でエラーが発生しました", + "app_label_deprecated": "このコマンドは非推奨です。新しいコマンド ’yunohost user permission update’ を使用して、アプリラベルを管理してください。", + "app_location_unavailable": "この URL は利用できないか、既にインストールされているアプリと競合しています。\n{apps}", + "app_make_default_location_already_used": "「{app}」をドメインのデフォルトアプリにすることはできません。「{domain}」は「{other_app}」によってすでに使用されています", + "app_manifest_install_ask_admin": "このアプリの管理者ユーザーを選択する", + "app_manifest_install_ask_domain": "このアプリをインストールするドメインを選択してください", + "app_manifest_install_ask_init_admin_permission": "このアプリの管理機能にアクセスできるのは誰ですか?(これは後で変更できます)", + "app_manifest_install_ask_init_main_permission": "誰がこのアプリにアクセスできる必要がありますか?(これは後で変更できます)", + "app_manifest_install_ask_is_public": "このアプリは匿名の訪問者に公開する必要がありますか?", + "app_not_correctly_installed": "{app}が正しくインストールされていないようです", + "app_not_enough_disk": "このアプリには{required}の空き容量が必要です。", + "app_not_enough_ram": "このアプリのインストール/アップグレードには{required} のRAMが必要ですが、現在利用可能なのは {current} だけです。", + "app_not_installed": "インストールされているアプリのリストに{app}が見つかりませんでした: {all_apps}", + "app_not_upgraded_broken_system": "アプリ「{failed_app}」はアップグレードに失敗し、システムを壊れた状態にしたため、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_not_upgraded_broken_system_continue": "アプリ ’{failed_app}’ はアップグレードに失敗し、システムを壊れた状態にした(そのためcontinue-on-failureは無視されます)ので、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_restore_failed": "{app}を復元できませんでした: {error}", + "app_restore_script_failed": "アプリのリストアスクリプト内でエラーが発生しました", + "app_sources_fetch_failed": "ソースファイルをフェッチできませんでしたが、URLは正しいですか?", + "app_packaging_format_not_supported": "このアプリは、パッケージ形式がYunoHostバージョンでサポートされていないため、インストールできません。おそらく、システムのアップグレードを検討する必要があります。", + "app_remove_after_failed_install": "インストールの失敗後にアプリを削除しています...", + "app_removed": "'{app}' はアンインストール済", + "app_requirements_checking": "{app} の依存関係を確認しています…", + "app_resource_failed": "{app}のリソースのプロビジョニング、プロビジョニング解除、または更新に失敗しました: {error}", + "app_start_backup": "{app}用にバックアップするファイルを収集しています...", + "app_start_install": "‘{app}’ をインストールしています…", + "app_unknown": "未知のアプリ", + "app_unsupported_remote_type": "アプリで使用されている、サポートされないリモートの種類", + "apps_catalog_init_success": "アプリ カタログ システムが初期化されました!", + "apps_catalog_obsolete_cache": "アプリケーションカタログキャッシュが空であるか、古くなっています。", + "apps_catalog_update_success": "アプリケーションカタログを更新しました!", + "apps_catalog_updating": "アプリケーションカタログを更新しています...", + "app_upgrade_app_name": "'{app}' をアップグレードしています…", + "app_upgrade_failed": "アップグレードに失敗しました {app}: {error}", + "app_upgrade_script_failed": "アプリのアップグレードスクリプト内でエラーが発生しました", + "app_upgrade_several_apps": "次のアプリがアップグレードされます: {apps}", + "app_upgrade_some_app_failed": "一部のアプリをアップグレードできませんでした", + "app_upgraded": "'{app}' アップグレード済", + "app_yunohost_version_not_supported": "このアプリは YunoHost >= {required} を必要としますが、現在インストールされているバージョンは{current} です", + "apps_already_up_to_date": "全てのアプリが最新になりました!", + "apps_catalog_failed_to_download": "{apps_catalog} アプリ カタログをダウンロードできません: {error}", + "apps_failed_to_upgrade": "これらのアプリケーションのアップグレードに失敗しました: {apps}", + "apps_failed_to_upgrade_line": "\n * {app_id} (対応するログを表示するには、’yunohost log show {operation_logger_name}’ を実行してください)", + "ask_admin_fullname": "管理者 フルネーム", + "ask_admin_username": "管理者ユーザー名", + "ask_fullname": "フルネーム", + "backup_app_failed": "{app}をバックアップできませんでした", + "backup_applying_method_copy": "すべてのファイルをバックアップにコピーしています...", + "backup_applying_method_custom": "カスタムバックアップメソッド ’{method}’ を呼び出しています...", + "backup_applying_method_tar": "バックアップ TAR アーカイブを作成しています...", + "backup_archive_app_not_found": "バックアップアーカイブに{app}が見つかりませんでした", + "backup_archive_broken_link": "バックアップアーカイブにアクセスできませんでした({path}へのリンクが壊れています)", + "backup_archive_cant_retrieve_info_json": "アーカイブ '{archive}' の情報を読み込めませんでした... info.json ファイルを取得できません (または有効な json ではありません)。", + "backup_archive_writing_error": "圧縮アーカイブ '{archive}' にバックアップするファイル '{source}' (アーカイブ '{dest}' で指定) を追加できませんでした", + "backup_ask_for_copying_if_needed": "一時的に{size}MBを使用してバックアップを実行しますか?(この方法は、より効率的な方法で準備できなかったファイルがあるため、この方法が使用されます。", + "backup_cant_mount_uncompress_archive": "非圧縮アーカイブを書き込み保護としてマウントできませんでした", + "backup_cleaning_failed": "一時バックアップフォルダをクリーンアップできませんでした", + "backup_copying_to_organize_the_archive": "アーカイブを整理するために{size}MBをコピーしています", + "backup_couldnt_bind": "{src}を{dest}にバインドできませんでした。", + "backup_create_size_estimation": "アーカイブには約{size}のデータが含まれます。", + "backup_created": "バックアップを作成しました: {name}'", + "backup_creation_failed": "バックアップ作成できませんでした", + "backup_csv_addition_failed": "バックアップするファイルをCSVファイルに追加できませんでした", + "backup_csv_creation_failed": "復元に必要な CSV ファイルを作成できませんでした", + "backup_custom_backup_error": "カスタムバックアップ方法は「バックアップ」ステップを通過できませんでした", + "backup_custom_mount_error": "カスタムバックアップ方法は「マウント」ステップを通過できませんでした", + "backup_delete_error": "‘{path}’ を削除する", + "backup_deleted": "バックアップは削除されました: {name}", + "backup_nothings_done": "保存するものがありません", + "backup_output_directory_forbidden": "別の出力ディレクトリを選択します。バックアップは、/bin、/boot、/dev、/etc、/lib、/root、/run、/sbin、/sys、/usr、/var、または/home/yunohost.backup/archives のサブフォルダには作成できません", + "backup_output_directory_not_empty": "空の出力ディレクトリを選択する必要があります", + "backup_output_directory_required": "バックアップ用の出力ディレクトリを指定する必要があります", + "backup_hook_unknown": "バックアップ フック '{hook}' が不明です", + "backup_method_copy_finished": "バックアップコピーがファイナライズされました", + "backup_method_tar_finished": "TARバックアップアーカイブが作成されました", + "backup_output_symlink_dir_broken": "アーカイブディレクトリ '{path}' は壊れたシンボリックリンクです。たぶん、あなたはそれが指す記憶媒体を再/マウントまたは差し込むのを忘れました。", + "backup_mount_archive_for_restore": "復元のためにアーカイブを準備しています...", + "backup_no_uncompress_archive_dir": "そのような圧縮されていないアーカイブディレクトリはありません", + "certmanager_warning_subdomain_dns_record": "サブドメイン '{subdomain}' は '{domain}' と同じ IP アドレスに解決されません。一部の機能は、これを修正して証明書を再生成するまで使用できません。", + "config_action_disabled": "アクション '{action}' は無効になっているため実行できませんでした。制約を満たしていることを確認してください。ヘルプ: {help}", + "backup_permission": "{app}のバックアップ権限", + "backup_running_hooks": "バックアップフックを実行しています...", + "backup_system_part_failed": "「{part}」システム部分をバックアップできませんでした", + "backup_unable_to_organize_files": "簡単な方法を使用してアーカイブ内のファイルを整理できませんでした", + "backup_with_no_backup_script_for_app": "アプリ「{app}」にはバックアップスクリプトがありません。無視。", + "backup_with_no_restore_script_for_app": "{app}には復元スクリプトがないため、このアプリのバックアップを自動的に復元することはできません。", + "certmanager_acme_not_configured_for_domain": "ACMEチャレンジは、nginx confに対応するコードスニペットがないため、現在{domain}実行できません...'yunohost tools regen-conf nginx --dry-run --with-diff' を使用して、nginx の設定が最新であることを確認してください。", + "certmanager_attempt_to_renew_nonLE_cert": "ドメイン '{domain}' の証明書は、Let's Encryptによって発行されていません。自動的に更新できません!", + "certmanager_attempt_to_renew_valid_cert": "ドメイン '{domain}' の証明書の有効期限が近づいていません。(あなたが何をしているのかわかっている場合は、--forceを使用できます)", + "certmanager_cert_renew_failed": "{domains}のLet’s Encrypt 証明書更新に失敗しました", + "certmanager_cert_renew_success": "{domains}のLet’s Encrypt 証明書が更新されました", + "certmanager_cert_signing_failed": "新しい証明書に署名できませんでした", + "certmanager_certificate_fetching_or_enabling_failed": "{domain}に新しい証明書を使用しようとしましたが、機能しませんでした...", + "certmanager_domain_cert_not_selfsigned": "ドメイン {domain} の証明書は自己署名されていません。置き換えてよろしいですか(これを行うには '--force' を使用してください)?", + "certmanager_hit_rate_limit": "最近{domain}、この正確なドメインのセットに対して既に発行されている証明書が多すぎます。しばらくしてからもう一度お試しください。詳細については、https://letsencrypt.org/docs/rate-limits/ を参照してください。", + "certmanager_no_cert_file": "ドメイン {domain} (ファイル: {file}) の証明書ファイルを読み取れませんでした。", + "certmanager_self_ca_conf_file_not_found": "自己署名機関の設定ファイルが見つかりませんでした(ファイル:{file})", + "config_forbidden_readonly_type": "型 '{type}' は読み取り専用として設定できず、別の型を使用してこの値をレンダリングします (関連する引数 ID: '{id}')。", + "config_no_panel": "設定パネルが見つかりません。", + "config_unknown_filter_key": "フィルター キー '{filter_key}' が正しくありません。", + "config_validate_color": "有効な RGB 16 進色である必要があります", + "config_validate_date": "YYYY-MM-DD の形式のような有効な日付である必要があります。", + "config_validate_email": "有効なメールアドレスである必要があります", + "config_action_failed": "アクション '{action}' の実行に失敗しました: {error}", + "config_apply_failed": "新しい構成の適用に失敗しました: {error}", + "config_cant_set_value_on_section": "構成セクション全体に 1 つの値を設定することはできません。", + "config_forbidden_keyword": "キーワード '{keyword}' は予約されており、この ID の質問を含む設定パネルを作成または使用することはできません。", + "config_validate_time": "HH:MM のような有効な時刻である必要があります", + "config_validate_url": "有効なウェブ URL である必要があります", + "confirm_app_install_danger": "危険!このアプリはまだ実験的であることが知られています(明示的に動作していない場合)!あなたが何をしているのかわからない限り、おそらくそれをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません...とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", + "confirm_app_install_thirdparty": "危険!このアプリはYunoHostのアプリカタログの一部ではありません。サードパーティのアプリをインストールすると、システムの整合性とセキュリティが損なわれる可能性があります。あなたが何をしているのかわからない限り、おそらくそれをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません...とにかくそのリスクを冒しても構わないと思っているなら、「{answers}」と入力してください", + "confirm_app_install_warning": "警告:このアプリは動作する可能性がありますが、YunoHostにうまく統合されていません。シングル サインオンやバックアップ/復元などの一部の機能は使用できない場合があります。とにかくインストールしますか?[{answers}] ", + "diagnosis_apps_allgood": "インストールされているすべてのアプリは、基本的なパッケージ化プラクティスを尊重します", + "diagnosis_apps_bad_quality": "このアプリケーションは現在、YunoHostのアプリケーションカタログで壊れているとフラグが付けられています。これは、メンテナが問題を修正しようとしている間の一時的な問題である可能性があります。それまでの間、このアプリのアップグレードは無効になります。", + "diagnosis_apps_broken": "このアプリケーションは現在、YunoHostのアプリケーションカタログで壊れているとフラグが付けられています。これは、メンテナが問題を修正しようとしている間の一時的な問題である可能性があります。それまでの間、このアプリのアップグレードは無効になります。", + "diagnosis_apps_deprecated_practices": "このアプリのインストール済みバージョンでは、非常に古い非推奨のパッケージ化プラクティスがまだ使用されています。あなたは本当にそれをアップグレードすることを検討する必要があります。", + "diagnosis_basesystem_hardware": "サーバーのハードウェア アーキテクチャが{virt} {arch}", + "diagnosis_basesystem_hardware_model": "サーバーモデルが{model}", + "diagnosis_apps_issue": "アプリ '{app}' をアップグレードする", + "diagnosis_apps_not_in_app_catalog": "このアプリケーションは、YunoHostのアプリケーションカタログにはありません。過去に存在し、削除された場合は、アップグレードを受け取らず、システムの整合性とセキュリティが損なわれる可能性があるため、このアプリのアンインストールを検討する必要があります。", + "diagnosis_apps_outdated_ynh_requirement": "このアプリのインストール済みバージョンには、yunohost >= 2.xまたは3.xのみが必要であり、推奨されるパッケージングプラクティスとヘルパーが最新ではないことを示す傾向があります。あなたは本当にそれをアップグレードすることを検討する必要があります。", + "diagnosis_backports_in_sources_list": "apt(パッケージマネージャー)はバックポートリポジトリを使用するように構成されているようです。あなたが何をしているのか本当にわからない限り、バックポートからパッケージをインストールすることは、システムに不安定性や競合を引き起こす可能性があるため、強くお勧めしません。", + "diagnosis_basesystem_host": "サーバは Debian {debian_version} を実行しています", + "diagnosis_basesystem_kernel": "サーバーはLinuxカーネル{kernel_version}を実行しています", + "diagnosis_basesystem_ynh_inconsistent_versions": "一貫性のないバージョンのYunoHostパッケージを実行しています...ほとんどの場合、アップグレードの失敗または部分的なことが原因です。", + "diagnosis_basesystem_ynh_main_version": "サーバーがYunoHost{main_version}を実行しています({repo})", + "diagnosis_basesystem_ynh_single_version": "{package}バージョン:{version}({repo})", + "diagnosis_cache_still_valid": "(キャッシュは{category}診断に有効です。まだ再診断しません!", + "diagnosis_description_regenconf": "システム設定", + "diagnosis_description_services": "サービスステータスチェック", + "diagnosis_description_systemresources": "システムリソース", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "ストレージ<0>(デバイス<1>上)には、( )残りの領域({free_percent} )しかありません{free}。{total}注意してください。", + "diagnosis_diskusage_ok": "ストレージ<0>(デバイス<1>上)にはまだ({free_percent}%)スペースが{free}残っています(から{total})!", + "diagnosis_diskusage_verylow": "ストレージ<0>(デバイス<1>上)には、( )残りの領域({free_percent} )しかありません{free}。{total}あなたは本当にいくつかのスペースをきれいにすることを検討する必要があります!", + "diagnosis_display_tip": "見つかった問題を確認するには、ウェブ管理者の診断セクションに移動するか、コマンドラインから「yunohost診断ショー--問題--人間が読める」を実行します。", + "diagnosis_dns_bad_conf": "一部の DNS レコードが見つからないか、ドメイン {domain} (カテゴリ {category}) が正しくない", + "diagnosis_dns_discrepancy": "次の DNS レコードは、推奨される構成に従っていないようです。
種類: <0>
名前: <1>
現在の値: <2>
期待値: <3>", + "diagnosis_dns_good_conf": "DNS レコードがドメイン {domain} (カテゴリ {category}) 用に正しく構成されている", + "diagnosis_dns_missing_record": "推奨される DNS 構成に従って、次の情報を含む DNS レコードを追加する必要があります。
種類: <0>
名前: <1>
価値: <2>", + "diagnosis_dns_point_to_doc": "DNS レコードの構成についてサポートが必要な場合は 、https://yunohost.org/dns_config のドキュメントを確認してください。", + "diagnosis_dns_specialusedomain": "ドメイン {domain} は、.local や .test などの特殊な用途のトップレベル ドメイン (TLD) に基づいているため、実際の DNS レコードを持つことは想定されていません。", + "diagnosis_dns_try_dyndns_update_force": "このドメインのDNS設定は、YunoHostによって自動的に管理されます。そうでない場合は、 yunohost dyndns update --force を使用して更新を強制することができます。", + "diagnosis_domain_expiration_error": "一部のドメインはすぐに期限切れになります!", + "diagnosis_failed_for_category": "カテゴリ '{category}' の診断に失敗しました: {error}", + "diagnosis_domain_expiration_not_found": "一部のドメインの有効期限を確認できない", + "diagnosis_domain_expiration_not_found_details": "ドメイン{domain}のWHOIS情報に有効期限に関する情報が含まれていないようですね?", + "diagnosis_found_errors": "{category}に関連する{errors}重大な問題が見つかりました!", + "diagnosis_domain_expiration_success": "ドメインは登録されており、すぐに期限切れになることはありません。", + "diagnosis_domain_expiration_warning": "一部のドメインはまもなく期限切れになります!", + "diagnosis_domain_expires_in": "{domain} の有効期限は {days}日です。", + "diagnosis_found_errors_and_warnings": "{category}に関連する重大な問題が{errors}(および{warnings}の警告)見つかりました!", + "diagnosis_found_warnings": "{category}{warnings}改善できるアイテムが見つかりました。", + "diagnosis_domain_not_found_details": "ドメイン{domain}がWHOISデータベースに存在しないか、有効期限が切れています!", + "diagnosis_everything_ok": "{category}はすべて大丈夫そうです!", + "diagnosis_failed": "カテゴリ '{category}' の診断結果を取得できませんでした: {error}", + "diagnosis_http_connection_error": "接続エラー: 要求されたドメインに接続できませんでした。到達できない可能性が非常に高いです。", + "diagnosis_http_could_not_diagnose": "ドメインが IPv{ipversion} の外部から到達可能かどうかを診断できませんでした。", + "diagnosis_http_could_not_diagnose_details": "エラー: {error}", + "diagnosis_http_hairpinning_issue": "ローカルネットワークでヘアピニングが有効になっていないようです。", + "diagnosis_http_nginx_conf_not_up_to_date": "このドメインのnginx設定は手動で変更されたようで、YunoHostがHTTPで到達可能かどうかを診断できません。", + "diagnosis_http_nginx_conf_not_up_to_date_details": "状況を修正するには、コマンドラインからの違いを調べて、 yunohostツールregen-conf nginx --dry-run --with-diff を使用し、問題がない場合は、 yunohostツールregen-conf nginx --forceで変更を適用します。", + "diagnosis_http_ok": "ドメイン {domain} は、ローカル ネットワークの外部から HTTP 経由で到達できます。", + "diagnosis_http_partially_unreachable": "ドメイン {domain} は、IPv{passed} では機能しますが、IPv{failed} ではローカル ネットワークの外部から HTTP 経由で到達できないように見えます。", + "diagnosis_http_special_use_tld": "ドメイン {domain} は、.local や .test などの特殊な用途のトップレベル ドメイン (TLD) に基づいているため、ローカル ネットワークの外部に公開されることは想定されていません。", + "diagnosis_http_timeout": "外部からサーバーに接続しようとしているときにタイムアウトしました。到達できないようです。
1.この問題の最も一般的な原因は、ポート80(および443)が サーバーに正しく転送されていないことです。
2. サービスnginxが実行されていることも確認する必要があります
3.より複雑なセットアップでは、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_http_unreachable": "ドメイン {domain} は、ローカル ネットワークの外部から HTTP 経由で到達できないように見えます。", + "diagnosis_ip_broken_dnsresolution": "ドメイン名の解決が何らかの理由で壊れているようです...ファイアウォールはDNSリクエストをブロックしていますか?", + "diagnosis_ip_broken_resolvconf": "ドメインの名前解決がサーバー上で壊れているようですが、これは/etc/resolv.conf127.0.0.1を指定していないことに関連しているようです。", + "diagnosis_ip_connected_ipv4": "サーバーはIPv4経由でインターネットに接続されています!", + "diagnosis_ip_connected_ipv6": "サーバーはIPv6経由でインターネットに接続されています!", + "diagnosis_ip_global": "グローバルIP: {global}", + "diagnosis_ip_local": "ローカル IP: {local}", + "diagnosis_ip_no_ipv4": "サーバーに機能している IPv4 がありません。", + "diagnosis_ip_no_ipv6": "サーバーに機能している IPv6 がありません。", + "diagnosis_ip_no_ipv6_tip": "IPv6を機能させることは、サーバーが機能するために必須ではありませんが、インターネット全体の健全性にとってはより良いことです。IPv6 は通常、システムまたはプロバイダー (使用可能な場合) によって自動的に構成されます。それ以外の場合は、こちらのドキュメントで説明されているように、いくつかのことを手動で構成する必要があります。 https://yunohost.org/#/ipv6。IPv6を有効にできない場合、または技術的に難しすぎると思われる場合は、この警告を無視しても問題ありません。", + "diagnosis_mail_blacklist_reason": "ブラックリストの登録理由は次のとおりです: {reason}", + "diagnosis_mail_blacklist_website": "リストされている理由を特定して修正した後、IPまたはドメインを削除するように依頼してください: {blacklist_website}", + "diagnosis_mail_ehlo_bad_answer": "SMTP 以外のサービスが IPv{ipversion} のポート 25 で応答しました", + "diagnosis_mail_ehlo_bad_answer_details": "あなたのサーバーの代わりに別のマシンが応答していることが原因である可能性があります。", + "diagnosis_mail_ehlo_could_not_diagnose": "メール サーバ(postfix)が IPv{ipversion} の外部から到達可能かどうかを診断できませんでした。", + "diagnosis_mail_ehlo_ok": "SMTPメールサーバーは外部から到達可能であるため、電子メールを受信できます!", + "diagnosis_mail_ehlo_unreachable": "SMTP メール サーバは、IPv{ipversion} の外部から到達できません。メールを受信できません。", + "diagnosis_mail_ehlo_unreachable_details": "ポート 25 で IPv{ipversion} のサーバーへの接続を開くことができませんでした。到達できないようです。
1.この問題の最も一般的な原因は、ポート25 がサーバーに正しく転送されていないことです。
2. また、サービス接尾辞が実行されていることも確認する必要があります。
3.より複雑なセットアップでは、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_mail_ehlo_wrong": "別の SMTP メール サーバーが IPv{ipversion} で応答します。サーバーはおそらく電子メールを受信できないでしょう。", + "diagnosis_mail_ehlo_wrong_details": "リモート診断ツールが IPv{ipversion} で受信した EHLO は、サーバーのドメインとは異なります。
受信したEHLO: <1>
期待: <2>
この問題の最も一般的な原因は、ポート 25 が サーバーに正しく転送されていないことです。または、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "逆引き DNS が IPv{ipversion} 用に正しく構成されていません。一部のメールは配信されないか、スパムとしてフラグが立てられる場合があります。", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "現在の逆引きDNS: <0>
期待値: <1>", + "diagnosis_mail_fcrdns_dns_missing": "IPv{ipversion} では逆引き DNS は定義されていません。一部のメールは配信されないか、スパムとしてフラグが立てられる場合があります。", + "diagnosis_mail_fcrdns_nok_alternatives_6": "一部のプロバイダーでは、逆引きDNSを構成できません(または機能が壊れている可能性があります...)。逆引きDNSがIPv4用に正しく設定されている場合は、 yunohost設定email.smtp.smtp_allow_ipv6-vオフに設定して、メールを送信するときにIPv6の使用を無効にしてみてください。注:この最後の解決策は、そこにあるいくつかのIPv6専用サーバーから電子メールを送受信できないことを意味します。", + "diagnosis_mail_fcrdns_nok_details": "まず、インターネットルーターインターフェイスまたはホスティングプロバイダーインターフェイスで <0> 逆引きDNSを構成してみてください。(一部のホスティングプロバイダーでは、このためのサポートチケットを送信する必要がある場合があります)。", + "diagnosis_mail_outgoing_port_25_blocked": "送信ポート 25 が IPv{ipversion} でブロックされているため、SMTP メール サーバーは他のサーバーに電子メールを送信できません。", + "diagnosis_mail_outgoing_port_25_blocked_details": "まず、インターネットルーターインターフェイスまたはホスティングプロバイダーインターフェイスの送信ポート25のブロックを解除する必要があります。(一部のホスティングプロバイダーでは、このために問い合わせを行う必要がある場合があります)。", + "diagnosis_never_ran_yet": "このサーバーは最近セットアップされたようで、表示する診断レポートはまだありません。Web管理画面またはコマンドラインから ’yunohost diagnosis run’ を実行して、完全な診断を実行することから始める必要があります。", + "diagnosis_package_installed_from_sury": "一部のシステムパッケージはダウングレードする必要があります", + "diagnosis_processes_killed_by_oom_reaper": "一部のプロセスは、メモリが不足したため、最近システムによって強制終了されました。これは通常、システム上のメモリ不足、またはプロセスがメモリを消費しすぎていることを示しています。強制終了されたプロセスの概要:\n{kills_summary}", + "diagnosis_ram_low": "システムには{available}({available_percent}%)の使用可能なRAMがあります({total}のうち)。注意してください。", + "diagnosis_package_installed_from_sury_details": "一部のパッケージは、Suryと呼ばれるサードパーティのリポジトリから誤ってインストールされました。YunoHostチームはこれらのパッケージを処理する戦略を改善しましたが、Stretchを使用している間にPHP7.3アプリをインストールした一部のセットアップには、いくつかの矛盾が残っていると予想されます。この状況を修正するには、次のコマンドを実行してみてください。 {cmd_to_fix}", + "diagnosis_ports_could_not_diagnose": "IPv{ipversion} で外部からポートに到達できるかどうかを診断できませんでした。", + "diagnosis_ports_could_not_diagnose_details": "エラー: {error}", + "diagnosis_ports_unreachable": "ポート {port} は外部から到達できません。", + "diagnosis_regenconf_allgood": "すべての構成ファイルは、推奨される構成と一致しています!", + "diagnosis_regenconf_manually_modified": "{file} 構成ファイルが手動で変更されたようです。", + "diagnosis_regenconf_manually_modified_details": "あなたが何をしているのかを知っていれば、これはおそらく大丈夫です!YunoHostはこのファイルの自動更新を停止します... ただし、YunoHostのアップグレードには重要な推奨変更が含まれている可能性があることに注意してください。必要に応じて、yunohost tools regen-conf {category} --dry-run --with-diffで違いを調べ、yunohost tools regen-conf {category} --forceを使用して推奨構成に強制的にリセットすることができます", + "diagnosis_rootfstotalspace_critical": "ルートファイルシステムには合計{space}しかありませんが、これは非常に心配な値です!ディスク容量がすぐに枯渇する可能性があります。ルートファイルシステム用には少なくとも16GBを用意することをお勧めします。", + "diagnosis_ram_ok": "システムには、{total}のうち{available} ({available_percent}%) の RAM がまだ使用可能です。", + "diagnosis_ram_verylow": "システムには{available}({available_percent}%)のRAMしか使用できません。({total}のうち)", + "diagnosis_rootfstotalspace_warning": "ルートファイルシステムには合計{space}しかありません。これは問題ないかもしれませんが、最終的にはディスク容量がすぐに枯渇する可能性があるため、注意してください... ルートファイルシステム用に少なくとも16GBを用意することをお勧めします。", + "diagnosis_security_vulnerable_to_meltdown_details": "これを修正するには、システムをアップグレードして再起動し、新しいLinuxカーネルをロードする必要があります(または、これが機能しない場合はサーバープロバイダーに連絡してください)。詳細については、https://meltdownattack.com/ を参照してください。", + "diagnosis_services_bad_status": "サービス{service} のステータスは {status} です :(", + "diagnosis_services_bad_status_tip": "サービスの再起動を試みることができ、それが機能しない場合は、webadminのサービスログを確認してください(コマンドラインから、yunohostサービスの再起動{service}とyunohostサービスログ{service}を使用してこれを行うことができます)。。", + "diagnosis_sshd_config_inconsistent_details": "security.ssh.ssh_port -v YOUR_SSH_PORT に設定された yunohost 設定を実行して SSH ポートを定義し、yunohost tools regen-conf ssh --dry-run --with-diff および yunohost tools regen-conf ssh --force をチェックして、会議を YunoHost の推奨事項にリセットしてください。", + "diagnosis_sshd_config_insecure": "SSH構成は手動で変更されたようで、許可されたユーザーへのアクセスを制限するための「許可グループ」または「許可ユーザー」ディレクティブが含まれていないため、安全ではありません。", + "diagnosis_swap_tip": "サーバーがSDカードまたはSSDストレージでスワップをホストしている場合、デバイスの平均寿命が大幅に短くなる可能性があることに注意してください。", + "diagnosis_unknown_categories": "次のカテゴリは不明です: {categories}", + "diagnosis_using_stable_codename": "apt (システムのパッケージマネージャ) は現在、現在の Debian バージョン (bullseye) のコードネームではなく、コードネーム 'stable' からパッケージをインストールするように設定されています。", + "disk_space_not_sufficient_install": "このアプリケーションをインストールするのに十分なディスク領域が残っていません", + "diagnosis_using_stable_codename_details": "これは通常、ホスティングプロバイダーからの構成が正しくないことが原因です。なぜなら、Debian の次のバージョンが新しい「安定版」になるとすぐに、apt は適切な移行手順を経ずにすべてのシステムパッケージをアップグレードしたくなるからです。ベース Debian リポジトリの apt ソースを編集してこれを修正し、安定版キーワードを bullseye に置き換えることをお勧めします。対応する設定ファイルは /etc/apt/sources.list、または /etc/apt/sources.list.d/ 内のファイルでなければなりません。", + "diagnosis_using_yunohost_testing": "apt (システムのパッケージマネージャー)は現在、YunoHostコアの「テスト」アップグレードをインストールするように構成されています。", + "diagnosis_using_yunohost_testing_details": "自分が何をしているのかを知っていれば、これはおそらく問題ありませんが、YunoHostのアップグレードをインストールする前にリリースノートに注意してください!「テスト版」のアップグレードを無効にしたい場合は、/etc/apt/sources.list.d/yunohost.list から testing キーワードを削除する必要があります。", + "disk_space_not_sufficient_update": "このアプリケーションを更新するのに十分なディスク領域が残っていません", + "domain_cannot_add_muc_upload": "「muc.」で始まるドメインを追加することはできません。この種の名前は、YunoHostに統合されたXMPPマルチユーザーチャット機能のために予約されています。", + "domain_cannot_add_xmpp_upload": "「xmpp-upload」で始まるドメインを追加することはできません。この種の名前は、YunoHostに統合されたXMPPアップロード機能のために予約されています。", + "domain_cannot_remove_main": "'{domain}'はメインドメインなので削除できないので、まず「yunohost domain main-domain -n」を使用して別のドメインをメインドメインとして設定する必要があります。 候補 ドメインのリストは次のとおりです。 {other_domains}", + "domain_config_api_protocol": "API プロトコル", + "domain_cannot_remove_main_add_new_one": "「{domain}」はメインドメインであり唯一のドメインであるため、最初に「yunohostドメイン追加」を使用して別のドメインを追加し、次に「yunohostドメインメインドメイン-n 」を使用してメインドメインとして設定し、「yunohostドメイン削除{domain}」を使用してドメイン「{domain}」を削除する必要があります。", + "domain_config_acme_eligible_explain": "このドメインは、Let's Encrypt証明書の準備ができていないようです。DNS 構成と HTTP サーバーの到達可能性を確認してください。 診断ページの 「DNSレコード」と「Web」セクションは、何が誤って構成されているかを理解するのに役立ちます。", + "domain_config_auth_application_key": "アプリケーションキー", + "domain_config_auth_application_secret": "アプリケーション秘密鍵", + "domain_config_auth_consumer_key": "消費者キー", + "domain_config_auth_entrypoint": "API エントリ ポイント", + "domain_config_default_app": "デフォルトのアプリ", + "domain_config_default_app_help": "このドメインを開くと、ユーザーは自動的にこのアプリにリダイレクトされます。アプリが指定されていない場合、ユーザーはユーザーポータルのログインフォームにリダイレクトされます。", + "domain_config_mail_in": "受信メール", + "domain_config_auth_key": "認証キー", + "domain_config_auth_secret": "認証シークレット", + "domain_config_auth_token": "認証トークン", + "domain_config_cert_install": "Let's Encrypt証明書をインストールする", + "domain_config_cert_issuer": "証明機関", + "domain_config_cert_no_checks": "診断チェックを無視する", + "domain_config_cert_renew": "Let’s Encrypt証明書を更新する", + "domain_config_cert_renew_help": "証明書は、有効期間の最後の 15 日間に自動的に更新されます。必要に応じて手動で更新できます(推奨されません)。", + "domain_config_cert_summary_letsencrypt": "やった!有効なLet's Encrypt証明書を使用しています!", + "domain_config_cert_summary_ok": "さて、現在の証明書は良さそうです!", + "domain_config_cert_summary_selfsigned": "警告: 現在の証明書は自己署名です。ブラウザは新しい訪問者に不気味な警告を表示します!", + "domain_config_mail_out": "送信メール", + "domain_config_xmpp_help": "注意: 一部のXMPP機能では、DNSレコードを更新し、Lets Encrypt 証明書を再生成して有効にする必要があります", + "domain_created": "作成されたドメイン", + "domain_creation_failed": "ドメイン {domain}を作成できません: {error}", + "domain_deleted": "ドメインが削除されました", + "domain_deletion_failed": "ドメイン {domain}を削除できません: {error}", + "domain_dns_push_failed_to_authenticate": "ドメイン '{domain}' のレジストラーの API で認証に失敗しました。おそらく資格情報が正しくないようです?(エラー: {error})", + "domain_dns_push_failed_to_list": "レジストラの API を使用して現在のレコードを一覧表示できませんでした: {error}", + "domain_dns_push_not_applicable": "自動 DNS 構成機能は、ドメイン {domain}には適用されません。https://yunohost.org/dns_config のドキュメントに従って、DNS レコードを手動で構成する必要があります。", + "domain_dns_push_managed_in_parent_domain": "自動 DNS 構成機能は、親ドメイン {parent_domain}で管理されます。", + "domain_dns_push_partial_failure": "DNS レコードが部分的に更新されました: いくつかの警告/エラーが報告されました。", + "domain_dns_push_record_failed": "{action} {type}/{name} の記録に失敗しました: {error}", + "domain_dns_push_success": "DNS レコードが更新されました!", + "domain_dns_pushing": "DNS レコードをプッシュしています...", + "domain_dns_registrar_experimental": "これまでのところ、**{registrar}**のAPIとのインターフェースは、YunoHostコミュニティによって適切にテストおよびレビューされていません。サポートは**非常に実験的**です-注意してください!", + "domain_dns_registrar_managed_in_parent_domain": "このドメインは{parent_domain_link}のサブドメインです。DNS レジストラーの構成は、{parent_domain}の設定パネルで管理する必要があります。", + "domain_dns_registrar_not_supported": "YunoHost は、このドメインを処理するレジストラを自動的に検出できませんでした。DNS レコードは、https://yunohost.org/dns のドキュメントに従って手動で構成する必要があります。", + "domain_dns_registrar_supported": "YunoHost は、このドメインがレジストラ**{registrar}**によって処理されていることを自動的に検出しました。必要に応じて、適切なAPI資格情報を提供すると、YunoHostはこのDNSゾーンを自動的に構成します。API 資格情報の取得方法に関するドキュメントは、https://yunohost.org/registar_api_{registrar} ページにあります。(https://yunohost.org/dns のドキュメントに従ってDNSレコードを手動で構成することもできます)", + "domain_dns_registrar_yunohost": "このドメインは nohost.me / nohost.st / ynh.fr であるため、そのDNS構成は、それ以上の構成なしでYunoHostによって自動的に処理されます。(「YunoHost Dyndns Update」コマンドを参照)", + "domain_dyndns_root_unknown": "'{domain}' ドメインのルートを '{name}' にリダイレクト", + "domain_exists": "この名前のバックアップアーカイブはすでに存在します。", + "domain_hostname_failed": "新しいホスト名を設定できません。これにより、後で問題が発生する可能性があります(問題ない可能性があります)。", + "domain_registrar_is_not_configured": "レジストラーは、ドメイン {domain} 用にまだ構成されていません。", + "domain_remove_confirm_apps_removal": "このドメインを削除すると、これらのアプリケーションが削除されます。\n{apps}\n\nよろしいですか?[{answers}]", + "domain_uninstall_app_first": "これらのアプリケーションは、ドメインに引き続きインストールされます。\n{apps}\n\nドメインの削除に進む前に、「yunohostアプリ削除the_app_id」を使用してアンインストールするか、「yunohostアプリ変更URL the_app_id」を使用して別のドメインに移動してください。", + "domain_unknown": "ドメイン {domain}を作成できません: {error}", + "domains_available": "ドメイン管理", + "done": "完了", + "downloading": "ダウンロード中...", + "dpkg_is_broken": "dpkg / APT(システムパッケージマネージャー)が壊れた状態にあるように見えるため、現在はこれを行うことができません...SSH経由で接続し、 'sudo apt install --fix-broken'および/または 'sudo dpkg --configure -a'および/または 'sudo dpkg --audit'を実行することで、この問題を解決しようとすることができます。", + "dpkg_lock_not_available": "別のプログラムがdpkg(システムパッケージマネージャー)のロックを使用しているように見えるため、このコマンドは現在実行できません", + "dyndns_could_not_check_available": "{domain}{provider}で利用できるかどうかを確認できませんでした。", + "dyndns_ip_update_failed": "IP アドレスを DynDNS に更新できませんでした", + "dyndns_ip_updated": "DynDNSでIPを更新しました", + "dyndns_no_domain_registered": "このカテゴリーにログが登録されていません", + "dyndns_provider_unreachable": "DynDNSプロバイダー{provider}に到達できません:YunoHostがインターネットに正しく接続されていないか、ダイネットサーバーがダウンしています。", + "dyndns_registered": "このカテゴリーにログが登録されていません", + "dyndns_registration_failed": "DynDNS ドメインを登録できませんでした: {error}", + "dyndns_unavailable": "ドメイン {domain}を作成できません: {error}", + "dyndns_domain_not_provided": "DynDNS プロバイダー{provider}ドメイン{domain}を提供できません。", + "extracting": "抽出。。。", + "field_invalid": "フィールドは必要です。", + "file_does_not_exist": "ファイル {path}が存在しません。", + "firewall_reloaded": "ファイアウォールがリロードされました", + "firewall_rules_cmd_failed": "一部のファイアウォール規則コマンドが失敗しました。ログの詳細情報。", + "global_settings_reset_success": "グローバルIP: {global}", + "global_settings_setting_admin_strength": "管理者パスワードの強度要件", + "global_settings_setting_admin_strength_help": "これらの要件は、パスワードを初期化または変更する場合にのみ適用されます", + "global_settings_setting_backup_compress_tar_archives": "バックアップの圧縮", + "global_settings_setting_backup_compress_tar_archives_help": "新しいバックアップを作成するときは、圧縮されていないアーカイブ (.tar) ではなく、アーカイブを圧縮 (.tar.gz) します。注意:このオプションを有効にすると、バックアップアーカイブの作成が軽くなりますが、最初のバックアップ手順が大幅に長くなり、CPUに負担がかかります。", + "global_settings_setting_dns_exposure": "DNS の構成と診断で考慮すべき IP バージョン", + "global_settings_setting_dns_exposure_help": "注意:これは、推奨されるDNS構成と診断チェックにのみ影響します。これはシステム構成には影響しません。", + "global_settings_setting_nginx_compatibility": "NGINXの互換性", + "global_settings_setting_nginx_compatibility_help": "WebサーバーNGINXの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します", + "global_settings_setting_nginx_redirect_to_https": "HTTPSを強制", + "global_settings_setting_nginx_redirect_to_https_help": "デフォルトでHTTPリクエストをHTTPにリダイレクトします(あなたが何をしているのか本当にわからない限り、オフにしないでください!", + "global_settings_setting_passwordless_sudo": "管理者がパスワードを再入力せずに「sudo」を使用できるようにする", + "global_settings_setting_portal_theme_help": "カスタム ポータル テーマの作成の詳細については、https://yunohost.org/theming を参照してください。", + "global_settings_setting_postfix_compatibility": "後置の互換性", + "global_settings_setting_pop3_enabled": "POP3 を有効にする", + "global_settings_setting_pop3_enabled_help": "メール サーバーの POP3 プロトコルを有効にする", + "global_settings_setting_portal_theme": "ユーザーポータルでタイルに表示する", + "global_settings_setting_root_access_explain": "Linux システムでは、「ルート」が絶対管理者です。YunoHost のコンテキストでは、サーバーのローカルネットワークからを除き、直接の「ルート」SSH ログインはデフォルトで無効になっています。'admins' グループのメンバーは、sudo コマンドを使用して、コマンドラインから root として動作できます。ただし、何らかの理由で通常の管理者がログインできなくなった場合に、システムをデバッグするための(堅牢な)rootパスワードがあると便利です。", + "global_settings_setting_security_experimental_enabled": "実験的なセキュリティ機能", + "global_settings_setting_security_experimental_enabled_help": "実験的なセキュリティ機能を有効にします(何をしているのかわからない場合は有効にしないでください)。", + "global_settings_setting_smtp_allow_ipv6_help": "IPv6 を使用したメールの送受信を許可する", + "global_settings_setting_smtp_relay_enabled": "SMTP リレーを有効にする", + "global_settings_setting_smtp_relay_enabled_help": "この yunohost インスタンスの代わりにメールを送信するために使用する SMTP リレーを有効にします。このような状況のいずれかにある場合に便利です:25ポートがISPまたはVPSプロバイダーによってブロックされている、DUHLにリストされている住宅用IPがある、逆引きDNSを構成できない、またはこのサーバーがインターネットに直接公開されておらず、他のものを使用してメールを送信したい。", + "global_settings_setting_smtp_relay_host": "SMTP リレー ホスト", + "global_settings_setting_smtp_relay_password": "SMTP リレー パスワード", + "global_settings_setting_smtp_relay_port": "SMTP リレー ポート", + "global_settings_setting_smtp_relay_user": "SMTP リレー ユーザー", + "global_settings_setting_ssh_compatibility": "SSH の互換性", + "global_settings_setting_ssh_compatibility_help": "SSHサーバーの互換性とセキュリティのトレードオフ。暗号(およびその他のセキュリティ関連の側面)に影響します。詳細については、https://infosec.mozilla.org/guidelines/openssh を参照してください。", + "global_settings_setting_ssh_password_authentication": "パスワード認証", + "global_settings_setting_ssh_password_authentication_help": "SSH のパスワード認証を許可する", + "global_settings_setting_ssh_port": "SSH ポート", + "global_settings_setting_ssowat_panel_overlay_enabled": "アプリで小さな「YunoHost」ポータルショートカットの正方形を有効にします", + "global_settings_setting_user_strength": "ユーザー パスワードの強度要件", + "global_settings_setting_webadmin_allowlist_help": "ウェブ管理者へのアクセスを許可されたIPアドレス。", + "global_settings_setting_webadmin_allowlist": "ウェブ管理者 IP 許可リスト", + "global_settings_setting_webadmin_allowlist_enabled": "ウェブ管理 IP 許可リストを有効にする", + "global_settings_setting_webadmin_allowlist_enabled_help": "一部の IP のみにウェブ管理者へのアクセスを許可します。", + "good_practices_about_admin_password": "次に、新しい管理パスワードを定義しようとしています。パスワードは8文字以上である必要がありますが、より長いパスワード(パスフレーズなど)を使用したり、さまざまな文字(大文字、小文字、数字、特殊文字)を使用したりすることをお勧めします。", + "good_practices_about_user_password": "次に、新しいユーザー・パスワードを定義しようとしています。パスワードは少なくとも8文字の長さである必要がありますが、より長いパスワード(パスフレーズなど)や、さまざまな文字(大文字、小文字、数字、特殊文字)を使用することをお勧めします。", + "group_already_exist": "グループ {group} は既に存在します", + "group_already_exist_on_system": "グループ {group} はシステム グループに既に存在します。", + "group_already_exist_on_system_but_removing_it": "グループ{group}はすでにシステムグループに存在しますが、YunoHostはそれを削除します...", + "group_cannot_edit_all_users": "グループ 'all_users' は手動で編集できません。これは、YunoHostに登録されているすべてのユーザーを含むことを目的とした特別なグループです", + "invalid_shell": "無効なシェル: {shell}", + "ip6tables_unavailable": "ここではip6tablesを使うことはできません。あなたはコンテナ内にいるか、カーネルがサポートしていません", + "group_cannot_edit_primary_group": "グループ '{group}' を手動で編集することはできません。これは、特定のユーザーを 1 人だけ含むためのプライマリ グループです。", + "group_cannot_edit_visitors": "グループの「訪問者」を手動で編集することはできません。匿名の訪問者を代表する特別なグループです", + "group_creation_failed": "グループ '{group}' を作成できませんでした: {error}", + "group_deleted": "グループ '{group}' が削除されました", + "group_deletion_failed": "グループ '{group}' を削除できませんでした: {error}", + "group_update_aliases": "グループ '{group}' のエイリアスの更新", + "group_update_failed": "グループ '{group}' を更新できませんでした: {error}", + "group_updated": "グループ '{group}' が更新されました", + "group_user_add": "ユーザー '{user}' がグループ '{group}' に追加されます。", + "hook_json_return_error": "フック{path}からリターンを読み取れませんでした。エラー: {msg}. 生のコンテンツ: {raw_content}", + "hook_list_by_invalid": "このプロパティは、フックを一覧表示するために使用することはできません", + "hook_name_unknown": "不明なフック名 '{name}'", + "installation_complete": "インストールが完了しました", + "invalid_credentials": "無効なパスワードまたはユーザー名", + "invalid_number": "数値にする必要があります", + "invalid_number_max": "{max}より小さくする必要があります", + "invalid_number_min": "{min}より大きい値にする必要があります", + "invalid_regex": "無効な正規表現: '{regex}'", + "iptables_unavailable": "ここではiptablesを使うことはできません。あなたはコンテナ内にいるか、カーネルがサポートしていません", + "ldap_attribute_already_exists": "LDAP 属性 '{attribute}' は、値 '{value}' で既に存在します。", + "ldap_server_down": "LDAP サーバーに到達できません", + "ldap_server_is_down_restart_it": "LDAP サービスがダウンしています。再起動を試みます...", + "log_app_action_run": "{} アプリのアクションの実行", + "log_app_change_url": "{} アプリのアクセスURLを変更", + "log_app_config_set": "‘{}’ アプリに設定を適用する", + "log_app_makedefault": "‘{}’ をデフォルトのアプリにする", + "log_app_remove": "「{}」アプリを削除する", + "log_app_upgrade": "「{}」アプリをアップグレードする", + "log_available_on_yunopaste": "このログは、{url}", + "log_backup_create": "バックアップアーカイブを作成する", + "log_backup_restore_app": "バックアップを復元する ‘{name}’", + "log_backup_restore_system": "収集したファイルからバックアップアーカイブを作成しています...", + "log_corrupted_md_file": "ログに関連付けられている YAML メタデータ ファイルが破損しています: '{md_file}\nエラー: {error}'", + "log_does_exists": "「{log}」という名前の操作ログはありません。「yunohostログリスト」を使用して、利用可能なすべての操作ログを表示します", + "log_domain_add": "ドメイン ‘{name}’ を追加する", + "log_domain_config_set": "ドメイン '{}' の構成を更新する", + "log_domain_dns_push": "‘{name}’ DNSレコードを登録する", + "log_domain_main_domain": "「{}」をメインドメインにする", + "log_domain_remove": "システム構成から「{}」ドメインを削除する", + "log_dyndns_subscribe": "YunoHostコアのアップグレードを開始しています...", + "log_dyndns_update": "YunoHostサブドメイン「{}」に関連付けられているIPを更新します", + "log_help_to_get_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、「yunohostログ共有{name}」コマンドを使用してこの操作の完全なログを共有してください", + "log_help_to_get_log": "操作「{desc}」のログを表示するには、「yunohostログショー{name}」コマンドを使用します。", + "log_letsencrypt_cert_install": "「{}」ドメインにLet's Encrypt証明書をインストールする", + "log_letsencrypt_cert_renew": "Let’s Encrypt証明書を更新する", + "log_link_to_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、 ここをクリックして この操作の完全なログを提供してください", + "log_link_to_log": "この操作の完全なログ: ''{desc}", + "log_operation_unit_unclosed_properly": "操作ユニットが正しく閉じられていません", + "log_permission_create": "作成権限 '{}'", + "log_permission_delete": "削除権限 '{}'", + "log_permission_url": "権限 '{}' に関連する URL を更新する", + "log_regen_conf": "システム設定", + "log_remove_on_failed_install": "インストールに失敗した後に「{}」を削除します", + "log_resource_snippet": "リソースのプロビジョニング/プロビジョニング解除/更新", + "log_selfsigned_cert_install": "「{}」ドメインに自己署名証明書をインストールする", + "log_user_create": "「{}」ユーザーを追加する", + "log_user_delete": "「{}」ユーザーの削除", + "log_user_group_create": "「{}」グループの作成", + "log_settings_reset": "設定をリセット", + "log_settings_reset_all": "すべての設定をリセット", + "log_settings_set": "設定を適用", + "log_tools_migrations_migrate_forward": "移行を実行する", + "log_tools_postinstall": "YunoHostサーバーをポストインストールします", + "log_tools_reboot": "サーバーを再起動", + "log_tools_shutdown": "サーバーをシャットダウン", + "log_tools_upgrade": "システムパッケージのアップグレード", + "log_user_group_delete": "「{}」グループの削除", + "log_user_group_update": "'{}' グループを更新", + "log_user_import": "ユーザーのインポート", + "mailbox_used_space_dovecot_down": "使用済みメールボックススペースをフェッチする場合は、Dovecotメールボックスサービスが稼働している必要があります", + "log_user_permission_reset": "アクセス許可 '{}' をリセットします", + "mailbox_disabled": "ユーザーの{user}に対して電子メールがオフになっている", + "main_domain_change_failed": "メインドメインを変更できません", + "main_domain_changed": "メインドメインが変更されました", + "migration_0021_cleaning_up": "キャッシュとパッケージのクリーンアップはもう役に立たなくなりました...", + "migration_0021_general_warning": "この移行はデリケートな操作であることに注意してください。YunoHostチームはそれをレビューしてテストするために最善を尽くしましたが、移行によってシステムまたはそのアプリの一部が破損する可能性があります。\n\nしたがって、次のことをお勧めします。\n - 重要なデータやアプリのバックアップを実行します。関する詳細情報: https://yunohost.org/backup\n - 移行を開始した後はしばらくお待ちください: インターネット接続とハードウェアによっては、すべてがアップグレードされるまでに最大数時間かかる場合があります。", + "migration_0021_main_upgrade": "メインアップグレードを開始しています...", + "migration_0021_not_enough_free_space": "/var/の空き容量はかなり少ないです!この移行を実行するには、少なくとも 1 GB の空き容量が必要です。", + "migration_0021_modified_files": "次のファイルは手動で変更されていることが判明し、アップグレード後に上書きされる可能性があることに注意してください: {manually_modified_files}", + "migration_0021_not_buster2": "現在の Debian ディストリビューションは Buster ではありません!すでにBuster->Bullseyeの移行を実行している場合、このエラーは移行手順が100% s成功しなかったという事実の兆候です(そうでなければ、YunoHostは完了のフラグを立てます)。Webadminのツール>ログにある移行の**完全な**ログを必要とするサポートチームで何が起こったのかを調査することをお勧めします。", + "migration_0021_patch_yunohost_conflicts": "競合の問題を回避するためにパッチを適用しています...", + "migration_0021_patching_sources_list": "sources.listsにパッチを適用しています...", + "migration_0021_problematic_apps_warning": "以下の問題のあるインストール済みアプリが検出されました。これらはYunoHostアプリカタログからインストールされていないか、「working」としてフラグが立てられていないようです。したがって、アップグレード後も動作することを保証することはできません: {problematic_apps}", + "migration_0021_still_on_buster_after_main_upgrade": "メインのアップグレード中に問題が発生しましたが、システムはまだDebian Busterです", + "migration_0021_system_not_fully_up_to_date": "システムが完全に最新ではありません。Bullseyeへの移行を実行する前に、まずは通常のアップグレードを実行してください。", + "migration_0023_not_enough_space": "移行を実行するのに十分な領域を {path} で使用できるようにします。", + "migration_0023_postgresql_11_not_installed": "PostgreSQL がシステムにインストールされていません。何もすることはありません。", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11はインストールされていますが、PostgreSQL 13はインストールされてい!?:(システムで何か奇妙なことが起こった可能性があります...", + "migration_0024_rebuild_python_venv_broken_app": "このアプリ用にvirtualenvを簡単に再構築できないため、{app}スキップします。代わりに、「yunohostアプリのアップグレード-{app}を強制」を使用してこのアプリを強制的にアップグレードして、状況を修正する必要があります。", + "migration_0024_rebuild_python_venv_disclaimer_base": "Debian Bullseye へのアップグレード後、Debian に同梱されている新しい Python バージョンに変換するために、いくつかの Python アプリケーションを部分的に再構築する必要があります (技術的には、「virtualenv」と呼ばれるものを再作成する必要があります)。それまでの間、これらのPythonアプリケーションは機能しない可能性があります。YunoHostは、以下に詳述するように、それらのいくつかについて仮想環境の再構築を試みることができます。他のアプリの場合、または再構築の試行が失敗した場合は、それらのアプリのアップグレードを手動で強制する必要があります。", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "これらのアプリに対して Virtualenvs を自動的に再構築することはできません。あなたはそれらのアップグレードを強制する必要があります、それはコマンドラインから行うことができます: 'yunohostアプリのアップグレード - -force APP':{ignored_apps}", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "virtualenvの再構築は、次のアプリに対して試行されます(注意:操作には時間がかかる場合があります)。 {rebuild_apps}", + "migration_0024_rebuild_python_venv_failed": "{app} の Python virtualenv の再構築に失敗しました。これが解決されない限り、アプリは機能しない場合があります。「yunohostアプリのアップグレード--強制{app}」を使用してこのアプリのアップグレードを強制して、状況を修正する必要があります。", + "migration_0024_rebuild_python_venv_in_progress": "現在、 '{app}'のPython仮想環境を再構築しようとしています", + "migration_description_0021_migrate_to_bullseye": "システムを Debian ブルズアイと YunoHost 11.x にアップグレードする", + "migration_description_0022_php73_to_php74_pools": "php7.3-fpm 'pool' conf ファイルを php7.4 に移行します。", + "migration_description_0023_postgresql_11_to_13": "PostgreSQL 11 から 13 へのデータベースの移行", + "migration_description_0024_rebuild_python_venv": "ブルズアイ移行後にPythonアプリを修復する", + "migration_description_0025_global_settings_to_configpanel": "従来のグローバル設定の命名法を新しい最新の命名法に移行する", + "migration_ldap_rollback_success": "システムがロールバックされました。", + "migrations_already_ran": "これらの移行は既に完了しています: {ids}", + "migrations_dependencies_not_satisfied": "移行{id}の前に、次の移行を実行します: '{dependencies_id}'。", + "migrations_exclusive_options": "'--auto'、'--skip'、および '--force-rerun' は相互に排他的なオプションです。", + "migrations_failed_to_load_migration": "移行{id}を読み込めませんでした: {error}", + "migrations_list_conflict_pending_done": "'--previous' と '--done' の両方を同時に使用することはできません。", + "migrations_loading_migration": "移行{id}を読み込んでいます...", + "migrations_migration_has_failed": "移行{id}が完了しなかったため、中止されました。エラー: {exception}", + "migrations_must_provide_explicit_targets": "'--skip' または '--force-rerun' を使用する場合は、明示的なターゲットを指定する必要があります。", + "migrations_need_to_accept_disclaimer": "移行{id}を実行するには、次の免責事項に同意する必要があります。\n---\n{disclaimer}\n---\n移行の実行に同意する場合は、'--accept-disclaimer' オプションを指定してコマンドを再実行してください。", + "migrations_running_forward": "移行{id}を実行しています...", + "migrations_skip_migration": "移行{id}スキップしています...", + "migrations_success_forward": "移行{id}完了しました", + "migrations_to_be_ran_manually": "移行{id}は手動で実行する必要があります。ウェブ管理ページの移行→ツールに移動するか、「yunohostツールの移行実行」を実行してください。", + "not_enough_disk_space": "'{path}'に十分な空き容量がありません", + "operation_interrupted": "操作は手動で中断されたようですね?", + "migrations_no_migrations_to_run": "実行する移行はありません", + "migrations_no_such_migration": "「{id}」と呼ばれる移行はありません", + "other_available_options": "...および{n}個の表示されない他の使用可能なオプション", + "migrations_not_pending_cant_skip": "これらの移行は保留中ではないため、スキップすることはできません。 {ids}", + "migrations_pending_cant_rerun": "これらの移行はまだ保留中であるため、再度実行することはできません{ids}", + "password_confirmation_not_the_same": "パスワードが一致しません", + "password_listed": "このパスワードは、世界で最も使用されているパスワードの1つです。もっとユニークなものを選んでください。", + "password_too_long": "127文字未満のパスワードを選択してください", + "password_too_simple_2": "パスワードは8文字以上で、数字、大文字、小文字を含める必要があります", + "password_too_simple_3": "パスワードは8文字以上で、数字、大文字、小文字、特殊文字を含める必要があります", + "password_too_simple_4": "パスワードは12文字以上で、数字、大文字、小文字、特殊文字を含める必要があります", + "pattern_backup_archive_name": "最大 30 文字、英数字、-_ を含む有効なファイル名である必要があります。文字のみ", + "pattern_domain": "有効なドメイン名である必要があります(例:my-domain.org)", + "pattern_email": "「+」記号のない有効な電子メールアドレスである必要があります(例:someone@example.com)", + "pattern_email_forward": "有効な電子メールアドレスである必要があり、「+」記号が受け入れられます(例:someone+tag@example.com)", + "pattern_firstname": "有効な名前(3 文字以上)である必要があります。", + "pattern_fullname": "有効なフルネーム (3 文字以上) である必要があります。", + "pattern_lastname": "有効な姓 (3 文字以上) である必要があります。", + "pattern_mailbox_quota": "クォータを持たない場合は、接尾辞が b/k/M/G/T または 0 を含むサイズである必要があります", + "pattern_password": "3 文字以上である必要があります", + "pattern_password_app": "申し訳ありませんが、パスワードに次の文字を含めることはできません: {forbidden_chars}", + "pattern_port_or_range": "有効なポート番号(例:0-65535)またはポート範囲(例:100:200)である必要があります", + "pattern_username": "小文字の英数字とアンダースコア(_)のみにする必要があります", + "permission_already_allowed": "グループ '{group}' には既にアクセス許可 '{permission}' が有効になっています", + "permission_already_disallowed": "グループ '{group}' には既にアクセス許可 '{permission}' が無効になっています", + "permission_already_exist": "アクセス許可 '{permission}' は既に存在します", + "permission_already_up_to_date": "追加/削除要求が既に現在の状態と一致しているため、アクセス許可は更新されませんでした。", + "permission_cannot_remove_main": "メイン権限の削除は許可されていません", + "permission_cant_add_to_all_users": "権限{permission}すべてのユーザーに追加することはできません。", + "permission_created": "アクセス許可 '{permission}' が作成されました", + "permission_creation_failed": "アクセス許可 '{permission}' を作成できませんでした: {error}", + "permission_currently_allowed_for_all_users": "このアクセス許可は現在、他のユーザーに加えてすべてのユーザーに付与されています。「all_users」権限を削除するか、現在付与されている他のグループを削除することをお勧めします。", + "permission_deleted": "権限 '{permission}' が削除されました", + "permission_deletion_failed": "アクセス許可 '{permission}' を削除できませんでした: {error}", + "permission_not_found": "アクセス許可 '{permission}' が見つかりません", + "permission_protected": "アクセス許可{permission}は保護されています。このアクセス許可に対して訪問者グループを追加または削除することはできません。", + "permission_require_account": "権限{permission}は、アカウントを持つユーザーに対してのみ意味があるため、訪問者に対して有効にすることはできません。", + "permission_update_failed": "アクセス許可 '{permission}' を更新できませんでした: {error}", + "port_already_closed": "ポート {port} は既に{ip_version}接続のために閉じられています", + "port_already_opened": "ポート {port} は既に{ip_version}接続用に開かれています", + "postinstall_low_rootfsspace": "ルートファイルシステムの総容量は10GB未満で、かなり気になります。ディスク容量がすぐに不足する可能性があります。ルートファイルシステム用に少なくとも16GBを用意することをお勧めします。この警告にもかかわらずYunoHostをインストールする場合は、--force-diskspaceを使用してポストインストールを再実行してください", + "regenconf_dry_pending_applying": "カテゴリ '{category}' に適用された保留中の構成を確認しています...", + "regenconf_failed": "カテゴリの設定を再生成できませんでした: {categories}", + "regenconf_file_backed_up": "構成ファイル '{conf}' が '{backup}' にバックアップされました", + "regenconf_file_copy_failed": "新しい構成ファイル '{new}' を '{conf}' にコピーできませんでした", + "regenconf_file_kept_back": "設定ファイル '{conf}' は regen-conf (カテゴリ {category}) によって削除される予定でしたが、元に戻されました。", + "regenconf_file_manually_modified": "構成ファイル '{conf}' は手動で変更されており、更新されません", + "regenconf_file_manually_removed": "構成ファイル '{conf}' は手動で削除され、作成されません", + "regenconf_file_remove_failed": "構成ファイル '{conf}' を削除できませんでした", + "regenconf_file_removed": "構成ファイル '{conf}' が削除されました", + "regenconf_file_updated": "構成ファイル '{conf}' が更新されました", + "regenconf_need_to_explicitly_specify_ssh": "ssh構成は手動で変更されていますが、実際に変更を適用するには、--forceでカテゴリ「ssh」を明示的に指定する必要があります。", + "regenconf_now_managed_by_yunohost": "設定ファイル '{conf}' が YunoHost (カテゴリ {category}) によって管理されるようになりました。", + "regenconf_pending_applying": "カテゴリ '{category}' に保留中の構成を適用しています...", + "regenconf_up_to_date": "カテゴリ '{category}' の設定は既に最新です", + "regenconf_updated": "このカテゴリーにログが登録されていません", + "regenconf_would_be_updated": "カテゴリ '{category}' の構成が更新されているはずです。", + "regex_incompatible_with_tile": "パッケージャー!アクセス許可 '{permission}' show_tile が 'true' に設定されているため、正規表現 URL をメイン URL として定義できません", + "regex_with_only_domain": "ドメインに正規表現を使用することはできませんが、パスにのみ使用できます", + "registrar_infos": "レジストラ情報", + "restore_already_installed_app": "'{name}' の ‘{id}’ パネル設定をアップデートする", + "restore_already_installed_apps": "次のアプリは既にインストールされているため復元できません。 {apps}", + "restore_backup_too_old": "このバックアップアーカイブは、古すぎるYunoHostバージョンからのものであるため、復元できません。", + "restore_cleaning_failed": "一時復元ディレクトリをクリーンアップできませんでした", + "restore_complete": "復元が完了しました", + "restore_may_be_not_enough_disk_space": "システムに十分なスペースがないようです(空き:{free_space} B、必要なスペース:{needed_space} B、セキュリティマージン:{margin} B)", + "root_password_desynchronized": "管理者パスワードが変更されましたが、YunoHostはこれをrootパスワードに伝播できませんでした!", + "server_reboot_confirm": "サーバーはすぐに再起動しますが、よろしいですか?[{answers}]", + "server_shutdown": "サーバーがシャットダウンします", + "service_already_stopped": "サービス '{service}' は既に停止されています", + "service_cmd_exec_failed": "コマンド '{command}' を実行できませんでした", + "service_description_nginx": "サーバーでホストされているすべてのWebサイトへのアクセスを提供または提供します", + "service_description_redis-server": "高速データ・アクセス、タスク・キュー、およびプログラム間の通信に使用される特殊なデータベース", + "service_description_rspamd": "スパムやその他の電子メール関連機能をフィルタリングします", + "service_description_slapd": "ユーザー、ドメイン、関連情報を格納します", + "service_description_ssh": "ターミナル経由でサーバーにリモート接続できます(SSHプロトコル)", + "service_description_yunohost-api": "YunoHostウェブインターフェイスとシステム間の相互作用を管理します", + "service_description_yunohost-firewall": "サービスへの接続ポートの開閉を管理", + "service_description_yunomdns": "ローカルネットワークで「yunohost.local」を使用してサーバーに到達できます", + "service_disable_failed": "起動時にサービス '{service}' を開始できませんでした。\n\n最近のサービスログ:{logs}", + "service_disabled": "システムの起動時にサービス '{service}' は開始されなくなります。", + "service_reload_failed": "サービス '{service}' をリロードできませんでした\n\n最近のサービスログ:{logs}", + "service_reload_or_restart_failed": "サービス '{service}' をリロードまたは再起動できませんでした\n\n最近のサービスログ:{logs}", + "service_reloaded_or_restarted": "サービス '{service}' が再読み込みまたは再起動されました", + "service_remove_failed": "サービス '{service}' を削除できませんでした", + "service_removed": "サービス '{service}' が削除されました", + "service_restart_failed": "サービス '{service}' を再起動できませんでした\n\n最近のサービスログ:{logs}", + "service_restarted": "サービス '{service}' が再起動しました", + "service_start_failed": "サービス '{service}' を開始できませんでした\n\n最近のサービスログ:{logs}", + "service_started": "サービス '{service}' が開始されました", + "service_stop_failed": "サービス '{service}' を停止できません\n\n最近のサービスログ:{logs}", + "service_stopped": "サービス '{service}' が停止しました", + "service_unknown": "不明なサービス '{service}'", + "system_username_exists": "ユーザー名はシステムユーザーのリストにすでに存在します", + "this_action_broke_dpkg": "このアクションはdpkg / APT(システムパッケージマネージャ)を壊しました...SSH経由で接続し、「sudo apt install --fix-broken」および/または「sudo dpkg --configure -a」を実行することで、この問題を解決できます。", + "tools_upgrade": "システムパッケージのアップグレード", + "tools_upgrade_failed": "パッケージをアップグレードできませんでした: {packages_list}", + "unbackup_app": "{app}は保存されません", + "unexpected_error": "予期しない問題が発生しました:{error}", + "unknown_main_domain_path": "'{app}' の不明なドメインまたはパス。アクセス許可の URL を指定できるようにするには、ドメインとパスを指定する必要があります。", + "unrestore_app": "{app}は復元されません", + "updating_apt_cache": "システムパッケージの利用可能なアップグレードを取得しています...", + "upgrade_complete": "アップグレート完了", + "upgrading_packages": "パッケージをアップグレードしています...", + "upnp_dev_not_found": "UPnP デバイスが見つかりません", + "upnp_disabled": "UPnP がオフになりました", + "upnp_enabled": "UPnP がオンになりました", + "upnp_port_open_failed": "UPnP 経由でポートを開けませんでした", + "user_already_exists": "ユーザー '{user}' は既に存在します", + "user_created": "ユーザーが作成されました。", + "user_creation_failed": "ユーザー {user}を作成できませんでした: {error}", + "user_deleted": "ユーザーが削除されました", + "user_deletion_failed": "ユーザー {user}を削除できませんでした: {error}", + "user_home_creation_failed": "ユーザーのホームフォルダ '{home}' を作成できませんでした", + "user_import_bad_file": "CSVファイルが正しくフォーマットされていないため、データ損失の可能性を回避するために無視されます", + "user_import_bad_line": "行{line}が正しくありません: {details}", + "user_import_failed": "ユーザーのインポート操作が完全に失敗しました", + "user_import_missing_columns": "次の列がありません: {columns}", + "user_import_nothing_to_do": "インポートする必要があるユーザーはいません", + "user_import_partial_failed": "ユーザーのインポート操作が部分的に失敗しました", + "user_import_success": "ユーザーが正常にインポートされました", + "user_unknown": "不明なユーザー: {user}", + "user_update_failed": "ユーザー {user}を更新できませんでした: {error}", + "user_updated": "ユーザー情報が変更されました", + "visitors": "訪問者", + "yunohost_already_installed": "YunoHostはすでにインストールされています", + "yunohost_configured": "YunoHost が構成されました", + "yunohost_installing": "YunoHostをインストールしています...", + "yunohost_not_installed": "YunoHostが正しくインストールされていません。’yunohost tools postinstall’ を実行してください", + "yunohost_postinstall_end_tip": "インストール後処理が完了しました!セットアップを完了するには、次の点を考慮してください。\n - ウェブ管理画面の「診断」セクション(またはコマンドラインで’yunohost diagnosis run’)を通じて潜在的な問題を診断します。\n - 管理ドキュメントの「セットアップの最終処理」と「YunoHostを知る」の部分を読む: https://yunohost.org/admindoc。", + "additional_urls_already_removed": "アクセス許可 ‘{permission}’ に対する追加URLで ‘{url}’ は既に削除されています" +} diff --git a/locales/kab.json b/locales/kab.json index 5daa7cef0..f6e3722cf 100644 --- a/locales/kab.json +++ b/locales/kab.json @@ -1,14 +1,11 @@ { - "ask_firstname": "Isem", - "ask_lastname": "Isem n tmagit", "ask_password": "Awal n uɛeddi", "diagnosis_description_apps": "Isnasen", "diagnosis_description_mail": "Imayl", "domain_deleted": "Taɣult tettwakkes", "done": "Immed", - "invalid_password": "Yir awal uffir", "user_created": "Aseqdac yettwarna", "diagnosis_description_dnsrecords": "Ikalasen DNS", "diagnosis_description_web": "Réseau", "domain_created": "Taɣult tettwarna" -} +} \ No newline at end of file diff --git a/locales/lt.json b/locales/lt.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/locales/lt.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/locales/nb_NO.json b/locales/nb_NO.json index e81d3af05..8cacaff6d 100644 --- a/locales/nb_NO.json +++ b/locales/nb_NO.json @@ -1,9 +1,6 @@ { "aborting": "Avbryter…", "admin_password": "Administrasjonspassord", - "admin_password_change_failed": "Kan ikke endre passord", - "admin_password_changed": "Administrasjonspassord endret", - "admin_password_too_long": "Velg et passord kortere enn 127 tegn", "app_already_installed": "{app} er allerede installert", "app_already_up_to_date": "{app} er allerede oppdatert", "app_argument_invalid": "Velg en gydlig verdi for argumentet '{name}': {error}", @@ -32,7 +29,6 @@ "downloading": "Laster ned…", "dyndns_could_not_check_available": "Kunne ikke sjekke om {domain} er tilgjengelig på {provider}.", "mail_domain_unknown": "Ukjent e-postadresse for domenet '{domain}'", - "log_remove_on_failed_restore": "Fjern '{}' etter mislykket gjenoppretting fra sikkerhetskopiarkiv", "log_letsencrypt_cert_install": "Installer et Let's Encrypt-sertifikat på '{}'-domenet", "log_letsencrypt_cert_renew": "Forny '{}'-Let's Encrypt-sertifikat", "log_user_update": "Oppdater brukerinfo for '{}'", @@ -86,9 +82,7 @@ "dyndns_key_generating": "Oppretter DNS-nøkkel… Dette kan ta en stund.", "dyndns_no_domain_registered": "Inget domene registrert med DynDNS", "dyndns_registered": "DynDNS-domene registrert", - "global_settings_setting_security_password_admin_strength": "Admin-passordets styrke", "dyndns_registration_failed": "Kunne ikke registrere DynDNS-domene: {error}", - "global_settings_setting_security_password_user_strength": "Brukerpassordets styrke", "log_backup_restore_app": "Gjenopprett '{}' fra sikkerhetskopiarkiv", "log_remove_on_failed_install": "Fjern '{}' etter mislykket installasjon", "log_selfsigned_cert_install": "Installer selvsignert sertifikat på '{}'-domenet", @@ -100,8 +94,6 @@ "app_upgrade_failed": "Kunne ikke oppgradere {app}", "app_upgrade_some_app_failed": "Noen programmer kunne ikke oppgraderes", "app_upgraded": "{app} oppgradert", - "ask_firstname": "Fornavn", - "ask_lastname": "Etternavn", "ask_main_domain": "Hoveddomene", "ask_new_admin_password": "Nytt administrasjonspassord", "app_upgrade_several_apps": "Følgende programmer vil oppgraderes: {apps}", @@ -115,5 +107,7 @@ "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log show {name}'", "log_user_create": "Legg til '{}' bruker", "app_change_url_success": "{app} nettadressen er nå {domain}{path}", - "app_install_failed": "Kunne ikke installere {app}: {error}" + "app_install_failed": "Kunne ikke installere {app}: {error}", + "global_settings_setting_admin_strength": "Admin-passordets styrke", + "global_settings_setting_user_strength": "Brukerpassordets styrke" } \ No newline at end of file diff --git a/locales/nl.json b/locales/nl.json index f8b6df327..bcfb76acd 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -1,7 +1,6 @@ { "action_invalid": "Ongeldige actie '{action}'", "admin_password": "Administrator wachtwoord", - "admin_password_changed": "Het administratie wachtwoord is gewijzigd", "app_already_installed": "{app} is al geïnstalleerd", "app_argument_invalid": "Kies een geldige waarde voor '{name}': {error}", "app_argument_required": "Het '{name}' moet ingevuld worden", @@ -14,11 +13,9 @@ "app_unknown": "Onbekende app", "app_upgrade_failed": "Het is niet gelukt app {app} bij te werken: {error}", "app_upgraded": "{app} is bijgewerkt", - "ask_firstname": "Voornaam", - "ask_lastname": "Achternaam", "ask_new_admin_password": "Nieuw administratorwachtwoord", "ask_password": "Wachtwoord", - "backup_archive_name_exists": "Een backuparchief met dezelfde naam bestaat al", + "backup_archive_name_exists": "Er bestaat al een backuparchief met dezelfde naam.", "backup_cleaning_failed": "Kan tijdelijke backup map niet leeg maken", "backup_output_directory_not_empty": "Doelmap is niet leeg", "custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app} bij te werken", @@ -30,13 +27,13 @@ "domain_dyndns_already_subscribed": "U heeft reeds een domein bij DynDNS geregistreerd", "domain_dyndns_root_unknown": "Onbekend DynDNS root domein", "domain_exists": "Domein bestaat al", - "domain_uninstall_app_first": "Een of meerdere apps zijn geïnstalleerd op dit domein, verwijder deze voordat u het domein verwijdert", + "domain_uninstall_app_first": "Deze applicaties zijn nog steeds op je domein geïnstalleerd:\n{apps}\n\nVerwijder ze met 'yunohost app remove the_app_id' of verplaats ze naar een ander domein met 'yunohost app change-url the_app_id' voordat je doorgaat met het verwijderen van het domein", "done": "Voltooid", "downloading": "Downloaden...", "dyndns_ip_update_failed": "Kan het IP adres niet updaten bij DynDNS", "dyndns_ip_updated": "IP adres is aangepast bij DynDNS", "dyndns_key_generating": "DNS sleutel word aangemaakt, wacht een moment...", - "dyndns_unavailable": "DynDNS subdomein is niet beschikbaar", + "dyndns_unavailable": "Domein '{domain}' is niet beschikbaar.", "extracting": "Uitpakken...", "installation_complete": "Installatie voltooid", "mail_alias_remove_failed": "Kan mail-alias '{mail}' niet verwijderen", @@ -50,10 +47,10 @@ "service_add_failed": "Kan service '{service}' niet toevoegen", "service_already_started": "Service '{service}' draait al", "service_cmd_exec_failed": "Kan '{command}' niet uitvoeren", - "service_disabled": "Service '{service}' is uitgeschakeld", + "service_disabled": "Service '{service}' wordt niet meer gestart als het systeem opstart.", "service_remove_failed": "Kan service '{service}' niet verwijderen", "service_removed": "Service werd verwijderd", - "service_stop_failed": "Kan service '{service}' niet stoppen", + "service_stop_failed": "Kan service '{service}' niet stoppen\n\nRecente servicelogs: {logs}", "service_unknown": "De service '{service}' bestaat niet", "unexpected_error": "Er is een onbekende fout opgetreden", "unrestore_app": "App '{app}' wordt niet teruggezet", @@ -69,12 +66,10 @@ "user_unknown": "Gebruikersnaam {user} is onbekend", "user_update_failed": "Kan gebruiker niet bijwerken", "yunohost_configured": "YunoHost configuratie is OK", - "admin_password_change_failed": "Wachtwoord wijzigen is niet gelukt", "app_argument_choice_invalid": "Kiel een geldige waarde voor argument '{name}'; {value}' komt niet voor in de keuzelijst {choices}", "app_not_correctly_installed": "{app} schijnt niet juist geïnstalleerd te zijn", "app_not_properly_removed": "{app} werd niet volledig verwijderd", "app_requirements_checking": "Noodzakelijke pakketten voor {app} aan het controleren...", - "app_requirements_unmeet": "Er wordt niet aan de aanvorderingen voldaan, het pakket {pkgname} ({version}) moet {spec} zijn", "app_unsupported_remote_type": "Niet ondersteund besturings type voor de app", "ask_main_domain": "Hoofd-domein", "backup_app_failed": "Kon geen backup voor app '{app}' aanmaken", @@ -90,7 +85,6 @@ "backup_nothings_done": "Niets om op te slaan", "password_too_simple_1": "Het wachtwoord moet minimaal 8 tekens lang zijn", "already_up_to_date": "Er is niets te doen, alles is al up-to-date.", - "admin_password_too_long": "Gelieve een wachtwoord te kiezen met minder dan 127 karakters", "app_action_cannot_be_ran_because_required_services_down": "De volgende diensten moeten actief zijn om deze actie uit te voeren: {services}. Probeer om deze te herstarten om verder te gaan (en om eventueel te onderzoeken waarom ze niet werken).", "aborting": "Annulatie.", "app_upgrade_app_name": "Bezig {app} te upgraden...", @@ -120,7 +114,7 @@ "app_action_broke_system": "Deze actie lijkt de volgende belangrijke services te hebben kapotgemaakt: {services}", "app_config_unable_to_apply": "De waarden in het configuratiescherm konden niet toegepast worden.", "app_config_unable_to_read": "Het is niet gelukt de waarden van het configuratiescherm te lezen.", - "app_argument_password_no_default": "Foutmelding tijdens het lezen van wachtwoordargument '{name}': het wachtwoordargument mag om veiligheidsredenen geen standaardwaarde hebben.", + "app_argument_password_no_default": "Foutmelding tijdens het lezen van wachtwoordargument '{name}': het wachtwoordargument mag om veiligheidsredenen geen standaardwaarde hebben", "app_already_installed_cant_change_url": "Deze app is al geïnstalleerd. De URL kan niet veranderd worden met deze functie. Probeer of dat lukt via `app changeurl`.", "apps_catalog_init_success": "De app-catalogus is succesvol geinitieerd!", "apps_catalog_failed_to_download": "Het is niet gelukt de {apps_catalog} app-catalogus te downloaden: {error}", @@ -128,7 +122,7 @@ "additional_urls_already_added": "Extra URL '{url:s}' is al toegevoegd in de extra URL voor privilege '{permission:s}'", "additional_urls_already_removed": "Extra URL '{url}' is al verwijderd in de extra URL voor privilege '{permission}'", "app_label_deprecated": "Dit commando is vervallen. Gebruik alsjeblieft het nieuwe commando 'yunohost user permission update' om het label van de app te beheren.", - "app_change_url_no_script": "De app '{app_name}' ondersteunt nog geen URL-aanpassingen. Misschien wel na een upgrade.", + "app_change_url_no_script": "App '{app_name}' ondersteunt nog geen URL-aanpassingen. Misschien wel na een upgrade.", "app_upgrade_some_app_failed": "Sommige apps konden niet worden bijgewerkt", "other_available_options": "... en {n} andere beschikbare opties die niet getoond worden", "password_listed": "Dit wachtwoord is een van de meest gebruikte wachtwoorden ter wereld. Kies alstublieft iets wat minder voor de hand ligt.", @@ -140,5 +134,9 @@ "pattern_domain": "Moet een geldige domeinnaam zijn (mijneigendomein.nl, bijvoorbeeld)", "pattern_firstname": "Het moet een geldige voornaam zijn", "pattern_lastname": "Het moet een geldige achternaam zijn", - "password_too_simple_3": "Het wachtwoord moet minimaal 8 tekens lang zijn en moet cijfers, hoofdletters, kleine letters en speciale tekens bevatten" + "password_too_simple_3": "Het wachtwoord moet minimaal 8 tekens lang zijn en moet cijfers, hoofdletters, kleine letters en speciale tekens bevatten", + "group_already_exist": "Groep {group} bestaat al", + "group_already_exist_on_system": "Groep {group} bestaat al in de systeemgroepen", + "good_practices_about_admin_password": "Je gaat nu een nieuw beheerderswachtwoordopgeven. Het wachtwoord moet minimaal 8 tekens lang zijn, hoewel het een goede gewoonte is om een langer wachtwoord te gebruiken (d.w.z. een wachtwoordzin) en/of een variatie van tekens te gebruiken (hoofdletters, kleine letters, cijfers en speciale tekens).", + "good_practices_about_user_password": "Je gaat nu een nieuw gebruikerswachtwoord pgeven. Het wachtwoord moet minimaal 8 tekens lang zijn, hoewel het een goede gewoonte is om een langer wachtwoord te gebruiken (d.w.z. een wachtwoordzin) en/of een variatie van tekens te gebruiken (hoofdletters, kleine letters, cijfers en speciale tekens)." } \ No newline at end of file diff --git a/locales/oc.json b/locales/oc.json index a6afa32e6..1c13fc6b5 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -1,7 +1,5 @@ { "admin_password": "Senhal d’administracion", - "admin_password_change_failed": "Impossible de cambiar lo senhal", - "admin_password_changed": "Lo senhal d’administracion es ben estat cambiat", "app_already_installed": "{app} es ja installat", "app_already_up_to_date": "{app} es ja a jorn", "installation_complete": "Installacion acabada", @@ -16,8 +14,6 @@ "app_upgrade_failed": "Impossible d’actualizar {app} : {error}", "app_upgrade_some_app_failed": "D’aplicacions se pòdon pas actualizar", "app_upgraded": "{app} es estada actualizada", - "ask_firstname": "Prenom", - "ask_lastname": "Nom", "ask_main_domain": "Domeni màger", "ask_new_admin_password": "Nòu senhal administrator", "ask_password": "Senhal", @@ -57,7 +53,6 @@ "backup_output_directory_required": "Vos cal especificar un dorsièr de sortida per la salvagarda", "backup_running_hooks": "Execucion dels scripts de salvagarda...", "backup_system_part_failed": "Impossible de salvagardar la part « {part} » del sistèma", - "app_requirements_unmeet": "Las condicions requesidas per {app} son pas complidas, lo paquet {pkgname} ({version}) deu èsser {spec}", "backup_abstract_method": "Aqueste metòde de salvagarda es pas encara implementat", "backup_applying_method_custom": "Crida del metòde de salvagarda personalizat « {method} »...", "backup_couldnt_bind": "Impossible de ligar {src} amb {dest}.", @@ -120,10 +115,6 @@ "dyndns_unavailable": "Lo domeni {domain} es pas disponible.", "extracting": "Extraccion…", "field_invalid": "Camp incorrècte : « {} »", - "global_settings_cant_open_settings": "Fracàs de la dobertura del fichièr de configuracion, rason : {reason}", - "global_settings_key_doesnt_exists": "La clau « {settings_key} » existís pas dins las configuracions globalas, podètz veire totas las claus disponiblas en executant « yunohost settings list »", - "global_settings_reset_success": "Configuracion precedenta ara salvagarda dins {path}", - "global_settings_unknown_setting_from_settings_file": "Clau desconeguda dins los paramètres : {setting_key}, apartada e salvagardada dins /etc/yunohost/settings-unknown.json", "main_domain_change_failed": "Modificacion impossibla del domeni màger", "main_domain_changed": "Lo domeni màger es estat modificat", "migrations_list_conflict_pending_done": "Podètz pas utilizar --previous e --done a l’encòp.", @@ -152,10 +143,6 @@ "firewall_reload_failed": "Impossible de recargar lo parafuòc", "firewall_reloaded": "Parafuòc recargat", "firewall_rules_cmd_failed": "Unas règlas del parafuòc an fracassat. Per mai informacions, consultatz lo jornal.", - "global_settings_bad_choice_for_enum": "La valor del paramètre {setting} es incorrècta. Recebut : {choice}, mas las opcions esperadas son : {available_choices}", - "global_settings_bad_type_for_setting": "Lo tipe del paramètre {setting} es incorrècte, recebut : {received_type}, esperat {expected_type}", - "global_settings_cant_write_settings": "Fracàs de l’escritura del fichièr de configuracion, rason : {reason}", - "global_settings_unknown_type": "Situacion inesperada, la configuracion {setting} sembla d’aver lo tipe {unknown_type} mas es pas un tipe pres en carga pel sistèma.", "hook_exec_failed": "Fracàs de l’execucion del script : « {path} »", "hook_exec_not_terminated": "Lo escript « {path} » a pas acabat corrèctament", "hook_list_by_invalid": "La proprietat de tria de las accions es invalida", @@ -216,7 +203,6 @@ "service_description_ssh": "vos permet de vos connectar a distància a vòstre servidor via un teminal (protocòl SSH)", "service_description_yunohost-api": "permet las interaccions entre l’interfàcia web de YunoHost e le sistèma", "service_description_yunohost-firewall": "gerís los pòrts de connexion dobèrts e tampats als servicis", - "global_settings_cant_serialize_settings": "Fracàs de la serializacion de las donadas de parametratge, rason : {reason}", "ip6tables_unavailable": "Podètz pas jogar amb ip6tables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", "iptables_unavailable": "Podètz pas jogar amb iptables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", "mail_alias_remove_failed": "Supression impossibla de l’alias de corrièl « {mail} »", @@ -237,7 +223,6 @@ "backup_cant_mount_uncompress_archive": "Impossible de montar en lectura sola lo repertòri de l’archiu descomprimit", "backup_no_uncompress_archive_dir": "Lo repertòri de l’archiu descomprimit existís pas", "pattern_username": "Deu èsser compausat solament de caractèrs alfanumerics en letras minusculas e de tirets basses", - "experimental_feature": "Atencion : aquesta foncionalitat es experimentala e deu pas èsser considerada coma establa, deuriatz pas l’utilizar levat que sapiatz çò que fasètz.", "log_corrupted_md_file": "Lo fichièr YAML de metadonadas ligat als jornals d’audit es damatjat : « {md_file} »\nError : {error}", "log_link_to_log": "Jornal complèt d’aquesta operacion : {desc}", "log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log show {name} »", @@ -253,7 +238,6 @@ "log_available_on_yunopaste": "Lo jornal es ara disponible via {url}", "log_backup_restore_system": "Restaurar lo sistèma a partir d’una salvagarda", "log_backup_restore_app": "Restaurar « {} » a partir d’una salvagarda", - "log_remove_on_failed_restore": "Levar « {} » aprèp un fracàs de restauracion a partir d’una salvagarda", "log_remove_on_failed_install": "Tirar « {} » aprèp una installacion pas reüssida", "log_domain_add": "Ajustar lo domeni « {} » dins la configuracion sistèma", "log_domain_remove": "Tirar lo domeni « {} » d’a la configuracion sistèma", @@ -293,11 +277,7 @@ "backup_mount_archive_for_restore": "Preparacion de l’archiu per restauracion...", "dyndns_could_not_check_available": "Verificacion impossibla de la disponibilitat de {domain} sus {provider}.", "file_does_not_exist": "Lo camin {path} existís pas.", - "global_settings_setting_security_password_admin_strength": "Fòrça del senhal administrator", - "global_settings_setting_security_password_user_strength": "Fòrça del senhal utilizaire", - "root_password_replaced_by_admin_password": "Lo senhal root es estat remplaçat pel senhal administrator.", "service_restarted": "Lo servici '{service}' es estat reaviat", - "admin_password_too_long": "Causissètz un senhal d’almens 127 caractèrs", "service_reloaded": "Lo servici « {service} » es estat tornat cargar", "already_up_to_date": "I a pas res a far ! Tot es ja a jorn !", "app_action_cannot_be_ran_because_required_services_down": "Aquestas aplicacions necessitan d’èsser lançadas per poder executar aquesta accion : {services}. Abans de contunhar deuriatz ensajar de reaviar los servicis seguents (e tanben cercar perque son tombats en pana) : {services}", @@ -308,7 +288,6 @@ "log_regen_conf": "Regenerar las configuracions del sistèma « {} »", "service_reloaded_or_restarted": "Lo servici « {service} » es estat recargat o reaviat", "dpkg_is_broken": "Podètz pas far aquò pel moment perque dpkg/APT (los gestionaris de paquets del sistèma) sembla èsser mal configurat… Podètz ensajar de solucionar aquò en vos connectar via SSH e en executar « sudo dpkg --configure -a ».", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Autorizar l’utilizacion de la clau òst DSA (obsolèta) per la configuracion del servici SSH", "hook_json_return_error": "Fracàs de la lectura del retorn de l’script {path}. Error : {msg}. Contengut brut : {raw_content}", "pattern_password_app": "O planhèm, los senhals devon pas conténer los caractèrs seguents : {forbidden_chars}", "regenconf_file_backed_up": "Lo fichièr de configuracion « {conf} » es estat salvagardat dins « {backup} »", @@ -325,9 +304,6 @@ "regenconf_dry_pending_applying": "Verificacion de la configuracion que seriá estada aplicada a la categoria « {category} »…", "regenconf_failed": "Regeneracion impossibla de la configuracion per la(s) categoria(s) : {categories}", "regenconf_pending_applying": "Aplicacion de la configuracion en espèra per la categoria « {category} »…", - "global_settings_setting_security_nginx_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor web NGINX Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", - "global_settings_setting_security_ssh_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor SSH. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", - "global_settings_setting_security_postfix_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor Postfix. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", "service_reload_failed": "Impossible de recargar lo servici « {service} »\n\nJornal d’audit recent : {logs}", "service_restart_failed": "Impossible de reaviar lo servici « {service} »\n\nJornal d’audit recent : {logs}", "service_reload_or_restart_failed": "Impossible de recargar o reaviar lo servici « {service} »\n\nJornal d’audit recent : {logs}", @@ -403,7 +379,7 @@ "diagnosis_services_bad_status": "Lo servici {service} es {status} :(", "diagnosis_swap_ok": "Lo sistèma a {total} d’escambi !", "diagnosis_regenconf_allgood": "Totes los fichièrs de configuracion son confòrmes a la configuracion recomandada !", - "diagnosis_regenconf_manually_modified": "Lo fichièr de configuracion {file} foguèt modificat manualament.", + "diagnosis_regenconf_manually_modified": "Lo fichièr de configuracion {file} foguèt modificat manualament.", "diagnosis_regenconf_manually_modified_details": "Es probablament bon tan que sabètz çò que fasètz ;) !", "diagnosis_security_vulnerable_to_meltdown": "Semblatz èsser vulnerable a la vulnerabilitat de seguretat critica de Meltdown", "diagnosis_description_basesystem": "Sistèma de basa", @@ -453,7 +429,6 @@ "diagnosis_ip_broken_resolvconf": "La resolucion del nom de domeni sembla copada sul servidor, poiriá èsser ligada al fait que /etc/resolv.conf manda pas a 127.0.0.1.", "diagnosis_ip_weird_resolvconf": "La resolucion del nom de domeni sembla foncionar, mas sembla qu’utiilizatz un fichièr /etc/resolv.conf personalizat.", "diagnosis_diskusage_verylow": "Lo lòc d’emmagazinatge {mountpoint} (sul periferic {device}) a solament {free} ({free_percent}%). Deuriatz considerar de liberar un pauc d’espaci.", - "global_settings_setting_pop3_enabled": "Activar lo protocòl POP3 pel servidor de corrièr", "diagnosis_diskusage_ok": "Lo lòc d’emmagazinatge {mountpoint} (sul periferic {device}) a encara {free} ({free_percent}%) de liure !", "diagnosis_swap_none": "Lo sistèma a pas cap de memòria d’escambi. Auriatz de considerar d’ajustar almens {recommended} d’escambi per evitar las situacions ont lo sistèma manca de memòria.", "diagnosis_swap_notsomuch": "Lo sistèma a solament {total} de memòria d’escambi. Auriatz de considerar d’ajustar almens {recommended} d’escambi per evitar las situacions ont lo sistèma manca de memòria.", @@ -488,5 +463,10 @@ "diagnosis_domain_not_found_details": "Lo domeni {domain} existís pas a la basa de donadas WHOIS o a expirat !", "diagnosis_domain_expiration_not_found": "Impossible de verificar la data d’expiracion d’unes domenis", "backup_create_size_estimation": "L’archiu contendrà apr’aquí {size} de donadas.", - "app_restore_script_failed": "Una error s’es producha a l’interior del script de restauracion de l’aplicacion" + "app_restore_script_failed": "Una error s’es producha a l’interior del script de restauracion de l’aplicacion", + "global_settings_setting_nginx_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor web NGINX Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", + "global_settings_setting_admin_strength": "Fòrça del senhal administrator", + "global_settings_setting_user_strength": "Fòrça del senhal utilizaire", + "global_settings_setting_postfix_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor Postfix. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", + "global_settings_setting_ssh_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor SSH. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)" } \ No newline at end of file diff --git a/locales/pl.json b/locales/pl.json index 01cd71471..52f2de3ca 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -1,12 +1,278 @@ { "password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków", "app_already_up_to_date": "{app} jest obecnie aktualna", - "app_already_installed": "{app} jest już zainstalowane", + "app_already_installed": "{app} jest już zainstalowana", "already_up_to_date": "Nic do zrobienia. Wszystko jest obecnie aktualne.", - "admin_password_too_long": "Proszę wybrać hasło krótsze niż 127 znaków", - "admin_password_changed": "Hasło administratora zostało zmienione", - "admin_password_change_failed": "Nie można zmienić hasła", "admin_password": "Hasło administratora", "action_invalid": "Nieprawidłowe działanie '{action:s}'", - "aborting": "Przerywanie." -} \ No newline at end of file + "aborting": "Przerywanie.", + "domain_config_auth_consumer_key": "Klucz konsumenta", + "domain_config_cert_validity": "Ważność", + "visitors": "Odwiedzający", + "app_start_install": "Instalowanie {app}...", + "app_unknown": "Nieznana aplikacja", + "ask_main_domain": "Domena główna", + "backup_created": "Utworzono kopię zapasową: {name}", + "firewall_reloaded": "Przeładowano zaporę sieciową", + "user_created": "Utworzono użytkownika", + "yunohost_installing": "Instalowanie YunoHost...", + "global_settings_setting_smtp_allow_ipv6": "Zezwól na IPv6", + "user_deleted": "Usunięto użytkownika", + "domain_config_default_app": "Domyślna aplikacja", + "restore_complete": "Przywracanie zakończone", + "domain_deleted": "Usunięto domenę", + "domains_available": "Dostępne domeny:", + "domain_config_api_protocol": "API protokołu", + "domain_config_auth_application_key": "Klucz aplikacji", + "diagnosis_description_systemresources": "Zasoby systemu", + "log_user_import": "Importuj użytkowników", + "system_upgraded": "Zaktualizowano system", + "diagnosis_description_regenconf": "Konfiguracja systemu", + "diagnosis_description_apps": "Aplikacje", + "diagnosis_description_basesystem": "Baza systemu", + "unlimit": "Brak limitu", + "global_settings_setting_pop3_enabled": "Włącz POP3", + "domain_created": "Utworzono domenę", + "ask_new_admin_password": "Nowe hasło administracyjne", + "ask_new_domain": "Nowa domena", + "ask_new_path": "Nowa ścieżka", + "downloading": "Pobieranie...", + "ask_password": "Hasło", + "backup_deleted": "Usunięto kopię zapasową: {name}.", + "done": "Gotowe", + "diagnosis_description_dnsrecords": "Rekordy DNS", + "diagnosis_description_ip": "Połączenie z internetem", + "diagnosis_description_mail": "Email", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Błąd: {error}", + "diagnosis_mail_queue_unavailable_details": "Błąd: {error}", + "diagnosis_http_could_not_diagnose_details": "Błąd: {error}", + "installation_complete": "Instalacja zakończona", + "app_start_remove": "Usuwanie {app}...", + "app_start_restore": "Przywracanie {app}...", + "app_upgraded": "Zaktualizowano {app}", + "extracting": "Rozpakowywanie...", + "app_removed": "Odinstalowano {app}", + "upgrade_complete": "Aktualizacja zakończona", + "global_settings_setting_backup_compress_tar_archives": "Kompresuj kopie zapasowe", + "global_settings_setting_nginx_compatibility": "Kompatybilność z NGINX", + "global_settings_setting_nginx_redirect_to_https": "Wymuszaj HTTPS", + "ask_admin_username": "Nazwa użytkownika administratora", + "ask_fullname": "Pełne imię i nazwisko", + "upgrading_packages": "Aktualizowanie paczek...", + "admins": "Administratorzy", + "diagnosis_ports_could_not_diagnose_details": "Błąd: {error}", + "log_settings_set": "Zastosuj ustawienia", + "domain_config_cert_issuer": "Organ certyfikacji", + "domain_config_cert_summary": "Status certyfikatu", + "global_settings_setting_ssh_compatibility": "Kompatybilność z SSH", + "global_settings_setting_ssh_port": "Port SSH", + "log_settings_reset": "Resetuj ustawienia", + "log_tools_migrations_migrate_forward": "Uruchom migracje", + "app_action_cannot_be_ran_because_required_services_down": "Następujące usługi powinny być uruchomione, aby rozpocząć to działanie: {services}. Spróbuj uruchomić je ponownie aby kontynuować (i dowiedzieć się, dlaczego były one wyłączone)", + "app_argument_choice_invalid": "Wybierz poprawną wartość dla argumentu '{name}': '{value}' nie znajduje się w liście poprawnych opcji ({choices})", + "app_action_broke_system": "Wydaje się, że ta akcja przerwała te ważne usługi: {services}", + "additional_urls_already_removed": "Dodatkowy URL '{url}' już usunięty w dodatkowym URL dla uprawnienia '{permission}'", + "additional_urls_already_added": "Dodatkowy URL '{url}' już dodany w dodatkowym URL dla uprawnienia '{permission}'", + "app_arch_not_supported": "Ta aplikacja może być zainstalowana tylko na architekturach {required}, a twoja architektura serwera to {current}", + "app_argument_invalid": "Wybierz poprawną wartość dla argumentu '{name}': {error}", + "all_users": "Wszyscy użytkownicy YunoHost", + "app_action_failed": "Nie udało się uruchomić akcji {action} dla aplikacji {app}", + "app_already_installed_cant_change_url": "Ta aplikacja jest już zainstalowana. URL nie może zostać zmieniony przy użyciu tej funkcji. Sprawdź czy można zmienić w `app changeurl`", + "app_id_invalid": "Nieprawidłowy identyfikator aplikacji(ID)", + "app_change_url_require_full_domain": "Nie można przenieść aplikacji {app} na nowy adres URL, ponieważ wymaga ona pełnej domeny (tj. ze ścieżką = /)", + "app_install_files_invalid": "Te pliki nie mogą zostać zainstalowane.", + "app_make_default_location_already_used": "Nie można ustawić '{app}' jako domyślnej aplikacji w domenie '{domain}' ponieważ jest już używana przez '{other_app}'", + "app_change_url_identical_domains": "Stara i nowa domena/ścieżka_url są identyczne („{domain}{path}”), nic nie trzeba robić.", + "app_config_unable_to_read": "Nie udało się odczytać wartości panelu konfiguracji.", + "app_config_unable_to_apply": "Nie udało się zastosować wartości panelu konfiguracji.", + "app_install_failed": "Nie udało się zainstalować {app}: {error}", + "apps_catalog_failed_to_download": "Nie można pobrać katalogu aplikacji app catalog: {error}", + "app_argument_required": "Argument „{name}” jest wymagany", + "app_not_properly_removed": "Aplikacja {app} nie została poprawnie usunięta", + "app_upgrade_failed": "Nie można uaktualnić {app}: {error}", + "backup_abstract_method": "Ta metoda tworzenia kopii zapasowych nie została jeszcze zaimplementowana", + "backup_actually_backuping": "Tworzenie archiwum kopii zapasowej z zebranych plików...", + "backup_applying_method_copy": "Kopiowanie wszystkich plików do kopii zapasowej...", + "backup_applying_method_tar": "Tworzenie kopii zapasowej archiwum TAR..", + "backup_archive_app_not_found": "Nie można znaleźć aplikacji {app} w archiwum kopii zapasowych", + "backup_archive_broken_link": "Nie można uzyskać dostępu do archiwum kopii zapasowych (broken link to {path})", + "backup_csv_addition_failed": "Nie udało się dodać plików do kopii zapasowej do pliku CSV.", + "backup_creation_failed": "Nie udało się utworzyć archiwum kopii zapasowej", + "backup_csv_creation_failed": "Nie udało się utworzyć wymaganego pliku CSV do przywracania.", + "backup_custom_mount_error": "Niestandardowa metoda tworzenia kopii zapasowej nie mogła przejść etapu „mount”", + "backup_applying_method_custom": "Wywołuję niestandardową metodę tworzenia kopii zapasowych '{method}'...", + "app_remove_after_failed_install": "Usuwanie aplikacji po niepowodzeniu instalacji...", + "app_upgrade_script_failed": "Wystąpił błąd w skrypcie aktualizacji aplikacji", + "apps_catalog_init_success": "Zainicjowano system katalogu aplikacji!", + "apps_catalog_obsolete_cache": "Pamięć podręczna katalogu aplikacji jest pusta lub przestarzała.", + "app_extraction_failed": "Nie można wyodrębnić plików instalacyjnych", + "app_packaging_format_not_supported": "Ta aplikacja nie może zostać zainstalowana, ponieważ jej format opakowania nie jest obsługiwany przez twoją wersję YunoHost. Prawdopodobnie powinieneś rozważyć aktualizację swojego systemu.", + "app_manifest_install_ask_domain": "Wybierz domenę, w której ta aplikacja ma zostać zainstalowana", + "app_manifest_install_ask_admin": "Wybierz użytkownika administratora dla tej aplikacji", + "app_manifest_install_ask_password": "Wybierz hasło administratora dla tej aplikacji", + "app_manifest_install_ask_is_public": "Czy ta aplikacja powinna być udostępniana anonimowym użytkownikom?", + "ask_user_domain": "Domena używana dla adresu e-mail użytkownika i konta XMPP", + "app_upgrade_app_name": "Aktualizuję {app}...", + "app_install_script_failed": "Wystąpił błąd w skrypcie instalacyjnym aplikacji", + "apps_catalog_update_success": "Katalog aplikacji został zaktualizowany!", + "apps_catalog_updating": "Aktualizowanie katalogu aplikacji...", + "app_label_deprecated": "To polecenie jest przestarzałe! Użyj nowego polecenia „yunohost user permission update”, aby zarządzać etykietą aplikacji.", + "app_change_url_no_script": "Aplikacja „{app_name}” nie obsługuje jeszcze modyfikacji adresów URL. Możesz spróbować ją zaaktualizować.", + "app_change_url_success": "Adres URL aplikacji {app} to teraz {domain}{path}", + "app_not_upgraded": "Nie udało się zaktualizować aplikacji „{failed_app}”, w związku z czym anulowano aktualizacje następujących aplikacji: {apps}", + "app_upgrade_several_apps": "Następujące aplikacje zostaną uaktualnione: {apps}", + "app_not_correctly_installed": "Wygląda na to, że aplikacja {app} jest nieprawidłowo zainstalowana", + "app_not_installed": "Nie można znaleźć aplikacji {app} na liście zainstalowanych aplikacji: {all_apps}", + "app_requirements_checking": "Sprawdzam wymagania dla aplikacji {app}...", + "app_upgrade_some_app_failed": "Niektórych aplikacji nie udało się zaktualizować", + "backup_app_failed": "Nie udało się utworzyć kopii zapasowej {app}", + "backup_archive_name_exists": "Archiwum kopii zapasowych o tej nazwie już istnieje.", + "backup_archive_open_failed": "Nie można otworzyć archiwum kopii zapasowej", + "backup_archive_writing_error": "Nie udało się dodać plików '{source}' (nazwanych w archiwum '{dest}') do utworzenia kopii zapasowej skompresowanego archiwum '{archive}'", + "backup_ask_for_copying_if_needed": "Czy chcesz wykonać kopię zapasową tymczasowo używając {size} MB? (Ta metoda jest stosowana, ponieważ niektóre pliki nie mogły zostać przygotowane przy użyciu bardziej wydajnej metody.)", + "backup_cant_mount_uncompress_archive": "Nie można zamontować nieskompresowanego archiwum jako chronione przed zapisem", + "backup_copying_to_organize_the_archive": "Kopiowanie {size} MB w celu zorganizowania archiwum", + "backup_couldnt_bind": "Nie udało się powiązać {src} z {dest}.", + "backup_archive_corrupted": "Wygląda na to, że archiwum kopii zapasowej '{archive}' jest uszkodzone: {error}", + "backup_cleaning_failed": "Nie udało się wyczyścić folderu tymczasowej kopii zapasowej", + "backup_create_size_estimation": "Archiwum będzie zawierać około {size} danych.", + "app_location_unavailable": "Ten adres URL jest niedostępny lub koliduje z już zainstalowanymi aplikacjami:\n{apps}", + "app_restore_failed": "Nie można przywrócić {app}: {error}", + "app_restore_script_failed": "Wystąpił błąd w skrypcie przywracania aplikacji", + "app_full_domain_unavailable": "Przepraszamy, ta aplikacja musi być zainstalowana we własnej domenie, ale inna aplikacja jest już zainstalowana w tej domenie „{domain}”. Zamiast tego możesz użyć subdomeny dedykowanej tej aplikacji.", + "app_resource_failed": "Nie udało się zapewnić, anulować obsługi administracyjnej lub zaktualizować zasobów aplikacji {app}: {error}", + "app_manifest_install_ask_path": "Wybierz ścieżkę adresu URL (po domenie), w której ta aplikacja ma zostać zainstalowana", + "app_not_enough_disk": "Ta aplikacja wymaga {required} wolnego miejsca.", + "app_not_enough_ram": "Ta aplikacja wymaga {required} pamięci RAM do zainstalowania/uaktualnienia, ale obecnie dostępna jest tylko {current}.", + "app_start_backup": "Zbieram pliki do utworzenia kopii zapasowej dla {app}...", + "app_yunohost_version_not_supported": "Ta aplikacja wymaga YunoHost >= {required}, ale aktualnie zainstalowana wersja to {current}", + "apps_already_up_to_date": "Wszystkie aplikacje są już aktualne", + "backup_archive_system_part_not_available": "Część systemowa '{part}' jest niedostępna w tej kopii zapasowej", + "backup_custom_backup_error": "Niestandardowa metoda tworzenia kopii zapasowej nie mogła przejść kroku 'backup'.", + "app_argument_password_no_default": "Błąd podczas analizowania argumentu hasła „{name}”: argument hasła nie może mieć wartości domyślnej ze względów bezpieczeństwa", + "app_sources_fetch_failed": "Nie można pobrać plików źródłowych, czy adres URL jest poprawny?", + "app_manifest_install_ask_init_admin_permission": "Kto powinien mieć dostęp do funkcji administracyjnych tej aplikacji? (Można to później zmienić)", + "app_manifest_install_ask_init_main_permission": "Kto powinien mieć dostęp do tej aplikacji? (Można to później zmienić)", + "ask_admin_fullname": "Pełne imię i nazwisko administratora", + "app_change_url_failed": "Nie udało się zmienić adresu URL aplikacji {app}: {error}", + "app_change_url_script_failed": "Wystąpił błąd w skrypcie zmiany adresu URL", + "app_failed_to_upgrade_but_continue": "Nie udało zaktualizować się aplikacji {failed_app}, przechodzenie do następnych aktualizacji według żądania. Uruchom komendę 'yunohost log show {operation_logger_name}', aby sprawdzić logi dotyczące błędów", + "certmanager_cert_signing_failed": "Nie udało się zarejestrować nowego certyfikatu", + "certmanager_cert_renew_success": "Pomyślne odnowienie certyfikatu Let's Encrypt dla domeny '{domain}'", + "backup_delete_error": "Nie udało się usunąć '{path}'", + "certmanager_attempt_to_renew_nonLE_cert": "Certyfikat dla domeny '{domain}' nie został wystawiony przez Let's Encrypt. Automatyczne odnowienie jest niemożliwe!", + "backup_archive_cant_retrieve_info_json": "Nieudane wczytanie informacji dla archiwum '{archive}'... Plik info.json nie może zostać odzyskany (lub jest niepoprawny).", + "backup_method_custom_finished": "Tworzenie kopii zapasowej według własnej metody '{method}' zakończone", + "backup_nothings_done": "Brak danych do zapisania", + "app_unsupported_remote_type": "Niewspierany typ zdalny użyty w aplikacji", + "backup_archive_name_unknown": "Nieznane, lokalne archiwum kopii zapasowej o nazwie '{name}'", + "backup_output_directory_not_empty": "Należy wybrać pusty katalog dla danych wyjściowych", + "certmanager_attempt_to_renew_valid_cert": "Certyfikat dla domeny '{domain}' nie jest bliski wygaśnięciu! (Możesz użyć komendy z dopiskiem --force jeśli wiesz co robisz)", + "certmanager_cert_install_success": "Pomyślna instalacja certyfikatu Let's Encrypt dla domeny '{domain}'", + "certmanager_attempt_to_replace_valid_cert": "Właśnie zamierzasz nadpisać dobry i poprawny certyfikat dla domeny '{domain}'! (Użyj komendy z dopiskiem --force, aby ominąć)", + "backup_method_copy_finished": "Zakończono tworzenie kopii zapasowej", + "certmanager_certificate_fetching_or_enabling_failed": "Próba użycia nowego certyfikatu dla {domain} zakończyła się niepowodzeniem...", + "backup_method_tar_finished": "Utworzono archiwum kopii zapasowej TAR", + "backup_mount_archive_for_restore": "Przygotowywanie archiwum do przywrócenia...", + "certmanager_cert_install_failed": "Nieudana instalacja certyfikatu Let's Encrypt dla {domains}", + "certmanager_cert_install_failed_selfsigned": "Nieudana instalacja certyfikatu self-signed dla {domains}", + "certmanager_cert_install_success_selfsigned": "Pomyślna instalacja certyfikatu self-signed dla domeny '{domain}'", + "certmanager_cert_renew_failed": "Nieudane odnowienie certyfikatu Let's Encrypt dla {domains}", + "apps_failed_to_upgrade": "Nieudana aktualizacja aplikacji: {apps}", + "backup_output_directory_required": "Musisz wybrać katalog dla kopii zapasowej", + "app_failed_to_download_asset": "Nie udało się pobrać zasobu '{source_id}' ({url}) dla {app}: {out}", + "backup_with_no_backup_script_for_app": "Aplikacja '{app}' nie posiada skryptu kopii zapasowej. Ignorowanie.", + "backup_with_no_restore_script_for_app": "Aplikacja {app} nie posiada skryptu przywracania, co oznacza, że nie będzie można automatycznie przywrócić kopii zapasowej tej aplikacji.", + "certmanager_acme_not_configured_for_domain": "Wyzwanie ACME nie może zostać uruchomione dla domeny {domain}, ponieważ jej konfiguracja nginx nie zawiera odpowiedniego fragmentu kodu... Upewnij się, że konfiguracja nginx jest aktualna, używając polecenia yunohost tools regen-conf nginx --dry-run --with-diff.", + "certmanager_domain_dns_ip_differs_from_public_ip": "Rekordy DNS dla domeny '{domain}' różnią się od adresu IP tego serwera. Sprawdź kategorię 'Rekordy DNS' (podstawowe) w diagnozie, aby uzyskać więcej informacji. Jeśli niedawno dokonałeś zmiany rekordu A, poczekaj, aż zostanie on zaktualizowany (można skorzystać z narzędzi online do sprawdzania propagacji DNS). (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", + "confirm_app_install_danger": "UWAGA! Ta aplikacja jest wciąż w fazie eksperymentalnej (jeśli nie działa jawnie)! Prawdopodobnie NIE powinieneś jej instalować, chyba że wiesz, co robisz. NIE ZOSTANIE udzielone wsparcie, jeśli ta aplikacja nie będzie działać poprawnie lub spowoduje uszkodzenie systemu... Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}", + "confirm_app_install_thirdparty": "UWAGA! Ta aplikacja nie jest częścią katalogu aplikacji YunoHost. Instalowanie aplikacji innych firm może naruszyć integralność i bezpieczeństwo systemu. Prawdopodobnie NIE powinieneś jej instalować, chyba że wiesz, co robisz. NIE ZOSTANIE udzielone wsparcie, jeśli ta aplikacja nie będzie działać poprawnie lub spowoduje uszkodzenie systemu... Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'", + "config_apply_failed": "Nie udało się zastosować nowej konfiguracji: {error}", + "config_cant_set_value_on_section": "Nie można ustawić pojedynczej wartości dla całej sekcji konfiguracji.", + "config_no_panel": "Nie znaleziono panelu konfiguracji.", + "config_unknown_filter_key": "Klucz filtru '{filter_key}' jest niepoprawny.", + "config_validate_email": "Proszę podać poprawny adres e-mail", + "backup_hook_unknown": "Nieznany jest hook kopii zapasowej '{hook}'.", + "backup_no_uncompress_archive_dir": "Nie istnieje taki katalog nieskompresowanego archiwum.", + "backup_output_symlink_dir_broken": "Twój katalog archiwum '{path}' to uszkodzony dowiązanie symboliczne. Być może zapomniałeś o ponownym zamontowaniu lub podłączeniu nośnika przechowującego, do którego on wskazuje.", + "backup_system_part_failed": "Nie można wykonać kopii zapasowej części systemu '{part}'", + "config_validate_color": "Powinien być poprawnym szesnastkowym kodem koloru RGB.", + "config_validate_date": "Data powinna być poprawna w formacie RRRR-MM-DD", + "config_validate_time": "Podaj poprawny czas w formacie GG:MM", + "certmanager_domain_not_diagnosed_yet": "Nie ma jeszcze wyników diagnozy dla domeny {domain}. Proszę ponownie uruchomić diagnozę dla kategorii 'Rekordy DNS' i 'Strona internetowa' w sekcji diagnozy, aby sprawdzić, czy domena jest gotowa do użycia Let's Encrypt. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", + "certmanager_cannot_read_cert": "Wystąpił problem podczas próby otwarcia bieżącego certyfikatu dla domeny {domain} (plik: {file}), przyczyna: {reason}", + "certmanager_no_cert_file": "Nie można odczytać pliku certyfikatu dla domeny {domain} (plik: {file}).", + "certmanager_self_ca_conf_file_not_found": "Nie można znaleźć pliku konfiguracyjnego dla autorytetu samopodpisującego (plik: {file})", + "backup_running_hooks": "Uruchamianie hooków kopii zapasowej...", + "backup_permission": "Uprawnienia kopii zapasowej dla aplikacji {app}", + "certmanager_domain_cert_not_selfsigned": "Certyfikat dla domeny {domain} nie jest samopodpisany. Czy na pewno chcesz go zastąpić? (Użyj opcji '--force', aby to zrobić.)", + "config_action_disabled": "Nie można uruchomić akcji '{action}', ponieważ jest ona wyłączona. Upewnij się, że spełnione są jej ograniczenia. Pomoc: {help}", + "config_action_failed": "Nie udało się uruchomić akcji '{action}': {error}", + "config_forbidden_readonly_type": "Typ '{type}' nie może być ustawiony jako tylko do odczytu. Użyj innego typu, aby wyświetlić tę wartość (odpowiednie ID argumentu: '{id}')", + "config_forbidden_keyword": "Słowo kluczowe '{keyword}' jest zastrzeżone. Nie można tworzyć ani używać panelu konfiguracji z pytaniem o tym identyfikatorze.", + "backup_output_directory_forbidden": "Wybierz inną ścieżkę docelową. Kopie zapasowe nie mogą być tworzone w podfolderach /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ani /home/yunohost.backup/archives", + "confirm_app_insufficient_ram": "UWAGA! Ta aplikacja wymaga {required} pamięci RAM do zainstalowania/aktualizacji, a obecnie dostępne jest tylko {current}. Nawet jeśli aplikacja mogłaby działać, proces instalacji/aktualizacji wymaga dużej ilości pamięci RAM, więc serwer może się zawiesić i niepowodzenie może być katastrofalne. Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'", + "app_not_upgraded_broken_system": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu. W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}", + "app_not_upgraded_broken_system_continue": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu (parametr --continue-on-failure jest ignorowany). W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}", + "certmanager_domain_http_not_working": "Domena {domain} wydaje się niedostępna przez HTTP. Sprawdź kategorię 'Strona internetowa' diagnostyki, aby uzyskać więcej informacji. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", + "migration_0021_system_not_fully_up_to_date": "Twój system nie jest w pełni zaktualizowany! Proszę, wykonaj zwykłą aktualizację oprogramowania zanim rozpoczniesz migrację na system Bullseye.", + "global_settings_setting_smtp_relay_port": "Port przekaźnika SMTP", + "domain_config_cert_renew": "Odnów certyfikat Let's Encrypt", + "root_password_changed": "Hasło root zostało zmienione", + "diagnosis_services_running": "Usługa {service} działa!", + "global_settings_setting_admin_strength": "Wymogi dotyczące siły hasła administratora", + "global_settings_setting_admin_strength_help": "Wymagania te są egzekwowane tylko podczas inicjalizacji lub zmiany hasła", + "global_settings_setting_pop3_enabled_help": "Włącz protokołu POP3 dla serwera poczty", + "global_settings_setting_postfix_compatibility": "Kompatybilność Postfix", + "global_settings_setting_smtp_relay_user": "Nazwa użytkownika przekaźnika SMTP", + "global_settings_setting_ssh_password_authentication_help": "Zezwól na logowanie hasłem przez SSH", + "diagnosis_apps_allgood": "Wszystkie zainstalowane aplikacje są zgodne z podstawowymi zasadami pakowania", + "diagnosis_basesystem_hardware": "Architektura sprzętowa serwera to {virt} {arch}", + "diagnosis_ip_connected_ipv4": "Serwer jest połączony z Internet z użyciem IPv4!", + "diagnosis_ip_no_ipv6": "Serwer nie ma działającego połączenia z użyciem IPv6.", + "diagnosis_http_hairpinning_issue": "Wygląda na to, że sieć lokalna nie ma \"hairpinning\".", + "backup_unable_to_organize_files": "Nie można użyć szybkiej metody porządkowania plików w archiwum", + "log_letsencrypt_cert_renew": "Odnów '{}' certyfikat Let's Encrypt", + "global_settings_setting_passwordless_sudo": "Umożliw administratorom korzystania z 'sudo' bez konieczności ponownego wpisywania hasła", + "global_settings_setting_smtp_relay_enabled": "Włącz przekaźnik SMTP", + "global_settings_setting_smtp_relay_host": "Host przekaźnika SMTP", + "global_settings_setting_user_strength": "Wymagania dotyczące siły hasła użytkownika", + "domain_config_mail_in": "Odbieranie maili", + "global_settings_setting_webadmin_allowlist_enabled_help": "Zezwól tylko kilku adresom IP na dostęp do panelu webadmin.", + "diagnosis_basesystem_kernel": "Serwer działa pod kontrolą jądra Linuksa {kernel_version}", + "diagnosis_dns_good_conf": "Rekordy DNS zostały poprawnie skonfigurowane dla domeny {domain} (category {category})", + "diagnosis_ram_ok": "System nadal ma {available} ({available_percent}%) wolnej pamięci RAM z całej puli {total}.", + "diagnosis_http_ok": "Domena {domain} jest dostępna przez HTTP z poziomu sieci zewnętrznej.", + "diagnosis_swap_tip": "Pamiętaj, że wykorzystywanie partycji swap na karcie pamięci SD lub na dysku SSD może znacznie skrócić czas działania tego urządzenia.", + "diagnosis_basesystem_host": "Serwer działa pod kontrolą systemu Debian {debian_version}", + "diagnosis_basesystem_ynh_main_version": "Serwer działa pod kontrolą oprogramowania YunoHost {main_version} ({repo})", + "diagnosis_diskusage_verylow": "Przestrzeń {mountpoint} (na dysku {device}) ma tylko {free} ({free_percent}%) wolnego miejsca z całej puli {total}! Rozważ pozbycie się niepotrzebnych plików!", + "global_settings_setting_root_password": "Nowe hasło root", + "global_settings_setting_root_password_confirm": "Powtórz nowe hasło root", + "global_settings_setting_security_experimental_enabled": "Eksperymentalne funkcje bezpieczeństwa", + "global_settings_setting_smtp_relay_password": "Hasło przekaźnika SMTP", + "global_settings_setting_user_strength_help": "Wymagania te są egzekwowane tylko podczas inicjalizacji lub zmiany hasła", + "global_settings_setting_webadmin_allowlist_enabled": "Włącz listę dozwolonych adresów IP dla panelu webadmin", + "root_password_desynchronized": "Hasło administratora zostało zmienione, ale YunoHost nie mógł wykorzystać tego hasła jako hasło root!", + "service_already_started": "Usługa '{service}' już jest włączona", + "diagnosis_ip_dnsresolution_working": "Rozpoznawanie nazw domen działa!", + "diagnosis_regenconf_manually_modified": "Wygląda na to, że plik konfiguracyjny {file} został zmodyfikowany ręcznie.", + "diagnosis_diskusage_ok": "Przestrzeń {mountpoint} (na dysku {device}) nadal ma {free} ({free_percent}%) wolnego miejsca z całej puli {total}!", + "diagnosis_diskusage_low": "Przestrzeń {mountpoint} (na dysku {device}) ma tylko {free} ({free_percent}%) wolnego miejsca z całej puli {total}! Uważaj na możliwe zapełnienie dysku w bliskiej przyszłości.", + "diagnosis_ip_connected_ipv6": "Serwer nie jest połączony z internetem z użyciem IPv6!", + "global_settings_setting_smtp_relay_enabled_help": "Włączenie przekaźnika SMTP, który ma być używany do wysyłania poczty zamiast tej instancji yunohost może być przydatne, jeśli znajdujesz się w jednej z następujących sytuacji: Twój port 25 jest zablokowany przez dostawcę usług internetowych lub dostawcę VPS, masz adres IP zamieszkania wymieniony w DUHL, nie jesteś w stanie skonfigurować odwrotnego DNS lub ten serwer nie jest bezpośrednio widoczny w Internecie i chcesz użyć innego do wysyłania wiadomości e-mail.", + "global_settings_setting_backup_compress_tar_archives_help": "Podczas tworzenia nowych kopii zapasowych archiwa będą skompresowane (.tar.gz), a nie nieskompresowane jak dotychczas (.tar). Uwaga: włączenie tej opcji oznacza tworzenie mniejszych archiwów kopii zapasowych, ale początkowa procedura tworzenia kopii zapasowej będzie znacznie dłuższa i mocniej obciąży procesor.", + "domain_config_mail_out": "Wysyłanie maili", + "domain_dns_registrar_supported": "YunoHost automatycznie wykrył, że ta domena jest obsługiwana przez rejestratora **{registrar}**. Jeśli chcesz, YunoHost automatycznie skonfiguruje rekordy DNS, ale musisz podać odpowiednie dane uwierzytelniające API. Dokumentację dotyczącą uzyskiwania poświadczeń API można znaleźć na tej stronie: https://yunohost.org/registar_api_{registrar}. (Można również ręcznie skonfigurować rekordy DNS zgodnie z dokumentacją na stronie https://yunohost.org/dns )", + "domain_config_cert_summary_letsencrypt": "Świetnie! Wykorzystujesz właściwy certyfikaty Let's Encrypt!", + "global_settings_setting_portal_theme": "Motyw portalu", + "global_settings_setting_portal_theme_help": "Więcej informacji na temat tworzenia niestandardowych motywów portalu można znaleźć na stronie https://yunohost.org/theming", + "global_settings_setting_dns_exposure": "Wersje IP do uwzględnienia w konfiguracji i diagnostyce DNS", + "domain_config_auth_token": "Token uwierzytelniający", + "global_settings_setting_dns_exposure_help": "Uwaga: Ma to wpływ tylko na zalecaną konfigurację DNS i kontrole diagnostyczne. Nie ma to wpływu na konfigurację systemu.", + "global_settings_setting_security_experimental_enabled_help": "Uruchom eksperymentalne funkcje bezpieczeństwa (nie włączaj, jeśli nie wiesz co robisz!)", + "global_settings_setting_smtp_allow_ipv6_help": "Zezwól na wykorzystywanie IPv7 do odbierania i wysyłania maili", + "global_settings_setting_ssh_password_authentication": "Logowanie hasłem", + "diagnosis_backports_in_sources_list": "Wygląda na to że apt (menedżer pakietów) został skonfigurowany tak, aby wykorzystywać repozytorium backported. Nie zalecamy wykorzystywania repozytorium backported, ponieważ może powodować problemy ze stabilnością i/lub konflikty z konfiguracją. No chyba, że wiesz co robisz.", + "domain_config_xmpp_help": "Uwaga: niektóre funkcje XMPP będą wymagały aktualizacji rekordów DNS i odnowienia certyfikatu Lets Encrypt w celu ich włączenia" +} diff --git a/locales/pt.json b/locales/pt.json index 6b462bb6f..0aa6b8223 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -1,8 +1,6 @@ { - "action_invalid": "Acção Inválida '{action}'", + "action_invalid": "Ação inválida '{action}'", "admin_password": "Senha de administração", - "admin_password_change_failed": "Não foi possível alterar a senha", - "admin_password_changed": "A senha da administração foi alterada", "app_already_installed": "{app} já está instalada", "app_extraction_failed": "Não foi possível extrair os arquivos para instalação", "app_id_invalid": "App ID invaĺido", @@ -13,8 +11,6 @@ "app_unknown": "Aplicação desconhecida", "app_upgrade_failed": "Não foi possível atualizar {app}: {error}", "app_upgraded": "{app} atualizado", - "ask_firstname": "Primeiro nome", - "ask_lastname": "Último nome", "ask_main_domain": "Domínio principal", "ask_new_admin_password": "Nova senha de administração", "ask_password": "Senha", @@ -123,14 +119,13 @@ "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo", "app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain}{path}'), nada para fazer.", "password_too_simple_1": "A senha precisa ter pelo menos 8 caracteres", - "admin_password_too_long": "Escolha uma senha que contenha menos de 127 caracteres", "aborting": "Abortando.", "app_change_url_no_script": "A aplicação '{app_name}' ainda não permite modificar a URL. Talvez devesse atualizá-la.", "app_argument_password_no_default": "Erro ao interpretar argumento da senha '{name}': O argumento da senha não pode ter um valor padrão por segurança", "app_action_cannot_be_ran_because_required_services_down": "Estes serviços devem estar funcionado para executar esta ação: {services}. Tente reiniciá-los para continuar (e possivelmente investigar o porquê de não estarem funcionado).", "app_action_broke_system": "Esta ação parece ter quebrado estes serviços importantes: {services}", "already_up_to_date": "Nada a ser feito. Tudo já está atualizado.", - "additional_urls_already_removed": "A URL adicional '{url}'já está removida para a permissão '{permission}'", + "additional_urls_already_removed": "A URL adicional '{url}' já foi removida da permissão '{permission}'", "additional_urls_already_added": "A URL adicional '{url}' já está adicionada para a permissão '{permission}'", "app_install_script_failed": "Ocorreu um erro dentro do script de instalação do aplicativo", "app_install_failed": "Não foi possível instalar {app}: {error}", @@ -151,7 +146,6 @@ "app_restore_script_failed": "Ocorreu um erro dentro do script de restauração da aplicação", "app_restore_failed": "Não foi possível restaurar {app}: {error}", "app_remove_after_failed_install": "Removendo a aplicação após a falha da instalação...", - "app_requirements_unmeet": "Os requisitos para a aplicação {app} não foram satisfeitos, o pacote {pkgname} ({version}) devem ser {spec}", "app_not_upgraded": "Não foi possível atualizar a aplicação '{failed_app}' e, como consequência, a atualização das seguintes aplicações foi cancelada: {apps}", "app_manifest_install_ask_is_public": "Essa aplicação deve ser visível para visitantes anônimos?", "app_manifest_install_ask_admin": "Escolha um usuário de administrador para essa aplicação", @@ -210,7 +204,6 @@ "config_cant_set_value_on_section": "Você não pode setar um único valor na seção de configuração inteira.", "config_validate_time": "Deve ser um horário válido como HH:MM", "config_validate_url": "Deve ser uma URL válida", - "config_version_not_supported": "Versões do painel de configuração '{version}' não são suportadas.", "danger": "Perigo:", "diagnosis_basesystem_ynh_inconsistent_versions": "Você está executando versões inconsistentes dos pacotes YunoHost... provavelmente por causa de uma atualização parcial ou que falhou.", "diagnosis_description_basesystem": "Sistema base", @@ -251,5 +244,6 @@ "diagnosis_basesystem_hardware_model": "O modelo do servidor é {model}", "diagnosis_backports_in_sources_list": "Parece que o apt (o gerenciador de pacotes) está configurado para usar o repositório backport. A não ser que você saiba o que você esteá fazendo, desencorajamos fortemente a instalação de pacotes de backports porque é provável que crie instabilidades ou conflitos no seu sistema.", "certmanager_cert_renew_success": "Certificado Let's Encrypt renovado para o domínio '{domain}'", - "certmanager_warning_subdomain_dns_record": "O subdomínio '{subdomain}' não resolve para o mesmo IP que '{domain}'. Algumas funcionalidades não estarão disponíveis até que você conserte isto e regenere o certificado." + "certmanager_warning_subdomain_dns_record": "O subdomínio '{subdomain}' não resolve para o mesmo IP que '{domain}'. Algumas funcionalidades não estarão disponíveis até que você conserte isto e regenere o certificado.", + "admins": "Admins" } \ No newline at end of file diff --git a/locales/pt_BR.json b/locales/pt_BR.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/locales/pt_BR.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index 1546c4d6e..a9c9da3f1 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,8 +1,6 @@ { "action_invalid": "Неверное действие '{action}'", "admin_password": "Пароль администратора", - "admin_password_change_failed": "Невозможно изменить пароль", - "admin_password_changed": "Пароль администратора был изменен", "app_already_installed": "{app} уже установлено", "app_already_installed_cant_change_url": "Это приложение уже установлено. URL не может быть изменен только с помощью этой функции. Изучите `app changeurl`, если это доступно.", "app_argument_choice_invalid": "Выберите корректное значение аргумента '{name}'; '{value}' не входит в число возможных вариантов: '{choices}'", @@ -29,7 +27,6 @@ "app_upgraded": "{app} обновлено", "installation_complete": "Установка завершена", "password_too_simple_1": "Пароль должен быть не менее 8 символов", - "admin_password_too_long": "Пожалуйста, выберите пароль короче 127 символов", "password_listed": "Этот пароль является одним из наиболее часто используемых паролей в мире. Пожалуйста, выберите что-то более уникальное.", "backup_applying_method_copy": "Копирование всех файлов в резервную копию...", "domain_dns_conf_is_just_a_recommendation": "Эта страница показывает вам *рекомендуемую* конфигурацию. Она не создаёт для вас конфигурацию DNS. Вы должны сами конфигурировать DNS у вашего регистратора в соответствии с этой рекомендацией.", @@ -37,13 +34,11 @@ "password_too_simple_3": "Пароль должен содержать не менее 8 символов и содержать цифры, заглавные и строчные буквы, а также специальные символы", "upnp_enabled": "UPnP включен", "user_deleted": "Пользователь удалён", - "ask_lastname": "Фамилия", "app_action_broke_system": "Это действие, по-видимому, нарушило эти важные службы: {services}", "already_up_to_date": "Ничего делать не требуется. Всё уже обновлено.", "operation_interrupted": "Действие было прервано вручную?", "user_created": "Пользователь создан", "aborting": "Прерывание.", - "ask_firstname": "Имя", "ask_main_domain": "Основной домен", "ask_new_admin_password": "Новый пароль администратора", "ask_new_domain": "Новый домен", @@ -74,7 +69,6 @@ "yunohost_already_installed": "YunoHost уже установлен", "yunohost_configured": "Теперь YunoHost настроен", "upgrading_packages": "Обновление пакетов...", - "app_requirements_unmeet": "Необходимые требования для {app} не выполнены, пакет {pkgname} ({version}) должен быть {spec}", "app_make_default_location_already_used": "Невозможно сделать '{app}' приложением по умолчанию на домене, '{domain}' уже используется '{other_app}'", "app_config_unable_to_apply": "Не удалось применить значения панели конфигурации.", "app_config_unable_to_read": "Не удалось прочитать значения панели конфигурации.", @@ -112,7 +106,6 @@ "certmanager_domain_dns_ip_differs_from_public_ip": "DNS-записи для домена '{domain}' отличаются от IP этого сервера. Пожалуйста, проверьте категорию 'DNS-записи' (основные) в диагностике для получения дополнительной информации. Если вы недавно изменили свою A-запись, пожалуйста, подождите, пока она распространится (некоторые программы проверки распространения DNS доступны в интернете). (Если вы знаете, что делаете, используйте '--no-checks', чтобы отключить эти проверки.)", "certmanager_domain_not_diagnosed_yet": "Для домена {domain} еще нет результатов диагностики. Пожалуйста, перезапустите диагностику для категорий 'DNS-записи' и 'Домены', чтобы проверить, готов ли домен к Let's Encrypt. (Или, если вы знаете, что делаете, используйте '--no-checks', чтобы отключить эти проверки.)", "config_validate_url": "Должна быть правильная ссылка", - "config_version_not_supported": "Версии конфигурационной панели '{version}' не поддерживаются.", "confirm_app_install_danger": "ОПАСНО! Это приложение все еще является экспериментальным (если не сказать, что оно явно не работает)! Вам НЕ следует устанавливать его, если вы НЕ знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы НЕ будем оказывать техническую поддержку... Если вы все равно готовы рискнуть, введите '{answers}'", "confirm_app_install_thirdparty": "ВАЖНО! Это приложение не входит в каталог приложений YunoHost. Установка сторонних приложений может нарушить целостность и безопасность вашей системы. Вам НЕ следует устанавливать его, если вы НЕ знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы НЕ будем оказывать техническую поддержку... Если вы все равно готовы рискнуть, введите '{answers}'", "config_apply_failed": "Не удалось применить новую конфигурацию: {error}", @@ -238,7 +231,6 @@ "regenconf_file_removed": "Файл конфигурации '{conf}' удален", "permission_not_found": "Разрешение '{permission}' не найдено", "group_cannot_edit_all_users": "Группа 'all_users' не может быть отредактирована вручную. Это специальная группа, предназначенная для всех пользователей, зарегистрированных в YunoHost", - "global_settings_setting_smtp_allow_ipv6": "Разрешить использование IPv6 для получения и отправки почты", "log_dyndns_subscribe": "Подписаться на субдомен YunoHost '{}'", "pattern_firstname": "Должно быть настоящее имя", "migrations_pending_cant_rerun": "Эти миграции еще не завершены, поэтому не могут быть запущены снова: {ids}", @@ -264,13 +256,10 @@ "log_backup_create": "Создание резервной копии", "group_update_failed": "Не удалось обновить группу '{group}': {error}", "permission_already_allowed": "В группе '{group}' уже включено разрешение '{permission}'", - "invalid_password": "Неверный пароль", "group_already_exist": "Группа {group} уже существует", "group_cannot_be_deleted": "Группа {group} не может быть удалена вручную.", "log_app_config_set": "Примените конфигурацию приложения '{}'", "log_backup_restore_app": "Восстановление '{}' из резервной копии", - "global_settings_setting_security_webadmin_allowlist": "IP-адреса, разрешенные для доступа к веб-интерфейсу администратора. Разделенные запятыми.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Разрешите доступ к веб-интерфейсу администратора только некоторым IP-адресам.", "log_domain_remove": "Удалить домен '{}' из конфигурации системы", "user_import_success": "Пользователи успешно импортированы", "group_user_already_in_group": "Пользователь {user} уже входит в группу {group}", @@ -284,7 +273,6 @@ "diagnosis_sshd_config_inconsistent_details": "Пожалуйста, выполните yunohost settings set security.ssh.port -v YOUR_SSH_PORT, чтобы определить порт SSH, и проверьте yunohost tools regen-conf ssh --dry-run --with-diff и yunohost tools regen-conf ssh --force, чтобы сбросить ваш conf в соответствии с рекомендациями YunoHost.", "log_domain_main_domain": "Сделать '{}' основным доменом", "diagnosis_sshd_config_insecure": "Похоже, что конфигурация SSH была изменена вручную, и она небезопасна, поскольку не содержит директив 'AllowGroups' или 'AllowUsers' для ограничения доступа авторизованных пользователей.", - "global_settings_setting_security_ssh_port": "SSH порт", "group_already_exist_on_system": "Группа {group} уже существует в системных группах", "group_already_exist_on_system_but_removing_it": "Группа {group} уже существует в системных группах, но YunoHost удалит ее...", "group_unknown": "Группа '{group}' неизвестна", @@ -303,7 +291,6 @@ "regenconf_failed": "Не удалось восстановить конфигурацию для категории(й): {categories}", "diagnosis_services_conf_broken": "Конфигурация нарушена для службы {service}!", "diagnosis_sshd_config_inconsistent": "Похоже, что порт SSH был вручную изменен в /etc/ssh/sshd_config. Начиная с версии YunoHost 4.2, доступен новый глобальный параметр 'security.ssh.port', позволяющий избежать ручного редактирования конфигурации.", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Разрешить использование (устаревшего) ключа хоста DSA для конфигурации демона SSH", "hook_exec_not_terminated": "Скрипт не завершился должным образом: {path}", "ip6tables_unavailable": "Вы не можете играть с ip6tables здесь. Либо Вы находитесь в контейнере, либо ваше ядро это не поддерживает", "iptables_unavailable": "Вы не можете играть с ip6tables здесь. Либо Вы находитесь в контейнере, либо ваше ядро это не поддерживает", @@ -332,7 +319,14 @@ "permission_already_up_to_date": "Разрешение не было обновлено, потому что запросы на добавление/удаление уже соответствуют текущему состоянию.", "group_cannot_edit_primary_group": "Группа '{group}' не может быть отредактирована вручную. Это основная группа, предназначенная для содержания только одного конкретного пользователя.", "log_app_remove": "Удалите приложение '{}'", - "not_enough_disk_space": "Недостаточно свободного места в '{путь}'", + "not_enough_disk_space": "Недостаточно свободного места в '{path}'", "pattern_email_forward": "Должен быть корректный адрес электронной почты, символ '+' допустим (например, someone+tag@example.com)", - "permission_deletion_failed": "Не удалось удалить разрешение '{permission}': {error}" + "permission_deletion_failed": "Не удалось удалить разрешение '{permission}': {error}", + "global_settings_setting_ssh_port": "SSH порт", + "global_settings_setting_webadmin_allowlist_help": "IP-адреса, разрешенные для доступа к веб-интерфейсу администратора. Разделенные запятыми.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Разрешите доступ к веб-интерфейсу администратора только некоторым IP-адресам.", + "global_settings_setting_smtp_allow_ipv6_help": "Разрешить использование IPv6 для получения и отправки почты", + "admins": "Администраторы", + "all_users": "Все пользователи YunoHost", + "app_action_failed": "Не удалось выполнить действие {action} для приложения {app}" } diff --git a/locales/sk.json b/locales/sk.json index ac9d565bc..bead46713 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -1,8 +1,6 @@ { "additional_urls_already_removed": "Dodatočná URL adresa '{url}' už bola odstránená pre oprávnenie '{permission}'", "admin_password": "Heslo pre správu", - "admin_password_change_failed": "Nebolo možné zmeniť heslo", - "admin_password_changed": "Heslo pre správu bolo zmenené", "app_action_broke_system": "Vyzerá, že táto akcia spôsobila nefunkčnosť nasledovných dôležitých služieb: {services}", "app_already_installed": "{app} je už nainštalovaný/á", "app_already_installed_cant_change_url": "Táto aplikácia je už nainštalovaná. Adresa URL nemôže byť touto akciou zmenená. Skontrolujte `app changeurl`, ak je dostupné.", @@ -15,7 +13,6 @@ "aborting": "Zrušené.", "action_invalid": "Nesprávna akcia '{action}'", "additional_urls_already_added": "Dodatočná URL adresa '{url}' už bola pridaná pre oprávnenie '{permission}'", - "admin_password_too_long": "Prosím, vyberte heslo kratšie ako 127 znakov", "already_up_to_date": "Nič netreba robiť. Všetko je už aktuálne.", "app_action_cannot_be_ran_because_required_services_down": "Pre vykonanie tejto akcie by mali byť spustené nasledovné služby: {services}. Skúste ich reštartovať, prípadne zistite, prečo nebežia.", "app_argument_password_no_default": "Chyba pri spracovaní obsahu hesla '{name}': z bezpečnostných dôvodov nemôže obsahovať predvolenú hodnotu", @@ -39,7 +36,7 @@ "app_packaging_format_not_supported": "Túto aplikáciu nie je možné nainštalovať, pretože formát balíčkov, ktorý používa, nie je podporovaný Vašou verziou YunoHost. Mali by ste zvážiť aktualizovanie Vášho systému.", "app_remove_after_failed_install": "Aplikácia sa po chybe počas inštalácie odstraňuje…", "app_removed": "{app} bola odinštalovaná", - "app_requirements_checking": "Kontrolujem programy vyžadované aplikáciou {app}…", + "app_requirements_checking": "Kontrolujem požiadavky aplikácie {app}…", "app_restore_failed": "Nepodarilo sa obnoviť {app}: {error}", "app_restore_script_failed": "Chyba nastala vo vnútri skriptu na obnovu aplikácie", "app_sources_fetch_failed": "Nepodarilo sa získať zdrojové súbory, je adresa URL správna?", @@ -63,12 +60,9 @@ "app_label_deprecated": "Tento príkaz je zastaraný! Prosím, použite nový príkaz 'yunohost user permission update' pre správu názvu aplikácie.", "app_not_installed": "{app} sa nepodarilo nájsť v zozname nainštalovaných aplikácií: {all_apps}", "app_not_upgraded": "Aplikáciu '{failed_app}' sa nepodarilo aktualizovať v dôsledku čoho boli aktualizácie nasledovných aplikácií zrušené: {apps}", - "app_requirements_unmeet": "Požiadavky aplikácie {app} neboli splnené, balíček {pkgname} ({version}) musí byť {spec}", "app_unsupported_remote_type": "Nepodporovaný vzdialený typ použitý pre aplikáciu", "app_upgrade_several_apps": "Nasledovné aplikácie budú aktualizované: {apps}", "apps_catalog_updating": "Aktualizujem repozitár aplikácií…", - "ask_firstname": "Krstné meno", - "ask_lastname": "Priezvisko", "ask_main_domain": "Hlavná doména", "ask_new_admin_password": "Nové heslo pre správu", "ask_new_domain": "Nová doména", @@ -150,7 +144,6 @@ "config_validate_email": "Toto by mal byť platný e-mail", "config_validate_time": "Toto by mal byť platný čas vo formáte HH:MM", "config_validate_url": "Toto by mala byť platná URL adresa webu", - "config_version_not_supported": "Verzie konfiguračného panela '{version}' nie sú podporované.", "danger": "Nebezpečenstvo:", "confirm_app_install_danger": "NEBEZPEČENSTVO! Táto aplikácia je experimentálna (ak vôbec funguje)! Pravdepodobne by ste ju NEMALI inštalovať, pokiaľ si nie ste istý, čo robíte. NEPOSKYTNEME VÁM ŽIADNU POMOC, ak táto aplikácia nebude fungovať alebo rozbije Váš systém… Ak sa rozhodnete i napriek tomu podstúpiť toto riziko, zadajte '{answers}'", "confirm_app_install_thirdparty": "NEBEZPEČENSTVO! Táto aplikácia nie je súčasťou katalógu aplikácií YunoHost. Inštalovaním aplikácií tretích strán môžete ohroziť integritu a bezpečnosť Vášho systému. Pravdepodobne by ste NEMALI pokračovať v inštalácií, pokiaľ neviete, čo robíte. NEPOSKYTNEME VÁM ŽIADNU POMOC, ak táto aplikácia nebude fungovať alebo rozbije Váš systém… Ak sa rozhodnete i napriek tomu podstúpiť toto riziko, zadajte '{answers}'", @@ -160,7 +153,7 @@ "diagnosis_apps_broken": "Táto aplikácia je v katalógu aplikácií YunoHost momentálne označená ako rozbitá. Toto môže byť dočasný problém do momentu, kedy jej správcovia danú chybu neopravia. Kým sa tak stane sú aktualizácie tejto aplikácie vypnuté.", "diagnosis_apps_deprecated_practices": "Táto verzia nainštalovanej aplikácie používa niektoré prehistorické a zastaralé zásady balíčkovania. Naozaj by ste mali zvážiť jej aktualizovanie.", "diagnosis_apps_issue": "V aplikácií {app} sa našla chyba", - "diagnosis_apps_outdated_ynh_requirement": "Tejto verzii nainštalovanej aplikácie stačí yunohost vo verzii 2.x, čo naznačuje, že neobsahuje aktuálne odporúčané zásady balíčkovania a pomocné skripty. Naozaj by ste mali zvážiť jej aktualizáciu.", + "diagnosis_apps_outdated_ynh_requirement": "Táto verzia nainštalovanej aplikácie vyžaduje yunohost iba vo verzii 2.x alebo 3.x, čo naznačuje, že neobsahuje aktuálne odporúčané zásady balíčkovania a pomocné skripty. Naozaj by ste mali zvážiť jej aktualizáciu.", "diagnosis_basesystem_hardware": "Hardvérová architektúra servera je {virt} {arch}", "diagnosis_basesystem_hardware_model": "Model servera je {model}", "diagnosis_basesystem_host": "Server beží na Debiane {debian_version}", @@ -204,12 +197,67 @@ "diagnosis_everything_ok": "V kategórii {category} vyzerá byť všetko v poriadku!", "diagnosis_failed": "Nepodarilo sa získať výsledok diagnostiky pre kategóriu '{category}': {error}", "diagnosis_failed_for_category": "Diagnostika pre kategóriu '{category}' skončila s chybou: {error}", - "diagnosis_found_errors": "Bolo nájdených {error} závažných chýb týkajúcich sa {category}!", - "diagnosis_found_errors_and_warnings": "Bolo nájdených {error} závažných chýb (a {warnings} varovaní) týkajúcich sa {category}!", + "diagnosis_found_errors": "Bolo nájdených {errors} závažných chýb týkajúcich sa {category}!", + "diagnosis_found_errors_and_warnings": "Bolo nájdených {errors} závažných chýb (a {warnings} varovaní) týkajúcich sa {category}!", "diagnosis_found_warnings": "V kategórii {category} bolo nájdených {warnings} položiek, ktoré je možné opraviť.", "diagnosis_http_connection_error": "Chyba pripojenia: nepodarilo sa pripojiť k požadovanej doméne, podľa všetkého je nedostupná.", "diagnosis_http_could_not_diagnose": "Nepodarilo sa zistiť, či sú domény dostupné zvonka pomocou IPv{ipversion}.", "diagnosis_http_could_not_diagnose_details": "Chyba: {error}", "diagnosis_http_hairpinning_issue": "Zdá sa, že Vaša miestna sieť nemá zapnutý NAT hairpinning.", - "diagnosis_high_number_auth_failures": "V poslednom čase bol zistený neobvykle vysoký počet neúspešných prihlásení. Uistite sa, či je služba fail2ban spustená a správne nastavená alebo použite vlastný port pre SSH ako je popísané na https://yunohost.org/security." + "diagnosis_high_number_auth_failures": "V poslednom čase bol zistený neobvykle vysoký počet neúspešných prihlásení. Uistite sa, či je služba fail2ban spustená a správne nastavená alebo použite vlastný port pre SSH ako je popísané na https://yunohost.org/security.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Pre opravu tohto problému preskúmajte rozdiely medzi konfiguráciami v termináli príkazom yunohost tools regen-conf nginx --dry-run --with-diff a ak so zmenami súhlasíte, aplikujte ich príkazom yunohost tools regen-conf nginx --force.", + "diagnosis_http_timeout": "Pri pokuse o kontaktovanie servera zvonku vypršal časový limit. Vyzerá byť nedostupný.
1. Najčastejšou príčinou tohto problému zvykne byť nesprávne nastavenie presmerovania portu 80 (a 443) na váš server.
2. Mali by ste skontrolovať, či je služba nginx spustená.
3. Pri komplexnejších inštaláciach: ubezpečte sa, že problém nie je spôsobený bránou firewall alebo reverznou proxy.", + "diagnosis_http_bad_status_code": "Zdá sa, že miesto vášho servera na vašu požiadavku zareagoval iný počítač (možno váš router).
1. Najčastejšou príčinou tohto problému zvykne byť nesprávne nastavenie presmerovania portu 80 (a 443) na váš server.
2. Pri komplexnejších inštaláciach: ubezpečte sa, že problém nie je spôsobený bránou firewall alebo reverznou proxy.", + "diagnosis_http_hairpinning_issue_details": "Toto pravdepodobne spôsobuje zariadenia od vášho poskytovateľa internetu / router. Vo výsledku pre používateľov mimo vašej miestnej siete (zvonku) funguje pripojenie na server normálne, to však neplatí pre používateľov v rámci miestnej siete (ako ste možno aj vy?) pri použití doménového mena alebo globálnej IP adresy. Túto situáciu sa vám možno podarí vyriešiť po prečítaní ", + "diagnosis_http_nginx_conf_not_up_to_date": "Nginx konfigurácia tejto domény sa zdá byť upravená ručne a znemožňuje YunoHost-u zistiť, či je dostupná na HTTP.", + "diagnosis_http_ok": "Doména {domain} je dostupná prostredníctvom HTTP mimo miestnej siete.", + "diagnosis_http_partially_unreachable": "Doména {domain} sa zdá byť nedostupná prostredníctvom HTTP mimo miestnej siete pri použití IPv{failed}, hoci funguje pri IPv{passed}.", + "diagnosis_http_special_use_tld": "Doména {domain} je založená na top-level doméne (TLD) pre zvláštne určenie ako je .local alebo .test a preto sa neočakáva, aby bola dostupná mimo miestnej siete.", + "diagnosis_http_unreachable": "Doména {domain} sa zdá byť nedostupná prostredníctvom HTTP mimo miestnej siete.", + "diagnosis_ignored_issues": "(+ {nb_ignored} ignorovaný(ch) problém(ov))", + "diagnosis_ip_no_ipv6_tip": "Váš server bude fungovať aj bez IPv6, no pre celkové zdravie internetu je lepšie ho nastaviť. V prípade, že je IPv6 dostupné, systém alebo váš poskytovateľ by ho mal automaticky nakonfigurovať. V opačnom prípade budete možno musieť nastaviť zopár vecí ručne tak, ako je vysvetlené v dokumentácii na https://yunohost.org/#/ipv6. Ak nemôžete povoliť IPv6 alebo je to na vás príliš technicky náročné, môžete pokojne toto upozornenie ignorovať.", + "diagnosis_ip_broken_dnsresolution": "Zdá sa, že z nejakého dôvodu nefunguje prekladanie názvov domén… Blokuje vaša brána firewall DNS požiadavky?", + "diagnosis_ip_broken_resolvconf": "Zdá sa, že na vašom serveri nefunguje prekladanie názvov domén, čo môže súvisieť s tým, že /etc/resolv.conf neukazuje na 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "Server nie je pripojený k internetu prostredníctvom IPv4!", + "diagnosis_ip_connected_ipv6": "Server nie je pripojený k internetu prostredníctvom IPv6!", + "diagnosis_ip_dnsresolution_working": "Preklad názvov domén nefunguje!", + "diagnosis_ip_global": "Globálna IP adresa: {global}", + "diagnosis_ip_local": "Miestna IP adresa: {local}", + "diagnosis_ip_no_ipv4": "Na serveri nefunguje spojenie cez protokol IPv4.", + "diagnosis_ip_no_ipv6": "Na serveri nefunguje spojenie cez protokol IPv6.", + "diagnosis_ip_not_connected_at_all": "Zdá sa, že tento server nie je vôbec pripojený k internetu!?", + "diagnosis_ip_weird_resolvconf": "Zdá sa, že preklad názvov domén funguje, ale podľa všetkého používate vlastný súbor /etc/resolv.conf.", + "root_password_desynchronized": "Heslo pre správu bolo zmenené, ale YunoHost nedokázal túto zmenu premietnuť do hesla používateľa root!", + "main_domain_changed": "Hlavná doména bola zmenená", + "user_updated": "Informácie o používateľovi boli zmenené", + "diagnosis_ram_verylow": "Systém má iba {available} ({available_percent} %) dostupnej pamäte RAM (z celkovej pamäte {total})!", + "diagnosis_mail_queue_unavailable_details": "Chyba: {error}", + "diagnosis_ram_ok": "Systém má ešte {available} ({available_percent} %) dostupnej pamäte RAM (z celkovej pamäte {total}).", + "diagnosis_ram_low": "Systém má {available} ({available_percent} %) dostupnej pamäte RAM (z celkovej pamäte {total}). Buďte opatrný.", + "diagnosis_sshd_config_inconsistent": "Zdá sa, že port SSH bol manuálne upravený v /etc/ssh/sshd_config. Od YunoHost 4.2 je dostupné nové globálne nastavenie 'security.ssh.port', aby ste nemuseli konfiguráciu editovať ručne.", + "domains_available": "Dostupné domény:", + "dyndns_could_not_check_available": "Nepodarilo sa zistiť, či je {domain} dostupná na {provider}.", + "dyndns_unavailable": "Doména '{domain}' nie je dostupná.", + "log_available_on_yunopaste": "Tento záznam je teraz dostupný na {url}", + "updating_apt_cache": "Získavam dostupné aktualizácie systémových balíčkov…", + "admins": "Správcovia", + "app_action_failed": "Nepodarilo sa spustiť akciu {action} v aplikácii {app}", + "app_manifest_install_ask_init_admin_permission": "Kto má mať prístup k nastaveniam určených správcovi tejto aplikácie? (Nastavenie môžete neskôr zmeniť)", + "ask_admin_fullname": "Celé meno správcu", + "ask_admin_username": "Používateľské meno správcu", + "ask_fullname": "Celé meno", + "all_users": "Všetci používatelia YunoHost", + "app_manifest_install_ask_init_main_permission": "Kto má mať prístup k tejto aplikácii? (Nastavenie môžete neskôr zmeniť)", + "certmanager_cert_install_failed": "Inštalácia Let's Encrypt certifikátu pre {domains} skončila s chybou", + "app_arch_not_supported": "Túto aplikáciu možno nainštalovať iba na architektúrach {required}, ale Váš server beží na architektúre {current}", + "log_help_to_get_failed_log": "Akciu '{desc}' sa nepodarilo dokončiť. Ak potrebujete pomoc, zdieľajte, prosím, úplný záznam tejto operácie pomocou príkazu 'yunohost log share {name}'", + "operation_interrupted": "Bola akcia manuálne prerušená?", + "log_link_to_failed_log": "Akciu '{desc}' sa nepodarilo dokončiť. Ak potrebujete pomoc, poskytnite, prosím, úplný záznam tejto operácie kliknutím sem", + "app_change_url_failed": "Nepodarilo sa zmeniť URL adresu aplikácie {app}: {error}", + "app_yunohost_version_not_supported": "Táto aplikácia vyžaduje YunoHost >= {required}, ale aktuálne nainštalovaná verzia je {current}", + "config_action_failed": "Nepodarilo sa spustiť operáciu '{action}': {error}", + "app_change_url_script_failed": "Vo skripte na zmenu URL adresy sa vyskytla chyba", + "app_not_enough_disk": "Táto aplikácia vyžaduje {required} voľného miesta.", + "app_not_enough_ram": "Táto aplikácia vyžaduje {required} pamäte na inštaláciu/aktualizáciu, ale k dispozícii je momentálne iba {current}.", + "apps_failed_to_upgrade": "Nasledovné aplikácie nebolo možné aktualizovať: {apps}" } diff --git a/locales/sv.json b/locales/sv.json index 39707d07c..6382612cd 100644 --- a/locales/sv.json +++ b/locales/sv.json @@ -3,9 +3,6 @@ "app_action_broke_system": "Åtgärden verkar ha fått följande viktiga tjänster att haverera: {services}", "already_up_to_date": "Ingenting att göra. Allt är redan uppdaterat.", "admin_password": "Administratörslösenord", - "admin_password_too_long": "Välj gärna ett lösenord som inte innehåller fler än 127 tecken", - "admin_password_change_failed": "Kan inte byta lösenord", "action_invalid": "Ej tillåten åtgärd '{action}'", - "admin_password_changed": "Administratörskontots lösenord ändrades", "aborting": "Avbryter." } \ No newline at end of file diff --git a/locales/te.json b/locales/te.json index fa6ac91c8..7a06f88ef 100644 --- a/locales/te.json +++ b/locales/te.json @@ -3,16 +3,42 @@ "action_invalid": "చెల్లని చర్య '{action}'", "additional_urls_already_removed": "'{permission}' అనుమతి కొరకు అదనపు URLలో అదనంగా URL '{url}' ఇప్పటికే జోడించబడింది", "admin_password": "అడ్మినిస్ట్రేషన్ పాస్వర్డ్", - "admin_password_changed": "అడ్మినిస్ట్రేషన్ పాస్వర్డ్ మార్చబడింది", "already_up_to_date": "చేయడానికి ఏమీ లేదు. ప్రతిదీ ఎప్పటికప్పుడు తాజాగా ఉంది.", "app_already_installed": "{app} ఇప్పటికే ఇన్స్టాల్ చేయబడింది", "app_already_up_to_date": "{app} ఇప్పటికే అప్-టూ-డేట్గా ఉంది", "app_argument_invalid": "ఆర్గ్యుమెంట్ '{name}' కొరకు చెల్లుబాటు అయ్యే వైల్యూ ఎంచుకోండి: {error}", "additional_urls_already_added": "'{permission}' అనుమతి కొరకు అదనపు URLలో అదనంగా URL '{url}' ఇప్పటికే జోడించబడింది", - "admin_password_change_failed": "అనుమతిపదాన్ని మార్చడం సాధ్యం కాదు", - "admin_password_too_long": "దయచేసి 127 క్యారెక్టర్ల కంటే చిన్న పాస్వర్డ్ ఎంచుకోండి", "app_action_broke_system": "ఈ చర్య ఈ ముఖ్యమైన సేవలను విచ్ఛిన్నం చేసినట్లుగా కనిపిస్తోంది: {services}", "app_action_cannot_be_ran_because_required_services_down": "ఈ చర్యను అమలు చేయడానికి ఈ అవసరమైన సేవలు అమలు చేయబడాలి: {services}. కొనసాగడం కొరకు వాటిని పునఃప్రారంభించడానికి ప్రయత్నించండి (మరియు అవి ఎందుకు పనిచేయడం లేదో పరిశోధించవచ్చు).", - "app_argument_choice_invalid": "ఆర్గ్యుమెంట్ '{name}' కొరకు చెల్లుబాటు అయ్యే వైల్యూ ఎంచుకోండి: '{value}' అనేది లభ్యం అవుతున్న ఎంపికల్లో ({Choices}) లేదు", - "app_argument_password_no_default": "పాస్వర్డ్ ఆర్గ్యుమెంట్ '{name}'ని పార్సింగ్ చేసేటప్పుడు దోషం: భద్రతా కారణం కొరకు పాస్వర్డ్ ఆర్గ్యుమెంట్ డిఫాల్ట్ విలువను కలిగి ఉండరాదు" -} + "app_argument_choice_invalid": "ఆర్గ్యుమెంట్ '{name}' కొరకు చెల్లుబాటు అయ్యే వైల్యూ ఎంచుకోండి: '{value}' అనేది లభ్యం అవుతున్న ఎంపికల్లో ({choices}) లేదు", + "app_argument_password_no_default": "పాస్వర్డ్ ఆర్గ్యుమెంట్ '{name}'ని పార్సింగ్ చేసేటప్పుడు దోషం: భద్రతా కారణం కొరకు పాస్వర్డ్ ఆర్గ్యుమెంట్ డిఫాల్ట్ విలువను కలిగి ఉండరాదు", + "app_extraction_failed": "ఇన్‌స్టాలేషన్ ఫైల్‌లను సంగ్రహించడం సాధ్యపడలేదు", + "app_id_invalid": "చెల్లని యాప్ ID", + "app_install_failed": "{app}ని ఇన్‌స్టాల్ చేయడం సాధ్యపడలేదు: {error}", + "app_install_script_failed": "యాప్ ఇన్‌స్టాలేషన్ స్క్రిప్ట్‌లో లోపం సంభవించింది", + "app_manifest_install_ask_domain": "ఈ యాప్‌ను ఇన్‌స్టాల్ చేయాల్సిన డొమైన్‌ను ఎంచుకోండి", + "app_manifest_install_ask_password": "ఈ యాప్‌కు అడ్మినిస్ట్రేషన్ పాస్‌వర్డ్‌ను ఎంచుకోండి", + "app_not_installed": "ఇన్‌స్టాల్ చేసిన యాప్‌ల జాబితాలో {app}ని కనుగొనడం సాధ్యపడలేదు: {all apps}", + "app_removed": "{app} అన్‌ఇన్‌స్టాల్ చేయబడింది", + "app_restore_failed": "{app}: {error}ని పునరుద్ధరించడం సాధ్యపడలేదు", + "app_start_backup": "{app} కోసం బ్యాకప్ చేయాల్సిన ఫైల్‌లను సేకరిస్తోంది...", + "app_start_install": "{app}ని ఇన్‌స్టాల్ చేస్తోంది...", + "app_start_restore": "{app}ని పునరుద్ధరిస్తోంది...", + "app_unknown": "తెలియని యాప్", + "app_upgrade_failed": "అప్‌గ్రేడ్ చేయడం సాధ్యపడలేదు {app}: {error}", + "app_manifest_install_ask_admin": "ఈ యాప్ కోసం నిర్వాహక వినియోగదారుని ఎంచుకోండి", + "app_argument_required": "ఆర్గ్యుమెంట్ '{name}' అవసరం", + "app_change_url_success": "{app} URL ఇప్పుడు {domain}{path}", + "app_config_unable_to_apply": "config ప్యానెల్ values దరఖాస్తు చేయడంలో విఫలమయ్యాము.", + "app_install_files_invalid": "ఈ ఫైల్‌లను ఇన్‌స్టాల్ చేయడం సాధ్యం కాదు", + "app_manifest_install_ask_is_public": "అనామక సందర్శకులకు ఈ యాప్ బహిర్గతం కావాలా?", + "app_not_correctly_installed": "{app} తప్పుగా ఇన్‌స్టాల్ చేయబడినట్లుగా ఉంది", + "app_not_properly_removed": "{app} సరిగ్గా తీసివేయబడలేదు", + "app_remove_after_failed_install": "ఇన్‌స్టాలేషన్ విఫలమైనందున యాప్‌ని తీసివేస్తోంది...", + "app_requirements_checking": "{app} కోసం అవసరమైన ప్యాకేజీలను తనిఖీ చేస్తోంది...", + "app_restore_script_failed": "యాప్ పునరుద్ధరణ స్క్రిప్ట్‌లో లోపం సంభవించింది", + "app_sources_fetch_failed": "మూలాధార ఫైల్‌లను పొందడం సాధ్యపడలేదు, URL సరైనదేనా?", + "app_start_remove": "{app}ని తీసివేస్తోంది...", + "app_upgrade_app_name": "ఇప్పుడు {app}ని అప్‌గ్రేడ్ చేస్తోంది...", + "app_config_unable_to_read": "కాన్ఫిగరేషన్ ప్యానెల్ విలువలను చదవడంలో విఫలమైంది." +} \ No newline at end of file diff --git a/locales/tr.json b/locales/tr.json index 6c881eec7..1af0ffd54 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -1,3 +1,19 @@ { - "password_too_simple_1": "Şifre en az 8 karakter uzunluğunda olmalı" + "password_too_simple_1": "Şifre en az 8 karakter uzunluğunda olmalı", + "action_invalid": "Geçersiz işlem '{action}'", + "admin_password": "Yönetici şifresi", + "already_up_to_date": "Yapılacak yeni bir şey yok. Her şey zaten güncel.", + "app_action_broke_system": "Bu işlem bazı hizmetleri bozmuş olabilir: {services}", + "good_practices_about_user_password": "Şimdi yeni bir kullanıcı şifresi tanımlamak üzeresiniz. Parola en az 8 karakter uzunluğunda olmalıdır - ancak daha uzun bir parola (yani bir parola) ve/veya çeşitli karakterler (büyük harf, küçük harf, rakamlar ve özel karakterler) daha iyidir.", + "aborting": "İptal ediliyor.", + "app_action_failed": "{app} uygulaması için {action} eylemini çalıştırma başarısız", + "admins": "Yöneticiler", + "all_users": "Tüm YunoHost kullanıcıları", + "app_already_up_to_date": "{app} zaten güncel", + "app_already_installed": "{app} zaten kurulu", + "app_already_installed_cant_change_url": "Bu uygulama zaten kurulu. URL yalnızca bu işlev kullanarak değiştirilemez. Eğer varsa `app changeurl`'i kontrol edin.", + "additional_urls_already_added": "Ek URL '{url}' zaten '{permission}' izni için ek URL'ye eklendi", + "additional_urls_already_removed": "Ek URL '{url}', '{permission}' izni için ek URL'de zaten kaldırıldı", + "app_action_cannot_be_ran_because_required_services_down": "Bu eylemi gerçekleştirmek için şu servisler çalışıyor olmalıdır: {services}. Devam etmek için onları yeniden başlatın (ve muhtemelen neden çalışmadığını araştırın).", + "app_arch_not_supported": "Bu uygulama yalnızca {required} işlemci mimarisi üzerine kurulabilir ancak sunucunuzun işlemci mimarisi {current}." } \ No newline at end of file diff --git a/locales/uk.json b/locales/uk.json index 9a32a597b..07cbfe6da 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -22,10 +22,7 @@ "app_action_broke_system": "Ця дія, схоже, порушила роботу наступних важливих служб: {services}", "app_action_cannot_be_ran_because_required_services_down": "Для виконання цієї дії повинні бути запущені наступні необхідні служби: {services}. Спробуйте перезапустити їх, щоб продовжити (і, можливо, з'ясувати, чому вони не працюють).", "already_up_to_date": "Нічого не потрібно робити. Все вже актуально.", - "admin_password_too_long": "Будь ласка, виберіть пароль коротше 127 символів", - "admin_password_changed": "Пароль адміністрації було змінено", - "admin_password_change_failed": "Неможливо змінити пароль", - "admin_password": "Пароль адміністрації", + "admin_password": "Пароль адмініструванні", "additional_urls_already_removed": "Додаткова URL-адреса '{url}' вже видалена в додатковій URL-адресі для дозволу '{permission}'", "additional_urls_already_added": "Додаткова URL-адреса '{url}' вже додана в додаткову URL-адресу для дозволу '{permission}'", "action_invalid": "Неприпустима дія '{action}'", @@ -66,7 +63,6 @@ "server_reboot": "Сервер буде перезавантажено", "server_shutdown_confirm": "Сервер буде негайно вимкнено, ви впевнені? [{answers}]", "server_shutdown": "Сервер буде вимкнено", - "root_password_replaced_by_admin_password": "Ваш кореневий (root) пароль було замінено на пароль адміністратора.", "root_password_desynchronized": "Пароль адміністратора було змінено, але YunoHost не зміг поширити це на кореневий (root) пароль!", "restore_system_part_failed": "Не вдалося відновити системний розділ '{part}'", "restore_running_hooks": "Запуск хуків відновлення…", @@ -126,8 +122,8 @@ "pattern_port_or_range": "Має бути припустимий номер порту (наприклад, 0-65535) або діапазон портів (наприклад, 100:200)", "pattern_password": "Має бути довжиною не менше 3 символів", "pattern_mailbox_quota": "Має бути розмір з суфіксом b/k/M/G/T або 0, щоб не мати квоти", - "pattern_lastname": "Має бути припустиме прізвище", - "pattern_firstname": "Має бути припустиме ім'я", + "pattern_lastname": "Має бути припустиме прізвище (принаймні 3 символи)", + "pattern_firstname": "Має бути припустиме ім'я (принаймні 3 символи)", "pattern_email": "Має бути припустима адреса е-пошти, без символу '+' (наприклад, someone@example.com)", "pattern_email_forward": "Має бути припустима адреса е-пошти, символ '+' приймається (наприклад, someone+tag@example.com)", "pattern_domain": "Має бути припустиме доменне ім'я (наприклад, my-domain.org)", @@ -140,7 +136,7 @@ "operation_interrupted": "Операція була вручну перервана?", "invalid_number": "Має бути числом", "not_enough_disk_space": "Недостатньо вільного місця на '{path}'", - "migrations_to_be_ran_manually": "Міграція {id} повинна бути запущена вручну. Будь ласка, перейдіть в розділ Засоби → Міграції на сторінці вебадміністрації або виконайте команду `yunohost tools migrations run`.", + "migrations_to_be_ran_manually": "Міграція {id} повинна бути запущена вручну. Будь ласка, перейдіть в розділ Засоби → Міграції на сторінці вебадмініструванні або виконайте команду `yunohost tools migrations run`.", "migrations_success_forward": "Міграцію {id} завершено", "migrations_skip_migration": "Пропускання міграції {id}...", "migrations_running_forward": "Виконання міграції {id}...", @@ -163,7 +159,7 @@ "migration_ldap_backup_before_migration": "Створення резервної копії бази даних LDAP і налаштування застосунків перед фактичною міграцією.", "main_domain_changed": "Основний домен було змінено", "main_domain_change_failed": "Неможливо змінити основний домен", - "mail_unavailable": "Ця е-пошта зарезервована і буде автоматично виділена найпершому користувачеві", + "mail_unavailable": "Ця адреса електронної пошти зарезервована для групи адміністраторів", "mailbox_used_space_dovecot_down": "Поштова служба Dovecot повинна бути запущена, якщо ви хочете отримати використане місце в поштовій скриньці", "mailbox_disabled": "Е-пошта вимкнена для користувача {user}", "mail_forward_remove_failed": "Не вдалося видалити переадресацію електронної пошти '{mail}'", @@ -184,7 +180,7 @@ "log_user_delete": "Видалення користувача '{}'", "log_user_create": "Додавання користувача '{}'", "log_regen_conf": "Перестворення системних конфігурацій '{}'", - "log_letsencrypt_cert_renew": "Оновлення сертифікату Let's Encrypt на домені '{}'", + "log_letsencrypt_cert_renew": "Поновлення сертифікату Let's Encrypt на домені '{}'", "log_selfsigned_cert_install": "Установлення самопідписаного сертифікату на домені '{}'", "log_permission_url": "Оновлення URL, пов'язаногл з дозволом '{}'", "log_permission_delete": "Видалення дозволу '{}'", @@ -195,7 +191,6 @@ "log_domain_remove": "Вилучення домену '{}' з конфігурації системи", "log_domain_add": "Додавання домену '{}' в конфігурацію системи", "log_remove_on_failed_install": "Вилучення '{}' після невдалого встановлення", - "log_remove_on_failed_restore": "Вилучення '{}' після невдалого відновлення з резервного архіву", "log_backup_restore_app": "Відновлення '{}' з архіву резервних копій", "log_backup_restore_system": "Відновлення системи з резервного архіву", "log_backup_create": "Створення резервного архіву", @@ -239,39 +234,16 @@ "group_already_exist_on_system": "Група {group} вже існує в групах системи", "group_already_exist": "Група {group} вже існує", "good_practices_about_user_password": "Зараз ви збираєтеся поставити новий пароль користувача. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", - "good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адміністрації. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольного гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", - "global_settings_unknown_type": "Несподівана ситуація, налаштування {setting} має тип {unknown_type}, але це не тип, підтримуваний системою.", - "global_settings_setting_backup_compress_tar_archives": "При створенні нових резервних копій стискати архіви (.tar.gz) замість нестислих архівів (.tar). NB: вмикання цієї опції означає створення легших архівів резервних копій, але початкова процедура резервного копіювання буде значно довшою і важчою для CPU.", - "global_settings_setting_security_webadmin_allowlist": "IP-адреси, яким дозволений доступ до вебадміністрації. Через кому.", - "global_settings_setting_security_webadmin_allowlist_enabled": "Дозволити доступ до вебадміністрації тільки деяким IP-адресам.", - "global_settings_setting_smtp_relay_password": "Пароль хоста SMTP-ретрансляції", - "global_settings_setting_smtp_relay_user": "Обліковий запис користувача SMTP-ретрансляції", + "good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адміністрування. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольної фрази) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", + "global_settings_setting_smtp_relay_password": "Пароль SMTP-ретрансляції", + "global_settings_setting_smtp_relay_user": "Користувач SMTP-ретрансляції", "global_settings_setting_smtp_relay_port": "Порт SMTP-ретрансляції", - "global_settings_setting_smtp_relay_host": "Хост SMTP-ретрансляції, який буде використовуватися для надсилання е-пошти замість цього зразка Yunohost. Корисно, якщо ви знаходитеся в одній із цих ситуацій: ваш 25 порт заблокований вашим провайдером або VPS провайдером, у вас є житловий IP в списку DUHL, ви не можете налаштувати зворотний DNS або цей сервер не доступний безпосередньо в Інтернеті і ви хочете використовувати інший сервер для відправки електронних листів.", - "global_settings_setting_smtp_allow_ipv6": "Дозволити використання IPv6 для отримання і надсилання листів е-пошти", - "global_settings_setting_ssowat_panel_overlay_enabled": "Увімкнути накладення панелі SSOwat", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Дозволити використання (застарілого) ключа DSA для конфігурації демона SSH", - "global_settings_unknown_setting_from_settings_file": "Невідомий ключ в налаштуваннях: '{setting_key}', відхиліть його і збережіть у /etc/yunohost/settings-unknown.json", - "global_settings_setting_security_ssh_port": "SSH-порт", - "global_settings_setting_security_postfix_compatibility": "Компроміс між сумісністю і безпекою для сервера Postfix. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", - "global_settings_setting_security_ssh_compatibility": "Компроміс між сумісністю і безпекою для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", - "global_settings_setting_security_password_user_strength": "Надійність пароля користувача", - "global_settings_setting_security_password_admin_strength": "Надійність пароля адміністратора", - "global_settings_setting_security_nginx_compatibility": "Компроміс між сумісністю і безпекою для вебсервера NGINX. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", - "global_settings_setting_pop3_enabled": "Увімкніть протокол POP3 для поштового сервера", - "global_settings_reset_success": "Попередні налаштування тепер збережені в {path}", - "global_settings_key_doesnt_exists": "Ключ '{settings_key}' не існує в глобальних налаштуваннях, ви можете побачити всі доступні ключі, виконавши команду 'yunohost settings list'", - "global_settings_cant_write_settings": "Неможливо зберегти файл налаштувань, причина: {reason}", - "global_settings_cant_serialize_settings": "Не вдалося серіалізувати дані налаштувань, причина: {reason}", - "global_settings_cant_open_settings": "Не вдалося відкрити файл налаштувань, причина: {reason}", - "global_settings_bad_type_for_setting": "Поганий тип для налаштування {setting}, отримано {received_type}, а очікується {expected_type}", - "global_settings_bad_choice_for_enum": "Поганий вибір для налаштування {setting}, отримано '{choice}', але доступні наступні варіанти: {available_choices}", + "global_settings_setting_ssowat_panel_overlay_enabled": "Увімкнути невеликий ярлик порталу YunoHost у застосунках", "firewall_rules_cmd_failed": "Деякі команди правил фаєрвола не спрацювали. Подробиці в журналі.", "firewall_reloaded": "Фаєрвол перезавантажено", "firewall_reload_failed": "Не вдалося перезавантажити фаєрвол", "file_does_not_exist": "Файл {path} не існує.", "field_invalid": "Неприпустиме поле '{}'", - "experimental_feature": "Попередження: Ця функція є експериментальною і не вважається стабільною, ви не повинні використовувати її, якщо не знаєте, що робите.", "extracting": "Витягнення...", "dyndns_unavailable": "Домен '{domain}' недоступний.", "dyndns_domain_not_provided": "DynDNS провайдер {provider} не може надати домен {domain}.", @@ -285,7 +257,7 @@ "dyndns_ip_update_failed": "Не вдалося оновити IP-адресу в DynDNS", "dyndns_could_not_check_available": "Не вдалося перевірити, чи {domain} доступний у {provider}.", "dpkg_lock_not_available": "Ця команда не може бути виконана прямо зараз, тому що інша програма, схоже, використовує блокування dpkg (системного менеджера пакетів)", - "dpkg_is_broken": "Ви не можете зробити це прямо зараз, тому що dpkg/APT (системні менеджери пакетів), схоже, знаходяться в зламаному стані... Ви можете спробувати вирішити цю проблему, під'єднавшись через SSH і виконавши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`.", + "dpkg_is_broken": "Ви не можете зробити це прямо зараз, тому що dpkg/APT (системні менеджери пакетів), схоже, знаходяться в зламаному стані... Ви можете спробувати вирішити цю проблему, під'єднавшись через SSH і виконавши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`та/або `sudo dpkg --audit`.", "downloading": "Завантаження…", "done": "Готово", "domains_available": "Доступні домени:", @@ -306,11 +278,11 @@ "domain_cannot_remove_main": "Ви не можете вилучити '{domain}', бо це основний домен, спочатку вам потрібно встановити інший домен в якості основного за допомогою 'yunohost domain main-domain -n '; ось список доменів-кандидатів: {other_domains}", "disk_space_not_sufficient_update": "Недостатньо місця на диску для оновлення цього застосунку", "disk_space_not_sufficient_install": "Недостатньо місця на диску для встановлення цього застосунку", - "diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте команду yunohost settings set security.ssh.port -v YOUR_SSH_PORT, щоб визначити порт SSH, і перевіртеyunohost tools regen-conf ssh --dry-run --with-diff і yunohost tools regen-conf ssh --force, щоб скинути ваш конфіг на рекомендований YunoHost.", - "diagnosis_sshd_config_inconsistent": "Схоже, що порт SSH був уручну змінений в /etc/ssh/sshd_config. Починаючи з версії YunoHost 4.2, доступний новий глобальний параметр 'security.ssh.port', що дозволяє уникнути ручного редагування конфігурації.", + "diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте команду yunohost settings set security.ssh.ssh port -v ВАШ_SSH_ПОРТ, щоб визначити порт SSH, і перевіртеyunohost tools regen-conf ssh --dry-run --with-diff і yunohost tools regen-conf ssh --force, щоб скинути ваш конфіг на рекомендований YunoHost.", + "diagnosis_sshd_config_inconsistent": "Схоже, що порт SSH був уручну змінений в /etc/ssh/sshd_config. Починаючи з версії YunoHost 4.2, доступний новий глобальний параметр 'security.ssh.ssh port', що дозволяє уникнути ручного редагування конфігурації.", "diagnosis_sshd_config_insecure": "Схоже, що конфігурація SSH була змінена вручну і є небезпечною, оскільки не містить директив 'AllowGroups' або 'AllowUsers' для обмеження доступу авторизованих користувачів.", "diagnosis_processes_killed_by_oom_reaper": "Деякі процеси було недавно вбито системою через брак пам'яті. Зазвичай це є симптомом нестачі пам'яті в системі або процесу, який з'їв дуже багато пам'яті. Зведення убитих процесів:\n{kills_summary}", - "diagnosis_never_ran_yet": "Схоже, що цей сервер був налаштований недавно, і поки немає звіту про діагностику. Вам слід почати з повної діагностики, або з вебадміністрації, або використовуючи 'yunohost diagnosis run' з командного рядка.", + "diagnosis_never_ran_yet": "Схоже, що цей сервер був налаштований недавно, і поки немає звіту про діагностику. Вам слід почати з повної діагностики, або з вебадмініструванні, або використовуючи 'yunohost diagnosis run' з командного рядка.", "diagnosis_unknown_categories": "Наступні категорії невідомі: {categories}", "diagnosis_http_nginx_conf_not_up_to_date_details": "Щоб виправити становище, перевірте різницю за допомогою командного рядка, використовуючи yunohost tools regen-conf nginx --dry-run --with-diff, і якщо все в порядку, застосуйте зміни за допомогою команди yunohost tools regen-conf nginx --force.", "diagnosis_http_nginx_conf_not_up_to_date": "Схоже, що конфігурація nginx цього домену була змінена вручну, що не дозволяє YunoHost визначити, чи доступний він по HTTP.", @@ -356,7 +328,7 @@ "diagnosis_mail_blacklist_ok": "IP-адреси і домени, які використовуються цим сервером, не внесені в чорний список", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Поточний зворотний DNS:{rdns_domain}
Очікуване значення: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Зворотний DNS неправильно налаштований в IPv{ipversion}. Деякі електронні листи можуть бути не доставлені або можуть бути відзначені як спам.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ваш зворотний DNS правильно налаштований для IPv4, ви можете спробувати вимкнути використання IPv6 при надсиланні листів, виконавши команду yunohost settings set smtp.allow_ipv6 -v off. Примітка: останнє рішення означає, що ви не зможете надсилати або отримувати електронні листи з нечисленних серверів, що використовують тільки IPv6.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ваш зворотний DNS правильно налаштований для IPv4, ви можете спробувати вимкнути використання IPv6 при надсиланні листів, виконавши команду yunohost settings set \nemail.smtp.smtp allow_ipv6 -v off. Примітка: останнє рішення означає, що ви не зможете надсилати або отримувати електронні листи з нечисленних серверів, що використовують тільки IPv6.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ви відчуваєте проблеми через це, розгляньте наступні рішення:
- Деякі провайдери надають альтернативу використання ретранслятора поштового сервера, хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN *з виділеним загальнодоступним IP* для обходу подібних обмежень. Дивіться https://yunohost.org/#/vpn_advantage
- Або можна переключитися на іншого провайдера", "diagnosis_mail_fcrdns_nok_details": "Спочатку спробуйте налаштувати зворотний DNS з {ehlo_domain} в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм запит у підтримку для цього).", "diagnosis_mail_fcrdns_dns_missing": "У IPv{ipversion} не визначений зворотний DNS. Деякі листи можуть не доставлятися або позначатися як спам.", @@ -374,7 +346,7 @@ "diagnosis_mail_outgoing_port_25_blocked_details": "Спочатку спробуйте розблокувати вихідний порт 25 в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм заявку в службу підтримки).", "diagnosis_mail_outgoing_port_25_blocked": "Поштовий сервер SMTP не може відправляти електронні листи на інші сервери, оскільки вихідний порт 25 заблоковано в IPv{ipversion}.", "app_manifest_install_ask_path": "Оберіть шлях URL (після домену), за яким має бути встановлено цей застосунок", - "yunohost_postinstall_end_tip": "Післявстановлення завершено! Щоб завершити доналаштування, будь ласка, розгляньте наступні варіанти:\n - додавання першого користувача через розділ 'Користувачі' вебадміністрації (або 'yunohost user create ' в командному рядку);\n - діагностика можливих проблем через розділ 'Діагностика' вебадміністрації (або 'yunohost diagnosis run' в командному рядку);\n - прочитання розділів 'Завершення встановлення' і 'Знайомство з YunoHost' у документації адміністратора: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "Післявстановлення завершено! Щоб завершити доналаштування, будь ласка, розгляньте наступні варіанти:\n - діагностика можливих проблем через розділ 'Діагностика' вебадмініструванні (або 'yunohost diagnosis run' в командному рядку);\n - прочитання розділів 'Завершення встановлення' і 'Знайомство з YunoHost' у документації адміністратора: https://yunohost.org/admindoc.", "yunohost_not_installed": "YunoHost установлений неправильно. Будь ласка, запустіть 'yunohost tools postinstall'", "yunohost_installing": "Установлення YunoHost...", "yunohost_configured": "YunoHost вже налаштовано", @@ -424,7 +396,7 @@ "diagnosis_diskusage_ok": "У сховищі {mountpoint} (на пристрої {device}) залишилося {free} ({free_percent}%) вільного місця (з {total})!", "diagnosis_diskusage_low": "Сховище {mountpoint} (на пристрої {device}) має тільки {free} ({free_percent}%) вільного місця (з {total}). Будьте уважні.", "diagnosis_diskusage_verylow": "Сховище {mountpoint} (на пристрої {device}) має тільки {free} ({free_percent}%) вільного місця (з {total}). Вам дійсно варто подумати про очищення простору!", - "diagnosis_services_bad_status_tip": "Ви можете спробувати перезапустити службу, а якщо це не допоможе, подивіться журнали служби в вебадміністрації (з командного рядка це можна зробити за допомогою yunohost service restart {service} і yunohost service log {service}).", + "diagnosis_services_bad_status_tip": "Ви можете спробувати перезапустити службу, а якщо це не допоможе, подивіться журнали служби в вебадмініструванні (з командного рядка це можна зробити за допомогою yunohost service restart {service} і yunohost service log {service}).", "diagnosis_services_bad_status": "Служба {service} у стані {status} :(", "diagnosis_services_conf_broken": "Для служби {service} порушена конфігурація!", "diagnosis_services_running": "Службу {service} запущено!", @@ -465,7 +437,7 @@ "diagnosis_cant_run_because_of_dep": "Неможливо запустити діагностику для {category}, поки є важливі проблеми, пов'язані з {dep}.", "diagnosis_cache_still_valid": "(Кеш все ще дійсний для діагностики {category}. Повторна діагностика поки не проводиться!)", "diagnosis_failed_for_category": "Не вдалося провести діагностику для категорії '{category}': {error}", - "diagnosis_display_tip": "Щоб побачити знайдені проблеми, ви можете перейти в розділ Діагностика в вебадміністрації або виконати команду 'yunohost diagnosis show --issues --human-readable' з командного рядка.", + "diagnosis_display_tip": "Щоб побачити знайдені проблеми, ви можете перейти в розділ Діагностика в вебадмініструванні або виконати команду 'yunohost diagnosis show --issues --human-readable' з командного рядка.", "diagnosis_package_installed_from_sury_details": "Деякі пакети були ненавмисно встановлені зі стороннього репозиторію під назвою Sury. Команда YunoHost поліпшила стратегію роботи з цими пакетами, але очікується, що в деяких системах, які встановили застосунки PHP7.3 ще на Stretch, залишаться деякі невідповідності. Щоб виправити це становище, спробуйте виконати наступну команду: {cmd_to_fix}", "diagnosis_package_installed_from_sury": "Деякі системні пакети мають бути зістарені у версії", "diagnosis_backports_in_sources_list": "Схоже, що apt (менеджер пакетів) налаштований на використання репозиторія backports. Якщо ви не знаєте, що робите, ми наполегливо не радимо встановлювати пакети з backports, тому що це може привести до нестабільності або конфліктів у вашій системі.", @@ -516,7 +488,7 @@ "backup_method_custom_finished": "Користувацький спосіб резервного копіювання '{method}' завершено", "backup_method_copy_finished": "Резервне копіювання завершено", "backup_hook_unknown": "Гачок (hook) резервного копіювання '{hook}' невідомий", - "backup_deleted": "Резервна копія видалена", + "backup_deleted": "Резервна копія '{name}' видалена", "backup_delete_error": "Не вдалося видалити '{path}'", "backup_custom_mount_error": "Користувацький спосіб резервного копіювання не зміг пройти етап 'монтування'", "backup_custom_backup_error": "Користувацький спосіб резервного копіювання не зміг пройти етап 'резервне копіювання'", @@ -524,7 +496,7 @@ "backup_csv_addition_failed": "Не вдалося додати файли для резервного копіювання в CSV-файл", "backup_creation_failed": "Не вдалося створити архів резервного копіювання", "backup_create_size_estimation": "Архів буде містити близько {size} даних.", - "backup_created": "Резервна копія створена", + "backup_created": "Резервна копія '{name}' створена", "backup_couldnt_bind": "Не вдалося зв'язати {src} з {dest}.", "backup_copying_to_organize_the_archive": "Копіювання {size} МБ для організації архіву", "backup_cleaning_failed": "Не вдалося очистити тимчасовий каталог резервного копіювання", @@ -533,7 +505,7 @@ "backup_archive_writing_error": "Не вдалося додати файли '{source}' (названі в архіві '{dest}') для резервного копіювання в стислий архів '{archive}'", "backup_archive_system_part_not_available": "Системна частина '{part}' недоступна в цій резервній копії", "backup_archive_corrupted": "Схоже, що архів резервної копії '{archive}' пошкоджений: {error}", - "backup_archive_cant_retrieve_info_json": "Не вдалося завантажити відомості для архіву '{archive}'... info.json не може бути отриманий(або не є правильним json).", + "backup_archive_cant_retrieve_info_json": "Не вдалося завантажити відомості для архіву '{archive}'... Файл info.json не може бути отриманий (або не є правильним json).", "backup_archive_open_failed": "Не вдалося відкрити архів резервної копії", "backup_archive_name_unknown": "Невідомий локальний архів резервного копіювання з назвою '{name}'", "backup_archive_name_exists": "Архів резервного копіювання з такою назвою вже існує.", @@ -548,10 +520,8 @@ "ask_password": "Пароль", "ask_new_path": "Новий шлях", "ask_new_domain": "Новий домен", - "ask_new_admin_password": "Новий пароль адміністрації", + "ask_new_admin_password": "Новий пароль адмініструванні", "ask_main_domain": "Основний домен", - "ask_lastname": "Прізвище", - "ask_firstname": "Ім'я", "ask_user_domain": "Домен для адреси е-пошти користувача і облікового запису XMPP", "apps_catalog_update_success": "Каталог застосунків був оновлений!", "apps_catalog_obsolete_cache": "Кеш каталогу застосунків порожній або застарів.", @@ -576,8 +546,7 @@ "app_restore_script_failed": "Сталася помилка всередині скрипта відновлення застосунку", "app_restore_failed": "Не вдалося відновити {app}: {error}", "app_remove_after_failed_install": "Вилучення застосунку після збою встановлення...", - "app_requirements_unmeet": "Вимоги не виконані для {app}, пакет {pkgname} ({version}) повинен бути {spec}", - "app_requirements_checking": "Перевіряння необхідних пакетів для {app}...", + "app_requirements_checking": "Перевіряння необхідних пакунків для {app}...", "app_removed": "{app} видалено", "app_not_properly_removed": "{app} не було видалено належним чином", "app_not_installed": "Не вдалося знайти {app} в списку встановлених застосунків: {all_apps}", @@ -585,7 +554,7 @@ "app_not_upgraded": "Застосунок '{failed_app}' не вдалося оновити, і, як наслідок, оновлення таких застосунків було скасовано: {apps}", "app_manifest_install_ask_is_public": "Чи має цей застосунок бути відкритим для анонімних відвідувачів?", "app_manifest_install_ask_admin": "Виберіть користувача-адміністратора для цього застосунку", - "app_manifest_install_ask_password": "Виберіть пароль адміністрації для цього застосунку", + "app_manifest_install_ask_password": "Виберіть пароль адмініструванні для цього застосунку", "diagnosis_description_apps": "Застосунки", "user_import_success": "Користувачів успішно імпортовано", "user_import_nothing_to_do": "Не потрібно імпортувати жодного користувача", @@ -594,20 +563,17 @@ "user_import_missing_columns": "Відсутні такі стовпці: {columns}", "user_import_bad_file": "Ваш файл CSV неправильно відформатовано, він буде знехтуваний, щоб уникнути потенційної втрати даних", "user_import_bad_line": "Неправильний рядок {line}: {details}", - "invalid_password": "Недійсний пароль", "log_user_import": "Імпорт користувачів", "ldap_server_is_down_restart_it": "Службу LDAP вимкнено, спробуйте перезапустити її...", "ldap_server_down": "Не вдається під'єднатися до сервера LDAP", - "global_settings_setting_security_experimental_enabled": "Увімкнути експериментальні функції безпеки (не вмикайте це, якщо ви не знаєте, що робите!)", - "diagnosis_apps_deprecated_practices": "Установлена версія цього застосунку все ще використовує деякі надзастарілі практики упакування. Вам дійсно варто подумати про його оновлення.", - "diagnosis_apps_outdated_ynh_requirement": "Установлена версія цього застосунку вимагає лише Yunohost >= 2.x, що, як правило, вказує на те, що воно не відповідає сучасним рекомендаційним практикам упакування та порадникам. Вам дійсно варто подумати про його оновлення.", + "diagnosis_apps_deprecated_practices": "Установлена версія цього застосунку все ще використовує деякі надто застарілі практики упакування. Вам дійсно варто подумати про його оновлення.", + "diagnosis_apps_outdated_ynh_requirement": "Установлена версія цього застосунку вимагає лише Yunohost >= 2.x чи 3.х, що, як правило, вказує на те, що воно не відповідає сучасним рекомендаційним практикам упакування та порадникам. Вам дійсно варто подумати про його оновлення.", "diagnosis_apps_bad_quality": "Цей застосунок наразі позначено як зламаний у каталозі застосунків YunoHost. Це може бути тимчасовою проблемою, поки організатори намагаються вирішити цю проблему. Тим часом оновлення цього застосунку вимкнено.", "diagnosis_apps_broken": "Цей застосунок наразі позначено як зламаний у каталозі застосунків YunoHost. Це може бути тимчасовою проблемою, поки організатори намагаються вирішити цю проблему. Тим часом оновлення цього застосунку вимкнено.", "diagnosis_apps_not_in_app_catalog": "Цей застосунок не міститься у каталозі застосунків YunoHost. Якщо він був у минулому і був видалений, вам слід подумати про видалення цього застосунку, оскільки він не отримає оновлення, і це може поставити під загрозу цілісність та безпеку вашої системи.", "diagnosis_apps_issue": "Виявлено проблему із застосунком {app}", "diagnosis_apps_allgood": "Усі встановлені застосунки дотримуються основних способів упакування", "diagnosis_high_number_auth_failures": "Останнім часом сталася підозріло велика кількість помилок автентифікації. Ви можете переконатися, що fail2ban працює і правильно налаштований, або скористатися власним портом для SSH, як описано в https://yunohost.org/security.", - "global_settings_setting_security_nginx_redirect_to_https": "Типово переспрямовувати HTTP-запити до HTTP (НЕ ВИМИКАЙТЕ, якщо ви дійсно не знаєте, що робите!)", "app_config_unable_to_apply": "Не вдалося застосувати значення панелі конфігурації.", "app_config_unable_to_read": "Не вдалося розпізнати значення панелі конфігурації.", "config_apply_failed": "Не вдалося застосувати нову конфігурацію: {error}", @@ -620,7 +586,6 @@ "config_validate_email": "Е-пошта має бути дійсною", "config_validate_time": "Час має бути дійсним, наприклад ГГ:ХХ", "config_validate_url": "Вебадреса має бути дійсною", - "config_version_not_supported": "Версії конфігураційної панелі '{version}' не підтримуються.", "danger": "Небезпека:", "invalid_number_min": "Має бути більшим за {min}", "invalid_number_max": "Має бути меншим за {max}", @@ -647,7 +612,6 @@ "domain_dns_push_failed_to_authenticate": "Неможливо пройти автентифікацію на API реєстратора для домену '{domain}'. Ймовірно, облікові дані недійсні? (Помилка: {error})", "domain_dns_push_failed_to_list": "Не вдалося скласти список поточних записів за допомогою API реєстратора: {error}", "domain_dns_push_record_failed": "Не вдалося виконати дію {action} запису {type}/{name} : {error}", - "domain_config_features_disclaimer": "Поки що вмикання/вимикання функцій пошти або XMPP впливає тільки на рекомендовану та автоконфігурацію DNS, але не на конфігурацію системи!", "domain_config_xmpp": "Миттєвий обмін повідомленнями (XMPP)", "domain_config_auth_key": "Ключ автентифікації", "domain_config_auth_secret": "Секрет автентифікації", @@ -665,7 +629,6 @@ "migration_0021_patching_sources_list": "Виправлення sources.lists...", "migration_0021_main_upgrade": "Початок основного оновлення...", "migration_0021_yunohost_upgrade": "Початок оновлення ядра YunoHost...", - "migration_0021_not_buster": "Поточний дистрибутив Debian не є Buster!", "migration_0021_problematic_apps_warning": "Зверніть увагу, що були виявлені наступні, ймовірно проблемні встановлені застосунки. Схоже, що вони не були встановлені з каталогу застосунків YunoHost або не зазначені як «робочі». Отже, не можна гарантувати, що вони будуть працювати після оновлення: {problematic_apps}", "migration_0021_modified_files": "Зверніть увагу, що такі файли були змінені вручну і можуть бути перезаписані після оновлення: {manually_modified_files}", "migration_0021_cleaning_up": "Очищення кеш-пам'яті і пакетів, які більше не потрібні...", @@ -675,7 +638,6 @@ "migration_0021_system_not_fully_up_to_date": "Ваша система не повністю оновлена. Будь ласка, виконайте регулярне оновлення перед запуском міграції на Bullseye.", "migration_0021_general_warning": "Будь ласка, зверніть увагу, що ця міграція є делікатною операцією. Команда YunoHost зробила все можливе, щоб перевірити і протестувати її, але міграція все ще може порушити частину системи або її застосунків.\n\nТому рекомендовано:\n - Виконати резервне копіювання всіх важливих даних або застосунків. Подробиці на сайті https://yunohost.org/backup; \n - Наберіться терпіння після запуску міграції: В залежності від вашого з'єднання з Інтернетом і апаратного забезпечення, оновлення може зайняти до декількох годин.", "migration_description_0021_migrate_to_bullseye": "Оновлення системи до Debian Bullseye і YunoHost 11.x", - "global_settings_setting_security_ssh_password_authentication": "Дозволити автентифікацію паролем для SSH", "service_description_postgresql": "Зберігає дані застосунків (база даних SQL)", "domain_config_default_app": "Типовий застосунок", "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 встановлено, але не PostgreSQL 13!? У вашій системі могло статися щось неприємне :(...", @@ -684,5 +646,125 @@ "tools_upgrade_failed": "Не вдалося оновити наступні пакети: {packages_list}", "migration_0023_not_enough_space": "Звільніть достатньо місця в {path} для виконання міграції.", "migration_0023_postgresql_11_not_installed": "PostgreSQL не було встановлено у вашій системі. Нічого робити.", - "migration_description_0022_php73_to_php74_pools": "Перенесення конфігураційних файлів php7.3-fpm 'pool' на php7.4" -} \ No newline at end of file + "migration_description_0022_php73_to_php74_pools": "Перенесення конфігураційних файлів php7.3-fpm 'pool' на php7.4", + "global_settings_setting_backup_compress_tar_archives_help": "При створенні нових резервних копій стискати архіви (.tar.gz) замість нестислих архівів (.tar). NB: вмикання цієї опції означає створення легших архівів резервних копій, але початкова процедура резервного копіювання буде значно довшою і важчою для CPU.", + "global_settings_setting_security_experimental_enabled_help": "Увімкнути експериментальні функції безпеки (не вмикайте це, якщо ви не знаєте, що робите!)", + "global_settings_setting_nginx_compatibility_help": "Компроміс між сумісністю і безпекою для вебсервера NGINX. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", + "global_settings_setting_nginx_redirect_to_https_help": "Типово переспрямовувати HTTP-запити до HTTP (НЕ ВИМИКАЙТЕ, якщо ви дійсно не знаєте, що робите!)", + "global_settings_setting_admin_strength": "Надійність пароля адміністратора", + "global_settings_setting_user_strength": "Надійність пароля користувача", + "global_settings_setting_postfix_compatibility_help": "Компроміс між сумісністю і безпекою для сервера Postfix. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", + "global_settings_setting_ssh_compatibility_help": "Компроміс між сумісністю і безпекою для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою).", + "global_settings_setting_ssh_password_authentication_help": "Дозволити автентифікацію паролем для SSH", + "global_settings_setting_ssh_port": "SSH-порт", + "global_settings_setting_webadmin_allowlist_help": "IP-адреси, яким дозволений доступ до вебадмініструванні. Через кому.", + "global_settings_setting_webadmin_allowlist_enabled_help": "Дозволити доступ до вебадмініструванні тільки деяким IP-адресам.", + "global_settings_setting_smtp_allow_ipv6_help": "Дозволити використання IPv6 для отримання і надсилання листів е-пошти", + "global_settings_setting_smtp_relay_enabled_help": "Хост SMTP-ретрансляції, який буде використовуватися для надсилання е-пошти замість цього зразка Yunohost. Корисно, якщо ви знаходитеся в одній із цих ситуацій: ваш 25 порт заблокований вашим провайдером або VPS провайдером, у вас є житловий IP в списку DUHL, ви не можете налаштувати зворотний DNS або цей сервер не доступний безпосередньо в Інтернеті і ви хочете використовувати інший сервер для відправки електронних листів.", + "migration_0024_rebuild_python_venv_disclaimer_base": "Після оновлення до Debian Bullseye деякі застосунки Python потрібно частково перебудувати, щоб їх було перетворено на нову версію Python, яка постачається в Debian (з технічної точки зору: те, що називається «virtualenv», потрібно створити заново). Тим часом ці застосунки Python можуть не працювати. YunoHost може спробувати перебудувати virtualenv для деяких із них, як описано нижче. Для інших застосунків або якщо спроба відновлення не вдається, вам потрібно буде вручну примусово оновити їх.", + "migration_0024_rebuild_python_venv_broken_app": "Пропущено {app}, бо virtualenv не можна легко перебудувати для цього застосунку. Натомість вам слід виправити ситуацію, примусово оновивши застосунок за допомогою `yunohost app upgrade --force {app}`.", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Буде зроблена спроба перебудувати virtualenv для таких застосунків (Примітка: операція може зайняти деякий час!): {rebuild_apps}", + "migration_0024_rebuild_python_venv_in_progress": "Намагаємося перебудувати Python virtualenv для `{app}`", + "migration_description_0024_rebuild_python_venv": "Відновлення застосунку Python після міграції до bullseye", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs не можна автоматично перебудувати для цих застосунків. Вам потрібно примусово оновити його для них, що можна зробити з командного рядка за допомогою: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_0024_rebuild_python_venv_failed": "Не вдалося перебудувати Python virtualenv для {app}. Застосунок може не працювати, доки це не вирішено. Ви повинні виправити ситуацію, примусово оновивши його за допомогою `yunohost app upgrade --force {app}`.", + "admins": "Адміністратори", + "all_users": "Усі користувачі Yunohost", + "app_manifest_install_ask_init_admin_permission": "Хто повинен мати доступ до функцій адміністратора для цього застосунку? (Пізніше це можна змінити)", + "certmanager_cert_install_failed": "Не вдалося встановити сертифікат Let's Encrypt для {domains}", + "config_forbidden_readonly_type": "Тип '{type}' не може бути встановлений як readonly, використовуйте інший тип для показу цього значення (відповідний arg id: '{id}').", + "diagnosis_using_stable_codename": "apt (системний менеджер пакунків) наразі налаштовано на встановлення пакунків з кодовою назвою \"stable\", замість кодової назви поточної версії Debian (bullseye).", + "diagnosis_using_stable_codename_details": "Зазвичай це спричинено неправильним налаштуванням від вашого хостинг-провайдера. Це небезпечно, оскільки як тільки наступна версія Debian стане новою \"стабільною\", apt захоче оновити всі системні пакунки без проходження належної процедури міграції. Радимо виправити це, відредагувавши джерело apt для базового репозиторію Debian, і замінити ключове слово stable на bullseye. Відповідний конфігураційний файл має бути в /etc/apt/sources.list, або файл у /etc/apt/sources.list.d/.", + "app_action_failed": "Не вдалося запустити дію {action} для застосунку {app}", + "app_manifest_install_ask_init_main_permission": "Хто повинен мати доступ до цього застосунку? (Пізніше це можна змінити)", + "ask_admin_fullname": "Повне ім'я адміністратора", + "ask_admin_username": "Ім'я користувача адміністратора", + "ask_fullname": "Повне ім'я", + "certmanager_cert_install_failed_selfsigned": "Не вдалося встановити самопідписаний сертифікат для {domains}", + "certmanager_cert_renew_failed": "Помилка оновлення сертифіката Let's Encrypt для {domains}", + "config_action_disabled": "Не вдалося запустити дію '{action}', оскільки вона вимкнена, переконайтеся, що виконані її обмеження. довідка: {help}", + "config_action_failed": "Не вдалося запустити дію '{action}': {error}", + "global_settings_setting_pop3_enabled": "Увімкнути POP3", + "domain_config_acme_eligible_explain": "Здається, цей домен не готовий для сертифіката Let's Encrypt. Будь ласка, перевірте конфігурацію DNS і доступність HTTP-сервера. Розділи \"DNS-записи\" та \"Веб\" на сторінці діагностики можуть допомогти вам зрозуміти, що саме налаштовано неправильно.", + "domain_config_cert_summary_letsencrypt": "Чудово! Ви використовуєте дійсний сертифікат Let's Encrypt!", + "domain_config_cert_summary_selfsigned": "ПОПЕРЕДЖЕННЯ: поточний сертифікат є самопідписаним. Браузери відображатимуть моторошне попередження новим відвідувачам!", + "global_settings_reset_success": "Скинути глобальні налаштування", + "global_settings_setting_admin_strength_help": "Ці вимоги застосовуються лише під час ініціалізації або зміни пароля", + "log_resource_snippet": "Надання/вилучення/оновлення ресурсу", + "global_settings_setting_security_experimental_enabled": "Експериментальні безпекові можливості", + "diagnosis_using_yunohost_testing": "apt (менеджер пакунків системи) наразі налаштований на встановлення будь-якого \"тестового\" оновлення для ядра YunoHost.", + "diagnosis_using_yunohost_testing_details": "Це, ймовірно, нормально, якщо ви знаєте, що робите, але зверніть увагу на примітки до випуску, перш ніж встановлювати оновлення YunoHost! Якщо ви хочете вимкнути 'тестування' оновлень, вам слід видалити ключове слово testing з /etc/apt/sources.list.d/yunohost.list.", + "domain_config_acme_eligible": "Відповідність ACME", + "domain_config_cert_install": "Установлення сертифікату Let's Encrypt", + "domain_config_cert_issuer": "Центр сертифікації", + "domain_config_cert_no_checks": "Нехтувати перевірками діагностики", + "domain_config_cert_renew": "Поновити сертифікат Let's Encrypt", + "domain_config_cert_renew_help": "Сертифікат буде автоматично поновлено протягом останніх 15 днів дії. Ви можете вручну поновити його, якщо хочете. (Не рекомендовано).", + "domain_config_cert_summary": "Стан сертифікату", + "domain_config_cert_summary_abouttoexpire": "Строк дії поточного сертифіката закінчується. Невдовзі його мають автоматично поновити.", + "domain_config_cert_summary_expired": "КРИТИЧНО: поточний сертифікат недійсний! HTTPS цілковито не працюватиме!", + "domain_config_cert_summary_ok": "Гаразд, поточний сертифікат виглядає добре!", + "domain_config_cert_validity": "Достовірність", + "global_settings_setting_backup_compress_tar_archives": "Стиснення резервних копій", + "global_settings_setting_nginx_compatibility": "Сумісність NGINX", + "global_settings_setting_nginx_redirect_to_https": "Примусово HTTPS", + "global_settings_setting_pop3_enabled_help": "Вмикає протокол POP3 для поштового сервера", + "global_settings_setting_postfix_compatibility": "Сумісність Postfix", + "global_settings_setting_root_access_explain": "У системах Linux \"root\" є абсолютним адміністратором. У контексті YunoHost прямий вхід в SSH від імені \"root\" типово вимкнено - за винятком локальної мережі сервера. Члени групи \"адміністратори\" можуть використовувати команду sudo, щоб діяти від імені root з командного рядка. Однак, може бути корисно мати (надійний) пароль root для налагодження системи, якщо з якихось причин звичайні адміністратори більше не можуть увійти в систему.", + "global_settings_setting_root_password": "Новий пароль root", + "global_settings_setting_root_password_confirm": "Новий пароль root (підтвердження)", + "global_settings_setting_smtp_allow_ipv6": "Дозвіл IPv6", + "global_settings_setting_smtp_relay_enabled": "Увімкнути ретрансляцію SMTP", + "global_settings_setting_smtp_relay_host": "Хост ретрансляції SMTP", + "global_settings_setting_ssh_compatibility": "Сумісність SSH", + "global_settings_setting_ssh_password_authentication": "Парольна автентифікація", + "global_settings_setting_user_strength_help": "Ці вимоги застосовуються лише під час ініціалізації або зміни пароля", + "global_settings_setting_webadmin_allowlist": "Білий список IP-адрес вебадміністрування", + "global_settings_setting_webadmin_allowlist_enabled": "Увімкнути білий список IP-адрес вебадміністрування", + "invalid_credentials": "Недійсний пароль чи ім'я користувача", + "log_settings_reset": "Скидання налаштування (одного)", + "log_settings_reset_all": "Скидання усіх налаштувань", + "log_settings_set": "Застосування налаштувань", + "group_update_aliases": "Оновлення псевдонімів для групи '{group}'", + "group_no_change": "Нічого не потрібно змінювати для групи '{group}'", + "registrar_infos": "Відомості про реєстратора", + "migration_0021_not_buster2": "Поточний дистрибутив Debian не Buster! Якщо ви вже виконували міграцію Buster->Bullseye, то ця помилка свідчить про те, що процедура міграції не була на 100% успішною (інакше YunoHost позначив би її як завершену). Радимо з'ясувати, що сталося, зі службою підтримки, якій знадобиться **повний** журнал `міграції, який можна знайти в розділі Засоби > Журнали у вебадмініструванні.", + "migration_description_0025_global_settings_to_configpanel": "Перенесіть застарілу номенклатуру глобальних налаштувань на нову, сучасну", + "migration_description_0026_new_admins_group": "Перейдіть до нової системи 'декількох адміністраторів'", + "root_password_changed": "пароль root було змінено", + "visitors": "Відвідувачі", + "password_confirmation_not_the_same": "Пароль і його підтвердження не збігаються", + "password_too_long": "Будь ласка, виберіть пароль коротший за 127 символів", + "pattern_fullname": "Має бути дійсне повне ім’я (принаймні 3 символи)", + "app_failed_to_upgrade_but_continue": "Застосунок {failed_app} не вдалося оновити, продовжуйте наступні оновлення відповідно до запиту. Запустіть 'yunohost log show {operation_logger_name}', щоб побачити журнал помилок", + "app_not_upgraded_broken_system": "Застосунок '{failed_app}' не зміг оновитися і перевів систему в неробочий стан, і як наслідок, оновлення наступних застосунків було скасовано: {apps}", + "app_not_upgraded_broken_system_continue": "Застосунок '{failed_app}' не зміг оновитися і перевів систему у неробочий стан (тому --continue-on-failure ігнорується), і як наслідок, оновлення наступних застосунків було скасовано: {apps}", + "confirm_app_insufficient_ram": "НЕБЕЗПЕКА! Цей застосунок вимагає {required} оперативної пам'яті для встановлення/оновлення, але зараз доступно лише {current}. Навіть якби цей застосунок можна було б запустити, процес його встановлення/оновлення вимагає великої кількості оперативної пам'яті, тому ваш сервер може зависнути і вийти з ладу. Якщо ви все одно готові піти на цей ризик, введіть '{answers}'", + "invalid_shell": "Недійсна оболонка: {shell}", + "domain_config_default_app_help": "Користувачі будуть автоматично перенаправлятися на цей застосунок при відкритті цього домену. Якщо застосунок не вказано, люди будуть перенаправлені на форму входу на портал користувача.", + "domain_config_xmpp_help": "Примітка: для ввімкнення деяких функцій XMPP потрібно оновити записи DNS та відновити сертифікат Lets Encrypt", + "global_settings_setting_dns_exposure_help": "Примітка: Це стосується лише рекомендованої конфігурації DNS і діагностичних перевірок. Це не впливає на конфігурацію системи.", + "global_settings_setting_passwordless_sudo": "Дозвіл адміністраторам використовувати \"sudo\" без повторного введення пароля", + "app_change_url_failed": "Не вдалося змінити url для {app}: {error}", + "app_change_url_require_full_domain": "{app} не може бути переміщено на цю нову URL-адресу, оскільки для цього потрібен повний домен (тобто зі шляхом = /)", + "app_change_url_script_failed": "Виникла помилка всередині скрипта зміни URL-адреси", + "app_yunohost_version_not_supported": "Для роботи застосунку потрібен YunoHost мінімум версії {required}, але поточна встановлена версія {current}", + "app_arch_not_supported": "Цей застосунок можна встановити лише на архітектурах {required}, але архітектура вашого сервера {current}", + "global_settings_setting_dns_exposure": "Версії IP, які слід враховувати при конфігурації та діагностиці DNS", + "domain_cannot_add_muc_upload": "Ви не можете додавати домени, що починаються на 'muc.'. Такі імена зарезервовані для багатокористувацького чату XMPP, інтегрованого в YunoHost.", + "confirm_notifications_read": "ПОПЕРЕДЖЕННЯ: Перш ніж продовжити, перевірте сповіщення застосунку вище, там можуть бути важливі повідомлення. [{answers}]", + "global_settings_setting_portal_theme": "Тема порталу", + "global_settings_setting_portal_theme_help": "Подробиці щодо створення користувацьких тем порталу на https://yunohost.org/theming", + "diagnosis_ip_no_ipv6_tip_important": "Зазвичай IPv6 має бути автоматично налаштований системою або вашим провайдером, якщо він доступний. В іншому випадку, можливо, вам доведеться налаштувати деякі речі вручну, як описано в документації тут: https://yunohost.org/#/ipv6.", + "app_not_enough_disk": "Цей застосунок вимагає {required} вільного місця.", + "app_not_enough_ram": "Для встановлення/оновлення цього застосунку потрібно {required} оперативної пам'яті, але наразі доступно лише {current}.", + "app_resource_failed": "Не вдалося надати, позбавити або оновити ресурси для {app}: {error}", + "apps_failed_to_upgrade": "Ці застосунки не вдалося оновити:{apps}", + "apps_failed_to_upgrade_line": "\n * {app_id} (щоб побачити відповідний журнал, виконайте 'yunohost log show {operation_logger_name}')", + "group_mailalias_add": "Псевдонім електронної пошти '{mail}' буде додано до групи '{group}'", + "group_mailalias_remove": "Псевдонім електронної пошти '{mail}' буде вилучено з групи '{group}'", + "group_user_add": "Користувача '{user}' буде додано до групи '{group}'", + "group_user_remove": "Користувача '{user}' буде вилучено з групи '{group}'", + "app_corrupt_source": "YunoHost зміг завантажити ресурс '{source_id}' ({url}) для {app}, але він не відповідає очікуваній контрольній сумі. Це може означати, що на вашому сервері стався тимчасовий збій мережі, АБО ресурс був якимось чином змінений висхідним супровідником (або зловмисником?), і пакувальникам YunoHost потрібно дослідити і оновити маніфест застосунку, щоб відобразити цю зміну.\n Очікувана контрольна сума sha256: {expected_sha256}\n Обчислена контрольна сума sha256: {computed_sha256}\n Розмір завантаженого файлу: {size}", + "app_failed_to_download_asset": "Не вдалося завантажити ресурс '{source_id}' ({url}) для {app}: {out}" +} diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index 2daf45483..18c6430c0 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -2,17 +2,13 @@ "password_too_simple_1": "密码长度至少为8个字符", "backup_created": "备份已创建", "app_start_remove": "正在删除{app}……", - "admin_password_change_failed": "无法修改密码", - "admin_password_too_long": "请选择一个小于127个字符的密码", "app_upgrade_failed": "不能升级{app}:{error}", "app_id_invalid": "无效 app ID", "app_unknown": "未知应用", - "admin_password_changed": "管理密码已更改", "aborting": "正在放弃。", "admin_password": "管理员密码", "app_start_restore": "正在恢复{app}……", "action_invalid": "无效操作 '{action}'", - "ask_lastname": "姓", "diagnosis_everything_ok": "{category}一切看起来不错!", "diagnosis_found_warnings": "找到{warnings}项,可能需要{category}进行改进。", "diagnosis_found_errors_and_warnings": "发现与{category}相关的{errors}个重要问题(和{warnings}警告)!", @@ -32,9 +28,9 @@ "diagnosis_basesystem_hardware_model": "服务器型号为 {model}", "diagnosis_basesystem_hardware": "服务器硬件架构为{virt} {arch}", "custom_app_url_required": "您必须提供URL才能升级自定义应用 {app}", - "confirm_app_install_thirdparty": "危险! 该应用程序不是YunoHost的应用程序目录的一部分。 安装第三方应用程序可能会损害系统的完整性和安全性。 除非您知道自己在做什么,否则可能不应该安装它, 如果此应用无法运行或无法正常使用系统,将不会提供任何支持。如果您仍然愿意承担此风险,请输入'{answers}'", + "confirm_app_install_thirdparty": "危险! 该应用不是YunoHost的应用目录的一部分。 安装第三方应用可能会损害系统的完整性和安全性。 除非您知道自己在做什么,否则可能不应该安装它, 如果此应用无法运行或无法正常使用系统,将不会提供任何支持。如果您仍然愿意承担此风险,请输入'{answers}'", "confirm_app_install_danger": "危险! 已知此应用仍处于实验阶段(如果未明确无法正常运行)! 除非您知道自己在做什么,否则可能不应该安装它。 如果此应用无法运行或无法正常使用系统,将不会提供任何支持。如果您仍然愿意承担此风险,请输入'{answers}'", - "confirm_app_install_warning": "警告:此应用程序可能可以运行,但未与YunoHost很好地集成。某些功能(例如单点登录和备份/还原)可能不可用, 仍要安装吗? [{answers}] ", + "confirm_app_install_warning": "警告:此应用可能可以运行,但未与YunoHost很好地集成。某些功能(例如单点登录和备份/还原)可能不可用, 仍要安装吗? [{answers}] ", "certmanager_unable_to_parse_self_CA_name": "无法解析自签名授权的名称 (file: {file})", "certmanager_self_ca_conf_file_not_found": "找不到用于自签名授权的配置文件(file: {file})", "certmanager_no_cert_file": "无法读取域{domain}的证书文件(file: {file})", @@ -54,7 +50,7 @@ "certmanager_attempt_to_renew_valid_cert": "域'{domain}'的证书不会过期!(如果知道自己在做什么,则可以使用--force)", "certmanager_attempt_to_renew_nonLE_cert": "“Let's Encrypt”未颁发域'{domain}'的证书,无法自动续订!", "certmanager_acme_not_configured_for_domain": "目前无法针对{domain}运行ACME挑战,因为其nginx conf缺少相应的代码段...请使用“yunohost tools regen-conf nginx --dry-run --with-diff”确保您的nginx配置是最新的。", - "backup_with_no_restore_script_for_app": "{app} 没有还原脚本,您将无法自动还原该应用程序的备份。", + "backup_with_no_restore_script_for_app": "{app} 没有还原脚本,您将无法自动还原该应用的备份。", "backup_with_no_backup_script_for_app": "应用'{app}'没有备份脚本。无视。", "backup_unable_to_organize_files": "无法使用快速方法来组织档案中的文件", "backup_system_part_failed": "无法备份'{part}'系统部分", @@ -104,39 +100,37 @@ "ask_new_domain": "新域名", "ask_new_admin_password": "新的管理密码", "ask_main_domain": "主域", - "ask_firstname": "名", "ask_user_domain": "用户的电子邮件地址和XMPP帐户要使用的域", - "apps_catalog_update_success": "应用程序目录已更新!", - "apps_catalog_obsolete_cache": "应用程序目录缓存为空或已过时。", + "apps_catalog_update_success": "应用目录已更新!", + "apps_catalog_obsolete_cache": "应用目录缓存为空或已过时。", "apps_catalog_failed_to_download": "无法下载{apps_catalog} 应用目录: {error}", - "apps_catalog_updating": "正在更新应用程序目录…", + "apps_catalog_updating": "正在更新应用目录…", "apps_catalog_init_success": "应用目录系统已初始化!", - "apps_already_up_to_date": "所有应用程序都是最新的", + "apps_already_up_to_date": "所有应用都是最新的", "app_packaging_format_not_supported": "无法安装此应用,因为您的YunoHost版本不支持其打包格式。 您应该考虑升级系统。", "app_upgraded": "{app}upgraded", "app_upgrade_some_app_failed": "某些应用无法升级", "app_upgrade_script_failed": "应用升级脚本内部发生错误", "app_upgrade_app_name": "现在升级{app} ...", "app_upgrade_several_apps": "以下应用将被升级: {apps}", - "app_unsupported_remote_type": "应用程序使用的远程类型不受支持", + "app_unsupported_remote_type": "应用使用的远程类型不受支持", "app_start_backup": "正在收集要备份的文件,用于{app} ...", "app_start_install": "{app}安装中...", "app_sources_fetch_failed": "无法获取源文件,URL是否正确?", "app_restore_script_failed": "应用还原脚本内部发生错误", "app_restore_failed": "无法还原 {app}: {error}", - "app_remove_after_failed_install": "安装失败后删除应用程序...", - "app_requirements_unmeet": "{app}不符合要求,软件包{pkgname}({version}) 必须为{spec}", + "app_remove_after_failed_install": "安装失败后删除应用...", "app_requirements_checking": "正在检查{app}所需的软件包...", - "app_removed": "{app} 已删除", + "app_removed": "{app} 已卸载", "app_not_properly_removed": "{app} 未正确删除", "app_not_correctly_installed": "{app} 似乎安装不正确", - "app_not_upgraded": "应用程序'{failed_app}'升级失败,因此以下应用程序的升级已被取消: {apps}", + "app_not_upgraded": "应用'{failed_app}'升级失败,因此以下应用的升级已被取消: {apps}", "app_manifest_install_ask_is_public": "该应用是否应该向匿名访问者公开?", "app_manifest_install_ask_admin": "选择此应用的管理员用户", "app_manifest_install_ask_password": "选择此应用的管理密码", "additional_urls_already_removed": "权限'{permission}'的其他URL中已经删除了附加URL'{url}'", - "app_manifest_install_ask_path": "选择安装此应用的路径", - "app_manifest_install_ask_domain": "选择应安装此应用程序的域", + "app_manifest_install_ask_path": "选择安装此应用的路径(在域名之后)", + "app_manifest_install_ask_domain": "选择应安装此应用的域", "app_location_unavailable": "该URL不可用,或与已安装的应用冲突:\n{apps}", "app_label_deprecated": "不推荐使用此命令!请使用新命令 'yunohost user permission update'来管理应用标签。", "app_make_default_location_already_used": "无法将'{app}' 设置为域上的默认应用,'{other_app}'已在使用'{domain}'", @@ -144,10 +138,10 @@ "app_install_failed": "无法安装 {app}: {error}", "app_install_files_invalid": "这些文件无法安装", "additional_urls_already_added": "附加URL '{url}' 已添加到权限'{permission}'的附加URL中", - "app_full_domain_unavailable": "抱歉,此应用必须安装在其自己的域中,但其他应用已安装在域“ {domain}”上。 您可以改用专用于此应用程序的子域。", + "app_full_domain_unavailable": "抱歉,此应用必须安装在其自己的域中,但其他应用已安装在域“ {domain}”上。 您可以改用专用于此应用的子域。", "app_extraction_failed": "无法解压缩安装文件", "app_change_url_success": "{app} URL现在为 {domain}{path}", - "app_change_url_no_script": "应用程序'{app_name}'尚不支持URL修改. 也许您应该升级它。", + "app_change_url_no_script": "应用'{app_name}'尚不支持URL修改. 也许您应该升级它。", "app_change_url_identical_domains": "新旧domain / url_path是相同的('{domain}{path}'),无需执行任何操作。", "app_argument_required": "参数'{name}'为必填项", "app_argument_password_no_default": "解析密码参数'{name}'时出错:出于安全原因,密码参数不能具有默认值", @@ -162,7 +156,7 @@ "port_already_opened": "{ip_version}个连接的端口 {port} 已打开", "port_already_closed": "{ip_version}个连接的端口 {port} 已关闭", "permission_require_account": "权限{permission}只对有账户的用户有意义,因此不能对访客启用。", - "permission_protected": "权限{permission}是受保护的。你不能向/从这个权限添加或删除访问者组。", + "permission_protected": "权限{permission}是受保护的。您不能向/从这个权限添加或删除访问者组。", "permission_updated": "权限 '{permission}' 已更新", "permission_update_failed": "无法更新权限 '{permission}': {error}", "permission_not_found": "找不到权限'{permission}'", @@ -204,7 +198,6 @@ "server_reboot": "服务器将重新启动", "server_shutdown_confirm": "服务器会立即关闭,确定吗?[{answers}]", "server_shutdown": "服务器将关闭", - "root_password_replaced_by_admin_password": "您的root密码已替换为您的管理员密码。", "root_password_desynchronized": "管理员密码已更改,但是YunoHost无法将此密码传播到root密码!", "restore_system_part_failed": "无法还原 '{part}'系统部分", "restore_running_hooks": "运行修复挂钩…", @@ -217,8 +210,8 @@ "service_description_rspamd": "过滤垃圾邮件和其他与电子邮件相关的功能", "service_description_redis-server": "用于快速数据访问,任务队列和程序之间通信的专用数据库", "service_description_postfix": "用于发送和接收电子邮件", - "service_description_nginx": "为你的服务器上托管的所有网站提供服务或访问", - "service_description_mysql": "存储应用程序数据(SQL数据库)", + "service_description_nginx": "为您的服务器上托管的所有网站提供服务或访问", + "service_description_mysql": "存储应用数据(SQL数据库)", "service_description_metronome": "管理XMPP即时消息传递帐户", "service_description_fail2ban": "防止来自互联网的暴力攻击和其他类型的攻击", "service_description_dovecot": "允许电子邮件客户端访问/获取电子邮件(通过IMAP和POP3)", @@ -241,7 +234,7 @@ "system_username_exists": "用户名已存在于系统用户列表中", "system_upgraded": "系统升级", "ssowat_conf_generated": "SSOwat配置已重新生成", - "show_tile_cant_be_enabled_for_regex": "你不能启用'show_tile',因为权限'{permission}'的URL是一个重合词", + "show_tile_cant_be_enabled_for_regex": "您不能启用'show_tile',因为权限'{permission}'的URL是一个重合词", "show_tile_cant_be_enabled_for_url_not_defined": "您现在无法启用 'show_tile' ,因为您必须先为权限'{permission}'定义一个URL", "service_unknown": "未知服务 '{service}'", "service_stopped": "服务'{service}' 已停止", @@ -273,7 +266,7 @@ "upnp_enabled": "UPnP已启用", "upnp_disabled": "UPnP已禁用", "yunohost_not_installed": "YunoHost没有正确安装,请运行 'yunohost tools postinstall'", - "yunohost_postinstall_end_tip": "后期安装完成! 为了最终完成你的设置,请考虑:\n -通过webadmin的“用户”部分添加第一个用户(或在命令行中'yunohost user create ' );\n -通过网络管理员的“诊断”部分(或命令行中的'yunohost diagnosis run')诊断潜在问题;\n -阅读管理文档中的“完成安装设置”和“了解YunoHost”部分: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "后期安装完成! 为了最终完成您的设置,请考虑:\n -通过webadmin的“用户”部分添加第一个用户(或在命令行中'yunohost user create ' );\n -通过网络管理员的“诊断”部分(或命令行中的'yunohost diagnosis run')诊断潜在问题;\n -阅读管理文档中的“完成安装设置”和“了解YunoHost”部分: https://yunohost.org/admindoc.", "operation_interrupted": "该操作是否被手动中断?", "invalid_regex": "无效的正则表达式:'{regex}'", "installation_complete": "安装完成", @@ -299,35 +292,15 @@ "group_already_exist_on_system": "系统组中已经存在组{group}", "group_already_exist": "群组{group}已经存在", "good_practices_about_admin_password": "现在,您将设置一个新的管理员密码。 密码至少应包含8个字符。并且出于安全考虑建议使用较长的密码同时尽可能使用各种字符(大写,小写,数字和特殊字符)。", - "global_settings_unknown_type": "意外的情况,设置{setting}似乎具有类型 {unknown_type} ,但是系统不支持该类型。", - "global_settings_setting_backup_compress_tar_archives": "创建新备份时,请压缩档案(.tar.gz) ,而不要压缩未压缩的档案(.tar)。注意:启用此选项意味着创建较小的备份存档,但是初始备份过程将明显更长且占用大量CPU。", "global_settings_setting_smtp_relay_password": "SMTP中继主机密码", "global_settings_setting_smtp_relay_user": "SMTP中继用户帐户", "global_settings_setting_smtp_relay_port": "SMTP中继端口", - "global_settings_setting_smtp_allow_ipv6": "允许使用IPv6接收和发送邮件", "global_settings_setting_ssowat_panel_overlay_enabled": "启用SSOwat面板覆盖", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "允许使用DSA主机密钥进行SSH守护程序配置(不建议使用)", - "global_settings_unknown_setting_from_settings_file": "设置中的未知密钥:'{setting_key}',将其丢弃并保存在/etc/yunohost/settings-unknown.json中", - "global_settings_setting_security_ssh_port": "SSH端口", - "global_settings_setting_security_postfix_compatibility": "Postfix服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)", - "global_settings_setting_security_ssh_compatibility": "SSH服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)", - "global_settings_setting_security_password_user_strength": "用户密码强度", - "global_settings_setting_security_password_admin_strength": "管理员密码强度", - "global_settings_setting_security_nginx_compatibility": "Web服务器NGINX的兼容性与安全性的权衡,影响密码(以及其他与安全性有关的方面)", - "global_settings_setting_pop3_enabled": "为邮件服务器启用POP3协议", - "global_settings_reset_success": "以前的设置现在已经备份到{path}", - "global_settings_key_doesnt_exists": "全局设置中不存在键'{settings_key}',您可以通过运行 'yunohost settings list'来查看所有可用键", - "global_settings_cant_write_settings": "无法保存设置文件,原因: {reason}", - "global_settings_cant_serialize_settings": "无法序列化设置数据,原因: {reason}", - "global_settings_cant_open_settings": "无法打开设置文件,原因: {reason}", - "global_settings_bad_type_for_setting": "设置 {setting},的类型错误,已收到{received_type},预期{expected_type}", - "global_settings_bad_choice_for_enum": "设置 {setting}的错误选择,收到了 '{choice}',但可用的选择有: {available_choices}", "firewall_rules_cmd_failed": "某些防火墙规则命令失败。日志中的更多信息。", "firewall_reloaded": "重新加载防火墙", "firewall_reload_failed": "无法重新加载防火墙", "file_does_not_exist": "文件{path} 不存在。", "field_invalid": "无效的字段'{}'", - "experimental_feature": "警告:此功能是实验性的,不稳定,请不要使用它,除非您知道自己在做什么。", "extracting": "提取中...", "dyndns_unavailable": "域'{domain}' 不可用。", "dyndns_domain_not_provided": "DynDNS提供者 {provider} 无法提供域 {domain}。", @@ -345,13 +318,13 @@ "downloading": "下载中…", "done": "完成", "domains_available": "可用域:", - "domain_uninstall_app_first": "这些应用程序仍安装在您的域中:\n{apps}\n\n请先使用 'yunohost app remove the_app_id' 将其卸载,或使用 'yunohost app change-url the_app_id'将其移至另一个域,然后再继续删除域", - "domain_remove_confirm_apps_removal": "删除该域将删除这些应用程序:\n{apps}\n\n您确定要这样做吗? [{answers}]", + "domain_uninstall_app_first": "这些应用仍安装在您的域中:\n{apps}\n\n请先使用 'yunohost app remove the_app_id' 将其卸载,或使用 'yunohost app change-url the_app_id'将其移至另一个域,然后再继续删除域", + "domain_remove_confirm_apps_removal": "删除该域将删除这些应用:\n{apps}\n\n您确定要这样做吗? [{answers}]", "domain_hostname_failed": "无法设置新的主机名。稍后可能会引起问题(可能没问题)。", "domain_exists": "该域已存在", "domain_dyndns_root_unknown": "未知的DynDNS根域", "domain_dyndns_already_subscribed": "您已经订阅了DynDNS域", - "domain_dns_conf_is_just_a_recommendation": "本页向你展示了*推荐的*配置。它并*不*为你配置DNS。你有责任根据该建议在你的DNS注册商处配置你的DNS区域。", + "domain_dns_conf_is_just_a_recommendation": "本页向您展示了*推荐的*配置。它并*不*为您配置DNS。您有责任根据该建议在您的DNS注册商处配置您的DNS区域。", "domain_deletion_failed": "无法删除域 {domain}: {error}", "domain_deleted": "域已删除", "domain_creation_failed": "无法创建域 {domain}: {error}", @@ -396,7 +369,7 @@ "diagnosis_description_ip": "互联网连接", "diagnosis_description_basesystem": "基本系统", "diagnosis_security_vulnerable_to_meltdown_details": "要解决此问题,您应该升级系统并重新启动以加载新的Linux内核(如果无法使用,请与您的服务器提供商联系)。有关更多信息,请参见https://meltdownattack.com/。", - "diagnosis_security_vulnerable_to_meltdown": "你似乎容易受到Meltdown关键安全漏洞的影响", + "diagnosis_security_vulnerable_to_meltdown": "您似乎容易受到Meltdown关键安全漏洞的影响", "diagnosis_regenconf_manually_modified": "配置文件 {file} 似乎已被手动修改。", "diagnosis_regenconf_allgood": "所有配置文件均符合建议的配置!", "diagnosis_mail_queue_too_big": "邮件队列中的待处理电子邮件过多({nb_pending} emails)", @@ -447,36 +420,35 @@ "diagnosis_ip_connected_ipv4": "服务器通过IPv4连接到Internet!", "diagnosis_no_cache": "尚无类别 '{category}'的诊断缓存", "diagnosis_failed": "无法获取类别 '{category}'的诊断结果: {error}", - "diagnosis_package_installed_from_sury_details": "一些软件包被无意中从一个名为Sury的第三方仓库安装。YunoHost团队改进了处理这些软件包的策略,但预计一些安装了PHP7.3应用程序的设置在仍然使用Stretch的情况下还有一些不一致的地方。为了解决这种情况,你应该尝试运行以下命令:{cmd_to_fix}", + "diagnosis_package_installed_from_sury_details": "一些软件包被无意中从一个名为Sury的第三方仓库安装。YunoHost团队改进了处理这些软件包的策略,但预计一些安装了PHP7.3应用的设置在仍然使用Stretch的情况下还有一些不一致的地方。为了解决这种情况,您应该尝试运行以下命令:{cmd_to_fix}", "app_not_installed": "在已安装的应用列表中找不到 {app}:{all_apps}", - "app_already_installed_cant_change_url": "这个应用程序已经被安装。URL不能仅仅通过这个函数来改变。在`app changeurl`中检查是否可用。", + "app_already_installed_cant_change_url": "这个应用已经被安装。URL不能仅仅通过这个函数来改变。在`app changeurl`中检查是否可用。", "restore_not_enough_disk_space": "没有足够的空间(空间: {free_space} B,需要的空间: {needed_space} B,安全系数: {margin} B)", "regenconf_pending_applying": "正在为类别'{category}'应用挂起的配置..", "regenconf_up_to_date": "类别'{category}'的配置已经是最新的", "regenconf_file_kept_back": "配置文件'{conf}'预计将被regen-conf(类别{category})删除,但被保留了下来。", "good_practices_about_user_password": "现在,您将设置一个新的管理员密码。 密码至少应包含8个字符。并且出于安全考虑建议使用较长的密码同时尽可能使用各种字符(大写,小写,数字和特殊字符)", - "global_settings_setting_smtp_relay_host": "使用SMTP中继主机来代替这个YunoHost实例发送邮件。如果你有以下情况,就很有用:你的25端口被你的ISP或VPS提供商封锁,你有一个住宅IP列在DUHL上,你不能配置反向DNS,或者这个服务器没有直接暴露在互联网上,你想使用其他服务器来发送邮件。", - "domain_cannot_remove_main_add_new_one": "你不能删除'{domain}',因为它是主域和你唯一的域,你需要先用'yunohost domain add '添加另一个域,然后用'yunohost domain main-domain -n '设置为主域,然后你可以用'yunohost domain remove {domain}'删除域", - "domain_cannot_add_xmpp_upload": "你不能添加以'xmpp-upload.'开头的域名。这种名称是为YunoHost中集成的XMPP上传功能保留的。", - "domain_cannot_remove_main": "你不能删除'{domain}',因为它是主域,你首先需要用'yunohost domain main-domain -n '设置另一个域作为主域;这里是候选域的列表: {other_domains}", + "domain_cannot_remove_main_add_new_one": "您不能删除'{domain}',因为它是主域和您唯一的域,您需要先用'yunohost domain add '添加另一个域,然后用'yunohost domain main-domain -n '设置为主域,然后您可以用'yunohost domain remove {domain}'删除域", + "domain_cannot_add_xmpp_upload": "您不能添加以'xmpp-upload.'开头的域名。这种名称是为YunoHost中集成的XMPP上传功能保留的。", + "domain_cannot_remove_main": "您不能删除'{domain}',因为它是主域,您首先需要用'yunohost domain main-domain -n '设置另一个域作为主域;这里是候选域的列表: {other_domains}", "diagnosis_sshd_config_inconsistent_details": "请运行yunohost settings set security.ssh.port -v YOUR_SSH_PORT来定义SSH端口,并检查yunohost tools regen-conf ssh --dry-run --with-diffyunohost tools regen-conf ssh --force将您的配置重置为YunoHost建议。", - "diagnosis_http_bad_status_code": "它看起来像另一台机器(也许是你的互联网路由器)回答,而不是你的服务器。
1。这个问题最常见的原因是80端口(和443端口)没有正确转发到您的服务器
2.在更复杂的设置中:确保没有防火墙或反向代理的干扰。", - "diagnosis_http_timeout": "当试图从外部联系你的服务器时,出现了超时。它似乎是不可达的。
1. 这个问题最常见的原因是80端口(和443端口)没有正确转发到你的服务器
2.你还应该确保nginx服务正在运行
3.对于更复杂的设置:确保没有防火墙或反向代理的干扰。", + "diagnosis_http_bad_status_code": "它看起来像另一台机器(也许是您的互联网路由器)回答,而不是您的服务器。
1。这个问题最常见的原因是80端口(和443端口)没有正确转发到您的服务器
2.在更复杂的设置中:确保没有防火墙或反向代理的干扰。", + "diagnosis_http_timeout": "当试图从外部联系您的服务器时,出现了超时。它似乎是不可达的。
1. 这个问题最常见的原因是80端口(和443端口)没有正确转发到您的服务器
2.您还应该确保nginx服务正在运行
3.对于更复杂的设置:确保没有防火墙或反向代理的干扰。", "diagnosis_rootfstotalspace_critical": "根文件系统总共只有{space},这很令人担忧!您可能很快就会用完磁盘空间!建议根文件系统至少有16 GB。", "diagnosis_rootfstotalspace_warning": "根文件系统总共只有{space}。这可能没问题,但要小心,因为最终您可能很快会用完磁盘空间...建议根文件系统至少有16 GB。", - "diagnosis_regenconf_manually_modified_details": "如果你知道自己在做什么的话,这可能是可以的! YunoHost会自动停止更新这个文件... 但是请注意,YunoHost的升级可能包含重要的推荐变化。如果你想,你可以用yunohost tools regen-conf {category} --dry-run --with-diff检查差异,然后用yunohost tools regen-conf {category} --force强制设置为推荐配置", - "diagnosis_mail_fcrdns_nok_alternatives_6": "有些供应商不会让你配置你的反向DNS(或者他们的功能可能被破坏......)。如果你的反向DNS正确配置为IPv4,你可以尝试在发送邮件时禁用IPv6,方法是运yunohost settings set smtp.allow_ipv6 -v off。注意:这应视为最后一个解决方案因为这意味着你将无法从少数只使用IPv6的服务器发送或接收电子邮件。", - "diagnosis_mail_fcrdns_nok_alternatives_4": "有些供应商不会让你配置你的反向DNS(或者他们的功能可能被破坏......)。如果您因此而遇到问题,请考虑以下解决方案:
- 一些ISP提供了使用邮件服务器中转的选择,尽管这意味着中转将能够监视您的电子邮件流量。
- 一个有利于隐私的选择是使用VPN*与专用公共IP*来绕过这类限制。见https://yunohost.org/#/vpn_advantage
- 或者可以切换到另一个供应商", - "diagnosis_mail_ehlo_wrong_details": "远程诊断器在IPv{ipversion}中收到的EHLO与你的服务器的域名不同。
收到的EHLO: {wrong_ehlo}
预期的: {right_ehlo}
这个问题最常见的原因是端口25没有正确转发到你的服务器。另外,请确保没有防火墙或反向代理的干扰。", - "diagnosis_mail_ehlo_unreachable_details": "在IPv{ipversion}中无法打开与您服务器的25端口连接。它似乎是不可达的。
1. 这个问题最常见的原因是端口25没有正确转发到你的服务器
2.你还应该确保postfix服务正在运行。
3.在更复杂的设置中:确保没有防火墙或反向代理的干扰。", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "一些供应商不会让你解除对出站端口25的封锁,因为他们不关心网络中立性。
- 其中一些供应商提供了使用邮件服务器中继的替代方案,尽管这意味着中继将能够监视你的电子邮件流量。
- 一个有利于隐私的替代方案是使用VPN*,用一个专用的公共IP*绕过这种限制。见https://yunohost.org/#/vpn_advantage
- 你也可以考虑切换到一个更有利于网络中立的供应商", + "diagnosis_regenconf_manually_modified_details": "如果您知道自己在做什么的话,这可能是可以的! YunoHost会自动停止更新这个文件... 但是请注意,YunoHost的升级可能包含重要的推荐变化。如果您想,您可以用yunohost tools regen-conf {category} --dry-run --with-diff检查差异,然后用yunohost tools regen-conf {category} --force强制设置为推荐配置", + "diagnosis_mail_fcrdns_nok_alternatives_6": "有些供应商不会让您配置您的反向DNS(或者他们的功能可能被破坏......)。如果您的反向DNS正确配置为IPv4,您可以尝试在发送邮件时禁用IPv6,方法是运yunohost settings set smtp.allow_ipv6 -v off。注意:这应视为最后一个解决方案因为这意味着您将无法从少数只使用IPv6的服务器发送或接收电子邮件。", + "diagnosis_mail_fcrdns_nok_alternatives_4": "有些供应商不会让您配置您的反向DNS(或者他们的功能可能被破坏......)。如果您因此而遇到问题,请考虑以下解决方案:
- 一些ISP提供了使用邮件服务器中转的选择,尽管这意味着中转将能够监视您的电子邮件流量。
- 一个有利于隐私的选择是使用VPN*与专用公共IP*来绕过这类限制。见https://yunohost.org/#/vpn_advantage
- 或者可以切换到另一个供应商", + "diagnosis_mail_ehlo_wrong_details": "远程诊断器在IPv{ipversion}中收到的EHLO与您的服务器的域名不同。
收到的EHLO: {wrong_ehlo}
预期的: {right_ehlo}
这个问题最常见的原因是端口25没有正确转发到您的服务器。另外,请确保没有防火墙或反向代理的干扰。", + "diagnosis_mail_ehlo_unreachable_details": "在IPv{ipversion}中无法打开与您服务器的25端口连接。它似乎是不可达的。
1. 这个问题最常见的原因是端口25没有正确转发到您的服务器
2.您还应该确保postfix服务正在运行。
3.在更复杂的设置中:确保没有防火墙或反向代理的干扰。", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "一些供应商不会让您解除对出站端口25的封锁,因为他们不关心网络中立性。
- 其中一些供应商提供了使用邮件服务器中继的替代方案,尽管这意味着中继将能够监视您的电子邮件流量。
- 一个有利于隐私的替代方案是使用VPN*,用一个专用的公共IP*绕过这种限制。见https://yunohost.org/#/vpn_advantage
- 您也可以考虑切换到一个更有利于网络中立的供应商", "diagnosis_ram_ok": "系统在{total}中仍然有 {available} ({available_percent}%) RAM可用。", "diagnosis_ram_low": "系统有 {available} ({available_percent}%) RAM可用(共{total}个)可用。小心。", "diagnosis_ram_verylow": "系统只有 {available} ({available_percent}%) 内存可用! (在{total}中)", "diagnosis_diskusage_ok": "存储器{mountpoint}(在设备{device}上)仍有 {free} ({free_percent}%) 空间(在{total}中)!", "diagnosis_diskusage_low": "存储器{mountpoint}(在设备{device}上)只有{free} ({free_percent}%) 的空间。({free_percent}%)的剩余空间(在{total}中)。要小心。", "diagnosis_diskusage_verylow": "存储器{mountpoint}(在设备{device}上)仅剩余{free} ({free_percent}%) (剩余{total})个空间。您应该真正考虑清理一些空间!", - "diagnosis_services_bad_status_tip": "你可以尝试重新启动服务,如果没有效果,可以看看webadmin中的服务日志(从命令行,你可以用yunohost service restart {service}yunohost service log {service})来做。", + "diagnosis_services_bad_status_tip": "您可以尝试重新启动服务,如果没有效果,可以看看webadmin中的服务日志(从命令行,您可以用yunohost service restart {service}yunohost service log {service})来做。", "diagnosis_dns_try_dyndns_update_force": "该域的DNS配置应由YunoHost自动管理,如果不是这种情况,您可以尝试使用 yunohost dyndns update --force强制进行更新。", "diagnosis_dns_point_to_doc": "如果您需要有关配置DNS记录的帮助,请查看 https://yunohost.org/dns_config 上的文档。", "diagnosis_dns_discrepancy": "以下DNS记录似乎未遵循建议的配置:
类型: {type}
名称: {name}
代码> 当前值: {current}期望值: {value}", @@ -495,8 +467,8 @@ "log_help_to_get_log": "要查看操作'{desc}'的日志,请使用命令'yunohost log show {name}'", "log_link_to_log": "此操作的完整日志: '{desc}'", "log_corrupted_md_file": "与日志关联的YAML元数据文件已损坏: '{md_file}\n错误: {error}'", - "iptables_unavailable": "你不能在这里使用iptables。你要么在一个容器中,要么你的内核不支持它", - "ip6tables_unavailable": "你不能在这里使用ip6tables。你要么在一个容器中,要么你的内核不支持它", + "iptables_unavailable": "您不能在这里使用iptables。您要么在一个容器中,要么您的内核不支持它", + "ip6tables_unavailable": "您不能在这里使用ip6tables。您要么在一个容器中,要么您的内核不支持它", "log_regen_conf": "重新生成系统配置'{}'", "log_letsencrypt_cert_renew": "续订'{}'的“Let's Encrypt”证书", "log_selfsigned_cert_install": "在 '{}'域上安装自签名证书", @@ -509,10 +481,9 @@ "log_domain_remove": "从系统配置中删除 '{}' 域", "log_domain_add": "将 '{}'域添加到系统配置中", "log_remove_on_failed_install": "安装失败后删除 '{}'", - "log_remove_on_failed_restore": "从备份存档还原失败后,删除 '{}'", "log_backup_restore_app": "从备份存档还原 '{}'", "log_backup_restore_system": "从备份档案还原系统", - "permission_currently_allowed_for_all_users": "这个权限目前除了授予其他组以外,还授予所有用户。你可能想删除'all_users'权限或删除目前授予它的其他组。", + "permission_currently_allowed_for_all_users": "这个权限目前除了授予其他组以外,还授予所有用户。您可能想删除'all_users'权限或删除目前授予它的其他组。", "permission_creation_failed": "无法创建权限'{permission}': {error}", "permission_created": "权限'{permission}'已创建", "permission_cannot_remove_main": "不允许删除主要权限", @@ -557,7 +528,7 @@ "migration_ldap_rollback_success": "系统回滚。", "migration_ldap_migration_failed_trying_to_rollback": "无法迁移...试图回滚系统。", "migration_ldap_can_not_backup_before_migration": "迁移失败之前,无法完成系统的备份。错误: {error}", - "migration_ldap_backup_before_migration": "在实际迁移之前,请创建LDAP数据库和应用程序设置的备份。", + "migration_ldap_backup_before_migration": "在实际迁移之前,请创建LDAP数据库和应用设置的备份。", "main_domain_changed": "主域已更改", "main_domain_change_failed": "无法更改主域", "mail_unavailable": "该电子邮件地址是保留的,并且将自动分配给第一个用户", @@ -569,7 +540,7 @@ "log_tools_reboot": "重新启动服务器", "log_tools_shutdown": "关闭服务器", "log_tools_upgrade": "升级系统软件包", - "log_tools_postinstall": "安装好你的YunoHost服务器后", + "log_tools_postinstall": "安装好您的YunoHost服务器后", "log_tools_migrations_migrate_forward": "运行迁移", "log_domain_main_domain": "将 '{}' 设为主要域", "log_user_permission_reset": "重置权限'{}'", @@ -582,16 +553,16 @@ "log_user_create": "添加用户'{}'", "domain_registrar_is_not_configured": "尚未为域 {domain} 配置注册商。", "domain_dns_push_not_applicable": "的自动DNS配置的特征是不适用域{domain}。您应该按照 https://yunohost.org/dns_config 上的文档手动配置DNS 记录。", - "disk_space_not_sufficient_update": "没有足够的磁盘空间来更新此应用程序", + "disk_space_not_sufficient_update": "没有足够的磁盘空间来更新此应用", "diagnosis_high_number_auth_failures": "最近出现了大量可疑的失败身份验证。您的fail2ban正在运行且配置正确,或使用自定义端口的SSH作为https://yunohost.org/解释的安全性。", - "diagnosis_apps_not_in_app_catalog": "此应用程序不在 YunoHost 的应用程序目录中。如果它过去有被删除过,您应该考虑卸载此应用程,因为它不会更新,并且可能会损害您系统的完整和安全性。", + "diagnosis_apps_not_in_app_catalog": "此应用不在 YunoHost 的应用目录中。如果它过去有被删除过,您应该考虑卸载此应用程,因为它不会更新,并且可能会损害您系统的完整和安全性。", "app_config_unable_to_apply": "无法应用配置面板值。", "app_config_unable_to_read": "无法读取配置面板值。", "config_forbidden_keyword": "关键字“{keyword}”是保留的,您不能创建或使用带有此 ID 的问题的配置面板。", "config_no_panel": "未找到配置面板。", "config_unknown_filter_key": "该过滤器钥匙“{filter_key}”有误。", - "diagnosis_apps_outdated_ynh_requirement": "此应用程序的安装 版本只需要 yunohost >= 2.x,这往往表明它与推荐的打包实践和帮助程序不是最新的。你真的应该考虑更新它。", - "disk_space_not_sufficient_install": "没有足够的磁盘空间来安装此应用程序", + "diagnosis_apps_outdated_ynh_requirement": "此应用的安装 版本只需要 yunohost >= 2.x,这往往表明它与推荐的打包实践和帮助程序不是最新的。您真的应该考虑更新它。", + "disk_space_not_sufficient_install": "没有足够的磁盘空间来安装此应用", "config_apply_failed": "应用新配置 失败:{error}", "config_cant_set_value_on_section": "无法在整个配置部分设置单个值 。", "config_validate_color": "是有效的 RGB 十六进制颜色", @@ -599,10 +570,25 @@ "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_allgood": "所有已安装的应用都遵守基本的打包原则", + "diagnosis_apps_deprecated_practices": "此应用的安装 版本仍然使用一些超旧的弃用打包原则。推荐您升级它。", "diagnosis_apps_issue": "发现应用{ app } 存在问题", - "diagnosis_description_apps": "应用" + "diagnosis_description_apps": "应用", + "global_settings_setting_backup_compress_tar_archives_help": "创建新备份时,请压缩档案(.tar.gz) ,而不要压缩未压缩的档案(.tar)。注意:启用此选项意味着创建较小的备份存档,但是初始备份过程将明显更长且占用大量CPU。", + "global_settings_setting_nginx_compatibility_help": "Web服务器NGINX的兼容性与安全性的权衡,影响密码(以及其他与安全性有关的方面)", + "global_settings_setting_admin_strength": "管理员密码强度", + "global_settings_setting_user_strength": "用户密码强度", + "global_settings_setting_postfix_compatibility_help": "Postfix服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)", + "global_settings_setting_ssh_compatibility_help": "SSH服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)", + "global_settings_setting_ssh_port": "SSH端口", + "global_settings_setting_smtp_allow_ipv6_help": "允许使用IPv6接收和发送邮件", + "global_settings_setting_smtp_relay_enabled_help": "使用SMTP中继主机来代替这个YunoHost实例发送邮件。如果您有以下情况,就很有用:您的25端口被您的ISP或VPS提供商封锁,您有一个住宅IP列在DUHL上,您不能配置反向DNS,或者这个服务器没有直接暴露在互联网上,您想使用其他服务器来发送邮件。", + "all_users": "所有的YunoHost用户", + "app_manifest_install_ask_init_admin_permission": "谁应该有权访问此应用的管理功能?(此配置可以稍后更改)", + "app_action_failed": "对应用{app}执行动作{action}失败", + "app_manifest_install_ask_init_main_permission": "谁应该有权访问此应用?(此配置稍后可以更改)", + "ask_admin_fullname": "管理员全名", + "ask_admin_username": "管理员用户名", + "ask_fullname": "全名" } \ No newline at end of file diff --git a/maintenance/agplv3.tpl b/maintenance/agplv3.tpl new file mode 100644 index 000000000..82f3b4cc6 --- /dev/null +++ b/maintenance/agplv3.tpl @@ -0,0 +1,16 @@ +Copyright (c) ${years} ${owner} + +This file is part of ${projectname} (see ${projecturl}) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/maintenance/autofix_locale_format.py b/maintenance/autofix_locale_format.py index 1c56ea386..caa36f9f2 100644 --- a/maintenance/autofix_locale_format.py +++ b/maintenance/autofix_locale_format.py @@ -32,7 +32,6 @@ def autofix_i18n_placeholders(): # We iterate over all keys/string in en.json for key, string in reference.items(): - # Ignore check if there's no translation yet for this key if key not in this_locale: continue @@ -89,7 +88,6 @@ Please fix it manually ! def autofix_orthotypography_and_standardized_words(): def reformat(lang, transformations): - locale = open(f"{LOCALE_FOLDER}{lang}.json").read() for pattern, replace in transformations.items(): locale = re.compile(pattern).sub(replace, locale) @@ -146,11 +144,9 @@ def autofix_orthotypography_and_standardized_words(): def remove_stale_translated_strings(): - reference = json.loads(open(LOCALE_FOLDER + "en.json").read()) for locale_file in TRANSLATION_FILES: - print(locale_file) this_locale = json.loads( open(LOCALE_FOLDER + locale_file).read(), object_pairs_hook=OrderedDict diff --git a/maintenance/make_changelog.sh b/maintenance/make_changelog.sh index 44171c5b6..f5d1572a6 100644 --- a/maintenance/make_changelog.sh +++ b/maintenance/make_changelog.sh @@ -1,18 +1,18 @@ VERSION="?" RELEASE="testing" REPO=$(basename $(git rev-parse --show-toplevel)) -REPO_URL=$(git remote get-url origin) +REPO_URL="https://github.com/yunohost/yunohost" ME=$(git config --global --get user.name) EMAIL=$(git config --global --get user.email) -LAST_RELEASE=$(git tag --list 'debian/11.*' | tail -n 1) +LAST_RELEASE=$(git tag --list 'debian/11.*' --sort="v:refname" | tail -n 1) echo "$REPO ($VERSION) $RELEASE; urgency=low" echo "" git log $LAST_RELEASE.. -n 10000 --first-parent --pretty=tformat:' - %b%s (%h)' \ -| sed -E "s@Merge .*#([0-9]+).*\$@ \([#\1]\($REPO_URL/pull/\1\)\)@g" \ -| grep -v "Update from Weblate" \ +| sed -E "s&Merge .*#([0-9]+).*\$& \([#\1]\($REPO_URL/pull/\1\)\)&g" \ +| grep -v "Translations update from Weblate" \ | tac TRANSLATIONS=$(git log $LAST_RELEASE... -n 10000 --pretty=format:"%s" \ @@ -23,7 +23,7 @@ TRANSLATIONS=$(git log $LAST_RELEASE... -n 10000 --pretty=format:"%s" \ echo "" CONTRIBUTORS=$(git logc $LAST_RELEASE... -n 10000 --pretty=format:"%an" \ - | sort | uniq | grep -v "$ME" \ + | sort | uniq | grep -v "$ME" | grep -v 'yunohost-bot' | grep -vi 'weblate' \ | tr '\n' ', ' | sed -e 's/,$//g' -e 's/,/, /g') [[ -z "$CONTRIBUTORS" ]] || echo " Thanks to all contributors <3 ! ($CONTRIBUTORS)" echo "" diff --git a/maintenance/missing_i18n_keys.py b/maintenance/missing_i18n_keys.py index 3dbca8027..2ed7fd141 100644 --- a/maintenance/missing_i18n_keys.py +++ b/maintenance/missing_i18n_keys.py @@ -19,7 +19,6 @@ REFERENCE_FILE = LOCALE_FOLDER + "en.json" def find_expected_string_keys(): - # Try to find : # m18n.n( "foo" # YunohostError("foo" @@ -99,15 +98,6 @@ def find_expected_string_keys(): for m in ("log_" + match for match in p4.findall(content)): yield m - # Global settings descriptions - # Will be on a line like : ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", ... - p5 = re.compile(r" \(\n*\s*[\"\'](\w[\w\.]+)[\"\'],") - content = open(ROOT + "src/settings.py").read() - for m in ( - "global_settings_setting_" + s.replace(".", "_") for s in p5.findall(content) - ): - yield m - # Keys for the actionmap ... for category in yaml.safe_load(open(ROOT + "share/actionsmap.yml")).values(): if "actions" not in category.keys(): @@ -143,6 +133,7 @@ def find_expected_string_keys(): for key in registrars[registrar].keys(): yield f"domain_config_{key}" + # Domain config panel domain_config = toml.load(open(ROOT + "share/config_domain.toml")) for panel in domain_config.values(): if not isinstance(panel, dict): @@ -155,6 +146,35 @@ def find_expected_string_keys(): continue yield f"domain_config_{key}" + # Global settings + global_config = toml.load(open(ROOT + "share/config_global.toml")) + # Boring hard-coding because there's no simple other way idk + settings_without_help_key = [ + "passwordless_sudo", + "smtp_relay_host", + "smtp_relay_password", + "smtp_relay_port", + "smtp_relay_user", + "ssh_port", + "ssowat_panel_overlay_enabled", + "root_password", + "root_access_explain", + "root_password_confirm", + ] + + for panel in global_config.values(): + if not isinstance(panel, dict): + continue + for section in panel.values(): + if not isinstance(section, dict): + continue + for key, values in section.items(): + if not isinstance(values, dict): + continue + yield f"global_settings_setting_{key}" + if key not in settings_without_help_key: + yield f"global_settings_setting_{key}_help" + ############################################################################### # Compare keys used and keys defined # @@ -176,7 +196,6 @@ undefined_keys = sorted(undefined_keys) mode = sys.argv[1].strip("-") if mode == "check": - # Unused keys are not too problematic, will be automatically # removed by the other autoreformat script, # but still informative to display them diff --git a/maintenance/update_copyright_headers.sh b/maintenance/update_copyright_headers.sh new file mode 100644 index 000000000..bc4fe24db --- /dev/null +++ b/maintenance/update_copyright_headers.sh @@ -0,0 +1,12 @@ +# To run this you'll need to: +# +# pip3 install licenseheaders + +licenseheaders \ + -o "YunoHost Contributors" \ + -n "YunoHost" \ + -u "https://yunohost.org" \ + -t ./agplv3.tpl \ + --current-year \ + -f ../src/*.py ../src/{utils,diagnosers,authenticators}/*.py + diff --git a/share/100000-most-used-passwords-length8plus.txt.gz b/share/100000-most-used-passwords-length8plus.txt.gz new file mode 100644 index 000000000..6059a5af8 Binary files /dev/null and b/share/100000-most-used-passwords-length8plus.txt.gz differ diff --git a/share/100000-most-used-passwords.txt.gz b/share/100000-most-used-passwords.txt.gz deleted file mode 100644 index 43887119b..000000000 Binary files a/share/100000-most-used-passwords.txt.gz and /dev/null differ diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 89c6e914d..0a12b94a1 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -37,14 +37,6 @@ _global: authentication: api: ldap_admin cli: null - arguments: - -v: - full: --version - help: Display YunoHost packages versions - action: callback - callback: - method: yunohost.utils.packages.ynh_packages_version - return: true ############################# # User # @@ -73,19 +65,28 @@ user: pattern: &pattern_username - !!str ^[a-z0-9_]+$ - "pattern_username" + -F: + full: --fullname + help: The full name of the user. For example 'Camille Dupont' + extra: + ask: ask_fullname + required: False + pattern: &pattern_fullname + - !!str ^([^\W_]{1,30}[ ,.'-]{0,3})+$ + - "pattern_fullname" -f: full: --firstname + help: Deprecated. Use --fullname instead. extra: - ask: ask_firstname - required: True + required: False pattern: &pattern_firstname - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - "pattern_firstname" -l: full: --lastname + help: Deprecated. Use --fullname instead. extra: - ask: ask_lastname - required: True + required: False pattern: &pattern_lastname - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - "pattern_lastname" @@ -115,6 +116,11 @@ user: pattern: &pattern_mailbox_quota - !!str ^(\d+[bkMGT])|0$ - "pattern_mailbox_quota" + -s: + full: --loginShell + help: The login shell used + default: "/bin/bash" + ### user_delete() delete: @@ -136,12 +142,19 @@ user: arguments: username: help: Username to update + -F: + full: --fullname + help: The full name of the user. For example 'Camille Dupont' + extra: + pattern: *pattern_fullname -f: full: --firstname + help: Deprecated. Use --fullname instead. extra: pattern: *pattern_firstname -l: full: --lastname + help: Deprecated. Use --fullname instead. extra: pattern: *pattern_lastname -m: @@ -187,6 +200,10 @@ user: metavar: "{SIZE|0}" extra: pattern: *pattern_mailbox_quota + -s: + full: --loginShell + help: The login shell used + default: "/bin/bash" ### user_info() info: @@ -305,6 +322,35 @@ user: extra: pattern: *pattern_username + add-mailalias: + action_help: Add mail aliases to group + api: PUT /users/groups//aliases/ + arguments: + groupname: + help: Name of the group to add user(s) to + extra: + pattern: *pattern_groupname + aliases: + help: Mail aliases to add + nargs: "+" + metavar: MAIL + extra: + pattern: *pattern_email + remove-mailalias: + action_help: Remove mail aliases to group + api: DELETE /users/groups//aliases/ + arguments: + groupname: + help: Name of the group to add user(s) to + extra: + pattern: *pattern_groupname + aliases: + help: Mail aliases to remove + nargs: "+" + metavar: MAIL + + + permission: subcategory_help: Manage permissions actions: @@ -443,6 +489,22 @@ domain: --exclude-subdomains: help: Filter out domains that are obviously subdomains of other declared domains action: store_true + --tree: + help: Display domains as a tree + action: store_true + --features: + help: List only domains with features enabled (xmpp, mail_in, mail_out) + nargs: "*" + + ### domain_info() + info: + action_help: Get domain aggredated data + api: GET /domains/ + arguments: + domain: + help: Domain to check + extra: + pattern: *pattern_domain ### domain_add() add: @@ -492,9 +554,7 @@ domain: action_help: Check the current main domain, or change it deprecated_alias: - maindomain - api: - - GET /domains/main - - PUT /domains//main + api: PUT /domains//main arguments: -n: full: --new-main-domain @@ -552,6 +612,7 @@ domain: ### domain_url_available() url-available: + hide_in_help: True action_help: Check availability of a web path api: GET /domain//urlavailable arguments: @@ -562,6 +623,20 @@ domain: path: help: The path to check (e.g. /coffee) + ### domain_action_run() + action-run: + hide_in_help: True + action_help: Run domain action + api: PUT /domain//actions/ + arguments: + domain: + help: Domain name + action: + help: action id + -a: + full: --args + help: Serialized arguments for action (i.e. "foo=bar&lorem=ipsum") + subcategories: config: @@ -571,7 +646,9 @@ domain: ### domain_config_get() get: action_help: Display a domain configuration - api: GET /domains//config + api: + - GET /domains//config + - GET /domains//config/ arguments: domain: help: Domain name @@ -590,7 +667,7 @@ domain: ### domain_config_set() set: action_help: Apply a new configuration - api: PUT /domains//config + api: PUT /domains//config/ arguments: domain: help: Domain name @@ -710,6 +787,10 @@ app: full: --with-categories help: Also return a list of app categories action: store_true + -a: + full: --with-antifeatures + help: Also return a list of antifeatures categories + action: store_true ### app_search() search: @@ -725,6 +806,10 @@ app: arguments: app: help: Name, local path or git URL of the app to fetch the manifest of + -s: + full: --with-screenshot + help: Also return a base64 screenshot if any (API only) + action: store_true ### app_list() list: @@ -826,6 +911,10 @@ app: full: --no-safety-backup help: Disable the safety backup during upgrade action: store_true + -c: + full: --continue-on-failure + help: Continue to upgrade apps event if one or more upgrade failed + action: store_true ### app_change_url() change-url: @@ -865,9 +954,18 @@ app: help: Delete the key action: store_true + ### app_shell() + shell: + action_help: Open an interactive shell with the app environment already loaded + # Here we set a GET only not to lock the command line. There is no actual API endpoint for app_shell() + api: GET /apps//shell + arguments: + app: + help: App ID ### app_register_url() register-url: + hide_in_help: True action_help: Book/register a web path for a given app arguments: app: @@ -880,6 +978,7 @@ app: ### app_makedefault() makedefault: + hide_in_help: True action_help: Redirect domain root to an app api: PUT /apps//default arguments: @@ -893,6 +992,17 @@ app: help: Undo redirection action: store_true + ### app_dismiss_notification + dismiss-notification: + hide_in_help: True + action_help: Dismiss post_install or post_upgrade notification + api: PUT /apps//dismiss_notification/ + arguments: + app: + help: App ID to dismiss notification for + name: + help: Notification name, either post_install or post_upgrade + ### app_ssowatconf() ssowatconf: action_help: Regenerate SSOwat configuration file @@ -941,7 +1051,9 @@ app: ### app_config_get() get: action_help: Display an app configuration - api: GET /apps//config-panel + api: + - GET /apps//config + - GET /apps//config/ arguments: app: help: App name @@ -960,7 +1072,7 @@ app: ### app_config_set() set: action_help: Apply a new configuration - api: PUT /apps//config + api: PUT /apps//config/ arguments: app: help: App name @@ -1065,6 +1177,7 @@ backup: ### backup_download() download: + hide_in_help: True action_help: (API only) Request to download the file api: GET /backups//download arguments: @@ -1093,6 +1206,11 @@ settings: list: action_help: list all entries of the settings api: GET /settings + arguments: + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true ### settings_get() get: @@ -1101,22 +1219,29 @@ settings: arguments: key: help: Settings key - --full: - help: Show more details + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true + -e: + full: --export + help: Only export key/values, meant to be reimported using "config set --args-file" action: store_true ### settings_set() set: action_help: set an entry value in the settings - api: POST /settings/ + api: PUT /settings/ arguments: key: - help: Settings key + help: The question or form key + nargs: '?' -v: full: --value help: new value - extra: - required: True + -a: + full: --args + help: Serialized arguments for new configuration (i.e. "mail_in=0&mail_out=0") ### settings_reset_all() reset-all: @@ -1433,10 +1558,10 @@ tools: category_help: Specific tools actions: - ### tools_adminpw() - adminpw: - action_help: Change password of admin and root users - api: PUT /adminpw + ### tools_rootpw() + rootpw: + action_help: Change root password + api: PUT /rootpw arguments: -n: full: --new-password @@ -1471,6 +1596,20 @@ tools: ask: ask_main_domain pattern: *pattern_domain required: True + -u: + full: --username + help: Username for the first (admin) user. For example 'camille' + extra: + ask: ask_admin_username + pattern: *pattern_username + required: True + -F: + full: --fullname + help: The full name for the first (admin) user. For example 'Camille Dupont' + extra: + ask: ask_admin_fullname + required: True + pattern: *pattern_fullname -p: full: --password help: YunoHost admin password @@ -1482,14 +1621,10 @@ tools: --ignore-dyndns: help: Do not subscribe domain to a DynDNS service action: store_true - --force-password: - help: Use this if you really want to set a weak password - action: store_true --force-diskspace: help: Use this if you really want to install YunoHost on a setup with less than 10 GB on the root filesystem action: store_true - ### tools_update() update: action_help: YunoHost update @@ -1651,6 +1786,7 @@ hook: ### hook_info() info: + hide_in_help: True action_help: Get information about a given hook arguments: action: @@ -1680,6 +1816,7 @@ hook: ### hook_callback() callback: + hide_in_help: True action_help: Execute all scripts binded to an action arguments: action: @@ -1702,6 +1839,7 @@ hook: ### hook_exec() exec: + hide_in_help: True action_help: Execute hook from a file with arguments arguments: path: diff --git a/share/config_domain.toml b/share/config_domain.toml index 65e755365..82ef90c32 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -1,44 +1,25 @@ version = "1.0" i18n = "domain_config" -# -# Other things we may want to implement in the future: -# -# - maindomain handling -# - default app -# - autoredirect www in nginx conf -# - ? -# - [feature] +name = "Features" + [feature.app] [feature.app.default_app] type = "app" filter = "is_webapp" default = "_none" - - [feature.mail] - #services = ['postfix', 'dovecot'] - [feature.mail.features_disclaimer] - type = "alert" - style = "warning" - icon = "warning" + [feature.mail] [feature.mail.mail_out] type = "boolean" default = 1 - + [feature.mail.mail_in] type = "boolean" default = 1 - - #[feature.mail.backup_mx] - #type = "tags" - #default = [] - #pattern.regexp = '^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$' - #pattern.error = "pattern_error" - + [feature.xmpp] [feature.xmpp.xmpp] @@ -46,15 +27,56 @@ i18n = "domain_config" default = 0 [dns] - +name = "DNS" + [dns.registrar] - optional = true + # This part is automatically generated in DomainConfigPanel - # This part is automatically generated in DomainConfigPanel +[cert] +name = "Certificate" -# [dns.advanced] -# -# [dns.advanced.ttl] -# type = "number" -# min = 0 -# default = 3600 + [cert.cert] + + [cert.cert.cert_summary] + type = "alert" + # Automatically filled by DomainConfigPanel + + [cert.cert.cert_validity] + type = "number" + readonly = true + visible = "false" + # Automatically filled by DomainConfigPanel + + [cert.cert.cert_issuer] + type = "string" + visible = false + # Automatically filled by DomainConfigPanel + + [cert.cert.acme_eligible] + type = "boolean" + visible = false + # Automatically filled by DomainConfigPanel + + [cert.cert.acme_eligible_explain] + type = "alert" + style = "warning" + visible = "acme_eligible == false || acme_eligible == null" + + [cert.cert.cert_no_checks] + type = "boolean" + default = false + visible = "acme_eligible == false || acme_eligible == null" + + [cert.cert.cert_install] + type = "button" + icon = "star" + style = "success" + visible = "cert_issuer != 'letsencrypt'" + enabled = "acme_eligible || cert_no_checks" + + [cert.cert.cert_renew] + type = "button" + icon = "refresh" + style = "warning" + visible = "cert_issuer == 'letsencrypt'" + enabled = "acme_eligible || cert_no_checks" diff --git a/share/config_global.toml b/share/config_global.toml new file mode 100644 index 000000000..40b71ab19 --- /dev/null +++ b/share/config_global.toml @@ -0,0 +1,171 @@ +version = "1.0" +i18n = "global_settings_setting" + +[security] +name = "Security" + [security.password] + name = "Passwords" + + [security.password.admin_strength] + type = "select" + choices.1 = "Require at least 8 chars" + choices.2 = "ditto, but also require at least one digit, one lower and one upper char" + choices.3 = "ditto, but also require at least one special char" + choices.4 = "ditto, but also require at least 12 chars" + default = "1" + + [security.password.user_strength] + type = "select" + choices.1 = "Require at least 8 chars" + choices.2 = "ditto, but also require at least one digit, one lower and one upper char" + choices.3 = "ditto, but also require at least one special char" + choices.4 = "ditto, but also require at least 12 chars" + default = "1" + + [security.password.passwordless_sudo] + type = "boolean" + # The actual value is dynamically computed by checking the sudoOption of cn=admins,ou=sudo + default = false + + [security.ssh] + name = "SSH" + [security.ssh.ssh_compatibility] + type = "select" + choices.intermediate = "Intermediate (compatible with older softwares)" + choices.modern = "Modern (recommended)" + default = "modern" + + [security.ssh.ssh_port] + type = "number" + default = 22 + + [security.ssh.ssh_password_authentication] + type = "boolean" + default = true + + [security.nginx] + name = "NGINX (web server)" + [security.nginx.nginx_redirect_to_https] + type = "boolean" + default = true + + [security.nginx.nginx_compatibility] + type = "select" + choices.intermediate = "Intermediate (compatible with Firefox 27, Android 4.4.2, Chrome 31, Edge, IE 11, Opera 20, and Safari 9)" + choices.modern = "Modern (compatible with Firefox 63, Android 10.0, Chrome 70, Edge 75, Opera 57, and Safari 12.1)" + default = "intermediate" + + [security.postfix] + name = "Postfix (SMTP email server)" + [security.postfix.postfix_compatibility] + type = "select" + choices.intermediate = "Intermediate (allows TLS 1.2)" + choices.modern = "Modern (TLS 1.3 only)" + default = "intermediate" + + [security.webadmin] + name = "Webadmin" + [security.webadmin.webadmin_allowlist_enabled] + type = "boolean" + default = false + + [security.webadmin.webadmin_allowlist] + type = "tags" + visible = "webadmin_allowlist_enabled" + optional = true + default = "" + + [security.root_access] + name = "Change root password" + + [security.root_access.root_access_explain] + type = "alert" + style = "info" + icon = "info" + + [security.root_access.root_password] + type = "password" + optional = true + default = "" + + [security.root_access.root_password_confirm] + type = "password" + optional = true + default = "" + + [security.experimental] + name = "Experimental" + [security.experimental.security_experimental_enabled] + type = "boolean" + default = false + + +[email] +name = "Email" + [email.pop3] + name = "POP3" + [email.pop3.pop3_enabled] + type = "boolean" + default = false + + [email.smtp] + name = "SMTP" + [email.smtp.smtp_allow_ipv6] + type = "boolean" + default = true + + [email.smtp.smtp_relay_enabled] + type = "boolean" + default = false + + [email.smtp.smtp_relay_host] + type = "string" + default = "" + optional = true + visible="smtp_relay_enabled" + + [email.smtp.smtp_relay_port] + type = "number" + default = 587 + visible="smtp_relay_enabled" + + [email.smtp.smtp_relay_user] + type = "string" + default = "" + optional = true + visible="smtp_relay_enabled" + + [email.smtp.smtp_relay_password] + type = "password" + default = "" + optional = true + visible="smtp_relay_enabled" + help = "" # This is empty string on purpose, otherwise the core automatically set the 'good_practice_admin_password' string here which is not relevant, because the admin is not actually "choosing" the password ... + +[misc] +name = "Other" + [misc.portal] + name = "User portal" + [misc.portal.ssowat_panel_overlay_enabled] + type = "boolean" + default = true + + [misc.portal.portal_theme] + type = "select" + # Choices are loaded dynamically in the python code + default = "default" + + [misc.backup] + name = "Backup" + [misc.backup.backup_compress_tar_archives] + type = "boolean" + default = false + + [misc.network] + name = "Network" + [misc.network.dns_exposure] + type = "select" + choices.both = "Both" + choices.ipv4 = "IPv4 Only" + choices.ipv6 = "IPv6 Only" + default = "both" diff --git a/share/html/502.html b/share/html/502.html new file mode 100644 index 000000000..bef0275df --- /dev/null +++ b/share/html/502.html @@ -0,0 +1,20 @@ + + + +502 Bad Gateway + + + +

502 Bad Gateway

+

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

+

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

+

Thank you for using YunoHost.

+ + diff --git a/share/registrar_list.toml b/share/registrar_list.toml index afb213aa1..3f478a03f 100644 --- a/share/registrar_list.toml +++ b/share/registrar_list.toml @@ -501,6 +501,15 @@ [pointhq.auth_token] type = "string" redact = true + +[porkbun] + [porkbun.auth_key] + type = "string" + redact = true + + [porkbun.auth_secret] + type = "string" + redact = true [powerdns] [powerdns.auth_token] @@ -622,7 +631,14 @@ [vultr.auth_token] type = "string" redact = true - + +[webgo] + [webgo.auth_username] + type = "string" + + [webgo.auth_password] + type = "password" + [yandex] [yandex.auth_token] type = "string" diff --git a/src/__init__.py b/src/__init__.py index cdf845de0..146485d2d 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,5 +1,22 @@ #! /usr/bin/python -# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import sys @@ -15,7 +32,6 @@ def is_installed(): def cli(debug, quiet, output_as, timeout, args, parser): - init_logging(interface="cli", debug=debug, quiet=quiet) # Check that YunoHost is installed @@ -34,7 +50,6 @@ def cli(debug, quiet, output_as, timeout, args, parser): def api(debug, host, port): - init_logging(interface="api", debug=debug) def is_installed_api(): @@ -68,7 +83,6 @@ def portalapi(debug, host, port): def check_command_is_valid_before_postinstall(args): - allowed_if_not_postinstalled = [ "tools postinstall", "tools versions", @@ -106,7 +120,6 @@ def init_i18n(): def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yunohost"): - logfile = os.path.join(logdir, "yunohost-%s.log" % interface) if not os.path.isdir(logdir): diff --git a/src/app.py b/src/app.py index 11216895e..069134798 100644 --- a/src/app.py +++ b/src/app.py @@ -1,28 +1,23 @@ -# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_app.py - - Manage apps -""" +import glob import os import toml import json @@ -32,8 +27,10 @@ import time import re import subprocess import tempfile +import copy from collections import OrderedDict -from typing import List, Tuple, Dict, Any +from typing import List, Tuple, Dict, Any, Iterator, Optional +from packaging import version from moulinette import Moulinette, m18n from moulinette.utils.log import getActionLogger @@ -51,22 +48,30 @@ from moulinette.utils.filesystem import ( chmod, ) -from yunohost.utils import packages -from yunohost.utils.config import ( - ConfigPanel, - ask_questions_and_parse_answers, - DomainQuestion, - PathQuestion, +from yunohost.utils.configpanel import ConfigPanel, ask_questions_and_parse_answers +from yunohost.utils.form import ( + DomainOption, + WebPathOption, 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.utils.system import ( + free_space_in_directory, + dpkg_is_broken, + get_ynh_package_version, + system_arch, + debian_version, + human_to_binary, + binary_to_human, + ram_available, +) from yunohost.log import is_unit_operation, OperationLogger from yunohost.app_catalog import ( # noqa app_catalog, app_search, _load_apps_catalog, + APPS_CATALOG_LOGOS, ) logger = getActionLogger("yunohost.app") @@ -79,7 +84,7 @@ re_app_instance_name = re.compile( ) APP_REPO_URL = re.compile( - r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./~]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?tree/[a-zA-Z0-9-_.]+)?(\.git)?/?$" + r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./~]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?(tree|src/(branch|tag|commit))/[a-zA-Z0-9-_.]+)?(\.git)?/?$" ) APP_FILES_TO_COPY = [ @@ -147,6 +152,13 @@ def app_info(app, full=False, upgradable=False): absolute_app_name, _ = _parse_app_instance_name(app) from_catalog = _load_apps_catalog()["apps"].get(absolute_app_name, {}) + # Check if $app.png exists in the app logo folder, this is a trick to be able to easily customize the logo + # of an app just by creating $app.png (instead of the hash.png) in the corresponding folder + ret["logo"] = ( + app + if os.path.exists(f"{APPS_CATALOG_LOGOS}/{app}.png") + else from_catalog.get("logo_hash") + ) ret["upgradable"] = _app_upgradable({**ret, "from_catalog": from_catalog}) if ret["upgradable"] == "yes": @@ -160,18 +172,42 @@ def app_info(app, full=False, upgradable=False): ret["current_version"] = f" ({current_revision})" ret["new_version"] = f" ({new_revision})" + ret["settings"] = settings + if not full: return ret ret["setting_path"] = setting_path ret["manifest"] = local_manifest - ret["manifest"]["arguments"] = _set_default_ask_questions( - ret["manifest"].get("arguments", {}) + + # FIXME: maybe this is not needed ? default ask questions are + # already set during the _get_manifest_of_app earlier ? + ret["manifest"]["install"] = _set_default_ask_questions( + ret["manifest"].get("install", {}) ) - ret["settings"] = settings ret["from_catalog"] = from_catalog + # Hydrate app notifications and doc + for pagename, content_per_lang in ret["manifest"]["doc"].items(): + for lang, content in content_per_lang.items(): + ret["manifest"]["doc"][pagename][lang] = _hydrate_app_template( + content, settings + ) + + # Filter dismissed notification + ret["manifest"]["notifications"] = { + k: v + for k, v in ret["manifest"]["notifications"].items() + if not _notification_is_dismissed(k, settings) + } + + # Hydrate notifications (also filter uneeded post_upgrade notification based on version) + for step, notifications in ret["manifest"]["notifications"].items(): + for name, content_per_lang in notifications.items(): + for lang, content in content_per_lang.items(): + notifications[name][lang] = _hydrate_app_template(content, settings) + ret["is_webapp"] = "domain" in settings and "path" in settings if ret["is_webapp"]: @@ -185,8 +221,8 @@ def app_info(app, full=False, upgradable=False): ret["supports_backup_restore"] = os.path.exists( os.path.join(setting_path, "scripts", "backup") ) and os.path.exists(os.path.join(setting_path, "scripts", "restore")) - ret["supports_multi_instance"] = is_true( - local_manifest.get("multi_instance", False) + ret["supports_multi_instance"] = local_manifest.get("integration", {}).get( + "multi_instance", False ) ret["supports_config_panel"] = os.path.exists( os.path.join(setting_path, "config_panel.toml") @@ -202,8 +238,6 @@ def app_info(app, full=False, upgradable=False): def _app_upgradable(app_infos): - from packaging import version - # Determine upgradability app_in_catalog = app_infos.get("from_catalog") @@ -339,7 +373,6 @@ def app_map(app=None, raw=False, user=None): ) for url in perm_all_urls: - # Here, we decide to completely ignore regex-type urls ... # Because : # - displaying them in regular "yunohost app map" output creates @@ -378,7 +411,7 @@ def app_change_url(operation_logger, app, domain, path): path -- New path at which the application will be move """ - from yunohost.hook import hook_exec, hook_callback + from yunohost.hook import hook_exec_with_script_debug_if_failure, hook_callback from yunohost.service import service_reload_or_restart installed = _is_installed(app) @@ -397,10 +430,10 @@ def app_change_url(operation_logger, app, domain, path): # Normalize path and domain format - domain = DomainQuestion.normalize(domain) - old_domain = DomainQuestion.normalize(old_domain) - path = PathQuestion.normalize(path) - old_path = PathQuestion.normalize(old_path) + domain = DomainOption.normalize(domain) + old_domain = DomainOption.normalize(old_domain) + path = WebPathOption.normalize(path) + old_path = WebPathOption.normalize(old_path) if (domain, path) == (old_domain, old_path): raise YunohostValidationError( @@ -412,51 +445,102 @@ def app_change_url(operation_logger, app, domain, path): _validate_webpath_requirement( {"domain": domain, "path": path}, path_requirement, ignore_app=app ) + if path_requirement == "full_domain" and path != "/": + raise YunohostValidationError("app_change_url_require_full_domain", app=app) tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script(app, workdir=tmp_workdir_for_app) + env_dict = _make_environment_for_app_script( + app, workdir=tmp_workdir_for_app, action="change_url" + ) + env_dict["YNH_APP_OLD_DOMAIN"] = old_domain env_dict["YNH_APP_OLD_PATH"] = old_path env_dict["YNH_APP_NEW_DOMAIN"] = domain env_dict["YNH_APP_NEW_PATH"] = path + env_dict["old_domain"] = old_domain + env_dict["old_path"] = old_path + env_dict["new_domain"] = domain + env_dict["new_path"] = path + env_dict["change_path"] = "1" if old_path != path else "0" + env_dict["change_domain"] = "1" if old_domain != domain else "0" + if domain != old_domain: operation_logger.related_to.append(("domain", old_domain)) operation_logger.extra.update({"env": env_dict}) operation_logger.start() + old_nginx_conf_path = f"/etc/nginx/conf.d/{old_domain}.d/{app}.conf" + new_nginx_conf_path = f"/etc/nginx/conf.d/{domain}.d/{app}.conf" + old_nginx_conf_backup = None + if not os.path.exists(old_nginx_conf_path): + logger.warning( + f"Current nginx config file {old_nginx_conf_path} doesn't seem to exist ... wtf ?" + ) + else: + old_nginx_conf_backup = read_file(old_nginx_conf_path) + change_url_script = os.path.join(tmp_workdir_for_app, "scripts/change_url") # Execute App change_url script - ret = hook_exec(change_url_script, env=env_dict)[0] - if ret != 0: - msg = f"Failed to change '{app}' url." - logger.error(msg) - operation_logger.error(msg) + change_url_failed = True + try: + ( + change_url_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( + change_url_script, + env=env_dict, + operation_logger=operation_logger, + error_message_if_script_failed=m18n.n("app_change_url_script_failed"), + error_message_if_failed=lambda e: m18n.n( + "app_change_url_failed", app=app, error=e + ), + ) + finally: + shutil.rmtree(tmp_workdir_for_app) - # restore values modified by app_checkurl - # see begining of the function - app_setting(app, "domain", value=old_domain) - app_setting(app, "path", value=old_path) - return - shutil.rmtree(tmp_workdir_for_app) + if change_url_failed: + logger.warning("Restoring initial nginx config file") + if old_nginx_conf_path != new_nginx_conf_path and os.path.exists( + new_nginx_conf_path + ): + rm(new_nginx_conf_path, force=True) + if old_nginx_conf_backup: + write_to_file(old_nginx_conf_path, old_nginx_conf_backup) + service_reload_or_restart("nginx") - # this should idealy be done in the change_url script but let's avoid common mistakes - app_setting(app, "domain", value=domain) - app_setting(app, "path", value=path) + # restore values modified by app_checkurl + # see begining of the function + app_setting(app, "domain", value=old_domain) + app_setting(app, "path", value=old_path) + raise YunohostError(failure_message_with_debug_instructions, raw_msg=True) + else: + # make sure the domain/path setting are propagated + app_setting(app, "domain", value=domain) + app_setting(app, "path", value=path) - app_ssowatconf() + app_ssowatconf() - service_reload_or_restart("nginx") + service_reload_or_restart("nginx") - logger.success(m18n.n("app_change_url_success", app=app, domain=domain, path=path)) + logger.success( + m18n.n("app_change_url_success", app=app, domain=domain, path=path) + ) - hook_callback("post_app_change_url", env=env_dict) + hook_callback("post_app_change_url", env=env_dict) -def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False): +def app_upgrade( + app=[], + url=None, + file=None, + force=False, + no_safety_backup=False, + continue_on_failure=False, +): """ Upgrade app @@ -467,7 +551,6 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False no_safety_backup -- Disable the safety backup during upgrade """ - from packaging import version from yunohost.hook import ( hook_add, hook_remove, @@ -477,6 +560,12 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False from yunohost.permission import permission_sync_to_user from yunohost.regenconf import manually_modified_files from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers + from yunohost.backup import ( + backup_list, + backup_create, + backup_delete, + backup_restore, + ) apps = app # Check if disk space available @@ -502,6 +591,9 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False if len(apps) > 1: logger.info(m18n.n("app_upgrade_several_apps", apps=", ".join(apps))) + notifications = {} + failed_to_upgrade_apps = [] + for number, app_instance_name in enumerate(apps): logger.info(m18n.n("app_upgrade_app_name", app=app_instance_name)) @@ -563,34 +655,119 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False upgrade_type = "UPGRADE_FULL" # Check requirements - _check_manifest_requirements(manifest) + for name, passed, values, err in _check_manifest_requirements( + manifest, action="upgrade" + ): + if not passed: + if name == "ram": + # i18n: confirm_app_insufficient_ram + _ask_confirmation( + "confirm_app_insufficient_ram", params=values, force=force + ) + else: + raise YunohostValidationError(err, **values) + + # Display pre-upgrade notifications and ask for simple confirm + if ( + manifest["notifications"]["PRE_UPGRADE"] + and Moulinette.interface.type == "cli" + ): + settings = _get_app_settings(app_instance_name) + notifications = _filter_and_hydrate_notifications( + manifest["notifications"]["PRE_UPGRADE"], + current_version=app_current_version, + data=settings, + ) + _display_notifications(notifications, force=force) + + if manifest["packaging_format"] >= 2: + if no_safety_backup: + # FIXME: i18n + logger.warning( + "Skipping the creation of a backup prior to the upgrade." + ) + else: + # FIXME: i18n + logger.info("Creating a safety backup prior to the upgrade") + + # Switch between pre-upgrade1 or pre-upgrade2 + safety_backup_name = f"{app_instance_name}-pre-upgrade1" + other_safety_backup_name = f"{app_instance_name}-pre-upgrade2" + if safety_backup_name in backup_list()["archives"]: + safety_backup_name = f"{app_instance_name}-pre-upgrade2" + other_safety_backup_name = f"{app_instance_name}-pre-upgrade1" + + backup_create( + name=safety_backup_name, apps=[app_instance_name], system=None + ) + + if safety_backup_name in backup_list()["archives"]: + # if the backup suceeded, delete old safety backup to save space + if other_safety_backup_name in backup_list()["archives"]: + backup_delete(other_safety_backup_name) + else: + # Is this needed ? Shouldn't backup_create report an expcetion if backup failed ? + raise YunohostError( + "Uhoh the safety backup failed ?! Aborting the upgrade process.", + raw_msg=True, + ) + _assert_system_is_sane_for_app(manifest, "pre") - app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) - - # Prepare env. var. to pass to script - env_dict = _make_environment_for_app_script( - app_instance_name, workdir=extracted_app_folder - ) - env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type - env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version) - env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version) - env_dict["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0" - # We'll check that the app didn't brutally edit some system configuration manually_modified_files_before_install = manually_modified_files() + app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) + # Attempt to patch legacy helpers ... _patch_legacy_helpers(extracted_app_folder) # Apply dirty patch to make php5 apps compatible with php7 _patch_legacy_php_versions(extracted_app_folder) + # Prepare env. var. to pass to script + env_dict = _make_environment_for_app_script( + app_instance_name, workdir=extracted_app_folder, action="upgrade" + ) + + env_dict_more = { + "YNH_APP_UPGRADE_TYPE": upgrade_type, + "YNH_APP_MANIFEST_VERSION": str(app_new_version), + "YNH_APP_CURRENT_VERSION": str(app_current_version), + } + + if manifest["packaging_format"] < 2: + env_dict_more["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0" + + env_dict.update(env_dict_more) + # Start register change on system related_to = [("app", app_instance_name)] operation_logger = OperationLogger("app_upgrade", related_to, env=env_dict) operation_logger.start() + if manifest["packaging_format"] >= 2: + from yunohost.utils.resources import AppResourceManager + + AppResourceManager( + app_instance_name, wanted=manifest, current=app_dict["manifest"] + ).apply( + rollback_and_raise_exception_if_failure=True, + operation_logger=operation_logger, + action="upgrade", + ) + + # Boring stuff : the resource upgrade may have added/remove/updated setting + # so we need to reflect this in the env_dict used to call the actual upgrade script x_x + # Or: the old manifest may be in v1 and the new in v2, so force to add the setting in env + env_dict = _make_environment_for_app_script( + app_instance_name, + workdir=extracted_app_folder, + action="upgrade", + force_include_app_settings=True, + ) + env_dict.update(env_dict_more) + # Execute the app upgrade script upgrade_failed = True try: @@ -607,6 +784,25 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False ), ) finally: + # If upgrade failed, try to restore the safety backup + if ( + upgrade_failed + and manifest["packaging_format"] >= 2 + and not no_safety_backup + ): + logger.warning( + "Upgrade failed ... attempting to restore the satefy backup (Yunohost first need to remove the app for this) ..." + ) + + app_remove(app_instance_name, force_workdir=extracted_app_folder) + backup_restore( + name=safety_backup_name, apps=[app_instance_name], force=True + ) + if not _is_installed(app_instance_name): + logger.error( + "Uhoh ... Yunohost failed to restore the app to the way it was before the failed upgrade :|" + ) + # Whatever happened (install success or failure) we check if it broke the system # and warn the user about it try: @@ -633,21 +829,51 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False # If upgrade failed or broke the system, # raise an error and interrupt all other pending upgrades if upgrade_failed or broke_the_system: + if not continue_on_failure or broke_the_system: + # display this if there are remaining apps + if apps[number + 1 :]: + not_upgraded_apps = apps[number:] + if broke_the_system and not continue_on_failure: + logger.error( + m18n.n( + "app_not_upgraded_broken_system", + failed_app=app_instance_name, + apps=", ".join(not_upgraded_apps), + ) + ) + elif broke_the_system and continue_on_failure: + logger.error( + m18n.n( + "app_not_upgraded_broken_system_continue", + failed_app=app_instance_name, + apps=", ".join(not_upgraded_apps), + ) + ) + else: + logger.error( + m18n.n( + "app_not_upgraded", + failed_app=app_instance_name, + apps=", ".join(not_upgraded_apps), + ) + ) - # display this if there are remaining apps - if apps[number + 1 :]: - not_upgraded_apps = apps[number:] - logger.error( - m18n.n( - "app_not_upgraded", - failed_app=app_instance_name, - apps=", ".join(not_upgraded_apps), - ) + raise YunohostError( + failure_message_with_debug_instructions, raw_msg=True ) - raise YunohostError( - failure_message_with_debug_instructions, raw_msg=True - ) + else: + operation_logger.close() + logger.error( + m18n.n( + "app_failed_to_upgrade_but_continue", + failed_app=app_instance_name, + operation_logger_name=operation_logger.name, + ) + ) + failed_to_upgrade_apps.append( + (app_instance_name, operation_logger.name) + ) # Otherwise we're good and keep going ! now = int(time.time()) @@ -684,6 +910,24 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False # So much win logger.success(m18n.n("app_upgraded", app=app_instance_name)) + # Format post-upgrade notifications + if manifest["notifications"]["POST_UPGRADE"]: + # Get updated settings to hydrate notifications + settings = _get_app_settings(app_instance_name) + notifications = _filter_and_hydrate_notifications( + manifest["notifications"]["POST_UPGRADE"], + current_version=app_current_version, + data=settings, + ) + if Moulinette.interface.type == "cli": + # ask for simple confirm + _display_notifications(notifications, force=force) + + # Reset the dismiss flag for post upgrade notification + app_setting( + app_instance_name, "_dismiss_notification_post_upgrade", delete=True + ) + hook_callback("post_app_upgrade", env=env_dict) operation_logger.success() @@ -691,19 +935,83 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False logger.success(m18n.n("upgrade_complete")) + if failed_to_upgrade_apps: + apps = "" + for app_id, operation_logger_name in failed_to_upgrade_apps: + apps += m18n.n( + "apps_failed_to_upgrade_line", + app_id=app_id, + operation_logger_name=operation_logger_name, + ) -def app_manifest(app): + logger.warning(m18n.n("apps_failed_to_upgrade", apps=apps)) + if Moulinette.interface.type == "api": + return {"notifications": {"POST_UPGRADE": notifications}} + + +def app_manifest(app, with_screenshot=False): manifest, extracted_app_folder = _extract_app(app) + raw_questions = manifest.get("install", {}).values() + manifest["install"] = hydrate_questions_with_choices(raw_questions) + + # Add a base64 image to be displayed in web-admin + if with_screenshot and Moulinette.interface.type == "api": + import base64 + + manifest["screenshot"] = None + screenshots_folder = os.path.join(extracted_app_folder, "doc", "screenshots") + + if os.path.exists(screenshots_folder): + with os.scandir(screenshots_folder) as it: + for entry in it: + ext = os.path.splitext(entry.name)[1].replace(".", "").lower() + if entry.is_file() and ext in ("png", "jpg", "jpeg", "webp", "gif"): + with open(entry.path, "rb") as img_file: + data = base64.b64encode(img_file.read()).decode("utf-8") + manifest[ + "screenshot" + ] = f"data:image/{ext};charset=utf-8;base64,{data}" + break + shutil.rmtree(extracted_app_folder) - raw_questions = manifest.get("arguments", {}).get("install", []) - manifest["arguments"]["install"] = hydrate_questions_with_choices(raw_questions) + manifest["requirements"] = {} + for name, passed, values, err in _check_manifest_requirements( + manifest, action="install" + ): + if Moulinette.interface.type == "api": + manifest["requirements"][name] = { + "pass": passed, + "values": values, + } + else: + manifest["requirements"][name] = "ok" if passed else m18n.n(err, **values) return manifest +def _confirm_app_install(app, force=False): + # Ignore if there's nothing for confirm (good quality app), if --force is used + # or if request on the API (confirm already implemented on the API side) + if force or Moulinette.interface.type == "api": + return + + quality = _app_quality(app) + if quality == "success": + return + + # i18n: confirm_app_install_warning + # i18n: confirm_app_install_danger + # i18n: confirm_app_install_thirdparty + + if quality in ["danger", "thirdparty"]: + _ask_confirmation("confirm_app_install_" + quality, kind="hard") + else: + _ask_confirmation("confirm_app_install_" + quality, kind="soft") + + @is_unit_operation() def app_install( operation_logger, @@ -745,63 +1053,50 @@ def app_install( if free_space_in_directory("/") <= 512 * 1000 * 1000: raise YunohostValidationError("disk_space_not_sufficient_install") - def confirm_install(app): - - # Ignore if there's nothing for confirm (good quality app), if --force is used - # or if request on the API (confirm already implemented on the API side) - if force or Moulinette.interface.type == "api": - return - - quality = _app_quality(app) - if quality == "success": - return - - # i18n: confirm_app_install_warning - # i18n: confirm_app_install_danger - # i18n: confirm_app_install_thirdparty - - if quality in ["danger", "thirdparty"]: - answer = Moulinette.prompt( - m18n.n("confirm_app_install_" + quality, answers="Yes, I understand"), - color="red", - ) - if answer != "Yes, I understand": - raise YunohostError("aborting") - - else: - answer = Moulinette.prompt( - m18n.n("confirm_app_install_" + quality, answers="Y/N"), color="yellow" - ) - if answer.upper() != "Y": - raise YunohostError("aborting") - - confirm_install(app) + _confirm_app_install(app, force) manifest, extracted_app_folder = _extract_app(app) + # Display pre_install notices in cli mode + if manifest["notifications"]["PRE_INSTALL"] and Moulinette.interface.type == "cli": + notifications = _filter_and_hydrate_notifications( + manifest["notifications"]["PRE_INSTALL"] + ) + _display_notifications(notifications, force=force) + + packaging_format = manifest["packaging_format"] + # Check ID if "id" not in manifest or "__" in manifest["id"] or "." in manifest["id"]: raise YunohostValidationError("app_id_invalid") app_id = manifest["id"] - label = label if label else manifest["name"] # Check requirements - _check_manifest_requirements(manifest) + for name, passed, values, err in _check_manifest_requirements( + manifest, action="install" + ): + if not passed: + if name == "ram": + _ask_confirmation( + "confirm_app_insufficient_ram", params=values, force=force + ) + else: + raise YunohostValidationError(err, **values) + _assert_system_is_sane_for_app(manifest, "pre") # Check if app can be forked instance_number = _next_instance_number_for_app(app_id) if instance_number > 1: - if "multi_instance" not in manifest or not is_true(manifest["multi_instance"]): - raise YunohostValidationError("app_already_installed", app=app_id) - # Change app_id to the forked app id app_instance_name = app_id + "__" + str(instance_number) else: app_instance_name = app_id + app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) + # Retrieve arguments list for install script - raw_questions = manifest.get("arguments", {}).get("install", {}) + raw_questions = manifest["install"] questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) args = { question.name: question.value @@ -810,11 +1105,13 @@ def app_install( } # Validate domain / path availability for webapps + # (ideally this should be handled by the resource system for manifest v >= 2 path_requirement = _guess_webapp_path_requirement(extracted_app_folder) _validate_webpath_requirement(args, path_requirement) - # Attempt to patch legacy helpers ... - _patch_legacy_helpers(extracted_app_folder) + if packaging_format < 2: + # Attempt to patch legacy helpers ... + _patch_legacy_helpers(extracted_app_folder) # Apply dirty patch to make php5 apps compatible with php7 _patch_legacy_php_versions(extracted_app_folder) @@ -831,7 +1128,6 @@ def app_install( logger.info(m18n.n("app_start_install", app=app_id)) # Create app directory - app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) os.makedirs(app_setting_path) @@ -842,6 +1138,17 @@ def app_install( "install_time": int(time.time()), "current_revision": manifest.get("remote", {}).get("revision", "?"), } + + # If packaging_format v2+, save all install questions as settings + if packaging_format >= 2: + for question in questions: + # Except user-provider passwords + # ... which we need to reinject later in the env_dict + if question.type == "password": + continue + + app_settings[question.name] = question.value + _set_app_settings(app_instance_name, app_settings) # Move scripts and manifest to the right place @@ -853,28 +1160,59 @@ def app_install( recursive=True, ) - # Initialize the main permission for the app - # The permission is initialized with no url associated, and with tile disabled - # For web app, the root path of the app will be added as url and the tile - # will be enabled during the app install. C.f. 'app_register_url()' below. - permission_create( - app_instance_name + ".main", - allowed=["all_users"], - label=label, - show_tile=False, - protected=False, - ) + # Override manifest name by given label + # This info is also later picked-up by the 'permission' resource initialization + if label: + manifest["name"] = label + + if packaging_format >= 2: + from yunohost.utils.resources import AppResourceManager + + try: + AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( + rollback_and_raise_exception_if_failure=True, + operation_logger=operation_logger, + action="install", + ) + except (KeyboardInterrupt, EOFError, Exception) as e: + shutil.rmtree(app_setting_path) + raise e + else: + # Initialize the main permission for the app + # The permission is initialized with no url associated, and with tile disabled + # For web app, the root path of the app will be added as url and the tile + # will be enabled during the app install. C.f. 'app_register_url()' below + # or the webpath resource + permission_create( + app_instance_name + ".main", + allowed=["all_users"], + label=manifest["name"], + show_tile=False, + protected=False, + ) # Prepare env. var. to pass to script env_dict = _make_environment_for_app_script( - app_instance_name, args=args, workdir=extracted_app_folder + app_instance_name, args=args, workdir=extracted_app_folder, action="install" ) + # If packaging_format v2+, save all install questions as settings + if packaging_format >= 2: + for question in questions: + # Reinject user-provider passwords which are not in the app settings + # (cf a few line before) + if question.type == "password": + env_dict[question.name] = question.value + + # We want to hav the env_dict in the log ... but not password values env_dict_for_logging = env_dict.copy() for question in questions: # Or should it be more generally question.redact ? if question.type == "password": - del env_dict_for_logging[f"YNH_APP_ARG_{question.name.upper()}"] + if f"YNH_APP_ARG_{question.name.upper()}" in env_dict_for_logging: + del env_dict_for_logging[f"YNH_APP_ARG_{question.name.upper()}"] + if question.name in env_dict_for_logging: + del env_dict_for_logging[question.name] operation_logger.extra.update({"env": env_dict_for_logging}) @@ -914,10 +1252,12 @@ def app_install( "Packagers /!\\ This app manually modified some system configuration files! This should not happen! If you need to do so, you should implement a proper conf_regen hook. Those configuration were affected:\n - " + "\n -".join(manually_modified_files_by_app) ) + # Actually forbid this for app packaging >= 2 + if packaging_format >= 2: + broke_the_system = True # If the install failed or broke the system, we remove it if install_failed or broke_the_system: - # This option is meant for packagers to debug their apps more easily if no_remove_on_failure: raise YunohostError( @@ -929,7 +1269,7 @@ def app_install( # Setup environment for remove script env_dict_remove = _make_environment_for_app_script( - app_instance_name, workdir=extracted_app_folder + app_instance_name, workdir=extracted_app_folder, action="remove" ) # Execute remove script @@ -960,10 +1300,17 @@ def app_install( m18n.n("unexpected_error", error="\n" + traceback.format_exc()) ) - # Remove all permission in LDAP - for permission_name in user_permission_list()["permissions"].keys(): - if permission_name.startswith(app_instance_name + "."): - permission_delete(permission_name, force=True, sync_perm=False) + if packaging_format >= 2: + from yunohost.utils.resources import AppResourceManager + + AppResourceManager( + app_instance_name, wanted={}, current=manifest + ).apply(rollback_and_raise_exception_if_failure=False, action="remove") + else: + # Remove all permission in LDAP + for permission_name in user_permission_list()["permissions"].keys(): + if permission_name.startswith(app_instance_name + "."): + permission_delete(permission_name, force=True, sync_perm=False) if remove_retcode != 0: msg = m18n.n("app_not_properly_removed", app=app_instance_name) @@ -999,18 +1346,33 @@ def app_install( logger.success(m18n.n("installation_complete")) + # Get the generated settings to hydrate notifications + settings = _get_app_settings(app_instance_name) + notifications = _filter_and_hydrate_notifications( + manifest["notifications"]["POST_INSTALL"], data=settings + ) + + # Display post_install notices in cli mode + if notifications and Moulinette.interface.type == "cli": + _display_notifications(notifications, force=force) + + # Call postinstall hook hook_callback("post_app_install", env=env_dict) + # Return hydrated post install notif for API + if Moulinette.interface.type == "api": + return {"notifications": notifications} + @is_unit_operation() -def app_remove(operation_logger, app, purge=False): +def app_remove(operation_logger, app, purge=False, force_workdir=None): """ Remove app Keyword arguments: app -- App(s) to delete purge -- Remove with all app data - + force_workdir -- Special var to force the working directoy to use, in context such as remove-after-failed-upgrade or remove-after-failed-restore """ from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers from yunohost.hook import hook_exec, hook_remove, hook_callback @@ -1029,7 +1391,6 @@ def app_remove(operation_logger, app, purge=False): operation_logger.start() logger.info(m18n.n("app_start_remove", app=app)) - app_setting_path = os.path.join(APPS_SETTING_PATH, app) # Attempt to patch legacy helpers ... @@ -1039,12 +1400,26 @@ def app_remove(operation_logger, app, purge=False): # script might date back from jessie install) _patch_legacy_php_versions(app_setting_path) - manifest = _get_manifest_of_app(app_setting_path) - tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) + if force_workdir: + # This is when e.g. calling app_remove() from the upgrade-failed case + # where we want to remove using the *new* remove script and not the old one + # and also get the new manifest + # It's especially important during v1->v2 app format transition where the + # setting names change (e.g. install_dir instead of final_path) and + # running the old remove script doesnt make sense anymore ... + tmp_workdir_for_app = tempfile.mkdtemp(prefix="app_", dir=APP_TMP_WORKDIRS) + os.system(f"cp -a {force_workdir}/* {tmp_workdir_for_app}/") + else: + tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) + + manifest = _get_manifest_of_app(tmp_workdir_for_app) + remove_script = f"{tmp_workdir_for_app}/scripts/remove" env_dict = {} - env_dict = _make_environment_for_app_script(app, workdir=tmp_workdir_for_app) + env_dict = _make_environment_for_app_script( + app, workdir=tmp_workdir_for_app, action="remove" + ) env_dict["YNH_APP_PURGE"] = str(1 if purge else 0) operation_logger.extra.update({"env": env_dict}) @@ -1064,15 +1439,19 @@ def app_remove(operation_logger, app, purge=False): finally: shutil.rmtree(tmp_workdir_for_app) - if ret == 0: - logger.success(m18n.n("app_removed", app=app)) - hook_callback("post_app_remove", env=env_dict) - else: - logger.warning(m18n.n("app_not_properly_removed", app=app)) + packaging_format = manifest["packaging_format"] + if packaging_format >= 2: + from yunohost.utils.resources import AppResourceManager - # Remove all permission in LDAP - for permission_name in user_permission_list(apps=[app])["permissions"].keys(): - permission_delete(permission_name, force=True, sync_perm=False) + AppResourceManager(app, wanted={}, current=manifest).apply( + rollback_and_raise_exception_if_failure=False, + purge_data_dir=purge, + action="remove", + ) + else: + # Remove all permission in LDAP + for permission_name in user_permission_list(apps=[app])["permissions"].keys(): + permission_delete(permission_name, force=True, sync_perm=False) if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) @@ -1083,6 +1462,12 @@ def app_remove(operation_logger, app, purge=False): if domain_config_get(domain, "feature.app.default_app") == app: domain_config_set(domain, "feature.app.default_app", "_none") + if ret == 0: + logger.success(m18n.n("app_removed", app=app)) + hook_callback("post_app_remove", env=env_dict) + else: + logger.warning(m18n.n("app_not_properly_removed", app=app)) + permission_sync_to_user() _assert_system_is_sane_for_app(manifest, "post") @@ -1140,7 +1525,6 @@ def app_setting(app, key, value=None, delete=False): ) if is_legacy_permission_setting: - from yunohost.permission import ( user_permission_list, user_permission_update, @@ -1183,7 +1567,6 @@ def app_setting(app, key, value=None, delete=False): # SET else: - urls = value # If the request is about the root of the app (/), ( = the vast majority of cases) # we interpret this as a change for the main permission @@ -1195,7 +1578,6 @@ def app_setting(app, key, value=None, delete=False): else: user_permission_update(app + ".main", remove="visitors") else: - urls = urls.split(",") if key.endswith("_regex"): urls = ["re:" + url for url in urls] @@ -1264,6 +1646,23 @@ def app_setting(app, key, value=None, delete=False): _set_app_settings(app, app_settings) +def app_shell(app): + """ + Open an interactive shell with the app environment already loaded + + Keyword argument: + app -- App ID + + """ + subprocess.run( + [ + "/bin/bash", + "-c", + "source /usr/share/yunohost/helpers && ynh_spawn_app_shell " + app, + ] + ) + + def app_register_url(app, domain, path): """ Book/register a web path for a given app @@ -1279,8 +1678,8 @@ def app_register_url(app, domain, path): permission_sync_to_user, ) - domain = DomainQuestion.normalize(domain) - path = PathQuestion.normalize(path) + domain = DomainOption.normalize(domain) + path = WebPathOption.normalize(path) # We cannot change the url of an app already installed simply by changing # the settings... @@ -1291,7 +1690,7 @@ def app_register_url(app, domain, path): raise YunohostValidationError("app_already_installed_cant_change_url") # Check the url is available - _assert_no_conflicting_apps(domain, path) + _assert_no_conflicting_apps(domain, path, ignore_app=app) app_setting(app, "domain", value=domain) app_setting(app, "path", value=path) @@ -1315,6 +1714,7 @@ def app_ssowatconf(): """ from yunohost.domain import domain_list, _get_maindomain, domain_config_get from yunohost.permission import user_permission_list + from yunohost.settings import settings_get main_domain = _get_maindomain() domains = domain_list()["domains"] @@ -1333,6 +1733,7 @@ def app_ssowatconf(): + [domain + "/yunohost/api" for domain in domains] + [domain + "/yunohost/portalapi" for domain in domains] + [ + "re:^[^/]/502%.html$", "re:^[^/]*/%.well%-known/ynh%-diagnosis/.*$", "re:^[^/]*/%.well%-known/acme%-challenge/.*$", "re:^[^/]*/%.well%-known/autoconfig/mail/config%-v1%.1%.xml.*$", @@ -1344,9 +1745,16 @@ def app_ssowatconf(): } redirected_urls = {} - for app in _installed_apps(): + apps_using_remote_user_var_in_nginx = ( + check_output( + "grep -nri '$remote_user' /etc/yunohost/apps/*/conf/*nginx*conf | awk -F/ '{print $5}' || true" + ) + .strip() + .split("\n") + ) - app_settings = read_yaml(APPS_SETTING_PATH + app + "/settings.yml") + for app in _installed_apps(): + app_settings = read_yaml(APPS_SETTING_PATH + app + "/settings.yml") or {} # Redirected redirected_urls.update(app_settings.get("redirected_urls", {})) @@ -1371,7 +1779,6 @@ def app_ssowatconf(): # New permission system for perm_name, perm_info in all_permissions.items(): - uris = ( [] + ([perm_info["url"]] if perm_info["url"] else []) @@ -1382,7 +1789,11 @@ def app_ssowatconf(): if not uris: continue + app_id = perm_name.split(".")[0] + permissions[perm_name] = { + "use_remote_user_var_in_nginx_conf": app_id + in apps_using_remote_user_var_in_nginx, "users": perm_info["corresponding_users"], "label": perm_info["label"], "show_tile": perm_info["show_tile"] @@ -1396,6 +1807,7 @@ def app_ssowatconf(): conf_dict = { "cookie_secret_file": "/etc/yunohost/.ssowat_cookie_secret", "cookie_name": "yunohost.portal", + "theme": settings_get("misc.portal.portal_theme"), "portal_domain": main_domain, "portal_path": "/yunohost/sso/", "additional_headers": { @@ -1432,90 +1844,14 @@ def app_change_label(app, new_label): def app_action_list(app): - logger.warning(m18n.n("experimental_feature")) - - # this will take care of checking if the app is installed - app_info_dict = app_info(app) - - return { - "app": app, - "app_name": app_info_dict["name"], - "actions": _get_app_actions(app), - } + return AppConfigPanel(app).list_actions() @is_unit_operation() -def app_action_run(operation_logger, app, action, args=None): - logger.warning(m18n.n("experimental_feature")) - - from yunohost.hook import hook_exec - - # will raise if action doesn't exist - actions = app_action_list(app)["actions"] - actions = {x["id"]: x for x in actions} - - if action not in actions: - available_actions = (", ".join(actions.keys()),) - raise YunohostValidationError( - f"action '{action}' not available for app '{app}', available actions are: {available_actions}", - raw_msg=True, - ) - - operation_logger.start() - - action_declaration = actions[action] - - # Retrieve arguments list for install script - raw_questions = actions[action].get("arguments", {}) - questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) - args = { - question.name: question.value - for question in questions - if question.value is not None - } - - tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) - - env_dict = _make_environment_for_app_script( - app, args=args, args_prefix="ACTION_", workdir=tmp_workdir_for_app +def app_action_run(operation_logger, app, action, args=None, args_file=None): + return AppConfigPanel(app).run_action( + action, args=args, args_file=args_file, operation_logger=operation_logger ) - env_dict["YNH_ACTION"] = action - - _, action_script = tempfile.mkstemp(dir=tmp_workdir_for_app) - - with open(action_script, "w") as script: - script.write(action_declaration["command"]) - - if action_declaration.get("cwd"): - cwd = action_declaration["cwd"].replace("$app", app) - else: - cwd = tmp_workdir_for_app - - try: - retcode = hook_exec( - action_script, - env=env_dict, - chdir=cwd, - user=action_declaration.get("user", "root"), - )[0] - # Calling hook_exec could fail miserably, or get - # manually interrupted (by mistake or because script was stuck) - # In that case we still want to delete the tmp work dir - except (KeyboardInterrupt, EOFError, Exception): - retcode = -1 - import traceback - - logger.error(m18n.n("unexpected_error", error="\n" + traceback.format_exc())) - finally: - shutil.rmtree(tmp_workdir_for_app) - - if retcode not in action_declaration.get("accepted_return_codes", [0]): - msg = f"Error while executing action '{action}' of app '{app}': return code {retcode}" - operation_logger.error(msg) - raise YunohostError(msg, raw_msg=True) - - operation_logger.success() - return logger.success("Action successed!") def app_config_get(app, key="", full=False, export=False): @@ -1534,8 +1870,15 @@ def app_config_get(app, key="", full=False, export=False): else: mode = "classic" - config_ = AppConfigPanel(app) - return config_.get(key, mode) + try: + config_ = AppConfigPanel(app) + return config_.get(key, mode) + except YunohostValidationError as e: + if Moulinette.interface.type == "api" and e.key == "config_no_panel": + # Be more permissive when no config panel found + return {} + else: + raise @is_unit_operation() @@ -1556,7 +1899,11 @@ class AppConfigPanel(ConfigPanel): save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml") config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.toml") - def _load_current_values(self): + def _run_action(self, action): + env = {key: str(value) for key, value in self.new_values.items()} + self._call_config_script(action, env=env) + + def _get_raw_settings(self): self.values = self._call_config_script("show") def _apply(self): @@ -1604,6 +1951,7 @@ ynh_app_config_run $1 "app": app, "app_instance_nb": str(app_instance_nb), "final_path": settings.get("final_path", ""), + "install_dir": settings.get("install_dir", ""), "YNH_APP_BASEDIR": os.path.join(APPS_SETTING_PATH, app), } ) @@ -1612,8 +1960,10 @@ ynh_app_config_run $1 if ret != 0: if action == "show": raise YunohostError("app_config_unable_to_read") - else: + elif action == "apply": raise YunohostError("app_config_unable_to_apply") + else: + raise YunohostError("app_action_failed", action=action, app=app) return values @@ -1622,58 +1972,6 @@ def _get_app_actions(app_id): actions_toml_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.toml") actions_json_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.json") - # sample data to get an idea of what is going on - # this toml extract: - # - - # [restart_service] - # name = "Restart service" - # command = "echo pouet $YNH_ACTION_SERVICE" - # user = "root" # optional - # cwd = "/" # optional - # accepted_return_codes = [0, 1, 2, 3] # optional - # description.en = "a dummy stupid exemple or restarting a service" - # - # [restart_service.arguments.service] - # type = "string", - # ask.en = "service to restart" - # example = "nginx" - # - # will be parsed into this: - # - # OrderedDict([(u'restart_service', - # OrderedDict([(u'name', u'Restart service'), - # (u'command', u'echo pouet $YNH_ACTION_SERVICE'), - # (u'user', u'root'), - # (u'cwd', u'/'), - # (u'accepted_return_codes', [0, 1, 2, 3]), - # (u'description', - # OrderedDict([(u'en', - # u'a dummy stupid exemple or restarting a service')])), - # (u'arguments', - # OrderedDict([(u'service', - # OrderedDict([(u'type', u'string'), - # (u'ask', - # OrderedDict([(u'en', - # u'service to restart')])), - # (u'example', - # u'nginx')]))]))])), - # - # - # and needs to be converted into this: - # - # [{u'accepted_return_codes': [0, 1, 2, 3], - # u'arguments': [{u'ask': {u'en': u'service to restart'}, - # u'example': u'nginx', - # u'name': u'service', - # u'type': u'string'}], - # u'command': u'echo pouet $YNH_ACTION_SERVICE', - # u'cwd': u'/', - # u'description': {u'en': u'a dummy stupid exemple or restarting a service'}, - # u'id': u'restart_service', - # u'name': u'Restart service', - # u'user': u'root'}] - if os.path.exists(actions_toml_path): toml_actions = toml.load(open(actions_toml_path, "r"), _dict=OrderedDict) @@ -1683,15 +1981,7 @@ def _get_app_actions(app_id): for key, value in toml_actions.items(): action = dict(**value) action["id"] = key - - arguments = [] - for argument_name, argument in value.get("arguments", {}).items(): - argument = dict(**argument) - argument["name"] = argument_name - - arguments.append(argument) - - action["arguments"] = arguments + action["arguments"] = value.get("arguments", {}) actions.append(action) return actions @@ -1716,11 +2006,20 @@ def _get_app_settings(app): ) try: with open(os.path.join(APPS_SETTING_PATH, app, "settings.yml")) as f: - settings = yaml.safe_load(f) + settings = yaml.safe_load(f) or {} # If label contains unicode char, this may later trigger issues when building strings... # FIXME: this should be propagated to read_yaml so that this fix applies everywhere I think... settings = {k: v for k, v in settings.items()} + # App settings should never be empty, there should always be at least some standard, internal keys like id, install_time etc. + # Otherwise, this probably means that the app settings disappeared somehow... + if not settings: + logger.error( + f"It looks like settings.yml for {app} is empty ... This should not happen ..." + ) + logger.error(m18n.n("app_not_correctly_installed", app=app)) + return {} + # Stupid fix for legacy bullshit # In the past, some setups did not have proper normalization for app domain/path # Meaning some setups (as of January 2021) still have path=/foobar/ (with a trailing slash) @@ -1735,6 +2034,9 @@ def _get_app_settings(app): settings["path"] = "/" + settings["path"].strip("/") _set_app_settings(app, settings) + # Make the app id available as $app too + settings["app"] = app + if app == settings["id"]: return settings except (IOError, TypeError, KeyError): @@ -1862,21 +2164,7 @@ def _get_manifest_of_app(path): # ¦ ¦ }, if os.path.exists(os.path.join(path, "manifest.toml")): - manifest_toml = read_toml(os.path.join(path, "manifest.toml")) - - manifest = manifest_toml.copy() - - install_arguments = [] - for name, values in ( - manifest_toml.get("arguments", {}).get("install", {}).items() - ): - args = values.copy() - args["name"] = name - - install_arguments.append(args) - - manifest["arguments"]["install"] = install_arguments - + manifest = read_toml(os.path.join(path, "manifest.toml")) elif os.path.exists(os.path.join(path, "manifest.json")): manifest = read_json(os.path.join(path, "manifest.json")) else: @@ -1885,25 +2173,190 @@ def _get_manifest_of_app(path): raw_msg=True, ) - manifest["arguments"] = _set_default_ask_questions(manifest.get("arguments", {})) + manifest["packaging_format"] = float( + str(manifest.get("packaging_format", "")).strip() or "0" + ) + + if manifest["packaging_format"] < 2: + manifest = _convert_v1_manifest_to_v2(manifest) + + manifest["install"] = _set_default_ask_questions(manifest.get("install", {})) + manifest["doc"], manifest["notifications"] = _parse_app_doc_and_notifications(path) + return manifest -def _set_default_ask_questions(arguments): +def _parse_app_doc_and_notifications(path): + doc = {} + notification_names = ["PRE_INSTALL", "POST_INSTALL", "PRE_UPGRADE", "POST_UPGRADE"] + for filepath in glob.glob(os.path.join(path, "doc") + "/*.md"): + # to be improved : [a-z]{2,3} is a clumsy way of parsing the + # lang code ... some lang code are more complex that this é_è + m = re.match("([A-Z]*)(_[a-z]{2,3})?.md", filepath.split("/")[-1]) + + if not m: + # FIXME: shall we display a warning ? idk + continue + + pagename, lang = m.groups() + + if pagename in notification_names: + continue + + lang = lang.strip("_") if lang else "en" + + if pagename not in doc: + doc[pagename] = {} + + try: + doc[pagename][lang] = read_file(filepath).strip() + except Exception as e: + logger.error(e) + continue + + notifications = {} + + for step in notification_names: + notifications[step] = {} + for filepath in glob.glob(os.path.join(path, "doc", f"{step}*.md")): + m = re.match(step + "(_[a-z]{2,3})?.md", filepath.split("/")[-1]) + if not m: + continue + pagename = "main" + lang = m.groups()[0].strip("_") if m.groups()[0] else "en" + if pagename not in notifications[step]: + notifications[step][pagename] = {} + try: + notifications[step][pagename][lang] = read_file(filepath).strip() + except Exception as e: + logger.error(e) + continue + + for filepath in glob.glob(os.path.join(path, "doc", f"{step}.d") + "/*.md"): + m = re.match( + r"([A-Za-z0-9\.\~]*)(_[a-z]{2,3})?.md", filepath.split("/")[-1] + ) + if not m: + continue + pagename, lang = m.groups() + lang = lang.strip("_") if lang else "en" + if pagename not in notifications[step]: + notifications[step][pagename] = {} + + try: + notifications[step][pagename][lang] = read_file(filepath).strip() + except Exception as e: + logger.error(e) + continue + + return doc, notifications + + +def _hydrate_app_template(template, data): + stuff_to_replace = set(re.findall(r"__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__", template)) + + for stuff in stuff_to_replace: + varname = stuff.strip("_").lower() + + if varname in data: + template = template.replace(stuff, str(data[varname])) + + return template + + +def _convert_v1_manifest_to_v2(manifest): + manifest = copy.deepcopy(manifest) + + if "upstream" not in manifest: + manifest["upstream"] = {} + + if "license" in manifest and "license" not in manifest["upstream"]: + manifest["upstream"]["license"] = manifest["license"] + + if "url" in manifest and "website" not in manifest["upstream"]: + manifest["upstream"]["website"] = manifest["url"] + + manifest["integration"] = { + "yunohost": manifest.get("requirements", {}) + .get("yunohost", "") + .replace(">", "") + .replace("=", "") + .replace(" ", ""), + "architectures": "?", + "multi_instance": manifest.get("multi_instance", False), + "ldap": "?", + "sso": "?", + "disk": "?", + "ram": {"build": "?", "runtime": "?"}, + } + + maintainers = manifest.get("maintainer", {}) + if isinstance(maintainers, list): + maintainers = [m["name"] for m in maintainers] + else: + maintainers = [maintainers["name"]] if maintainers.get("name") else [] + + manifest["maintainers"] = maintainers + + install_questions = manifest["arguments"]["install"] + + manifest["install"] = {} + for question in install_questions: + name = question.pop("name") + if "ask" in question and name in [ + "domain", + "path", + "admin", + "is_public", + "password", + ]: + question.pop("ask") + if question.get("example") and question.get("type") in [ + "domain", + "path", + "user", + "boolean", + "password", + ]: + question.pop("example") + + manifest["install"][name] = question + + manifest["resources"] = {"system_user": {}, "install_dir": {"alias": "final_path"}} + + keys_to_keep = [ + "packaging_format", + "id", + "name", + "description", + "version", + "maintainers", + "upstream", + "integration", + "install", + "resources", + ] + + keys_to_del = [key for key in manifest.keys() if key not in keys_to_keep] + for key in keys_to_del: + del manifest[key] + + return manifest + + +def _set_default_ask_questions(questions, script_name="install"): # arguments is something like - # { "install": [ - # { "name": "domain", + # { "domain": + # { # "type": "domain", # .... # }, - # { "name": "path", - # "type": "path" + # "path": { + # "type": "path", # ... # }, # ... - # ], - # "upgrade": [ ... ] # } # We set a default for any question with these matching (type, name) @@ -1915,43 +2368,41 @@ def _set_default_ask_questions(arguments): ("path", "path"), # i18n: app_manifest_install_ask_path ("password", "password"), # i18n: app_manifest_install_ask_password ("user", "admin"), # i18n: app_manifest_install_ask_admin - ("boolean", "is_public"), - ] # i18n: app_manifest_install_ask_is_public + ("boolean", "is_public"), # i18n: app_manifest_install_ask_is_public + ( + "group", + "init_main_permission", + ), # i18n: app_manifest_install_ask_init_main_permission + ( + "group", + "init_admin_permission", + ), # i18n: app_manifest_install_ask_init_admin_permission + ] - for script_name, arg_list in arguments.items(): + for question_name, question in questions.items(): + question["name"] = question_name - # We only support questions for install so far, and for other - if script_name != "install": - continue - - for arg in arg_list: - - # Do not override 'ask' field if provided by app ?... Or shall we ? - # if "ask" in arg: - # continue - - # If this arg corresponds to a question with default ask message... - if any( - (arg.get("type"), arg["name"]) == question - for question in questions_with_default - ): - # The key is for example "app_manifest_install_ask_domain" - arg_name = arg["name"] - key = f"app_manifest_{script_name}_ask_{arg_name}" - arg["ask"] = m18n.n(key) + # If this question corresponds to a question with default ask message... + if any( + (question.get("type"), question["name"]) == question_with_default + for question_with_default in questions_with_default + ): + # The key is for example "app_manifest_install_ask_domain" + question["ask"] = m18n.n( + f"app_manifest_{script_name}_ask_{question['name']}" + ) # Also it in fact doesn't make sense for any of those questions to have an example value nor a default value... - if arg.get("type") in ["domain", "user", "password"]: - if "example" in arg: - del arg["example"] - if "default" in arg: - del arg["default"] + if question.get("type") in ["domain", "user", "password"]: + if "example" in question: + del question["example"] + if "default" in question: + del question["default"] - return arguments + return questions def _is_app_repo_url(string: str) -> bool: - string = string.strip() # Dummy test for ssh-based stuff ... should probably be improved somehow @@ -1968,7 +2419,6 @@ def _app_quality(src: str) -> str: raw_app_catalog = _load_apps_catalog()["apps"] if src in raw_app_catalog or _is_app_repo_url(src): - # If we got an app name directly (e.g. just "wordpress"), we gonna test this name if src in raw_app_catalog: app_name_to_test = src @@ -1981,7 +2431,6 @@ def _app_quality(src: str) -> str: return "thirdparty" if app_name_to_test in raw_app_catalog: - state = raw_app_catalog[app_name_to_test].get("state", "notworking") level = raw_app_catalog[app_name_to_test].get("level", None) if state in ["working", "validated"]: @@ -2019,19 +2468,21 @@ def _extract_app(src: str) -> Tuple[Dict, str]: url = app_info["git"]["url"] branch = app_info["git"]["branch"] revision = str(app_info["git"]["revision"]) - return _extract_app_from_gitrepo(url, branch, revision, app_info) + return _extract_app_from_gitrepo( + url, branch=branch, revision=revision, app_info=app_info + ) # App is a git repo url elif _is_app_repo_url(src): url = src.strip().strip("/") - branch = "master" - revision = "HEAD" # gitlab urls may look like 'https://domain/org/group/repo/-/tree/testing' # compated to github urls looking like 'https://domain/org/repo/tree/testing' if "/-/" in url: url = url.replace("/-/", "/") if "/tree/" in url: url, branch = url.split("/tree/", 1) - return _extract_app_from_gitrepo(url, branch, revision, {}) + else: + branch = None + return _extract_app_from_gitrepo(url, branch=branch) # App is a local folder elif os.path.exists(src): return _extract_app_from_folder(src) @@ -2061,6 +2512,10 @@ def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: if path[-1] != "/": path = path + "/" cp(path, extracted_app_folder, recursive=True) + # Change the last edit time which is used in _make_tmp_workdir_for_app + # to cleanup old dir ... otherwise it may end up being incorrectly removed + # at the end of the safety-backup-before-upgrade :/ + os.system(f"touch {extracted_app_folder}") else: try: shutil.unpack_archive(path, extracted_app_folder) @@ -2080,12 +2535,51 @@ def _extract_app_from_folder(path: str) -> Tuple[Dict, str]: logger.debug(m18n.n("done")) manifest["remote"] = {"type": "file", "path": path} + manifest["quality"] = {"level": -1, "state": "thirdparty"} + manifest["antifeatures"] = [] + manifest["potential_alternative_to"] = [] + return manifest, extracted_app_folder def _extract_app_from_gitrepo( - url: str, branch: str, revision: str, app_info: Dict = {} + url: str, branch: Optional[str] = None, revision: str = "HEAD", app_info: Dict = {} ) -> Tuple[Dict, str]: + logger.debug("Checking default branch") + + try: + git_ls_remote = check_output( + ["git", "ls-remote", "--symref", url, "HEAD"], + env={"GIT_TERMINAL_PROMPT": "0", "LC_ALL": "C"}, + shell=False, + ) + except Exception as e: + logger.error(str(e)) + raise YunohostError("app_sources_fetch_failed") + + if not branch: + default_branch = None + try: + for line in git_ls_remote.split("\n"): + # Look for the line formated like : + # ref: refs/heads/master HEAD + if "ref: refs/heads/" in line: + line = line.replace("/", " ").replace("\t", " ") + default_branch = line.split()[3] + except Exception: + pass + + if not default_branch: + logger.warning("Failed to parse default branch, trying 'main'") + branch = "main" + else: + if default_branch in ["testing", "dev"]: + logger.warning( + f"Trying 'master' branch instead of default '{default_branch}'" + ) + branch = "master" + else: + branch = default_branch logger.debug(m18n.n("downloading")) @@ -2126,9 +2620,36 @@ def _extract_app_from_gitrepo( manifest["remote"]["revision"] = revision manifest["lastUpdate"] = app_info.get("lastUpdate") + manifest["quality"] = { + "level": app_info.get("level", -1), + "state": app_info.get("state", "thirdparty"), + } + manifest["antifeatures"] = app_info.get("antifeatures", []) + manifest["potential_alternative_to"] = app_info.get("potential_alternative_to", []) + return manifest, extracted_app_folder +def _list_upgradable_apps(): + upgradable_apps = list(app_list(upgradable=True)["apps"]) + + # Retrieve next manifest pre_upgrade notifications + for app in upgradable_apps: + absolute_app_name, _ = _parse_app_instance_name(app["id"]) + manifest, extracted_app_folder = _extract_app(absolute_app_name) + app["notifications"] = {} + if manifest["notifications"]["PRE_UPGRADE"]: + app["notifications"]["PRE_UPGRADE"] = _filter_and_hydrate_notifications( + manifest["notifications"]["PRE_UPGRADE"], + app["current_version"], + app["settings"], + ) + del app["settings"] + shutil.rmtree(extracted_app_folder) + + return upgradable_apps + + # # ############################### # # Small utilities # @@ -2177,48 +2698,128 @@ def _get_all_installed_apps_id(): return all_apps_ids_formatted -def _check_manifest_requirements(manifest: Dict): +def _check_manifest_requirements( + manifest: Dict, action: str = "" +) -> Iterator[Tuple[str, bool, object, str]]: """Check if required packages are met from the manifest""" - packaging_format = int(manifest.get("packaging_format", 0)) - if packaging_format not in [0, 1]: + app_id = manifest["id"] + logger.debug(m18n.n("app_requirements_checking", app=app_id)) + + # Packaging format + if manifest["packaging_format"] not in [1, 2]: raise YunohostValidationError("app_packaging_format_not_supported") - requirements = manifest.get("requirements", dict()) + # Yunohost version + required_yunohost_version = ( + manifest["integration"].get("yunohost", "4.3").strip(">= ") + ) + current_yunohost_version = get_ynh_package_version("yunohost")["version"] - if not requirements: - return + yield ( + "required_yunohost_version", + version.parse(required_yunohost_version) + <= version.parse(current_yunohost_version), + {"current": current_yunohost_version, "required": required_yunohost_version}, + "app_yunohost_version_not_supported", # i18n: app_yunohost_version_not_supported + ) - app = manifest.get("id", "?") + # Architectures + arch_requirement = manifest["integration"]["architectures"] + arch = system_arch() - logger.debug(m18n.n("app_requirements_checking", app=app)) + yield ( + "arch", + arch_requirement in ["all", "?"] or arch in arch_requirement, + {"current": arch, "required": ", ".join(arch_requirement)}, + "app_arch_not_supported", # i18n: app_arch_not_supported + ) - # Iterate over requirements - for pkgname, spec in requirements.items(): - if not packages.meets_version_specifier(pkgname, spec): - version = packages.ynh_packages_version()[pkgname]["version"] - raise YunohostValidationError( - "app_requirements_unmeet", - pkgname=pkgname, - version=version, - spec=spec, - app=app, + # Multi-instance + if action == "install": + multi_instance = manifest["integration"]["multi_instance"] is True + if not multi_instance: + apps = _installed_apps() + sibling_apps = [ + a for a in apps if a == app_id or a.startswith(f"{app_id}__") + ] + multi_instance = len(sibling_apps) == 0 + + yield ( + "install", + multi_instance, + {"app": app_id}, + "app_already_installed", # i18n: app_already_installed + ) + + # Disk + if action == "install": + root_free_space = free_space_in_directory("/") + var_free_space = free_space_in_directory("/var") + if manifest["integration"]["disk"] == "?": + has_enough_disk = True + else: + disk_req_bin = human_to_binary(manifest["integration"]["disk"]) + has_enough_disk = ( + root_free_space > disk_req_bin and var_free_space > disk_req_bin ) + free_space = binary_to_human(min(root_free_space, var_free_space)) + + yield ( + "disk", + has_enough_disk, + {"current": free_space, "required": manifest["integration"]["disk"]}, + "app_not_enough_disk", # i18n: app_not_enough_disk + ) + + # Ram + ram_requirement = manifest["integration"]["ram"] + ram, swap = ram_available() + # Is "include_swap" really useful ? We should probably decide wether to always include it or not instead + if ram_requirement.get("include_swap", False): + ram += swap + can_build = ram_requirement["build"] == "?" or ram > human_to_binary( + ram_requirement["build"] + ) + can_run = ram_requirement["runtime"] == "?" or ram > human_to_binary( + ram_requirement["runtime"] + ) + + # Some apps have a higher runtime value than build ... + if ram_requirement["build"] != "?" and ram_requirement["runtime"] != "?": + max_build_runtime = ( + ram_requirement["build"] + if human_to_binary(ram_requirement["build"]) + > human_to_binary(ram_requirement["runtime"]) + else ram_requirement["runtime"] + ) + else: + max_build_runtime = ram_requirement["build"] + + yield ( + "ram", + can_build and can_run, + {"current": binary_to_human(ram), "required": max_build_runtime}, + "app_not_enough_ram", # i18n: app_not_enough_ram + ) 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. manifest = _get_manifest_of_app(app_folder) - raw_questions = manifest.get("arguments", {}).get("install", {}) + raw_questions = manifest["install"] domain_questions = [ - question for question in raw_questions if question.get("type") == "domain" + question + for question in raw_questions.values() + if question.get("type") == "domain" ] path_questions = [ - question for question in raw_questions if question.get("type") == "path" + question + for question in raw_questions.values() + if question.get("type") == "path" ] if len(domain_questions) == 0 and len(path_questions) == 0: @@ -2226,22 +2827,33 @@ def _guess_webapp_path_requirement(app_folder: str) -> str: if len(domain_questions) == 1 and len(path_questions) == 1: return "domain_and_path" if len(domain_questions) == 1 and len(path_questions) == 0: - # This is likely to be a full-domain app... + if manifest.get("packaging_format", 0) < 2: + # This is likely to be a full-domain app... - # Confirm that this is a full-domain app This should cover most cases - # ... though anyway the proper solution is to implement some mechanism - # in the manifest for app to declare that they require a full domain - # (among other thing) so that we can dynamically check/display this - # requirement on the webadmin form and not miserably fail at submit time + # Confirm that this is a full-domain app This should cover most cases + # ... though anyway the proper solution is to implement some mechanism + # in the manifest for app to declare that they require a full domain + # (among other thing) so that we can dynamically check/display this + # requirement on the webadmin form and not miserably fail at submit time - # Full-domain apps typically declare something like path_url="/" or path=/ - # and use ynh_webpath_register or yunohost_app_checkurl inside the install script - install_script_content = read_file(os.path.join(app_folder, "scripts/install")) + # Full-domain apps typically declare something like path_url="/" or path=/ + # and use ynh_webpath_register or yunohost_app_checkurl inside the install script + install_script_content = read_file( + os.path.join(app_folder, "scripts/install") + ) - if re.search( - r"\npath(_url)?=[\"']?/[\"']?", install_script_content - ) and re.search(r"ynh_webpath_register", install_script_content): - return "full_domain" + if re.search( + r"\npath(_url)?=[\"']?/[\"']?", install_script_content + ) and re.search(r"ynh_webpath_register", install_script_content): + return "full_domain" + + else: + # For packaging v2 apps, check if there's a permission with url being a string + perm_resource = manifest.get("resources", {}).get("permissions") + if perm_resource is not None and isinstance( + perm_resource.get("main", {}).get("url"), str + ): + return "full_domain" return "?" @@ -2249,7 +2861,6 @@ def _guess_webapp_path_requirement(app_folder: str) -> str: def _validate_webpath_requirement( args: Dict[str, Any], path_requirement: str, ignore_app=None ) -> None: - domain = args.get("domain") path = args.get("path") @@ -2274,8 +2885,8 @@ def _get_conflicting_apps(domain, path, ignore_app=None): from yunohost.domain import _assert_domain_exists - domain = DomainQuestion.normalize(domain) - path = PathQuestion.normalize(path) + domain = DomainOption.normalize(domain) + path = WebPathOption.normalize(path) # Abort if domain is unknown _assert_domain_exists(domain) @@ -2290,18 +2901,13 @@ def _get_conflicting_apps(domain, path, ignore_app=None): for p, a in apps_map[domain].items(): if a["id"] == ignore_app: continue - if path == p: - conflicts.append((p, a["id"], a["label"])) - # We also don't want conflicts with other apps starting with - # same name - elif path.startswith(p) or p.startswith(path): + if path == p or path == "/" or p == "/": conflicts.append((p, a["id"], a["label"])) return conflicts def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False): - conflicts = _get_conflicting_apps(domain, path, ignore_app) if conflicts: @@ -2318,12 +2924,17 @@ def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False def _make_environment_for_app_script( - app, args={}, args_prefix="APP_ARG_", workdir=None + app, + args={}, + args_prefix="APP_ARG_", + workdir=None, + action=None, + force_include_app_settings=False, ): - app_setting_path = os.path.join(APPS_SETTING_PATH, app) - manifest = _get_manifest_of_app(app_setting_path) + manifest = _get_manifest_of_app(workdir if workdir else app_setting_path) + app_id, app_instance_nb = _parse_app_instance_name(app) env_dict = { @@ -2331,16 +2942,37 @@ def _make_environment_for_app_script( "YNH_APP_INSTANCE_NAME": app, "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), "YNH_APP_MANIFEST_VERSION": manifest.get("version", "?"), - "YNH_ARCH": check_output("dpkg --print-architecture"), + "YNH_APP_PACKAGING_FORMAT": str(manifest["packaging_format"]), + "YNH_ARCH": system_arch(), + "YNH_DEBIAN_VERSION": debian_version(), } if workdir: env_dict["YNH_APP_BASEDIR"] = workdir + if action: + env_dict["YNH_APP_ACTION"] = action + for arg_name, arg_value in args.items(): arg_name_upper = arg_name.upper() env_dict[f"YNH_{args_prefix}{arg_name_upper}"] = str(arg_value) + # If packaging format v2, load all settings + if manifest["packaging_format"] >= 2 or force_include_app_settings: + env_dict["app"] = app + for setting_name, setting_value in _get_app_settings(app).items(): + # Ignore special internal settings like checksum__ + # (not a huge deal to load them but idk...) + if setting_name.startswith("checksum__"): + continue + + env_dict[setting_name] = str(setting_value) + + # Special weird case for backward compatibility... + # 'path' was loaded into 'path_url' ..... + if "path" in env_dict: + env_dict["path_url"] = env_dict["path"] + return env_dict @@ -2373,7 +3005,6 @@ def _parse_app_instance_name(app_instance_name: str) -> Tuple[str, int]: def _next_instance_number_for_app(app): - # Get list of sibling apps, such as {app}, {app}__2, {app}__4 apps = _installed_apps() sibling_app_ids = [a for a in apps if a == app or a.startswith(f"{app}__")] @@ -2391,7 +3022,6 @@ def _next_instance_number_for_app(app): def _make_tmp_workdir_for_app(app=None): - # Create parent dir if it doesn't exists yet if not os.path.exists(APP_TMP_WORKDIRS): os.makedirs(APP_TMP_WORKDIRS) @@ -2420,33 +3050,11 @@ def _make_tmp_workdir_for_app(app=None): return tmpdir -def is_true(arg): - """ - Convert a string into a boolean - - Keyword arguments: - arg -- The string to convert - - Returns: - Boolean - - """ - if isinstance(arg, bool): - return arg - elif isinstance(arg, str): - return arg.lower() in ["yes", "true", "on"] - else: - logger.debug(f"arg should be a boolean or a string, got {arg}") - return True if arg else False - - def unstable_apps(): - output = [] - deprecated_apps = ["mailman"] + deprecated_apps = ["mailman", "ffsync"] for infos in app_list(full=True)["apps"]: - if ( not infos.get("from_catalog") or infos.get("from_catalog").get("state") @@ -2462,7 +3070,6 @@ def unstable_apps(): def _assert_system_is_sane_for_app(manifest, when): - from yunohost.service import service_status logger.debug("Checking that required services are up and running...") @@ -2517,8 +3124,116 @@ def _assert_system_is_sane_for_app(manifest, when): "app_action_broke_system", services=", ".join(faulty_services) ) - if packages.dpkg_is_broken(): + if dpkg_is_broken(): if when == "pre": raise YunohostValidationError("dpkg_is_broken") elif when == "post": raise YunohostError("this_action_broke_dpkg") + + +def app_dismiss_notification(app, name): + assert isinstance(name, str) + name = name.lower() + assert name in ["post_install", "post_upgrade"] + _assert_is_installed(app) + + app_setting(app, f"_dismiss_notification_{name}", value="1") + + +def _notification_is_dismissed(name, settings): + # Check for _dismiss_notiication_$name setting and also auto-dismiss + # notifications after one week (otherwise people using mostly CLI would + # never really dismiss the notification and it would be displayed forever) + + if name == "POST_INSTALL": + return ( + settings.get("_dismiss_notification_post_install") + or (int(time.time()) - settings.get("install_time", 0)) / (24 * 3600) > 7 + ) + elif name == "POST_UPGRADE": + # Check on update_time also implicitly prevent the post_upgrade notification + # from being displayed after install, because update_time is only set during upgrade + return ( + settings.get("_dismiss_notification_post_upgrade") + or (int(time.time()) - settings.get("update_time", 0)) / (24 * 3600) > 7 + ) + else: + return False + + +def _filter_and_hydrate_notifications(notifications, current_version=None, data={}): + def is_version_more_recent_than_current_version(name, current_version): + current_version = str(current_version) + # Boring code to handle the fact that "0.1 < 9999~ynh1" is False + + if "~" in name: + return version.parse(name) > version.parse(current_version) + else: + return version.parse(name) > version.parse(current_version.split("~")[0]) + + return { + # Should we render the markdown maybe? idk + name: _hydrate_app_template(_value_for_locale(content_per_lang), data) + for name, content_per_lang in notifications.items() + if current_version is None + or name == "main" + or is_version_more_recent_than_current_version(name, current_version) + } + + +def _display_notifications(notifications, force=False): + if not notifications: + return + + for name, content in notifications.items(): + print("==========") + print(content) + print("==========") + + # i18n: confirm_notifications_read + _ask_confirmation("confirm_notifications_read", kind="simple", force=force) + + +# FIXME: move this to Moulinette +def _ask_confirmation( + question: str, + params: dict = {}, + kind: str = "hard", + force: bool = False, +): + """ + Ask confirmation + + Keyword argument: + question -- m18n key or string + params -- dict of values passed to the string formating + kind -- "hard": ask with "Yes, I understand", "soft": "Y/N", "simple": "press enter" + force -- Will not ask for confirmation + + """ + if force or Moulinette.interface.type == "api": + return + + # If ran from the CLI in a non-interactive context, + # skip confirmation (except in hard mode) + if not os.isatty(1) and kind in ["simple", "soft"]: + return + if kind == "simple": + answer = Moulinette.prompt( + m18n.n(question, answers="Press enter to continue", **params), + color="yellow", + ) + answer = True + elif kind == "soft": + answer = Moulinette.prompt( + m18n.n(question, answers="Y/N", **params), color="yellow" + ) + answer = answer.upper() == "Y" + else: + answer = Moulinette.prompt( + m18n.n(question, answers="Yes, I understand", **params), color="red" + ) + answer = answer == "Yes, I understand" + + if not answer: + raise YunohostError("aborting") diff --git a/src/app_catalog.py b/src/app_catalog.py index 5ae8ef30b..9fb662845 100644 --- a/src/app_catalog.py +++ b/src/app_catalog.py @@ -1,5 +1,24 @@ +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re +import hashlib from moulinette import m18n from moulinette.utils.log import getActionLogger @@ -18,17 +37,18 @@ from yunohost.utils.error import YunohostError logger = getActionLogger("yunohost.app_catalog") APPS_CATALOG_CACHE = "/var/cache/yunohost/repo" +APPS_CATALOG_LOGOS = "/usr/share/yunohost/applogos" APPS_CATALOG_CONF = "/etc/yunohost/apps_catalog.yml" -APPS_CATALOG_API_VERSION = 2 +APPS_CATALOG_API_VERSION = 3 APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default" -def app_catalog(full=False, with_categories=False): +def app_catalog(full=False, with_categories=False, with_antifeatures=False): """ Return a dict of apps available to installation from Yunohost's app catalog """ - from yunohost.app import _installed_apps, _set_default_ask_questions + from yunohost.app import _installed_apps # Get app list from catalog cache catalog = _load_apps_catalog() @@ -47,28 +67,38 @@ def app_catalog(full=False, with_categories=False): "description": infos["manifest"]["description"], "level": infos["level"], } - else: - infos["manifest"]["arguments"] = _set_default_ask_questions( - infos["manifest"].get("arguments", {}) - ) - # Trim info for categories if not using --full - for category in catalog["categories"]: - category["title"] = _value_for_locale(category["title"]) - category["description"] = _value_for_locale(category["description"]) - for subtags in category.get("subtags", []): - subtags["title"] = _value_for_locale(subtags["title"]) + _catalog = {"apps": catalog["apps"]} - if not full: - catalog["categories"] = [ - {"id": c["id"], "description": c["description"]} - for c in catalog["categories"] - ] + if with_categories: + for category in catalog["categories"]: + category["title"] = _value_for_locale(category["title"]) + category["description"] = _value_for_locale(category["description"]) + for subtags in category.get("subtags", []): + subtags["title"] = _value_for_locale(subtags["title"]) - if not with_categories: - return {"apps": catalog["apps"]} - else: - return {"apps": catalog["apps"], "categories": catalog["categories"]} + if not full: + catalog["categories"] = [ + {"id": c["id"], "description": c["description"]} + for c in catalog["categories"] + ] + + _catalog["categories"] = catalog["categories"] + + if with_antifeatures: + for antifeature in catalog["antifeatures"]: + antifeature["title"] = _value_for_locale(antifeature["title"]) + antifeature["description"] = _value_for_locale(antifeature["description"]) + + if not full: + catalog["antifeatures"] = [ + {"id": a["id"], "description": a["description"]} + for a in catalog["antifeatures"] + ] + + _catalog["antifeatures"] = catalog["antifeatures"] + + return _catalog def app_search(string): @@ -127,7 +157,6 @@ def _read_apps_catalog_list(): def _actual_apps_catalog_api_url(base_url): - return f"{base_url}/v{APPS_CATALOG_API_VERSION}/apps.json" @@ -154,7 +183,13 @@ def _update_apps_catalog(): logger.debug("Initialize folder for apps catalog cache") mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid="root") + if not os.path.exists(APPS_CATALOG_LOGOS): + mkdir(APPS_CATALOG_LOGOS, mode=0o755, parents=True, uid="root") + for apps_catalog in apps_catalog_list: + if apps_catalog["url"] is None: + continue + apps_catalog_id = apps_catalog["id"] actual_api_url = _actual_apps_catalog_api_url(apps_catalog["url"]) @@ -181,6 +216,46 @@ def _update_apps_catalog(): raw_msg=True, ) + # Download missing app logos + logos_to_download = [] + for app, infos in apps_catalog_content["apps"].items(): + logo_hash = infos.get("logo_hash") + if not logo_hash or os.path.exists(f"{APPS_CATALOG_LOGOS}/{logo_hash}.png"): + continue + logos_to_download.append(logo_hash) + + if len(logos_to_download) > 20: + logger.info( + f"(Will fetch {len(logos_to_download)} logos, this may take a couple minutes)" + ) + + import requests + from multiprocessing.pool import ThreadPool + + def fetch_logo(logo_hash): + try: + r = requests.get( + f"{apps_catalog['url']}/v{APPS_CATALOG_API_VERSION}/logos/{logo_hash}.png", + timeout=10, + ) + assert ( + r.status_code == 200 + ), f"Got status code {r.status_code}, expected 200" + if hashlib.sha256(r.content).hexdigest() != logo_hash: + raise Exception( + f"Found inconsistent hash while downloading logo {logo_hash}" + ) + open(f"{APPS_CATALOG_LOGOS}/{logo_hash}.png", "wb").write(r.content) + return True + except Exception as e: + logger.debug(f"Failed to download logo {logo_hash} : {e}") + return False + + results = ThreadPool(8).imap_unordered(fetch_logo, logos_to_download) + for result in results: + # Is this even needed to iterate on the results ? + pass + logger.success(m18n.n("apps_catalog_update_success")) @@ -190,10 +265,9 @@ def _load_apps_catalog(): corresponding to all known apps and categories """ - merged_catalog = {"apps": {}, "categories": []} + merged_catalog = {"apps": {}, "categories": [], "antifeatures": []} for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]: - # Let's load the json from cache for this catalog cache_file = f"{APPS_CATALOG_CACHE}/{apps_catalog_id}.json" @@ -222,7 +296,6 @@ def _load_apps_catalog(): # Add apps from this catalog to the output for app, info in apps_catalog_content["apps"].items(): - # (N.B. : there's a small edge case where multiple apps catalog could be listing the same apps ... # in which case we keep only the first one found) if app in merged_catalog["apps"]: @@ -232,10 +305,17 @@ def _load_apps_catalog(): ) continue + if info.get("level") == "?": + info["level"] = -1 + + # FIXME: we may want to autoconvert all v0/v1 manifest to v2 here + # so that everything is consistent in terms of APIs, datastructure format etc info["repository"] = apps_catalog_id merged_catalog["apps"][app] = info - # Annnnd categories - merged_catalog["categories"] += apps_catalog_content["categories"] + # Annnnd categories + antifeatures + # (we use .get here, only because the dev catalog doesnt include the categories/antifeatures keys) + merged_catalog["categories"] += apps_catalog_content.get("categories", []) + merged_catalog["antifeatures"] += apps_catalog_content.get("antifeatures", []) return merged_catalog diff --git a/src/authenticators/ldap_admin.py b/src/authenticators/ldap_admin.py index 872dd3c8d..b1b550bc0 100644 --- a/src/authenticators/ldap_admin.py +++ b/src/authenticators/ldap_admin.py @@ -1,5 +1,21 @@ -# -*- coding: utf-8 -*- - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import logging import ldap @@ -11,37 +27,72 @@ from moulinette.authentication import BaseAuthenticator from moulinette.utils.text import random_ascii from yunohost.utils.error import YunohostError, YunohostAuthenticationError - -logger = logging.getLogger("yunohost.authenticators.ldap_admin") +from yunohost.utils.ldap import _get_ldap_interface session_secret = random_ascii() +logger = logging.getLogger("yunohost.authenticators.ldap_admin") + +LDAP_URI = "ldap://localhost:389" +ADMIN_GROUP = "cn=admins,ou=groups" +AUTH_DN = "uid={uid},ou=users,dc=yunohost,dc=org" class Authenticator(BaseAuthenticator): - name = "ldap_admin" def __init__(self, *args, **kwargs): - self.uri = "ldap://localhost:389" - self.basedn = "dc=yunohost,dc=org" - self.admindn = "cn=admin,dc=yunohost,dc=org" + pass def _authenticate_credentials(self, credentials=None): + try: + admins = ( + _get_ldap_interface() + .search(ADMIN_GROUP, attrs=["memberUid"])[0] + .get("memberUid", []) + ) + except ldap.SERVER_DOWN: + # ldap is down, attempt to restart it before really failing + logger.warning(m18n.n("ldap_server_is_down_restart_it")) + os.system("systemctl restart slapd") + time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted - # TODO : change authentication format - # to support another dn to support multi-admins + # Force-reset existing LDAP interface + from yunohost.utils import ldap as ldaputils + + ldaputils._ldap_interface = None + + try: + admins = ( + _get_ldap_interface() + .search(ADMIN_GROUP, attrs=["memberUid"])[0] + .get("memberUid", []) + ) + except ldap.SERVER_DOWN: + raise YunohostError("ldap_server_down") + + try: + uid, password = credentials.split(":", 1) + except ValueError: + raise YunohostError("invalid_credentials") + + # Here we're explicitly using set() which are handled as hash tables + # and should prevent timing attacks to find out the admin usernames? + if uid not in set(admins): + raise YunohostError("invalid_credentials") + + dn = AUTH_DN.format(uid=uid) def _reconnect(): con = ldap.ldapobject.ReconnectLDAPObject( - self.uri, retry_max=10, retry_delay=0.5 + LDAP_URI, retry_max=10, retry_delay=0.5 ) - con.simple_bind_s(self.admindn, credentials) + con.simple_bind_s(dn, password) return con try: con = _reconnect() except ldap.INVALID_CREDENTIALS: - raise YunohostError("invalid_password") + raise YunohostError("invalid_credentials") except ldap.SERVER_DOWN: # ldap is down, attempt to restart it before really failing logger.warning(m18n.n("ldap_server_is_down_restart_it")) @@ -61,9 +112,9 @@ class Authenticator(BaseAuthenticator): logger.warning("Error during ldap authentication process: %s", e) raise else: - if who != self.admindn: + if who != dn: raise YunohostError( - f"Not logged with the appropriate identity ? Found {who}, expected {self.admindn} !?", + f"Not logged with the appropriate identity ? Found {who}, expected {dn} !?", raw_msg=True, ) finally: @@ -72,7 +123,6 @@ class Authenticator(BaseAuthenticator): con.unbind_s() def set_session_cookie(self, infos): - from bottle import response assert isinstance(infos, dict) @@ -92,7 +142,6 @@ class Authenticator(BaseAuthenticator): ) def get_session_cookie(self, raise_if_no_session_exists=True): - from bottle import request try: @@ -121,7 +170,6 @@ class Authenticator(BaseAuthenticator): return infos def delete_session_cookie(self): - from bottle import response response.set_cookie("yunohost.admin", "", max_age=-1) diff --git a/src/backup.py b/src/backup.py index bba60b895..ce1e8ba2c 100644 --- a/src/backup.py +++ b/src/backup.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_backup.py - - Manage backups -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import json @@ -39,9 +32,17 @@ from functools import reduce from packaging import version from moulinette import Moulinette, m18n -from moulinette.utils import filesystem +from moulinette.utils.text import random_ascii from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_file, mkdir, write_to_yaml, read_yaml +from moulinette.utils.filesystem import ( + read_file, + mkdir, + write_to_yaml, + read_yaml, + rm, + chown, + chmod, +) from moulinette.utils.process import check_output import yunohost.domain @@ -50,6 +51,8 @@ from yunohost.app import ( _is_installed, _make_environment_for_app_script, _make_tmp_workdir_for_app, + _get_manifest_of_app, + app_remove, ) from yunohost.hook import ( hook_list, @@ -67,8 +70,12 @@ from yunohost.tools import ( from yunohost.regenconf import regen_conf from yunohost.log import OperationLogger, is_unit_operation from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.utils.packages import ynh_packages_version -from yunohost.utils.filesystem import free_space_in_directory +from yunohost.utils.system import ( + free_space_in_directory, + get_ynh_package_version, + binary_to_human, + space_used_by_directory, +) from yunohost.settings import settings_get BACKUP_PATH = "/home/yunohost.backup" @@ -88,7 +95,6 @@ class BackupRestoreTargetsManager: """ def __init__(self): - self.targets = {} self.results = {"system": {}, "apps": {}} @@ -312,7 +318,7 @@ class BackupManager: "size_details": self.size_details, "apps": self.apps_return, "system": self.system_return, - "from_yunohost_version": ynh_packages_version()["yunohost"]["version"], + "from_yunohost_version": get_ynh_package_version("yunohost")["version"], } @property @@ -342,9 +348,8 @@ class BackupManager: # FIXME replace isdir by exists ? manage better the case where the path # exists if not os.path.isdir(self.work_dir): - filesystem.mkdir(self.work_dir, 0o750, parents=True, uid="admin") + mkdir(self.work_dir, 0o750, parents=True) elif self.is_tmp_work_dir: - logger.debug( "temporary directory for backup '%s' already exists... attempting to clean it", self.work_dir, @@ -357,8 +362,8 @@ class BackupManager: # If umount succeeded, remove the directory (we checked that # we're in /home/yunohost.backup/tmp so that should be okay... # c.f. method clean() which also does this) - filesystem.rm(self.work_dir, recursive=True, force=True) - filesystem.mkdir(self.work_dir, 0o750, parents=True, uid="admin") + rm(self.work_dir, recursive=True, force=True) + mkdir(self.work_dir, 0o750, parents=True) # # Backup target management # @@ -535,7 +540,7 @@ class BackupManager: successfull_system = self.targets.list("system", include=["Success", "Warning"]) if not successfull_apps and not successfull_system: - filesystem.rm(self.work_dir, True, True) + rm(self.work_dir, True, True) raise YunohostError("backup_nothings_done") # Add unlisted files from backup tmp dir @@ -577,7 +582,7 @@ class BackupManager: env_var["YNH_BACKUP_CSV"] = tmp_csv if app is not None: - env_var.update(_make_environment_for_app_script(app)) + env_var.update(_make_environment_for_app_script(app, action="backup")) env_var["YNH_APP_BACKUP_DIR"] = os.path.join( self.work_dir, "apps", app, "backup" ) @@ -647,7 +652,7 @@ class BackupManager: restore_hooks_dir = os.path.join(self.work_dir, "hooks", "restore") if not os.path.exists(restore_hooks_dir): - filesystem.mkdir(restore_hooks_dir, mode=0o700, parents=True, uid="root") + mkdir(restore_hooks_dir, mode=0o700, parents=True, uid="root") restore_hooks = hook_list("restore")["hooks"] @@ -714,7 +719,7 @@ class BackupManager: tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) try: # Prepare backup directory for the app - filesystem.mkdir(tmp_app_bkp_dir, 0o700, True, uid="root") + mkdir(tmp_app_bkp_dir, 0o700, True, uid="root") # Copy the app settings to be able to call _common.sh shutil.copytree(app_setting_path, settings_dir) @@ -753,7 +758,7 @@ class BackupManager: # Remove tmp files in all situations finally: shutil.rmtree(tmp_workdir_for_app) - filesystem.rm(env_dict["YNH_BACKUP_CSV"], force=True) + rm(env_dict["YNH_BACKUP_CSV"], force=True) # # Actual backup archive creation / method management # @@ -796,7 +801,7 @@ class BackupManager: if row["dest"] == "info.json": continue - size = disk_usage(row["source"]) + size = space_used_by_directory(row["source"], follow_symlinks=False) # Add size to apps details splitted_dest = row["dest"].split("/") @@ -882,7 +887,6 @@ class RestoreManager: @property def success(self): - successful_apps = self.targets.list("apps", include=["Success", "Warning"]) successful_system = self.targets.list("system", include=["Success", "Warning"]) @@ -934,7 +938,17 @@ class RestoreManager: ) logger.debug("executing the post-install...") - tools_postinstall(domain, "Yunohost", True) + + # Use a dummy password which is not gonna be saved anywhere + # because the next thing to happen should be that a full restore of the LDAP db will happen + tools_postinstall( + domain, + "tmpadmin", + "Tmp Admin", + password=random_ascii(70), + ignore_dyndns=True, + overwrite_root_password=False, + ) def clean(self): """ @@ -949,7 +963,7 @@ class RestoreManager: ret = subprocess.call(["umount", self.work_dir]) if ret != 0: logger.warning(m18n.n("restore_cleaning_failed")) - filesystem.rm(self.work_dir, recursive=True, force=True) + rm(self.work_dir, recursive=True, force=True) # # Restore target manangement # @@ -979,7 +993,7 @@ class RestoreManager: available_restore_system_hooks = hook_list("restore")["hooks"] custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, "restore") - filesystem.mkdir(custom_restore_hook_folder, 755, parents=True, force=True) + mkdir(custom_restore_hook_folder, 755, parents=True, force=True) for system_part in target_list: # By default, we'll use the restore hooks on the current install @@ -1084,7 +1098,7 @@ class RestoreManager: else: raise YunohostError("restore_removing_tmp_dir_failed") - filesystem.mkdir(self.work_dir, parents=True) + mkdir(self.work_dir, parents=True) self.method.mount() @@ -1182,7 +1196,8 @@ class RestoreManager: self._restore_apps() except Exception as e: raise YunohostError( - f"The following critical error happened during restoration: {e}" + f"The following critical error happened during restoration: {e}", + raw_msg=True, ) finally: self.clean() @@ -1361,8 +1376,6 @@ class RestoreManager: from yunohost.user import user_group_list from yunohost.permission import ( permission_create, - permission_delete, - user_permission_list, permission_sync_to_user, ) @@ -1402,7 +1415,7 @@ class RestoreManager: # Delete _common.sh file in backup common_file = os.path.join(app_backup_in_archive, "_common.sh") - filesystem.rm(common_file, force=True) + rm(common_file, force=True) # Check if the app has a restore script app_restore_script_in_archive = os.path.join(app_scripts_in_archive, "restore") @@ -1418,14 +1431,14 @@ class RestoreManager: ) app_scripts_new_path = os.path.join(app_settings_new_path, "scripts") shutil.copytree(app_settings_in_archive, app_settings_new_path) - filesystem.chmod(app_settings_new_path, 0o400, 0o400, True) - filesystem.chown(app_scripts_new_path, "root", None, True) + chmod(app_settings_new_path, 0o400, 0o400, True) + chown(app_scripts_new_path, "root", None, True) # Copy the app scripts to a writable temporary folder tmp_workdir_for_app = _make_tmp_workdir_for_app() copytree(app_scripts_in_archive, tmp_workdir_for_app) - filesystem.chmod(tmp_workdir_for_app, 0o700, 0o700, True) - filesystem.chown(tmp_workdir_for_app, "root", None, True) + chmod(tmp_workdir_for_app, 0o700, 0o700, True) + chown(tmp_workdir_for_app, "root", None, True) restore_script = os.path.join(tmp_workdir_for_app, "restore") # Restore permissions @@ -1438,7 +1451,6 @@ class RestoreManager: existing_groups = user_group_list()["groups"] for permission_name, permission_infos in permissions.items(): - if "allowed" not in permission_infos: logger.warning( f"'allowed' key corresponding to allowed groups for permission {permission_name} not found when restoring app {app_instance_name} … You might have to reconfigure permissions yourself." @@ -1494,7 +1506,7 @@ class RestoreManager: # FIXME : workdir should be a tmp workdir app_workdir = os.path.join(self.work_dir, "apps", app_instance_name, "settings") env_dict = _make_environment_for_app_script( - app_instance_name, workdir=app_workdir + app_instance_name, workdir=app_workdir, action="restore" ) env_dict.update( { @@ -1509,6 +1521,16 @@ class RestoreManager: operation_logger.extra["env"] = env_dict operation_logger.flush() + manifest = _get_manifest_of_app(app_settings_in_archive) + if manifest["packaging_format"] >= 2: + from yunohost.utils.resources import AppResourceManager + + AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( + rollback_and_raise_exception_if_failure=True, + operation_logger=operation_logger, + action="restore", + ) + # Execute the app install script restore_failed = True try: @@ -1533,39 +1555,9 @@ class RestoreManager: self.targets.set_result("apps", app_instance_name, "Success") operation_logger.success() else: - self.targets.set_result("apps", app_instance_name, "Error") - remove_script = os.path.join(app_scripts_in_archive, "remove") - - # Setup environment for remove script - env_dict_remove = _make_environment_for_app_script( - app_instance_name, workdir=app_workdir - ) - remove_operation_logger = OperationLogger( - "remove_on_failed_restore", - [("app", app_instance_name)], - env=env_dict_remove, - ) - remove_operation_logger.start() - - # Execute remove script - if hook_exec(remove_script, env=env_dict_remove)[0] != 0: - msg = m18n.n("app_not_properly_removed", app=app_instance_name) - logger.warning(msg) - remove_operation_logger.error(msg) - else: - remove_operation_logger.success() - - # Cleaning app directory - shutil.rmtree(app_settings_new_path, ignore_errors=True) - - # Remove all permission in LDAP for this app - for permission_name in user_permission_list()["permissions"].keys(): - if permission_name.startswith(app_instance_name + "."): - permission_delete(permission_name, force=True) - - # TODO Cleaning app hooks + app_remove(app_instance_name, force_workdir=app_workdir) logger.error(failure_message_with_debug_instructions) @@ -1727,7 +1719,7 @@ class BackupMethod: raise YunohostError("backup_cleaning_failed") if self.manager.is_tmp_work_dir: - filesystem.rm(self.work_dir, True, True) + rm(self.work_dir, True, True) def _check_is_enough_free_space(self): """ @@ -1775,11 +1767,11 @@ class BackupMethod: # Be sure the parent dir of destination exists if not os.path.isdir(dest_dir): - filesystem.mkdir(dest_dir, parents=True) + mkdir(dest_dir, parents=True) # For directory, attempt to mount bind if os.path.isdir(src): - filesystem.mkdir(dest, parents=True, force=True) + mkdir(dest, parents=True, force=True) try: subprocess.check_call(["mount", "--rbind", src, dest]) @@ -1832,7 +1824,10 @@ class BackupMethod: # to mounting error # Compute size to copy - size = sum(disk_usage(path["source"]) for path in paths_needed_to_be_copied) + size = sum( + space_used_by_directory(path["source"], follow_symlinks=False) + for path in paths_needed_to_be_copied + ) size /= 1024 * 1024 # Convert bytes to megabytes # Ask confirmation for copying @@ -1884,7 +1879,7 @@ class CopyBackupMethod(BackupMethod): dest_parent = os.path.dirname(dest) if not os.path.exists(dest_parent): - filesystem.mkdir(dest_parent, 0o700, True, uid="admin") + mkdir(dest_parent, 0o700, True) if os.path.isdir(source): shutil.copytree(source, dest) @@ -1902,7 +1897,7 @@ class CopyBackupMethod(BackupMethod): if not os.path.isdir(self.repo): raise YunohostError("backup_no_uncompress_archive_dir") - filesystem.mkdir(self.work_dir, parent=True) + mkdir(self.work_dir, parent=True) ret = subprocess.call(["mount", "-r", "--rbind", self.repo, self.work_dir]) if ret == 0: return @@ -1921,14 +1916,12 @@ class CopyBackupMethod(BackupMethod): class TarBackupMethod(BackupMethod): - method_name = "tar" @property def _archive_file(self): - if isinstance(self.manager, BackupManager) and settings_get( - "backup.compress_tar_archives" + "misc.backup.backup_compress_tar_archives" ): return os.path.join(self.repo, self.name + ".tar.gz") @@ -1946,7 +1939,7 @@ class TarBackupMethod(BackupMethod): """ if not os.path.exists(self.repo): - filesystem.mkdir(self.repo, 0o750, parents=True, uid="admin") + mkdir(self.repo, 0o750, parents=True) # Check free space in output self._check_is_enough_free_space() @@ -2285,7 +2278,7 @@ def backup_create( ) backup_manager.backup() - logger.success(m18n.n("backup_created")) + logger.success(m18n.n("backup_created", name=backup_manager.name)) operation_logger.success() return { @@ -2383,6 +2376,7 @@ def backup_list(with_info=False, human_readable=False): # (we do a realpath() to resolve symlinks) archives = glob(f"{ARCHIVES_PATH}/*.tar.gz") + glob(f"{ARCHIVES_PATH}/*.tar") archives = {os.path.realpath(archive) for archive in archives} + archives = {archive for archive in archives if os.path.exists(archive)} archives = sorted(archives, key=lambda x: os.path.getctime(x)) # Extract only filename without the extension @@ -2413,7 +2407,6 @@ def backup_list(with_info=False, human_readable=False): def backup_download(name): - if Moulinette.interface.type != "api": logger.error( "This option is only meant for the API/webadmin and doesn't make sense for the command line." @@ -2554,7 +2547,6 @@ def backup_info(name, with_details=False, human_readable=False): if "size_details" in info.keys(): for category in ["apps", "system"]: for name, key_info in info[category].items(): - if category == "system": # Stupid legacy fix for weird format between 3.5 and 3.6 if isinstance(key_info, dict): @@ -2614,7 +2606,7 @@ def backup_delete(name): hook_callback("post_backup_delete", args=[name]) - logger.success(m18n.n("backup_deleted")) + logger.success(m18n.n("backup_deleted", name=name)) # @@ -2628,9 +2620,9 @@ def _create_archive_dir(): if os.path.lexists(ARCHIVES_PATH): raise YunohostError("backup_output_symlink_dir_broken", path=ARCHIVES_PATH) - # Create the archive folder, with 'admin' as owner, such that + # Create the archive folder, with 'admins' as groupowner, such that # people can scp archives out of the server - mkdir(ARCHIVES_PATH, mode=0o750, parents=True, uid="admin", gid="root") + mkdir(ARCHIVES_PATH, mode=0o770, parents=True, gid="admins") def _call_for_each_path(self, callback, csv_path=None): @@ -2667,31 +2659,3 @@ def _recursive_umount(directory): continue return everything_went_fine - - -def disk_usage(path): - # We don't do this in python with os.stat because we don't want - # to follow symlinks - - du_output = check_output(["du", "-sb", path], shell=False) - return int(du_output.split()[0]) - - -def binary_to_human(n, customary=False): - """ - Convert bytes or bits into human readable format with binary prefix - Keyword argument: - n -- Number to convert - customary -- Use customary symbol instead of IEC standard - """ - symbols = ("Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi") - if customary: - symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") - prefix = {} - for i, s in enumerate(symbols): - prefix[s] = 1 << (i + 1) * 10 - for s in reversed(symbols): - if n >= prefix[s]: - value = float(n) / prefix[s] - return "{:.1f}{}".format(value, s) - return "%s" % n diff --git a/src/certificate.py b/src/certificate.py index 2a9fb4ce9..76d3f32b7 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -1,40 +1,33 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2016 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - - yunohost_certificate.py - - Manage certificates, in particular Let's encrypt -""" - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import sys import shutil -import pwd -import grp import subprocess -import glob +from glob import glob from datetime import datetime from moulinette import m18n from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_file +from moulinette.utils.filesystem import read_file, chown, chmod +from moulinette.utils.process import check_output from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate from yunohost.utils.error import YunohostError, YunohostValidationError @@ -48,8 +41,8 @@ from yunohost.log import OperationLogger logger = getActionLogger("yunohost.certmanager") CERT_FOLDER = "/etc/yunohost/certs/" -TMP_FOLDER = "/tmp/acme-challenge-private/" -WEBROOT_FOLDER = "/tmp/acme-challenge-public/" +TMP_FOLDER = "/var/www/.well-known/acme-challenge-private/" +WEBROOT_FOLDER = "/var/www/.well-known/acme-challenge-public/" SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem" ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem" @@ -95,15 +88,16 @@ def certificate_status(domains, full=False): if not full: del status["subject"] del status["CA_name"] - status["CA_type"] = status["CA_type"]["verbose"] - status["summary"] = status["summary"]["verbose"] if full: try: _check_domain_is_ready_for_ACME(domain) status["ACME_eligible"] = True - except Exception: - status["ACME_eligible"] = False + except Exception as e: + if e.key == "certmanager_domain_not_diagnosed_yet": + status["ACME_eligible"] = None # = unknown status + else: + status["ACME_eligible"] = False del status["domain"] certificates[domain] = status @@ -130,9 +124,8 @@ def certificate_install(domain_list, force=False, no_checks=False, self_signed=F def _certificate_install_selfsigned(domain_list, force=False): - + failed_cert_install = [] for domain in domain_list: - operation_logger = OperationLogger( "selfsigned_cert_install", [("domain", domain)], args={"force": force} ) @@ -154,7 +147,7 @@ def _certificate_install_selfsigned(domain_list, force=False): if not force and os.path.isfile(current_cert_file): status = _get_status(domain) - if status["summary"]["code"] in ("good", "great"): + if status["style"] == "success": raise YunohostValidationError( "certmanager_attempt_to_replace_valid_cert", domain=domain ) @@ -214,20 +207,24 @@ def _certificate_install_selfsigned(domain_list, force=False): # Check new status indicate a recently created self-signed certificate status = _get_status(domain) - if ( - status - and status["CA_type"]["code"] == "self-signed" - and status["validity"] > 3648 - ): + if status and status["CA_type"] == "selfsigned" and status["validity"] > 3648: logger.success( m18n.n("certmanager_cert_install_success_selfsigned", domain=domain) ) operation_logger.success() else: msg = f"Installation of self-signed certificate installation for {domain} failed !" + failed_cert_install.append(domain) logger.error(msg) + logger.error(status) operation_logger.error(msg) + if failed_cert_install: + raise YunohostError( + "certmanager_cert_install_failed_selfsigned", + domains=",".join(failed_cert_install), + ) + def _certificate_install_letsencrypt(domains, force=False, no_checks=False): from yunohost.domain import domain_list, _assert_domain_exists @@ -239,9 +236,8 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): # certificates if domains == []: for domain in domain_list()["domains"]: - status = _get_status(domain) - if status["CA_type"]["code"] != "self-signed": + if status["CA_type"] != "selfsigned": continue domains.append(domain) @@ -253,14 +249,14 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): # Is it self-signed? status = _get_status(domain) - if not force and status["CA_type"]["code"] != "self-signed": + if not force and status["CA_type"] != "selfsigned": raise YunohostValidationError( "certmanager_domain_cert_not_selfsigned", domain=domain ) # Actual install steps + failed_cert_install = [] for domain in domains: - if not no_checks: try: _check_domain_is_ready_for_ACME(domain) @@ -287,11 +283,17 @@ def _certificate_install_letsencrypt(domains, force=False, no_checks=False): logger.error( f"Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain {domain}." ) + failed_cert_install.append(domain) else: logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) operation_logger.success() + if failed_cert_install: + raise YunohostError( + "certmanager_cert_install_failed", domains=",".join(failed_cert_install) + ) + def certificate_renew(domains, force=False, no_checks=False, email=False): """ @@ -311,10 +313,9 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): # certificates if domains == []: for domain in domain_list()["domains"]: - # Does it have a Let's Encrypt cert? status = _get_status(domain) - if status["CA_type"]["code"] != "lets-encrypt": + if status["CA_type"] != "letsencrypt": continue # Does it expire soon? @@ -336,7 +337,6 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): # Else, validate the domain list given else: for domain in domains: - # Is it in Yunohost domain list? _assert_domain_exists(domain) @@ -349,7 +349,7 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): ) # Does it have a Let's Encrypt cert? - if status["CA_type"]["code"] != "lets-encrypt": + if status["CA_type"] != "letsencrypt": raise YunohostValidationError( "certmanager_attempt_to_renew_nonLE_cert", domain=domain ) @@ -361,8 +361,8 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): ) # Actual renew steps + failed_cert_install = [] for domain in domains: - if not no_checks: try: _check_domain_is_ready_for_ACME(domain) @@ -402,6 +402,8 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): logger.error(stack.getvalue()) logger.error(str(e)) + failed_cert_install.append(domain) + if email: logger.error("Sending email with details to root ...") _email_renewing_failed(domain, msg + "\n" + str(e), stack.getvalue()) @@ -409,6 +411,11 @@ def certificate_renew(domains, force=False, no_checks=False, email=False): logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) operation_logger.success() + if failed_cert_install: + raise YunohostError( + "certmanager_cert_renew_failed", domains=",".join(failed_cert_install) + ) + # # Back-end stuff # @@ -441,21 +448,24 @@ investigate : -- Certificate Manager """ - import smtplib + try: + import smtplib - smtp = smtplib.SMTP("localhost") - smtp.sendmail(from_, [to_], message.encode("utf-8")) - smtp.quit() + smtp = smtplib.SMTP("localhost") + smtp.sendmail(from_, [to_], message.encode("utf-8")) + smtp.quit() + except Exception as e: + # Dont miserably crash the whole auto renew cert when one renewal fails ... + # cf boring cases like https://github.com/YunoHost/issues/issues/2102 + logger.exception(f"Failed to send mail about cert renewal failure ... : {e}") def _check_acme_challenge_configuration(domain): - domain_conf = f"/etc/nginx/conf.d/{domain}.conf" return "include /etc/nginx/conf.d/acme-challenge.conf.inc" in read_file(domain_conf) def _fetch_and_enable_new_certificate(domain, no_checks=False): - if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() @@ -537,9 +547,9 @@ def _fetch_and_enable_new_certificate(domain, no_checks=False): _enable_certificate(domain, new_cert_folder) # Check the status of the certificate is now good - status_summary = _get_status(domain)["summary"] + status_style = _get_status(domain)["style"] - if status_summary["code"] != "great": + if status_style != "success": raise YunohostError( "certmanager_certificate_fetching_or_enabling_failed", domain=domain ) @@ -554,10 +564,11 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Set the domain csr.get_subject().CN = domain - from yunohost.domain import domain_list + from yunohost.domain import domain_config_get - # For "parent" domains, include xmpp-upload subdomain in subject alternate names - if domain in domain_list(exclude_subdomains=True)["domains"]: + # If XMPP is enabled for this domain, add xmpp-upload and muc subdomains + # in subject alternate names + if domain_config_get(domain, key="feature.xmpp.xmpp") == 1: subdomain = "xmpp-upload." + domain xmpp_records = ( Diagnoser.get_cached_report( @@ -565,24 +576,30 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): ).get("data") or {} ) - if xmpp_records.get("CNAME:xmpp-upload") == "OK": + sanlist = [] + for sub in ("xmpp-upload", "muc"): + subdomain = sub + "." + domain + if xmpp_records.get("CNAME:" + sub) == "OK": + sanlist.append(("DNS:" + subdomain)) + else: + logger.warning( + m18n.n( + "certmanager_warning_subdomain_dns_record", + subdomain=subdomain, + domain=domain, + ) + ) + + if sanlist: csr.add_extensions( [ crypto.X509Extension( b"subjectAltName", False, - ("DNS:" + subdomain).encode("utf8"), + (", ".join(sanlist)).encode("utf-8"), ) ] ) - else: - logger.warning( - m18n.n( - "certmanager_warning_subdomain_dns_record", - subdomain=subdomain, - domain=domain, - ) - ) # Set the key with open(key_file, "rt") as f: @@ -602,9 +619,6 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): - - import yunohost.domain - cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): @@ -633,59 +647,34 @@ def _get_status(domain): ) days_remaining = (valid_up_to - datetime.utcnow()).days - if cert_issuer in ["yunohost.org"] + yunohost.domain.domain_list()["domains"]: - CA_type = { - "code": "self-signed", - "verbose": "Self-signed", - } - + # Identify that a domain's cert is self-signed if the cert dir + # is actually a symlink to a dir ending with -selfsigned + if os.path.realpath(os.path.join(CERT_FOLDER, domain)).endswith("-selfsigned"): + CA_type = "selfsigned" elif organization_name == "Let's Encrypt": - CA_type = { - "code": "lets-encrypt", - "verbose": "Let's Encrypt", - } - + CA_type = "letsencrypt" else: - CA_type = { - "code": "other-unknown", - "verbose": "Other / Unknown", - } + CA_type = "other" if days_remaining <= 0: - status_summary = { - "code": "critical", - "verbose": "CRITICAL", - } - - elif CA_type["code"] in ("self-signed", "fake-lets-encrypt"): - status_summary = { - "code": "warning", - "verbose": "WARNING", - } - + style = "danger" + summary = "expired" + elif CA_type == "selfsigned": + style = "warning" + summary = "selfsigned" elif days_remaining < VALIDITY_LIMIT: - status_summary = { - "code": "attention", - "verbose": "About to expire", - } - - elif CA_type["code"] == "other-unknown": - status_summary = { - "code": "good", - "verbose": "Good", - } - - elif CA_type["code"] == "lets-encrypt": - status_summary = { - "code": "great", - "verbose": "Great!", - } - + style = "warning" + summary = "abouttoexpire" + elif CA_type == "other": + style = "success" + summary = "ok" + elif CA_type == "letsencrypt": + style = "success" + summary = "letsencrypt" else: - status_summary = { - "code": "unknown", - "verbose": "Unknown?", - } + # shouldnt happen, because CA_type can be only selfsigned, letsencrypt, or other + style = "" + summary = "wat" return { "domain": domain, @@ -693,7 +682,8 @@ def _get_status(domain): "CA_name": cert_issuer, "CA_type": CA_type, "validity": days_remaining, - "summary": status_summary, + "style": style, + "summary": summary, } @@ -719,11 +709,8 @@ def _generate_key(destination_path): def _set_permissions(path, user, group, permissions): - uid = pwd.getpwnam(user).pw_uid - gid = grp.getgrnam(group).gr_gid - - os.chown(path, uid, gid) - os.chmod(path, permissions) + chown(path, user, group) + chmod(path, permissions) def _enable_certificate(domain, new_cert_folder): @@ -746,11 +733,11 @@ 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 + for service in ("dovecot", "metronome"): + # Ugly trick to not restart metronome if it's not installed or no domain configured for XMPP + if service == "metronome" and ( + os.system("dpkg --list | grep -q 'ii *metronome'") != 0 + or not glob("/etc/metronome/conf.d/*.cfg.lua") ): continue _run_service_command("restart", service) @@ -758,7 +745,8 @@ def _enable_certificate(domain, new_cert_folder): if os.path.isfile("/etc/yunohost/installed"): # regen nginx conf to be sure it integrates OCSP Stapling # (We don't do this yet if postinstall is not finished yet) - regen_conf(names=["nginx"]) + # We also regenconf for postfix to propagate the SNI hash map thingy + regen_conf(names=["nginx", "postfix"]) _run_service_command("reload", "nginx") @@ -779,7 +767,6 @@ def _backup_current_cert(domain): def _check_domain_is_ready_for_ACME(domain): - from yunohost.domain import _get_parent_domain_of from yunohost.dns import _get_dns_zone_for_domain from yunohost.utils.dns import is_yunohost_dyndns_domain @@ -791,7 +778,7 @@ def _check_domain_is_ready_for_ACME(domain): or {} ) - parent_domain = _get_parent_domain_of(domain) + parent_domain = _get_parent_domain_of(domain, return_self=True) dnsrecords = ( Diagnoser.get_cached_report( @@ -866,9 +853,8 @@ def _regen_dnsmasq_if_needed(): do_regen = False # For all domain files in DNSmasq conf... - domainsconf = glob.glob("/etc/dnsmasq.d/*.*") + domainsconf = glob("/etc/dnsmasq.d/*.*") for domainconf in domainsconf: - # Look for the IP, it's in the lines with this format : # host-record=the.domain.tld,11.22.33.44 for line in open(domainconf).readlines(): @@ -908,6 +894,4 @@ def _name_self_CA(): def _tail(n, file_path): - from moulinette.utils.process import check_output - return check_output(f"tail -n {n} '{file_path}'") diff --git a/src/diagnosers/00-basesystem.py b/src/diagnosers/00-basesystem.py index a36394ce8..336271bd1 100644 --- a/src/diagnosers/00-basesystem.py +++ b/src/diagnosers/00-basesystem.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import json import subprocess @@ -9,28 +25,27 @@ from moulinette.utils import log from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_file, read_json, write_to_json from yunohost.diagnosis import Diagnoser -from yunohost.utils.packages import ynh_packages_version +from yunohost.utils.system import ( + ynh_packages_version, + system_virt, + system_arch, +) logger = log.getActionLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = [] def run(self): - - # Detect virt technology (if not bare metal) and arch - # Gotta have this "|| true" because it systemd-detect-virt return 'none' - # with an error code on bare metal ~.~ - virt = check_output("systemd-detect-virt || true", shell=True) + virt = system_virt() if virt.lower() == "none": virt = "bare-metal" # Detect arch - arch = check_output("dpkg --print-architecture") + arch = system_arch() hardware = dict( meta={"test": "hardware"}, status="INFO", @@ -137,6 +152,37 @@ class MyDiagnoser(Diagnoser): summary="diagnosis_backports_in_sources_list", ) + # Using yunohost testing channel + if ( + os.system( + "grep -q '^\\s*deb\\s*.*yunohost.org.*\\stesting' /etc/apt/sources.list /etc/apt/sources.list.d/*" + ) + == 0 + ): + yield dict( + meta={"test": "apt_yunohost_channel"}, + status="WARNING", + summary="diagnosis_using_yunohost_testing", + details=["diagnosis_using_yunohost_testing_details"], + ) + + # Apt being mapped to 'stable' (instead of 'buster/bullseye/bookworm/trixie/...') + # will cause the machine to spontaenously upgrade everything as soon as next debian is released ... + # Note that we grep this from the policy for libc6, because it's hard to know exactly which apt repo + # is configured (it may not be simply debian.org) + if ( + os.system( + "apt policy libc6 2>/dev/null | grep '^\\s*500' | awk '{print $3}' | tr '/' ' ' | awk '{print $1}' | grep -q 'stable'" + ) + == 0 + ): + yield dict( + meta={"test": "apt_debian_codename"}, + status="WARNING", + summary="diagnosis_using_stable_codename", + details=["diagnosis_using_stable_codename_details"], + ) + if self.number_of_recent_auth_failure() > 750: yield dict( meta={"test": "high_number_auth_failure"}, @@ -145,7 +191,6 @@ class MyDiagnoser(Diagnoser): ) def bad_sury_packages(self): - packages_to_check = ["openssl", "libssl1.1", "libssl-dev"] for package in packages_to_check: cmd = "dpkg --list | grep '^ii' | grep gbp | grep -q -w %s" % package @@ -161,12 +206,10 @@ class MyDiagnoser(Diagnoser): yield (package, version_to_downgrade_to) def backports_in_sources_list(self): - cmd = "grep -q -nr '^ *deb .*-backports' /etc/apt/sources.list*" return os.system(cmd) == 0 def number_of_recent_auth_failure(self): - # Those syslog facilities correspond to auth and authpriv # c.f. https://unix.stackexchange.com/a/401398 # and https://wiki.archlinux.org/title/Systemd/Journal#Facility diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index 247c486fc..4f9cd9708 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os import random @@ -12,18 +28,17 @@ from moulinette.utils.filesystem import read_file from yunohost.diagnosis import Diagnoser from yunohost.utils.network import get_network_interfaces +from yunohost.settings import settings_get logger = log.getActionLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = [] def run(self): - # ############################################################ # # PING : Check that we can ping outside at least in ipv4 or v6 # # ############################################################ # @@ -102,10 +117,17 @@ class MyDiagnoser(Diagnoser): else: return local_ip + def is_ipvx_important(x): + return settings_get("misc.network.dns_exposure") in ["both", "ipv" + str(x)] + yield dict( meta={"test": "ipv4"}, data={"global": ipv4, "local": get_local_ip("ipv4")}, - status="SUCCESS" if ipv4 else "ERROR", + status="SUCCESS" + if ipv4 + else "ERROR" + if is_ipvx_important(4) + else "WARNING", summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None, ) @@ -113,17 +135,24 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv6"}, data={"global": ipv6, "local": get_local_ip("ipv6")}, - status="SUCCESS" if ipv6 else "WARNING", + status="SUCCESS" + if ipv6 + else "ERROR" + if settings_get("misc.network.dns_exposure") == "ipv6" + else "WARNING", summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv6 - else ["diagnosis_ip_no_ipv6_tip"], + else [ + "diagnosis_ip_no_ipv6_tip_important" + if is_ipvx_important(6) + else "diagnosis_ip_no_ipv6_tip" + ], ) # TODO / FIXME : add some attempt to detect ISP (using whois ?) ? def can_ping_outside(self, protocol=4): - assert protocol in [ 4, 6, @@ -202,7 +231,6 @@ class MyDiagnoser(Diagnoser): return len(content) == 1 and content[0].split() == ["nameserver", "127.0.0.1"] def get_public_ip(self, protocol=4): - # FIXME - TODO : here we assume that DNS resolution for ip.yunohost.org is working # but if we want to be able to diagnose DNS resolution issues independently from # internet connectivity, we gotta rely on fixed IPs first.... @@ -221,5 +249,5 @@ class MyDiagnoser(Diagnoser): except Exception as e: protocol = str(protocol) e = str(e) - self.logger_debug(f"Could not get public IPv{protocol} : {e}") + logger.debug(f"Could not get public IPv{protocol} : {e}") return None diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py index 4d30bb1a7..196a2e1f9 100644 --- a/src/diagnosers/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re from typing import List @@ -27,13 +43,11 @@ logger = log.getActionLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = ["ip"] def run(self): - main_domain = _get_maindomain() major_domains = domain_list(exclude_subdomains=True)["domains"] @@ -61,7 +75,6 @@ class MyDiagnoser(Diagnoser): yield report def check_domain(self, domain, is_main_domain): - if is_special_use_tld(domain): yield dict( meta={"domain": domain}, @@ -81,13 +94,11 @@ class MyDiagnoser(Diagnoser): categories = ["basic", "mail", "xmpp", "extra"] for category in categories: - records = expected_configuration[category] discrepancies = [] results = {} for r in records: - id_ = r["type"] + ":" + r["name"] fqdn = r["name"] + "." + base_dns_zone if r["name"] != "@" else domain @@ -105,7 +116,7 @@ class MyDiagnoser(Diagnoser): if r["value"] == "@": r["value"] = domain + "." elif r["type"] == "CNAME": - r["value"] = r["value"] + f".{base_dns_zone}." + r["value"] = r["value"] # + f".{base_dns_zone}." if self.current_record_match_expected(r): results[id_] = "OK" @@ -166,12 +177,15 @@ class MyDiagnoser(Diagnoser): yield output def get_current_record(self, fqdn, type_): - success, answers = dig(fqdn, type_, resolvers="force_external") if success != "ok": return None else: + if type_ == "TXT" and isinstance(answers, list): + for part in answers: + if part.startswith('"v=spf1'): + return part return answers[0] if len(answers) == 1 else answers def current_record_match_expected(self, r): @@ -207,6 +221,11 @@ class MyDiagnoser(Diagnoser): expected = r["value"].split()[-1] current = r["current"].split()[-1] return expected == current + elif r["type"] == "CAA": + # For CAA, check only the last item, ignore the 0 / 128 nightmare + expected = r["value"].split()[-1] + current = r["current"].split()[-1] + return expected == current else: return r["current"] == r["value"] diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index be172e524..34c512f14 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -1,20 +1,35 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os from typing import List from yunohost.diagnosis import Diagnoser from yunohost.service import _get_services +from yunohost.settings import settings_get class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = ["ip"] def run(self): - # TODO: report a warning if port 53 or 5353 is exposed to the outside world... # This dict is something like : @@ -30,7 +45,10 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS": + if ( + ipv4.get("status") == "SUCCESS" + or settings_get("misc.network.dns_exposure") != "ipv6" + ): ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -104,7 +122,10 @@ class MyDiagnoser(Diagnoser): for record in dnsrecords.get("items", []) ) - if failed == 4 or ipv6_is_important(): + if ( + failed == 4 + and settings_get("misc.network.dns_exposure") in ["both", "ipv4"] + ) or (failed == 6 and ipv6_is_important()): yield dict( meta={"port": port}, data={ diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index 584505ad1..cc6edd7dc 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -1,31 +1,45 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import random import requests from typing import List -from moulinette.utils.filesystem import read_file +from moulinette.utils.filesystem import read_file, mkdir, rm from yunohost.diagnosis import Diagnoser from yunohost.domain import domain_list from yunohost.utils.dns import is_special_use_tld +from yunohost.settings import settings_get DIAGNOSIS_SERVER = "diagnosis.yunohost.org" class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = ["ip"] def run(self): - all_domains = domain_list()["domains"] domains_to_check = [] for domain in all_domains: - # If the diagnosis location ain't defined, can't do diagnosis, # probably because nginx conf manually modified... nginx_conf = "/etc/nginx/conf.d/%s.conf" % domain @@ -46,9 +60,9 @@ class MyDiagnoser(Diagnoser): domains_to_check.append(domain) self.nonce = "".join(random.choice("0123456789abcedf") for i in range(16)) - os.system("rm -rf /tmp/.well-known/ynh-diagnosis/") - os.system("mkdir -p /tmp/.well-known/ynh-diagnosis/") - os.system("touch /tmp/.well-known/ynh-diagnosis/%s" % self.nonce) + rm("/var/www/.well-known/ynh-diagnosis/", recursive=True, force=True) + mkdir("/var/www/.well-known/ynh-diagnosis/", parents=True, mode=0o0775) + os.system("touch /var/www/.well-known/ynh-diagnosis/%s" % self.nonce) if not domains_to_check: return @@ -60,7 +74,9 @@ class MyDiagnoser(Diagnoser): ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS": + if ipv4.get("status") == "SUCCESS" and settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv4"]: ipversions.append(4) # To be discussed: we could also make this check dependent on the @@ -80,7 +96,10 @@ class MyDiagnoser(Diagnoser): # "curl --head the.global.ip" will simply timeout... if self.do_hairpinning_test: global_ipv4 = ipv4.get("data", {}).get("global", None) - if global_ipv4: + if global_ipv4 and settings_get("misc.network.dns_exposure") in [ + "both", + "ipv4", + ]: try: requests.head("http://" + global_ipv4, timeout=5) except requests.exceptions.Timeout: @@ -97,7 +116,6 @@ class MyDiagnoser(Diagnoser): pass def test_http(self, domains, ipversions): - results = {} for ipversion in ipversions: try: @@ -122,7 +140,6 @@ class MyDiagnoser(Diagnoser): return for domain in domains: - # i18n: diagnosis_http_bad_status_code # i18n: diagnosis_http_connection_error # i18n: diagnosis_http_timeout @@ -131,7 +148,10 @@ class MyDiagnoser(Diagnoser): if all( results[ipversion][domain]["status"] == "ok" for ipversion in ipversions ): - if 4 in ipversions: + if 4 in ipversions and settings_get("misc.network.dns_exposure") in [ + "both", + "ipv4", + ]: self.do_hairpinning_test = True yield dict( meta={"domain": domain}, @@ -169,7 +189,9 @@ class MyDiagnoser(Diagnoser): ) AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") - return AAAA_status in ["OK", "WRONG"] + return AAAA_status in ["OK", "WRONG"] or settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv6"] if failed == 4 or ipv6_is_important_for_this_domain(): yield dict( diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index 7fe7a08db..df14222a5 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import dns.resolver import re @@ -22,13 +38,11 @@ logger = log.getActionLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 600 dependencies: List[str] = ["ip"] def run(self): - self.ehlo_domain = _get_maindomain() self.mail_domains = domain_list()["domains"] self.ipversions, self.ips = self.get_ips_checked() @@ -263,7 +277,7 @@ class MyDiagnoser(Diagnoser): data={"error": str(e)}, status="ERROR", summary="diagnosis_mail_queue_unavailable", - details="diagnosis_mail_queue_unavailable_details", + details=["diagnosis_mail_queue_unavailable_details"], ) else: if pending_emails > 100: @@ -285,13 +299,17 @@ class MyDiagnoser(Diagnoser): outgoing_ipversions = [] outgoing_ips = [] ipv4 = Diagnoser.get_cached_report("ip", {"test": "ipv4"}) or {} - if ipv4.get("status") == "SUCCESS": + if ipv4.get("status") == "SUCCESS" and settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv4"]: outgoing_ipversions.append(4) global_ipv4 = ipv4.get("data", {}).get("global", {}) if global_ipv4: outgoing_ips.append(global_ipv4) - if settings_get("smtp.allow_ipv6"): + if settings_get("email.smtp.smtp_allow_ipv6") or settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv6"]: ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": outgoing_ipversions.append(6) diff --git a/src/diagnosers/30-services.py b/src/diagnosers/30-services.py index f09688911..42ea9d18f 100644 --- a/src/diagnosers/30-services.py +++ b/src/diagnosers/30-services.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os from typing import List @@ -8,17 +24,14 @@ from yunohost.service import service_status class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 dependencies: List[str] = [] def run(self): - all_result = service_status() for service, result in sorted(all_result.items()): - item = dict( meta={"service": service}, data={ diff --git a/src/diagnosers/50-systemresources.py b/src/diagnosers/50-systemresources.py index 6ac7f0ec4..096c3483f 100644 --- a/src/diagnosers/50-systemresources.py +++ b/src/diagnosers/50-systemresources.py @@ -1,4 +1,21 @@ -#!/usr/bin/env python +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import psutil import datetime @@ -11,13 +28,11 @@ from yunohost.diagnosis import Diagnoser class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 dependencies: List[str] = [] def run(self): - MB = 1024**2 GB = MB * 1024 @@ -172,7 +187,6 @@ class MyDiagnoser(Diagnoser): return [] def analyzed_kern_log(): - cmd = 'tail -n 10000 /var/log/kern.log | grep "oom_reaper: reaped process" || true' out = check_output(cmd) lines = out.split("\n") if out else [] diff --git a/src/diagnosers/70-regenconf.py b/src/diagnosers/70-regenconf.py index 591f883a4..65195aac5 100644 --- a/src/diagnosers/70-regenconf.py +++ b/src/diagnosers/70-regenconf.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re from typing import List @@ -11,13 +27,11 @@ from moulinette.utils.filesystem import read_file class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 dependencies: List[str] = [] def run(self): - regenconf_modified_files = list(self.manually_modified_files()) if not regenconf_modified_files: @@ -53,7 +67,7 @@ class MyDiagnoser(Diagnoser): ) # Check consistency between actual ssh port in sshd_config vs. setting - ssh_port_setting = settings_get("security.ssh.port") + ssh_port_setting = settings_get("security.ssh.ssh_port") ssh_port_line = re.findall( r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config") ) @@ -66,7 +80,6 @@ class MyDiagnoser(Diagnoser): ) def manually_modified_files(self): - for category, infos in _get_regenconf_infos().items(): for path, hash_ in infos["conffiles"].items(): if hash_ != _calculate_hash(path): diff --git a/src/diagnosers/80-apps.py b/src/diagnosers/80-apps.py index 56e45f831..93cefeaaf 100644 --- a/src/diagnosers/80-apps.py +++ b/src/diagnosers/80-apps.py @@ -1,5 +1,21 @@ -#!/usr/bin/env python - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os from typing import List @@ -9,13 +25,11 @@ from yunohost.diagnosis import Diagnoser class MyDiagnoser(Diagnoser): - id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] cache_duration = 300 dependencies: List[str] = [] def run(self): - apps = app_list(full=True)["apps"] for app in apps: app["issues"] = list(self.issues(app)) @@ -28,7 +42,6 @@ class MyDiagnoser(Diagnoser): ) else: for app in apps: - if not app["issues"]: continue @@ -46,16 +59,15 @@ class MyDiagnoser(Diagnoser): ) def issues(self, app): - # Check quality level in catalog if not app.get("from_catalog") or app["from_catalog"].get("state") != "working": - yield ("error", "diagnosis_apps_not_in_app_catalog") + yield ("warning", "diagnosis_apps_not_in_app_catalog") elif ( not isinstance(app["from_catalog"].get("level"), int) or app["from_catalog"]["level"] == 0 ): - yield ("error", "diagnosis_apps_broken") + yield ("warning", "diagnosis_apps_broken") elif app["from_catalog"]["level"] <= 4: yield ("warning", "diagnosis_apps_bad_quality") @@ -64,7 +76,9 @@ class MyDiagnoser(Diagnoser): yunohost_version_req = ( app["manifest"].get("requirements", {}).get("yunohost", "").strip(">= ") ) - if yunohost_version_req.startswith("2."): + if yunohost_version_req.startswith("2.") or yunohost_version_req.startswith( + "3." + ): yield ("error", "diagnosis_apps_outdated_ynh_requirement") deprecated_helpers = [ diff --git a/src/diagnosers/__init__.py b/src/diagnosers/__init__.py index e69de29bb..7c1e7b0cd 100644 --- a/src/diagnosers/__init__.py +++ b/src/diagnosers/__init__.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# diff --git a/src/diagnosis.py b/src/diagnosis.py index 007719dfc..02047c001 100644 --- a/src/diagnosis.py +++ b/src/diagnosis.py @@ -1,29 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" diagnosis.py - - Look for possible issues on the server -""" - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os import time @@ -53,7 +45,6 @@ def diagnosis_list(): def diagnosis_get(category, item): - # Get all the categories all_categories_names = _list_diagnosis_categories() @@ -77,7 +68,6 @@ def diagnosis_get(category, item): def diagnosis_show( categories=[], issues=False, full=False, share=False, human_readable=False ): - if not os.path.exists(DIAGNOSIS_CACHE): logger.warning(m18n.n("diagnosis_never_ran_yet")) return @@ -98,7 +88,6 @@ def diagnosis_show( # Fetch all reports all_reports = [] for category in categories: - try: report = Diagnoser.get_cached_report(category) except Exception as e: @@ -147,7 +136,6 @@ def diagnosis_show( def _dump_human_readable_reports(reports): - output = "" for report in reports: @@ -167,7 +155,6 @@ def _dump_human_readable_reports(reports): def diagnosis_run( categories=[], force=False, except_if_never_ran_yet=False, email=False ): - if (email or except_if_never_ran_yet) and not os.path.exists(DIAGNOSIS_CACHE): return @@ -271,7 +258,6 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): return {"ignore_filters": configuration.get("ignore_filters", {})} def validate_filter_criterias(filter_): - # Get all the categories all_categories_names = _list_diagnosis_categories() @@ -294,7 +280,6 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): return category, criterias if add_filter: - category, criterias = validate_filter_criterias(add_filter) # Fetch current issues for the requested category @@ -328,7 +313,6 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): return if remove_filter: - category, criterias = validate_filter_criterias(remove_filter) # Make sure the subdicts/lists exists @@ -402,12 +386,10 @@ def add_ignore_flag_to_issues(report): class Diagnoser: def __init__(self): - self.cache_file = Diagnoser.cache_file(self.id_) self.description = Diagnoser.get_description(self.id_) def cached_time_ago(self): - if not os.path.exists(self.cache_file): return 99999999 return time.time() - os.path.getmtime(self.cache_file) @@ -418,7 +400,6 @@ class Diagnoser: return write_to_json(self.cache_file, report) def diagnose(self, force=False): - if not force and self.cached_time_ago() < self.cache_duration: logger.debug(f"Cache still valid : {self.cache_file}") logger.info( @@ -556,7 +537,6 @@ class Diagnoser: @staticmethod def i18n(report, force_remove_html_tags=False): - # "Render" the strings with m18n.n # N.B. : we do those m18n.n right now instead of saving the already-translated report # because we can't be sure we'll redisplay the infos with the same locale as it @@ -566,7 +546,6 @@ class Diagnoser: report["description"] = Diagnoser.get_description(report["id"]) for item in report["items"]: - # For the summary and each details, we want to call # m18n() on the string, with the appropriate data for string # formatting which can come from : @@ -605,7 +584,6 @@ class Diagnoser: @staticmethod def remote_diagnosis(uri, data, ipversion, timeout=30): - # Lazy loading for performance import requests import socket @@ -654,7 +632,6 @@ class Diagnoser: def _list_diagnosis_categories(): - paths = glob.glob(os.path.dirname(__file__) + "/diagnosers/??-*.py") names = [ name.split("-")[-1] @@ -665,7 +642,6 @@ def _list_diagnosis_categories(): def _load_diagnoser(diagnoser_name): - logger.debug(f"Loading diagnoser {diagnoser_name}") paths = glob.glob(os.path.dirname(__file__) + f"/diagnosers/??-{diagnoser_name}.py") diff --git a/src/dns.py b/src/dns.py index c8bebed41..e3a26044c 100644 --- a/src/dns.py +++ b/src/dns.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_domain.py - - Manage domains -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import time @@ -35,16 +28,17 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, write_to_file, read_toml, mkdir from yunohost.domain import ( - domain_list, _assert_domain_exists, domain_config_get, _get_domain_settings, _set_domain_settings, _list_subdomains_of, + _get_parent_domain_of, ) from yunohost.utils.dns import dig, is_yunohost_dyndns_domain, is_special_use_tld from yunohost.utils.error import YunohostValidationError, YunohostError from yunohost.utils.network import get_public_ip +from yunohost.settings import settings_get from yunohost.log import is_unit_operation from yunohost.hook import hook_callback @@ -144,7 +138,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): {"type": "A", "name": "*", "value": "123.123.123.123", "ttl": 3600}, # if ipv6 available {"type": "AAAA", "name": "*", "value": "valid-ipv6", "ttl": 3600}, - {"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600}, + {"type": "CAA", "name": "@", "value": "0 issue \"letsencrypt.org\"", "ttl": 3600}, ], "example_of_a_custom_rule": [ {"type": "SRV", "name": "_matrix", "value": "domain.tld.", "ttl": 3600} @@ -175,7 +169,6 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): base_dns_zone = _get_dns_zone_for_domain(base_domain) for domain, settings in domains_settings.items(): - # Domain # Base DNS zone # Basename # Suffix # # ------------------ # ----------------- # --------- # -------- # # domain.tld # domain.tld # @ # # @@ -192,7 +185,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): ########################### # Basic ipv4/ipv6 records # ########################### - if ipv4: + if ipv4 and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: basic.append([basename, ttl, "A", ipv4]) if ipv6: @@ -235,10 +228,10 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): "SRV", f"0 5 5269 {domain}.", ], - [f"muc{suffix}", ttl, "CNAME", basename], - [f"pubsub{suffix}", ttl, "CNAME", basename], - [f"vjud{suffix}", ttl, "CNAME", basename], - [f"xmpp-upload{suffix}", ttl, "CNAME", basename], + [f"muc{suffix}", ttl, "CNAME", f"{domain}."], + [f"pubsub{suffix}", ttl, "CNAME", f"{domain}."], + [f"vjud{suffix}", ttl, "CNAME", f"{domain}."], + [f"xmpp-upload{suffix}", ttl, "CNAME", f"{domain}."], ] ######### @@ -247,7 +240,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): # Only recommend wildcard and CAA for the top level if domain == base_domain: - if ipv4: + if ipv4 and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: extra.append([f"*{suffix}", ttl, "A", ipv4]) if ipv6: @@ -255,7 +248,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): elif include_empty_AAAA_if_no_ipv6: extra.append([f"*{suffix}", ttl, "AAAA", None]) - extra.append([basename, ttl, "CAA", '128 issue "letsencrypt.org"']) + extra.append([basename, ttl, "CAA", '0 issue "letsencrypt.org"']) #################### # Standard records # @@ -446,8 +439,8 @@ def _get_dns_zone_for_domain(domain): # This is another strick to try to prevent this function from being # a bottleneck on system with 1 main domain + 10ish subdomains # when building the dns conf for the main domain (which will call domain_config_get, etc...) - parent_domain = domain.split(".", 1)[1] - if parent_domain in domain_list()["domains"]: + parent_domain = _get_parent_domain_of(domain) + if parent_domain: parent_cache_file = f"{cache_folder}/{parent_domain}" if ( os.path.exists(parent_cache_file) @@ -468,7 +461,6 @@ def _get_dns_zone_for_domain(domain): # We don't wan't to do A NS request on the tld for parent in parent_list[0:-1]: - # Check if there's a NS record for that domain answer = dig(parent, rdtype="NS", full_answers=True, resolvers="force_external") @@ -509,20 +501,22 @@ def _get_relative_name_for_dns_zone(domain, base_dns_zone): def _get_registrar_config_section(domain): - from lexicon.providers.auto import _relevant_provider_for_domain - registrar_infos = {} + registrar_infos = { + "name": m18n.n( + "registrar_infos" + ), # This is meant to name the config panel section, for proper display in the webadmin + } dns_zone = _get_dns_zone_for_domain(domain) # If parent domain exists in yunohost - parent_domain = domain.split(".", 1)[1] - if parent_domain in domain_list()["domains"]: - + parent_domain = _get_parent_domain_of(domain, topest=True) + if parent_domain: # Dirty hack to have a link on the webadmin if Moulinette.interface.type == "api": - parent_domain_link = f"[{parent_domain}](#/domains/{parent_domain}/config)" + parent_domain_link = f"[{parent_domain}](#/domains/{parent_domain}/dns)" else: parent_domain_link = parent_domain @@ -532,7 +526,7 @@ def _get_registrar_config_section(domain): "style": "info", "ask": m18n.n( "domain_dns_registrar_managed_in_parent_domain", - parent_domain=domain, + parent_domain=parent_domain, parent_domain_link=parent_domain_link, ), "value": "parent_domain", @@ -574,7 +568,6 @@ def _get_registrar_config_section(domain): } ) else: - registrar_infos["registrar"] = OrderedDict( { "type": "alert", @@ -598,7 +591,12 @@ def _get_registrar_config_section(domain): # TODO : add a help tip with the link to the registar's API doc (c.f. Lexicon's README) registrar_list = read_toml(DOMAIN_REGISTRAR_LIST_PATH) - registrar_credentials = registrar_list[registrar] + registrar_credentials = registrar_list.get(registrar) + if registrar_credentials is None: + logger.warning( + f"Registrar {registrar} unknown / Should be added to YunoHost's registrar_list.toml by the development team!" + ) + registrar_credentials = {} for credential, infos in registrar_credentials.items(): infos["default"] = infos.get("default", "") infos["optional"] = infos.get("optional", "False") @@ -608,7 +606,6 @@ def _get_registrar_config_section(domain): def _get_registar_settings(domain): - _assert_domain_exists(domain) settings = domain_config_get(domain, key="dns.registrar", export=True) @@ -647,7 +644,7 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= return {} if registrar == "parent_domain": - parent_domain = domain.split(".", 1)[1] + parent_domain = _get_parent_domain_of(domain, topest=True) registar, registrar_credentials = _get_registar_settings(parent_domain) if any(registrar_credentials.values()): raise YunohostValidationError( @@ -672,7 +669,6 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= wanted_records = [] for records in _build_dns_conf(domain).values(): for record in records: - # Make sure the name is a FQDN name = ( f"{record['name']}.{base_dns_zone}" @@ -747,7 +743,6 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= ] for record in current_records: - # Try to get rid of weird stuff like ".domain.tld" or "@.domain.tld" record["name"] = record["name"].strip("@").strip(".") @@ -797,7 +792,6 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= comparison[(record["type"], record["name"])]["wanted"].append(record) for type_and_name, records in comparison.items(): - # # Step 1 : compute a first "diff" where we remove records which are the same on both sides # @@ -941,9 +935,7 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= results = {"warnings": [], "errors": []} for action in ["delete", "create", "update"]: - for record in changes[action]: - relative_name = _get_relative_name_for_dns_zone( record["name"], base_dns_zone ) @@ -968,6 +960,9 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy." ) continue + elif registrar == "gandi": + if record["name"] == base_dns_zone: + record["name"] = "@." + record["name"] record["action"] = action query = ( @@ -1028,7 +1023,6 @@ def _set_managed_dns_records_hashes(domain: str, hashes: list) -> None: def _hash_dns_record(record: dict) -> int: - fields = ["name", "type", "content"] record_ = {f: record.get(f) for f in fields} diff --git a/src/domain.py b/src/domain.py index e40b4f03c..4f96d08c4 100644 --- a/src/domain.py +++ b/src/domain.py @@ -1,30 +1,25 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_domain.py - - Manage domains -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os -from typing import Dict, Any +import time +from typing import List, Optional +from collections import OrderedDict from moulinette import m18n, Moulinette from moulinette.core import MoulinetteError @@ -38,96 +33,181 @@ from yunohost.app import ( _get_conflicting_apps, ) from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf -from yunohost.utils.config import ConfigPanel, Question +from yunohost.utils.configpanel import ConfigPanel +from yunohost.utils.form import BaseOption 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] = {} +# The cache automatically expire every 15 seconds, to prevent desync between +# yunohost CLI and API which run in different processes +domain_list_cache: List[str] = [] +domain_list_cache_timestamp = 0 +main_domain_cache: Optional[str] = None +main_domain_cache_timestamp = 0 +DOMAIN_CACHE_DURATION = 15 -def domain_list(exclude_subdomains=False): +def _get_maindomain(): + global main_domain_cache + global main_domain_cache_timestamp + if ( + not main_domain_cache + or abs(main_domain_cache_timestamp - time.time()) > DOMAIN_CACHE_DURATION + ): + with open("/etc/yunohost/current_host", "r") as f: + main_domain_cache = f.readline().rstrip() + main_domain_cache_timestamp = time.time() + + return main_domain_cache + + +def _get_domains(exclude_subdomains=False): + global domain_list_cache + global domain_list_cache_timestamp + if ( + not domain_list_cache + or abs(domain_list_cache_timestamp - time.time()) > DOMAIN_CACHE_DURATION + ): + from yunohost.utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + result = [ + entry["virtualdomain"][0] + for entry in ldap.search("ou=domains", "virtualdomain=*", ["virtualdomain"]) + ] + + def cmp_domain(domain): + # Keep the main part of the domain and the extension together + # eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] + domain = domain.split(".") + domain[-1] = domain[-2] + domain.pop() + return list(reversed(domain)) + + domain_list_cache = sorted(result, key=cmp_domain) + domain_list_cache_timestamp = time.time() + + if exclude_subdomains: + return [ + domain for domain in domain_list_cache if not _get_parent_domain_of(domain) + ] + + return domain_list_cache + + +def domain_list(exclude_subdomains=False, tree=False, features=[]): """ List domains Keyword argument: exclude_subdomains -- Filter out domains that are subdomains of other declared domains + tree -- Display domains as a hierarchy tree """ - global domain_list_cache - if not exclude_subdomains and domain_list_cache: - return domain_list_cache - from yunohost.utils.ldap import _get_ldap_interface + domains = _get_domains(exclude_subdomains) + main = _get_maindomain() - ldap = _get_ldap_interface() - result = [ - entry["virtualdomain"][0] - for entry in ldap.search("ou=domains", "virtualdomain=*", ["virtualdomain"]) - ] + if features: + domains_filtered = [] + for domain in domains: + config = domain_config_get(domain, key="feature", export=True) + if any(config.get(feature) == 1 for feature in features): + domains_filtered.append(domain) + domains = domains_filtered - result_list = [] - for domain in result: - if exclude_subdomains: - parent_domain = domain.split(".", 1)[1] - if parent_domain in result: - continue + if not tree: + return {"domains": domains, "main": main} - result_list.append(domain) + if tree and exclude_subdomains: + return { + "domains": OrderedDict({domain: {} for domain in domains}), + "main": main, + } - def cmp_domain(domain): - # Keep the main part of the domain and the extension together - # eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] - domain = domain.split(".") - domain[-1] = domain[-2] + domain.pop() - domain = list(reversed(domain)) - return domain + def get_parent_dict(tree, child): + # If parent exists it should be the last added (see `_get_domains` ordering) + possible_parent = next(reversed(tree)) if tree else None + if possible_parent and child.endswith(f".{possible_parent}"): + return get_parent_dict(tree[possible_parent], child) + return tree - result_list = sorted(result_list, key=cmp_domain) + result = OrderedDict() + for domain in domains: + parent = get_parent_dict(result, domain) + parent[domain] = OrderedDict() - # Don't cache answer if using exclude_subdomains - if exclude_subdomains: - return {"domains": result_list, "main": _get_maindomain()} + return {"domains": result, "main": main} - domain_list_cache = {"domains": result_list, "main": _get_maindomain()} - return domain_list_cache + +def domain_info(domain): + """ + Print aggregate data for a specific domain + + Keyword argument: + domain -- Domain to be checked + """ + + from yunohost.app import app_info + from yunohost.dns import _get_registar_settings + + _assert_domain_exists(domain) + + registrar, _ = _get_registar_settings(domain) + certificate = domain_cert_status([domain], full=True)["certificates"][domain] + + apps = [] + for app in _installed_apps(): + settings = _get_app_settings(app) + if settings.get("domain") == domain: + apps.append( + { + "name": app_info(app)["name"], + "id": app, + "path": settings.get("path", ""), + } + ) + + return { + "certificate": certificate, + "registrar": registrar, + "apps": apps, + "main": _get_maindomain() == domain, + "topest_parent": _get_parent_domain_of(domain, topest=True), + # TODO : add parent / child domains ? + } def _assert_domain_exists(domain): - if domain not in domain_list()["domains"]: + if domain not in _get_domains(): raise YunohostValidationError("domain_unknown", domain=domain) def _list_subdomains_of(parent_domain): - _assert_domain_exists(parent_domain) out = [] - for domain in domain_list()["domains"]: + for domain in _get_domains(): if domain.endswith(f".{parent_domain}"): out.append(domain) return out -def _get_parent_domain_of(domain): +def _get_parent_domain_of(domain, return_self=False, topest=False): + domains = _get_domains(exclude_subdomains=topest) - _assert_domain_exists(domain) + domain_ = domain + while "." in domain_: + domain_ = domain_.split(".", 1)[1] + if domain_ in domains: + return 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) + return domain if return_self else None @is_unit_operation() @@ -148,6 +228,9 @@ def domain_add(operation_logger, domain, dyndns=False): if domain.startswith("xmpp-upload."): raise YunohostValidationError("domain_cannot_add_xmpp_upload") + if domain.startswith("muc."): + raise YunohostError("domain_cannot_add_muc_upload") + ldap = _get_ldap_interface() try: @@ -164,7 +247,6 @@ def domain_add(operation_logger, domain, dyndns=False): # DynDNS domain if dyndns: - from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.dyndns import _guess_current_dyndns_domain @@ -199,7 +281,7 @@ def domain_add(operation_logger, domain, dyndns=False): raise YunohostError("domain_creation_failed", domain=domain, error=e) finally: global domain_list_cache - domain_list_cache = {} + domain_list_cache = [] # Don't regen these conf if we're still in postinstall if os.path.exists("/etc/yunohost/installed"): @@ -256,7 +338,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): # Check domain is not the main domain if domain == _get_maindomain(): - other_domains = domain_list()["domains"] + other_domains = _get_domains() other_domains.remove(domain) if other_domains: @@ -317,7 +399,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): raise YunohostError("domain_deletion_failed", domain=domain, error=e) finally: global domain_list_cache - domain_list_cache = {} + domain_list_cache = [] stuff_to_delete = [ f"/etc/yunohost/certs/{domain}", @@ -372,6 +454,8 @@ def domain_main_domain(operation_logger, new_main_domain=None): if not new_main_domain: return {"current_main_domain": _get_maindomain()} + old_main_domain = _get_maindomain() + # Check domain exists _assert_domain_exists(new_main_domain) @@ -381,8 +465,8 @@ def domain_main_domain(operation_logger, new_main_domain=None): # Apply changes to ssl certs try: write_to_file("/etc/yunohost/current_host", new_main_domain) - global domain_list_cache - domain_list_cache = {} + global main_domain_cache + main_domain_cache = new_main_domain _set_hostname(new_main_domain) except Exception as e: logger.warning(str(e), exc_info=1) @@ -395,6 +479,12 @@ def domain_main_domain(operation_logger, new_main_domain=None): if os.path.exists("/etc/yunohost/installed"): regen_conf() + from yunohost.user import _update_admins_group_aliases + + _update_admins_group_aliases( + old_main_domain=old_main_domain, new_main_domain=new_main_domain + ) + logger.success(m18n.n("main_domain_changed")) @@ -410,12 +500,6 @@ def domain_url_available(domain, path): return len(_get_conflicting_apps(domain, path)) == 0 -def _get_maindomain(): - with open("/etc/yunohost/current_host", "r") as f: - maindomain = f.readline().rstrip() - return maindomain - - def domain_config_get(domain, key="", full=False, export=False): """ Display a domain configuration @@ -444,7 +528,7 @@ def domain_config_set( """ Apply a new domain configuration """ - Question.operation_logger = operation_logger + BaseOption.operation_logger = operation_logger config = DomainConfigPanel(domain) return config.set(key, value, args, args_file, operation_logger=operation_logger) @@ -454,6 +538,83 @@ class DomainConfigPanel(ConfigPanel): save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml" save_mode = "diff" + def get(self, key="", mode="classic"): + result = super().get(key=key, mode=mode) + + if mode == "full": + for panel, section, option in self._iterate(): + # This injects: + # i18n: domain_config_cert_renew_help + # i18n: domain_config_default_app_help + # i18n: domain_config_xmpp_help + if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"): + option["help"] = m18n.n( + self.config["i18n"] + "_" + option["id"] + "_help" + ) + return self.config + + return result + + def _get_raw_config(self): + toml = super()._get_raw_config() + + 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 _get_raw_settings ... + self.registar_id = toml["dns"]["registrar"]["registrar"]["value"] + del toml["dns"]["registrar"]["registrar"]["value"] + + # Cert stuff + if not filter_key or filter_key[0] == "cert": + from yunohost.certificate import certificate_status + + status = certificate_status([self.entity], full=True)["certificates"][ + self.entity + ] + + toml["cert"]["cert"]["cert_summary"]["style"] = status["style"] + + # i18n: domain_config_cert_summary_expired + # i18n: domain_config_cert_summary_selfsigned + # i18n: domain_config_cert_summary_abouttoexpire + # i18n: domain_config_cert_summary_ok + # i18n: domain_config_cert_summary_letsencrypt + toml["cert"]["cert"]["cert_summary"]["ask"] = m18n.n( + f"domain_config_cert_summary_{status['summary']}" + ) + + # FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ... + self.cert_status = status + + return toml + + def _get_raw_settings(self): + # TODO add mechanism to share some settings with other domains on the same zone + super()._get_raw_settings() + + # FIXME: Ugly hack to save the registar id/value and reinject it in _get_raw_settings ... + filter_key = self.filter_key.split(".") if self.filter_key != "" else [] + if not filter_key or filter_key[0] == "dns": + self.values["registrar"] = self.registar_id + + # FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ... + if not filter_key or filter_key[0] == "cert": + self.values["cert_validity"] = self.cert_status["validity"] + self.values["cert_issuer"] = self.cert_status["CA_type"] + self.values["acme_eligible"] = self.cert_status["ACME_eligible"] + self.values["summary"] = self.cert_status["summary"] + def _apply(self): if ( "default_app" in self.future_values @@ -461,7 +622,7 @@ class DomainConfigPanel(ConfigPanel): ): from yunohost.app import app_ssowatconf, app_map - if "/" in app_map(raw=True)[self.entity]: + if "/" in app_map(raw=True).get(self.entity, {}): raise YunohostValidationError( "app_make_default_location_already_used", app=self.future_values["default_app"], @@ -478,42 +639,46 @@ class DomainConfigPanel(ConfigPanel): ): app_ssowatconf() - def _get_toml(self): + stuff_to_regen_conf = [] + if ( + "xmpp" in self.future_values + and self.future_values["xmpp"] != self.values["xmpp"] + ): + stuff_to_regen_conf.append("nginx") + stuff_to_regen_conf.append("metronome") - toml = super()._get_toml() + if ( + "mail_in" in self.future_values + and self.future_values["mail_in"] != self.values["mail_in"] + ) or ( + "mail_out" in self.future_values + and self.future_values["mail_out"] != self.values["mail_out"] + ): + if "nginx" not in stuff_to_regen_conf: + stuff_to_regen_conf.append("nginx") + stuff_to_regen_conf.append("postfix") + stuff_to_regen_conf.append("dovecot") + stuff_to_regen_conf.append("rspamd") - toml["feature"]["xmpp"]["xmpp"]["default"] = ( - 1 if self.entity == _get_maindomain() else 0 - ) + if stuff_to_regen_conf: + regen_conf(names=stuff_to_regen_conf) - # 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) +def domain_action_run(domain, action, args=None): + import urllib.parse - # 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"] + if action == "cert.cert.cert_install": + from yunohost.certificate import certificate_install as action_func + elif action == "cert.cert.cert_renew": + from yunohost.certificate import certificate_renew as action_func - return toml + args = dict(urllib.parse.parse_qsl(args or "", keep_blank_values=True)) + no_checks = args["cert_no_checks"] in ("y", "yes", "on", "1") - 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 + action_func([domain], force=True, no_checks=no_checks) def _get_domain_settings(domain: str) -> dict: - _assert_domain_exists(domain) if os.path.exists(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml"): @@ -523,7 +688,6 @@ def _get_domain_settings(domain: str) -> dict: def _set_domain_settings(domain: str, settings: dict) -> None: - _assert_domain_exists(domain) write_to_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", settings) diff --git a/src/dyndns.py b/src/dyndns.py index 34f3dd5dc..2594abe8f 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_dyndns.py - - Subscribe and Update DynDNS Hosts -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import json @@ -234,7 +227,6 @@ def dyndns_update( for dns_auth in DYNDNS_DNS_AUTH: for type_ in ["A", "AAAA"]: - ok, result = dig(dns_auth, type_) if ok == "ok" and len(result) and result[0]: auth_resolvers.append(result[0]) @@ -245,7 +237,6 @@ def dyndns_update( ) def resolve_domain(domain, rdtype): - ok, result = dig(domain, rdtype, resolvers=auth_resolvers) if ok == "ok": return result[0] if len(result) else None diff --git a/src/firewall.py b/src/firewall.py index a1c0b187f..392678fe1 100644 --- a/src/firewall.py +++ b/src/firewall.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_firewall.py - - Manage firewall rules -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import yaml import miniupnpc @@ -39,7 +32,13 @@ logger = getActionLogger("yunohost.firewall") def firewall_allow( - protocol, port, ipv4_only=False, ipv6_only=False, no_upnp=False, no_reload=False + protocol, + port, + ipv4_only=False, + ipv6_only=False, + no_upnp=False, + no_reload=False, + reload_only_if_change=False, ): """ Allow connections on a port @@ -77,14 +76,20 @@ def firewall_allow( "ipv6", ] + changed = False + for p in protocols: # Iterate over IP versions to add port for i in ipvs: if port not in firewall[i][p]: firewall[i][p].append(port) + changed = True else: ipv = "IPv%s" % i[3] - logger.warning(m18n.n("port_already_opened", port=port, ip_version=ipv)) + if not reload_only_if_change: + logger.warning( + m18n.n("port_already_opened", port=port, ip_version=ipv) + ) # Add port forwarding with UPnP if not no_upnp and port not in firewall["uPnP"][p]: firewall["uPnP"][p].append(port) @@ -96,12 +101,20 @@ def firewall_allow( # Update and reload firewall _update_firewall_file(firewall) - if not no_reload: + if (not reload_only_if_change and not no_reload) or ( + reload_only_if_change and changed + ): return firewall_reload() def firewall_disallow( - protocol, port, ipv4_only=False, ipv6_only=False, upnp_only=False, no_reload=False + protocol, + port, + ipv4_only=False, + ipv6_only=False, + upnp_only=False, + no_reload=False, + reload_only_if_change=False, ): """ Disallow connections on a port @@ -146,14 +159,20 @@ def firewall_disallow( elif upnp_only: ipvs = [] + changed = False + for p in protocols: # Iterate over IP versions to remove port for i in ipvs: if port in firewall[i][p]: firewall[i][p].remove(port) + changed = True else: ipv = "IPv%s" % i[3] - logger.warning(m18n.n("port_already_closed", port=port, ip_version=ipv)) + if not reload_only_if_change: + logger.warning( + m18n.n("port_already_closed", port=port, ip_version=ipv) + ) # Remove port forwarding with UPnP if upnp and port in firewall["uPnP"][p]: firewall["uPnP"][p].remove(port) @@ -163,7 +182,9 @@ def firewall_disallow( # Update and reload firewall _update_firewall_file(firewall) - if not no_reload: + if (not reload_only_if_change and not no_reload) or ( + reload_only_if_change and changed + ): return firewall_reload() @@ -310,7 +331,7 @@ def firewall_reload(skip_upnp=False): # Refresh port forwarding with UPnP firewall_upnp(no_refresh=False) - _run_service_command("reload", "fail2ban") + _run_service_command("restart", "fail2ban") if errors: logger.warning(m18n.n("firewall_rules_cmd_failed")) @@ -398,7 +419,6 @@ def firewall_upnp(action="status", no_refresh=False): for protocol in ["TCP", "UDP"]: if protocol + "_TO_CLOSE" in firewall["uPnP"]: for port in firewall["uPnP"][protocol + "_TO_CLOSE"]: - if not isinstance(port, int): # FIXME : how should we handle port ranges ? logger.warning("Can't use UPnP to close '%s'" % port) @@ -413,7 +433,6 @@ def firewall_upnp(action="status", no_refresh=False): firewall["uPnP"][protocol + "_TO_CLOSE"] = [] for port in firewall["uPnP"][protocol]: - if not isinstance(port, int): # FIXME : how should we handle port ranges ? logger.warning("Can't use UPnP to open '%s'" % port) diff --git a/src/hook.py b/src/hook.py index 70d3b281b..4b07d1c17 100644 --- a/src/hook.py +++ b/src/hook.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_hook.py - - Manage hooks -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import sys @@ -346,7 +339,6 @@ def hook_exec( raise YunohostError("file_does_not_exist", path=path) def is_relevant_warning(msg): - # Ignore empty warning messages... if not msg: return False @@ -360,6 +352,31 @@ def hook_exec( r"dpkg: warning: while removing .* not empty so not removed", r"apt-key output should not be parsed", r"update-rc.d: ", + r"update-alternatives: ", + # Postgresql boring messages -_- + r"Adding user postgres to group ssl-cert", + r"Building PostgreSQL dictionaries from .*", + r"Removing obsolete dictionary files", + r"Creating new PostgreSQL cluster", + r"/usr/lib/postgresql/13/bin/initdb", + r"The files belonging to this database system will be owned by user", + r"This user must also own the server process.", + r"The database cluster will be initialized with locale", + r"The default database encoding has accordingly been set to", + r"The default text search configuration will be set to", + r"Data page checksums are disabled.", + r"fixing permissions on existing directory /var/lib/postgresql/13/main ... ok", + r"creating subdirectories \.\.\. ok", + r"selecting dynamic .* \.\.\. ", + r"selecting default .* \.\.\. ", + r"creating configuration files \.\.\. ok", + r"running bootstrap script \.\.\. ok", + r"performing post-bootstrap initialization \.\.\. ok", + r"syncing data to disk \.\.\. ok", + r"Success. You can now start the database server using:", + r"pg_ctlcluster \d\d main start", + r"Ver\s*Cluster\s*Port\s*Status\s*Owner\s*Data\s*directory", + r"/var/lib/postgresql/\d\d/main /var/log/postgresql/postgresql-\d\d-main.log", ] return all(not re.search(w, msg) for w in irrelevant_warnings) @@ -396,7 +413,6 @@ def hook_exec( def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers): - from moulinette.utils.process import call_async_output # Construct command variables @@ -436,6 +452,8 @@ def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers): logger.debug("Executing command '%s'" % command) _env = os.environ.copy() + if "YNH_CONTEXT" in _env: + del _env["YNH_CONTEXT"] _env.update(env) # Remove the 'HOME' var which is causing some inconsistencies between @@ -484,7 +502,6 @@ def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers): def _hook_exec_python(path, args, env, loggers): - dir_ = os.path.dirname(path) name = os.path.splitext(os.path.basename(path))[0] @@ -504,7 +521,6 @@ def _hook_exec_python(path, args, env, loggers): def hook_exec_with_script_debug_if_failure(*args, **kwargs): - operation_logger = kwargs.pop("operation_logger") error_message_if_failed = kwargs.pop("error_message_if_failed") error_message_if_script_failed = kwargs.pop("error_message_if_script_failed") diff --git a/src/log.py b/src/log.py index 9f9e0b753..5ab918e76 100644 --- a/src/log.py +++ b/src/log.py @@ -1,29 +1,22 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_log.py - - Manage debug logs -""" - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import copy import os import re import yaml @@ -38,7 +31,7 @@ from io import IOBase from moulinette import m18n, Moulinette from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.utils.packages import get_ynh_package_version +from yunohost.utils.system import get_ynh_package_version from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, read_yaml @@ -103,7 +96,6 @@ def log_list(limit=None, with_details=False, with_suboperations=False): logs = logs[: limit * 5] for log in logs: - base_filename = log[: -len(METADATA_FILE_EXT)] md_path = os.path.join(OPERATIONS_PATH, log) @@ -272,7 +264,6 @@ def log_show( return for filename in os.listdir(OPERATIONS_PATH): - if not filename.endswith(METADATA_FILE_EXT): continue @@ -446,7 +437,6 @@ class RedactingFormatter(Formatter): return msg def identify_data_to_redact(self, record): - # Wrapping this in a try/except because we don't want this to # break everything in case it fails miserably for some reason :s try: @@ -505,7 +495,6 @@ class OperationLogger: os.makedirs(self.path) def parent_logger(self): - # If there are other operation logger instances for instance in reversed(self._instances): # Is one of these operation logger started but not yet done ? @@ -606,7 +595,17 @@ class OperationLogger: Write or rewrite the metadata file with all metadata known """ - dump = yaml.safe_dump(self.metadata, default_flow_style=False) + metadata = copy.copy(self.metadata) + + # Remove lower-case keys ... this is because with the new v2 app packaging, + # all settings are included in the env but we probably don't want to dump all of these + # which may contain various secret/private data ... + if "env" in metadata: + metadata["env"] = { + k: v for k, v in metadata["env"].items() if k == k.upper() + } + + dump = yaml.safe_dump(metadata, default_flow_style=False) for data in self.data_to_redact: # N.B. : we need quotes here, otherwise yaml isn't happy about loading the yml later dump = dump.replace(data, "'**********'") @@ -740,7 +739,6 @@ class OperationLogger: self.error(m18n.n("log_operation_unit_unclosed_properly")) def dump_script_log_extract_for_debugging(self): - with open(self.log_path, "r") as f: lines = f.readlines() @@ -782,7 +780,6 @@ class OperationLogger: def _get_datetime_from_name(name): - # Filenames are expected to follow the format: # 20200831-170740-short_description-and-stuff diff --git a/src/migrations/0021_migrate_to_bullseye.py b/src/migrations/0021_migrate_to_bullseye.py index 7577c852c..f320577e1 100644 --- a/src/migrations/0021_migrate_to_bullseye.py +++ b/src/migrations/0021_migrate_to_bullseye.py @@ -15,8 +15,8 @@ from yunohost.tools import ( ) from yunohost.app import unstable_apps from yunohost.regenconf import manually_modified_files, _force_clear_hashes -from yunohost.utils.filesystem import free_space_in_directory -from yunohost.utils.packages import ( +from yunohost.utils.system import ( + free_space_in_directory, get_ynh_package_version, _list_upgradable_apt_packages, ) @@ -27,9 +27,6 @@ logger = getActionLogger("yunohost.migration") N_CURRENT_DEBIAN = 10 N_CURRENT_YUNOHOST = 4 -N_NEXT_DEBAN = 11 -N_NEXT_YUNOHOST = 11 - VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bullseye_upgrade.txt" @@ -42,8 +39,11 @@ def _get_all_venvs(dir, level=0, maxlevel=3): maxlevel - the depth of the recursion level - do not edit this, used as an iterator """ - # Using os functions instead of glob, because glob doesn't support hidden folders, and we need recursion with a fixed depth + if not os.path.exists(dir): + return [] + result = [] + # Using os functions instead of glob, because glob doesn't support hidden folders, and we need recursion with a fixed depth for file in os.listdir(dir): path = os.path.join(dir, file) if os.path.isdir(path): @@ -66,17 +66,17 @@ def _backup_pip_freeze_for_python_app_venvs(): venvs = _get_all_venvs("/opt/") + _get_all_venvs("/var/www/") for venv in venvs: # Generate a requirements file from venv - os.system(f"{venv}/bin/pip freeze > {venv}{VENV_REQUIREMENTS_SUFFIX}") + os.system( + f"{venv}/bin/pip freeze > {venv}{VENV_REQUIREMENTS_SUFFIX} 2>/dev/null" + ) 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")) @@ -94,6 +94,9 @@ class MyMigration(Migration): logger.info(m18n.n("migration_0021_patching_sources_list")) self.patch_apt_sources_list() + # Stupid OVH has some repo configured which dont work with bullseye and break apt ... + os.system("sudo rm -f /etc/apt/sources.list.d/ovh-*.list") + # Force add sury if it's not there yet # This is to solve some weird issue with php-common breaking php7.3-common, # hence breaking many php7.3-deps @@ -104,9 +107,17 @@ class MyMigration(Migration): open("/etc/apt/sources.list.d/extra_php_version.list", "w").write( "deb https://packages.sury.org/php/ bullseye main" ) - os.system( - 'wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/extra_php_version.gpg"' - ) + + # Add Sury key even if extra_php_version.list was already there, + # because some old system may be using an outdated key not valid for Bullseye + # and that'll block the migration + os.system( + 'wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/extra_php_version.gpg"' + ) + + # Remove legacy, duplicated sury entry if it exists + if os.path.exists("/etc/apt/sources.list.d/sury.list"): + os.system("rm -rf /etc/apt/sources.list.d/sury.list") # # Get requirements of the different venvs from python apps @@ -185,6 +196,47 @@ class MyMigration(Migration): del services["postgresql"] _save_services(services) + # + # Critical fix for RPI otherwise network is down after rebooting + # https://forum.yunohost.org/t/20652 + # + if os.system("systemctl | grep -q dhcpcd") == 0: + logger.info("Applying fix for DHCPCD ...") + os.system("mkdir -p /etc/systemd/system/dhcpcd.service.d") + write_to_file( + "/etc/systemd/system/dhcpcd.service.d/wait.conf", + "[Service]\nExecStart=\nExecStart=/usr/sbin/dhcpcd -w", + ) + + # + # Another boring fix for the super annoying libc6-dev: Breaks libgcc-8-dev + # https://forum.yunohost.org/t/20617 + # + if ( + os.system("dpkg --list | grep '^ii' | grep -q ' libgcc-8-dev'") == 0 + and os.system( + "LC_ALL=C apt policy libgcc-8-dev | grep Candidate | grep -q rpi" + ) + == 0 + ): + logger.info( + "Attempting to fix the build-essential / libc6-dev / libgcc-8-dev hell ..." + ) + os.system("cp /var/lib/dpkg/status /root/dpkg_status.bkp") + # This removes the dependency to build-essential from $app-ynh-deps + os.system( + "perl -i~ -0777 -pe 's/(Package: .*-ynh-deps\\n(.+:.+\\n)+Depends:.*)(build-essential, ?)(.*)/$1$4/g' /var/lib/dpkg/status" + ) + self.apt_install( + "build-essential-" + ) # Note the '-' suffix to mean that we actually want to remove the packages + os.system( + "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt autoremove --assume-yes" + ) + self.apt_install( + "gcc-8- libgcc-8-dev- equivs" + ) # Note the '-' suffix to mean that we actually want to remove the packages .. we also explicitly add 'equivs' to the list because sometimes apt is dumb and will derp about it + # # Main upgrade # @@ -277,9 +329,19 @@ class MyMigration(Migration): # Clean the mess logger.info(m18n.n("migration_0021_cleaning_up")) - os.system("apt autoremove --assume-yes") + os.system( + "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt autoremove --assume-yes" + ) os.system("apt clean --assume-yes") + # + # Stupid hack for stupid dnsmasq not picking up its new init.d script then breaking everything ... + # https://forum.yunohost.org/t/20676 + # + if os.path.exists("/etc/init.d/dnsmasq.dpkg-dist"): + logger.info("Copying new version for /etc/init.d/dnsmasq ...") + os.system("cp /etc/init.d/dnsmasq.dpkg-dist /etc/init.d/dnsmasq") + # # Yunohost upgrade # @@ -325,7 +387,6 @@ class MyMigration(Migration): return int(get_ynh_package_version("yunohost")["version"].split(".")[0]) def check_assertions(self): - # Be on buster (10.x) and yunohost 4.x # NB : we do both check to cover situations where the upgrade crashed # in the middle and debian version could be > 9.x but yunohost package @@ -334,12 +395,32 @@ class MyMigration(Migration): not self.debian_major_version() == N_CURRENT_DEBIAN and not self.yunohost_major_version() == N_CURRENT_YUNOHOST ): - raise YunohostError("migration_0021_not_buster") + try: + # Here we try to find the previous migration log, which should be somewhat recent and be at least 10k (we keep the biggest one) + maybe_previous_migration_log_id = check_output( + "cd /var/log/yunohost/categories/operation && find -name '*migrate*.log' -size +10k -mtime -100 -exec ls -s {} \\; | sort -n | tr './' ' ' | awk '{print $2}' | tail -n 1" + ) + if maybe_previous_migration_log_id: + logger.info( + f"NB: the previous migration log id seems to be {maybe_previous_migration_log_id}. You can share it with the support team with : sudo yunohost log share {maybe_previous_migration_log_id}" + ) + except Exception: + # Yeah it's not that important ... it's to simplify support ... + pass + + raise YunohostError("migration_0021_not_buster2") # Have > 1 Go free space on /var/ ? if free_space_in_directory("/var/") / (1024**3) < 1.0: raise YunohostError("migration_0021_not_enough_free_space") + # Have > 70 MB free space on /var/ ? + if free_space_in_directory("/boot/") / (1024**2) < 70.0: + raise YunohostError( + "/boot/ has less than 70MB available. This will probably trigger a crash during the upgrade because a new kernel needs to be installed. Please look for advice on the forum on how to remove old, unused kernels to free up some space in /boot/.", + raw_msg=True, + ) + # Check system is up to date # (but we don't if 'bullseye' is already in the sources.list ... # which means maybe a previous upgrade crashed and we're re-running it) @@ -369,7 +450,6 @@ class MyMigration(Migration): @property def disclaimer(self): - # Avoid having a super long disclaimer + uncessary check if we ain't # on buster / yunohost 4.x anymore # NB : we do both check to cover situations where the upgrade crashed @@ -410,7 +490,6 @@ class MyMigration(Migration): return message def patch_apt_sources_list(self): - sources_list = glob.glob("/etc/apt/sources.list.d/*.list") if os.path.exists("/etc/apt/sources.list"): sources_list.append("/etc/apt/sources.list") @@ -432,7 +511,6 @@ class MyMigration(Migration): os.system(command) def get_apps_equivs_packages(self): - command = ( "dpkg --get-selections" " | grep -v deinstall" diff --git a/src/migrations/0022_php73_to_php74_pools.py b/src/migrations/0022_php73_to_php74_pools.py index a2e5eae54..dc428e504 100644 --- a/src/migrations/0022_php73_to_php74_pools.py +++ b/src/migrations/0022_php73_to_php74_pools.py @@ -27,7 +27,6 @@ MIGRATION_COMMENT = ( class MyMigration(Migration): - "Migrate php7.3-fpm 'pool' conf files to php7.4" dependencies = ["migrate_to_bullseye"] @@ -43,7 +42,6 @@ class MyMigration(Migration): oldphp_pool_files = [f for f in oldphp_pool_files if f != "www.conf"] for pf in oldphp_pool_files: - # Copy the files to the php7.3 pool src = "{}/{}".format(OLDPHP_POOLS, pf) dest = "{}/{}".format(NEWPHP_POOLS, pf) diff --git a/src/migrations/0023_postgresql_11_to_13.py b/src/migrations/0023_postgresql_11_to_13.py index 8f03f8c5f..6d37ffa74 100644 --- a/src/migrations/0023_postgresql_11_to_13.py +++ b/src/migrations/0023_postgresql_11_to_13.py @@ -1,23 +1,31 @@ import subprocess import time +import os from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils.log import getActionLogger from yunohost.tools import Migration -from yunohost.utils.filesystem import free_space_in_directory, space_used_by_directory +from yunohost.utils.system import free_space_in_directory, space_used_by_directory logger = getActionLogger("yunohost.migration") class MyMigration(Migration): - "Migrate DBs from Postgresql 11 to 13 after migrating to Bullseye" dependencies = ["migrate_to_bullseye"] def run(self): + if ( + os.system( + 'grep -A10 "ynh-deps" /var/lib/dpkg/status | grep -E "Package:|Depends:" | grep -B1 postgresql' + ) + != 0 + ): + logger.info("No YunoHost app seem to require postgresql... Skipping!") + return if not self.package_is_installed("postgresql-11"): logger.warning(m18n.n("migration_0023_postgresql_11_not_installed")) @@ -53,7 +61,6 @@ class MyMigration(Migration): self.runcmd("systemctl start postgresql") def package_is_installed(self, package_name): - (returncode, out, err) = self.runcmd( "dpkg --list | grep '^ii ' | grep -q -w {}".format(package_name), raise_on_errors=False, @@ -61,7 +68,6 @@ class MyMigration(Migration): return returncode == 0 def runcmd(self, cmd, raise_on_errors=True): - logger.debug("Running command: " + cmd) p = subprocess.Popen( diff --git a/src/migrations/0024_rebuild_python_venv.py b/src/migrations/0024_rebuild_python_venv.py index f39c27c49..01a229b87 100644 --- a/src/migrations/0024_rebuild_python_venv.py +++ b/src/migrations/0024_rebuild_python_venv.py @@ -14,7 +14,6 @@ VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bullseye_upgrade.txt" def extract_app_from_venv_path(venv_path): - venv_path = venv_path.replace("/var/www/", "") venv_path = venv_path.replace("/opt/yunohost/", "") venv_path = venv_path.replace("/opt/", "") @@ -30,6 +29,9 @@ def _get_all_venvs(dir, level=0, maxlevel=3): maxlevel - the depth of the recursion level - do not edit this, used as an iterator """ + if not os.path.exists(dir): + return [] + # Using os functions instead of glob, because glob doesn't support hidden # folders, and we need recursion with a fixed depth result = [] @@ -65,6 +67,7 @@ class MyMigration(Migration): "pgadmin", "tracim", "synapse", + "matrix-synapse", "weblate", ] @@ -95,6 +98,10 @@ class MyMigration(Migration): if not self.is_pending(): return None + # Disclaimer should be empty if in auto, otherwise it excepts the --accept-disclaimer option during debian postinst + if self.mode == "auto": + return None + ignored_apps = [] rebuild_apps = [] @@ -129,10 +136,11 @@ class MyMigration(Migration): return msg def run(self): + if self.mode == "auto": + return venvs = _get_all_venvs("/opt/") + _get_all_venvs("/var/www/") for venv in venvs: - app_corresponding_to_venv = extract_app_from_venv_path(venv) # Search for ignore apps diff --git a/src/migrations/0025_global_settings_to_configpanel.py b/src/migrations/0025_global_settings_to_configpanel.py new file mode 100644 index 000000000..3a8818461 --- /dev/null +++ b/src/migrations/0025_global_settings_to_configpanel.py @@ -0,0 +1,42 @@ +import os + +from yunohost.utils.error import YunohostError +from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_json, write_to_yaml + +from yunohost.tools import Migration +from yunohost.utils.legacy import translate_legacy_settings_to_configpanel_settings + +logger = getActionLogger("yunohost.migration") + +SETTINGS_PATH = "/etc/yunohost/settings.yml" +OLD_SETTINGS_PATH = "/etc/yunohost/settings.json" + + +class MyMigration(Migration): + "Migrate old global settings to the new ConfigPanel global settings" + + dependencies = ["migrate_to_bullseye"] + + def run(self): + if not os.path.exists(OLD_SETTINGS_PATH): + return + + try: + old_settings = read_json(OLD_SETTINGS_PATH) + except Exception as e: + raise YunohostError(f"Can't open setting file : {e}", raw_msg=True) + + settings = { + translate_legacy_settings_to_configpanel_settings(k).split(".")[-1]: v[ + "value" + ] + for k, v in old_settings.items() + } + + if settings.get("smtp_relay_host"): + settings["smtp_relay_enabled"] = True + + # Here we don't use settings_set() from settings.py to prevent + # Questions to be asked when one run the migration from CLI. + write_to_yaml(SETTINGS_PATH, settings) diff --git a/src/migrations/0026_new_admins_group.py b/src/migrations/0026_new_admins_group.py new file mode 100644 index 000000000..43f10a7b6 --- /dev/null +++ b/src/migrations/0026_new_admins_group.py @@ -0,0 +1,153 @@ +from moulinette.utils.log import getActionLogger + +from yunohost.tools import Migration + +logger = getActionLogger("yunohost.migration") + +################################################### +# Tools used also for restoration +################################################### + + +class MyMigration(Migration): + """ + Add new permissions around SSH/SFTP features + """ + + introduced_in_version = "11.1" # FIXME? + dependencies = [] + + ldap_migration_started = False + + @Migration.ldap_migration + def run(self, *args): + from yunohost.user import ( + user_list, + user_info, + user_group_update, + user_update, + user_group_add_mailalias, + ADMIN_ALIASES, + ) + from yunohost.utils.ldap import _get_ldap_interface + from yunohost.permission import permission_sync_to_user + from yunohost.domain import _get_maindomain + + main_domain = _get_maindomain() + ldap = _get_ldap_interface() + + all_users = user_list()["users"].keys() + new_admin_user = None + for user in all_users: + if any( + alias.startswith("root@") + for alias in user_info(user).get("mail-aliases", []) + ): + new_admin_user = user + break + + # For some reason some system have no user with root@ alias, + # but the user does has admin / postmaster / ... alias + # ... try to find it instead otherwise this creashes the migration + # later because the admin@, postmaster@, .. aliases will already exist + if not new_admin_user: + for user in all_users: + aliases = user_info(user).get("mail-aliases", []) + if any( + alias.startswith(f"admin@{main_domain}") for alias in aliases + ) or any( + alias.startswith(f"postmaster@{main_domain}") for alias in aliases + ): + new_admin_user = user + break + + self.ldap_migration_started = True + + if new_admin_user: + aliases = user_info(new_admin_user).get("mail-aliases", []) + old_admin_aliases_to_remove = [ + alias + for alias in aliases + if any( + alias.startswith(a) + for a in [ + "root@", + "admin@", + "admins@", + "webmaster@", + "postmaster@", + "abuse@", + ] + ) + ] + + user_update(new_admin_user, remove_mailalias=old_admin_aliases_to_remove) + + admin_hashs = ldap.search("cn=admin", attrs={"userPassword"})[0]["userPassword"] + + stuff_to_delete = [ + "cn=admin,ou=sudo", + "cn=admin", + "cn=admins,ou=groups", + ] + + for stuff in stuff_to_delete: + if ldap.search(stuff): + ldap.remove(stuff) + + ldap.add( + "cn=admins,ou=sudo", + { + "cn": ["admins"], + "objectClass": ["top", "sudoRole"], + "sudoCommand": ["ALL"], + "sudoUser": ["%admins"], + "sudoHost": ["ALL"], + }, + ) + + ldap.add( + "cn=admins,ou=groups", + { + "cn": ["admins"], + "objectClass": ["top", "posixGroup", "groupOfNamesYnh"], + "gidNumber": ["4001"], + }, + ) + + user_group_add_mailalias( + "admins", [f"{alias}@{main_domain}" for alias in ADMIN_ALIASES] + ) + + permission_sync_to_user() + + if new_admin_user: + user_group_update(groupname="admins", add=new_admin_user, sync_perm=True) + + # Re-add admin as a regular user + attr_dict = { + "objectClass": [ + "mailAccount", + "inetOrgPerson", + "posixAccount", + "userPermissionYnh", + ], + "givenName": ["Admin"], + "sn": ["Admin"], + "displayName": ["Admin"], + "cn": ["Admin"], + "uid": ["admin"], + "mail": "admin_legacy", + "maildrop": ["admin"], + "mailuserquota": ["0"], + "userPassword": admin_hashs, + "gidNumber": ["1007"], + "uidNumber": ["1007"], + "homeDirectory": ["/home/admin"], + "loginShell": ["/bin/bash"], + } + ldap.add("uid=admin,ou=users", attr_dict) + user_group_update(groupname="admins", add="admin", sync_perm=True) + + def run_after_system_restore(self): + self.run() diff --git a/src/permission.py b/src/permission.py index 2a6f6d954..72975561f 100644 --- a/src/permission.py +++ b/src/permission.py @@ -1,29 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2014 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_permission.py - - Manage permissions -""" - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import copy import grp @@ -87,7 +79,6 @@ def user_permission_list( permissions = {} for infos in permissions_infos: - name = infos["cn"][0] app = name.split(".")[0] @@ -487,6 +478,7 @@ def permission_url( url=None, add_url=None, remove_url=None, + set_url=None, auth_header=None, clear_urls=False, sync_perm=True, @@ -499,6 +491,7 @@ def permission_url( url -- (optional) URL for which access will be allowed/forbidden. add_url -- (optional) List of additional url to add for which access will be allowed/forbidden remove_url -- (optional) List of additional url to remove for which access will be allowed/forbidden + set_url -- (optional) List of additional url to set/replace for which access will be allowed/forbidden auth_header -- (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application clear_urls -- (optional) Clean all urls (url and additional_urls) """ @@ -564,6 +557,9 @@ def permission_url( new_additional_urls = [u for u in new_additional_urls if u not in remove_url] + if set_url: + new_additional_urls = set_url + if auth_header is None: auth_header = existing_permission["auth_header"] @@ -657,7 +653,6 @@ def permission_sync_to_user(): permissions = user_permission_list(full=True)["permissions"] for permission_name, permission_infos in permissions.items(): - # These are the users currently allowed because there's an 'inheritPermission' object corresponding to it currently_allowed_users = set(permission_infos["corresponding_users"]) @@ -743,7 +738,6 @@ def _update_ldap_group_permission( update["isProtected"] = [str(protected).upper()] if show_tile is not None: - if show_tile is True: if not existing_permission["url"]: logger.warning( @@ -820,10 +814,12 @@ def _update_ldap_group_permission( def _get_absolute_url(url, base_path): # # For example transform: - # (/api, domain.tld/nextcloud) into domain.tld/nextcloud/api - # (/api, domain.tld/nextcloud/) into domain.tld/nextcloud/api - # (re:/foo.*, domain.tld/app) into re:domain\.tld/app/foo.* - # (domain.tld/bar, domain.tld/app) into domain.tld/bar + # (/, domain.tld/) into domain.tld (no trailing /) + # (/api, domain.tld/nextcloud) into domain.tld/nextcloud/api + # (/api, domain.tld/nextcloud/) into domain.tld/nextcloud/api + # (re:/foo.*, domain.tld/app) into re:domain\.tld/app/foo.* + # (domain.tld/bar, domain.tld/app) into domain.tld/bar + # (some.other.domain/, domain.tld/app) into some.other.domain (no trailing /) # base_path = base_path.rstrip("/") if url is None: @@ -833,7 +829,7 @@ def _get_absolute_url(url, base_path): if url.startswith("re:/"): return "re:" + base_path.replace(".", "\\.") + url[3:] else: - return url + return url.rstrip("/") def _validate_and_sanitize_permission_url(url, app_base_path, app): @@ -879,7 +875,6 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): raise YunohostValidationError("invalid_regex", regex=regex) if url.startswith("re:"): - # regex without domain # we check for the first char after 're:' if url[3] in ["/", "^", "\\"]: diff --git a/src/regenconf.py b/src/regenconf.py index e513a1506..74bbdb17c 100644 --- a/src/regenconf.py +++ b/src/regenconf.py @@ -1,24 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2019 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import yaml import shutil @@ -80,7 +77,6 @@ def regen_conf( for category, conf_files in pending_conf.items(): for system_path, pending_path in conf_files.items(): - pending_conf[category][system_path] = { "pending_conf": pending_path, "diff": _get_files_diff(system_path, pending_path, True), @@ -143,6 +139,7 @@ def regen_conf( env["YNH_MAIN_DOMAINS"] = " ".join( domain_list(exclude_subdomains=True)["domains"] ) + env["YNH_CONTEXT"] = "regenconf" pre_result = hook_callback("conf_regen", names, pre_callback=_pre_call, env=env) @@ -598,7 +595,6 @@ def _update_conf_hashes(category, hashes): def _force_clear_hashes(paths): - categories = _get_regenconf_infos() for path in paths: for category in categories.keys(): @@ -678,7 +674,6 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): def manually_modified_files(): - output = [] regenconf_categories = _get_regenconf_infos() for category, infos in regenconf_categories.items(): @@ -693,7 +688,6 @@ def manually_modified_files(): def manually_modified_files_compared_to_debian_default( ignore_handled_by_regenconf=False, ): - # from https://serverfault.com/a/90401 files = check_output( "dpkg-query -W -f='${Conffiles}\n' '*' \ diff --git a/src/service.py b/src/service.py index 5800f6e4d..47bc1903a 100644 --- a/src/service.py +++ b/src/service.py @@ -1,29 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_service.py - - Manage services -""" - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os import time @@ -257,12 +249,10 @@ def service_reload_or_restart(names, test_conf=True): services = _get_services() for name in names: - logger.debug(f"Reloading service {name}") test_conf_cmd = services.get(name, {}).get("test_conf") if test_conf and test_conf_cmd: - p = subprocess.Popen( test_conf_cmd, shell=True, @@ -401,7 +391,6 @@ def _get_service_information_from_systemd(service): def _get_and_format_service_status(service, infos): - systemd_service = infos.get("actual_systemd_service", service) raw_status, raw_service = _get_service_information_from_systemd(systemd_service) @@ -422,7 +411,6 @@ def _get_and_format_service_status(service, infos): # If no description was there, try to get it from the .json locales if not description: - translation_key = f"service_description_{service}" if m18n.key_exists(translation_key): description = m18n.n(translation_key) @@ -529,7 +517,6 @@ def service_log(name, number=50): result["journalctl"] = _get_journalctl_logs(name, number).splitlines() for log_path in log_list: - if not os.path.exists(log_path): continue @@ -628,7 +615,6 @@ def _run_service_command(action, service): def _give_lock(action, service, p): - # Depending of the action, systemctl calls the PID differently :/ if action == "start" or action == "restart": systemctl_PID_name = "MainPID" @@ -720,6 +706,10 @@ def _get_services(): "category": "web", } + # Ignore metronome entirely if XMPP was disabled on all domains + if "metronome" in services and not glob("/etc/metronome/conf.d/*.cfg.lua"): + del services["metronome"] + # Remove legacy /var/log/daemon.log and /var/log/syslog from log entries # because they are too general. Instead, now the journalctl log is # returned by default which is more relevant. @@ -748,7 +738,6 @@ def _save_services(services): diff = {} for service_name, service_infos in services.items(): - # Ignore php-fpm services, they are to be added dynamically by the core, # but not actually saved if service_name.startswith("php") and service_name.endswith("-fpm"): @@ -786,7 +775,7 @@ def _tail(file, n): f = gzip.open(file) lines = f.read().splitlines() else: - f = open(file) + f = open(file, errors="replace") pos = 1 lines = [] while len(lines) < to_read and pos > 0: diff --git a/src/settings.py b/src/settings.py index cec416550..6690ab3fd 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,129 +1,40 @@ +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os -import json import subprocess -from datetime import datetime -from collections import OrderedDict - from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.configpanel import ConfigPanel +from yunohost.utils.form import BaseOption from moulinette.utils.log import getActionLogger from yunohost.regenconf import regen_conf from yunohost.firewall import firewall_reload +from yunohost.log import is_unit_operation +from yunohost.utils.legacy import translate_legacy_settings_to_configpanel_settings logger = getActionLogger("yunohost.settings") -SETTINGS_PATH = "/etc/yunohost/settings.json" -SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json" +SETTINGS_PATH = "/etc/yunohost/settings.yml" -def is_boolean(value): - TRUE = ["true", "on", "yes", "y", "1"] - FALSE = ["false", "off", "no", "n", "0"] - - """ - Ensure a string value is intended as a boolean - - Keyword arguments: - arg -- The string to check - - Returns: - (is_boolean, boolean_value) - - """ - if isinstance(value, bool): - return True, value - if value in [0, 1]: - return True, bool(value) - elif isinstance(value, str): - if str(value).lower() in TRUE + FALSE: - return True, str(value).lower() in TRUE - else: - return False, None - else: - return False, None - - -# a settings entry is in the form of: -# namespace.subnamespace.name: {type, value, default, description, [choices]} -# choices is only for enum -# the keyname can have as many subnamespace as needed but should have at least -# one level of namespace - -# description is implied from the translated strings -# the key is "global_settings_setting_%s" % key.replace(".", "_") - -# type can be: -# * bool -# * int -# * string -# * enum (in the form of a python list) - -DEFAULTS = OrderedDict( - [ - # Password Validation - # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest - ("security.password.admin.strength", {"type": "int", "default": 1}), - ("security.password.user.strength", {"type": "int", "default": 1}), - ( - "service.ssh.allow_deprecated_dsa_hostkey", - {"type": "bool", "default": False}, - ), - ( - "security.ssh.compatibility", - { - "type": "enum", - "default": "modern", - "choices": ["intermediate", "modern"], - }, - ), - ( - "security.ssh.port", - {"type": "int", "default": 22}, - ), - ( - "security.ssh.password_authentication", - {"type": "bool", "default": True}, - ), - ( - "security.nginx.redirect_to_https", - { - "type": "bool", - "default": True, - }, - ), - ( - "security.nginx.compatibility", - { - "type": "enum", - "default": "intermediate", - "choices": ["intermediate", "modern"], - }, - ), - ( - "security.postfix.compatibility", - { - "type": "enum", - "default": "intermediate", - "choices": ["intermediate", "modern"], - }, - ), - ("pop3.enabled", {"type": "bool", "default": False}), - ("smtp.allow_ipv6", {"type": "bool", "default": True}), - ("smtp.relay.host", {"type": "string", "default": ""}), - ("smtp.relay.port", {"type": "int", "default": 587}), - ("smtp.relay.user", {"type": "string", "default": ""}), - ("smtp.relay.password", {"type": "string", "default": ""}), - ("backup.compress_tar_archives", {"type": "bool", "default": False}), - ("ssowat.panel_overlay.enabled", {"type": "bool", "default": True}), - ("security.webadmin.allowlist.enabled", {"type": "bool", "default": False}), - ("security.webadmin.allowlist", {"type": "string", "default": ""}), - ("security.experimental.enabled", {"type": "bool", "default": False}), - ] -) - - -def settings_get(key, full=False): +def settings_get(key="", full=False, export=False): """ Get an entry value in the settings @@ -131,28 +42,38 @@ def settings_get(key, full=False): key -- Settings key """ - settings = _get_settings() - - if key not in settings: + if full and export: raise YunohostValidationError( - "global_settings_key_doesnt_exists", settings_key=key + "You can't use --full and --export together.", raw_msg=True ) if full: - return settings[key] + mode = "full" + elif export: + mode = "export" + else: + mode = "classic" - return settings[key]["value"] + settings = SettingsConfigPanel() + key = translate_legacy_settings_to_configpanel_settings(key) + return settings.get(key, mode) -def settings_list(): - """ - List all entries of the settings +def settings_list(full=False): + settings = settings_get(full=full) - """ - return _get_settings() + if full: + return settings + else: + return { + k: v + for k, v in settings.items() + if not k.startswith("security.root_access") + } -def settings_set(key, value): +@is_unit_operation() +def settings_set(operation_logger, key=None, value=None, args=None, args_file=None): """ Set an entry value in the settings @@ -161,78 +82,14 @@ def settings_set(key, value): value -- New value """ - settings = _get_settings() - - if key not in settings: - raise YunohostValidationError( - "global_settings_key_doesnt_exists", settings_key=key - ) - - key_type = settings[key]["type"] - - if key_type == "bool": - boolean_value = is_boolean(value) - if boolean_value[0]: - value = boolean_value[1] - else: - raise YunohostValidationError( - "global_settings_bad_type_for_setting", - setting=key, - received_type=type(value).__name__, - expected_type=key_type, - ) - elif key_type == "int": - if not isinstance(value, int) or isinstance(value, bool): - if isinstance(value, str): - try: - value = int(value) - except Exception: - raise YunohostValidationError( - "global_settings_bad_type_for_setting", - setting=key, - received_type=type(value).__name__, - expected_type=key_type, - ) - else: - raise YunohostValidationError( - "global_settings_bad_type_for_setting", - setting=key, - received_type=type(value).__name__, - expected_type=key_type, - ) - elif key_type == "string": - if not isinstance(value, str): - raise YunohostValidationError( - "global_settings_bad_type_for_setting", - setting=key, - received_type=type(value).__name__, - expected_type=key_type, - ) - elif key_type == "enum": - if value not in settings[key]["choices"]: - raise YunohostValidationError( - "global_settings_bad_choice_for_enum", - setting=key, - choice=str(value), - available_choices=", ".join(settings[key]["choices"]), - ) - else: - raise YunohostValidationError( - "global_settings_unknown_type", setting=key, unknown_type=key_type - ) - - old_value = settings[key].get("value") - settings[key]["value"] = value - _save_settings(settings) - - try: - trigger_post_change_hook(key, old_value, value) - except Exception as e: - logger.error(f"Post-change hook for setting {key} failed : {e}") - raise + BaseOption.operation_logger = operation_logger + settings = SettingsConfigPanel() + key = translate_legacy_settings_to_configpanel_settings(key) + return settings.set(key, value, args, args_file, operation_logger=operation_logger) -def settings_reset(key): +@is_unit_operation() +def settings_reset(operation_logger, key): """ Set an entry value to its default one @@ -240,18 +97,14 @@ def settings_reset(key): key -- Settings key """ - settings = _get_settings() - if key not in settings: - raise YunohostValidationError( - "global_settings_key_doesnt_exists", settings_key=key - ) - - settings[key]["value"] = settings[key]["default"] - _save_settings(settings) + settings = SettingsConfigPanel() + key = translate_legacy_settings_to_configpanel_settings(key) + return settings.reset(key, operation_logger=operation_logger) -def settings_reset_all(): +@is_unit_operation() +def settings_reset_all(operation_logger): """ Reset all settings to their default value @@ -259,110 +112,150 @@ def settings_reset_all(): yes -- Yes I'm sure I want to do that """ - settings = _get_settings() - - # For now on, we backup the previous settings in case of but we don't have - # any mecanism to take advantage of those backups. It could be a nice - # addition but we'll see if this is a common need. - # Another solution would be to use etckeeper and integrate those - # modification inside of it and take advantage of its git history - old_settings_backup_path = ( - SETTINGS_PATH_OTHER_LOCATION % datetime.utcnow().strftime("%F_%X") - ) - _save_settings(settings, location=old_settings_backup_path) - - for value in settings.values(): - value["value"] = value["default"] - - _save_settings(settings) - - return { - "old_settings_backup_path": old_settings_backup_path, - "message": m18n.n( - "global_settings_reset_success", path=old_settings_backup_path - ), - } + settings = SettingsConfigPanel() + return settings.reset(operation_logger=operation_logger) -def _get_setting_description(key): - return m18n.n(f"global_settings_setting_{key}".replace(".", "_")) +class SettingsConfigPanel(ConfigPanel): + entity_type = "global" + save_path_tpl = SETTINGS_PATH + save_mode = "diff" + virtual_settings = ["root_password", "root_password_confirm", "passwordless_sudo"] + def __init__(self, config_path=None, save_path=None, creation=False): + super().__init__("settings") -def _get_settings(): + def get(self, key="", mode="classic"): + result = super().get(key=key, mode=mode) - settings = {} - - for key, value in DEFAULTS.copy().items(): - settings[key] = value - settings[key]["value"] = value["default"] - settings[key]["description"] = _get_setting_description(key) - - if not os.path.exists(SETTINGS_PATH): - return settings - - # we have a very strict policy on only allowing settings that we know in - # the OrderedDict DEFAULTS - # For various reason, while reading the local settings we might encounter - # settings that aren't in DEFAULTS, those can come from settings key that - # we have removed, errors or the user trying to modify - # /etc/yunohost/settings.json - # To avoid to simply overwrite them, we store them in - # /etc/yunohost/settings-unknown.json in case of - unknown_settings = {} - unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown" - - if os.path.exists(unknown_settings_path): - try: - unknown_settings = json.load(open(unknown_settings_path, "r")) - except Exception as e: - logger.warning(f"Error while loading unknown settings {e}") - - try: - with open(SETTINGS_PATH) as settings_fd: - local_settings = json.load(settings_fd) - - for key, value in local_settings.items(): - if key in settings: - settings[key] = value - settings[key]["description"] = _get_setting_description(key) - else: - logger.warning( - m18n.n( - "global_settings_unknown_setting_from_settings_file", - setting_key=key, - ) + if mode == "full": + for panel, section, option in self._iterate(): + if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"): + option["help"] = m18n.n( + self.config["i18n"] + "_" + option["id"] + "_help" ) - unknown_settings[key] = value - except Exception as e: - raise YunohostValidationError("global_settings_cant_open_settings", reason=e) + return self.config + + # Dirty hack to let settings_get() to work from a python script + if isinstance(result, str) and result in ["True", "False"]: + result = bool(result == "True") + + return result + + def reset(self, key="", operation_logger=None): + self.filter_key = key + + # Read config panel toml + self._get_config_panel() + + if not self.config: + raise YunohostValidationError("config_no_panel") + + # Replace all values with default values + self.values = self._get_default_values() + + BaseOption.operation_logger = operation_logger + + if operation_logger: + operation_logger.start() - if unknown_settings: try: - _save_settings(unknown_settings, location=unknown_settings_path) - _save_settings(settings) - except Exception as e: - logger.warning(f"Failed to save unknown settings (because {e}), aborting.") + self._apply() + except YunohostError: + raise + # Script got manually interrupted ... + # N.B. : KeyboardInterrupt does not inherit from Exception + except (KeyboardInterrupt, EOFError): + error = m18n.n("operation_interrupted") + logger.error(m18n.n("config_apply_failed", error=error)) + raise + # Something wrong happened in Yunohost's code (most probably hook_exec) + except Exception: + import traceback - return settings + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error(m18n.n("config_apply_failed", error=error)) + raise + logger.success(m18n.n("global_settings_reset_success")) + operation_logger.success() -def _save_settings(settings, location=SETTINGS_PATH): - settings_without_description = {} - for key, value in settings.items(): - settings_without_description[key] = value - if "description" in value: - del settings_without_description[key]["description"] + def _get_raw_config(self): + toml = super()._get_raw_config() - try: - result = json.dumps(settings_without_description, indent=4) - except Exception as e: - raise YunohostError("global_settings_cant_serialize_settings", reason=e) + # Dynamic choice list for portal themes + THEMEDIR = "/usr/share/ssowat/portal/assets/themes/" + try: + themes = [d for d in os.listdir(THEMEDIR) if os.path.isdir(THEMEDIR + d)] + except Exception: + themes = ["unsplash", "vapor", "light", "default", "clouds"] + toml["misc"]["portal"]["portal_theme"]["choices"] = themes - try: - with open(location, "w") as settings_fd: - settings_fd.write(result) - except Exception as e: - raise YunohostError("global_settings_cant_write_settings", reason=e) + return toml + + def _get_raw_settings(self): + super()._get_raw_settings() + + # Specific logic for those settings who are "virtual" settings + # and only meant to have a custom setter mapped to tools_rootpw + self.values["root_password"] = "" + self.values["root_password_confirm"] = "" + + # Specific logic for virtual setting "passwordless_sudo" + try: + from yunohost.utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + self.values["passwordless_sudo"] = "!authenticate" in ldap.search( + "ou=sudo", "cn=admins", ["sudoOption"] + )[0].get("sudoOption", []) + except Exception: + self.values["passwordless_sudo"] = False + + def _apply(self): + root_password = self.new_values.pop("root_password", None) + root_password_confirm = self.new_values.pop("root_password_confirm", None) + passwordless_sudo = self.new_values.pop("passwordless_sudo", None) + + self.values = { + k: v for k, v in self.values.items() if k not in self.virtual_settings + } + self.new_values = { + k: v for k, v in self.new_values.items() if k not in self.virtual_settings + } + + assert all(v not in self.future_values for v in self.virtual_settings) + + if root_password and root_password.strip(): + if root_password != root_password_confirm: + raise YunohostValidationError("password_confirmation_not_the_same") + + from yunohost.tools import tools_rootpw + + tools_rootpw(root_password, check_strength=True) + + if passwordless_sudo is not None: + from yunohost.utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + ldap.update( + "cn=admins,ou=sudo", + {"sudoOption": ["!authenticate"] if passwordless_sudo else []}, + ) + + super()._apply() + + settings = { + k: v for k, v in self.future_values.items() if self.values.get(k) != v + } + for setting_name, value in settings.items(): + try: + trigger_post_change_hook( + setting_name, self.values.get(setting_name), value + ) + except Exception as e: + logger.error(f"Post-change hook for setting failed : {e}") + raise # Meant to be a dict of setting_name -> function to call @@ -370,13 +263,8 @@ post_change_hooks = {} def post_change_hook(setting_name): + # TODO: Check that setting_name exists def decorator(func): - assert ( - setting_name in DEFAULTS.keys() - ), f"The setting {setting_name} does not exists" - assert ( - setting_name not in post_change_hooks - ), f"You can only register one post change hook per setting (in particular for {setting_name})" post_change_hooks[setting_name] = func return func @@ -404,55 +292,63 @@ def trigger_post_change_hook(setting_name, old_value, new_value): # =========================================== -@post_change_hook("ssowat.panel_overlay.enabled") -@post_change_hook("security.nginx.redirect_to_https") -@post_change_hook("security.nginx.compatibility") -@post_change_hook("security.webadmin.allowlist.enabled") -@post_change_hook("security.webadmin.allowlist") +@post_change_hook("portal_theme") +def regen_ssowatconf(setting_name, old_value, new_value): + if old_value != new_value: + from yunohost.app import app_ssowatconf + + app_ssowatconf() + + +@post_change_hook("ssowat_panel_overlay_enabled") +@post_change_hook("nginx_redirect_to_https") +@post_change_hook("nginx_compatibility") +@post_change_hook("webadmin_allowlist_enabled") +@post_change_hook("webadmin_allowlist") def reconfigure_nginx(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["nginx"]) -@post_change_hook("security.experimental.enabled") +@post_change_hook("security_experimental_enabled") def reconfigure_nginx_and_yunohost(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["nginx", "yunohost"]) -@post_change_hook("security.ssh.compatibility") -@post_change_hook("security.ssh.password_authentication") +@post_change_hook("ssh_compatibility") +@post_change_hook("ssh_password_authentication") def reconfigure_ssh(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["ssh"]) -@post_change_hook("security.ssh.port") +@post_change_hook("ssh_port") def reconfigure_ssh_and_fail2ban(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["ssh", "fail2ban"]) firewall_reload() -@post_change_hook("smtp.allow_ipv6") -@post_change_hook("smtp.relay.host") -@post_change_hook("smtp.relay.port") -@post_change_hook("smtp.relay.user") -@post_change_hook("smtp.relay.password") -@post_change_hook("security.postfix.compatibility") +@post_change_hook("smtp_allow_ipv6") +@post_change_hook("smtp_relay_host") +@post_change_hook("smtp_relay_port") +@post_change_hook("smtp_relay_user") +@post_change_hook("smtp_relay_password") +@post_change_hook("postfix_compatibility") def reconfigure_postfix(setting_name, old_value, new_value): if old_value != new_value: regen_conf(names=["postfix"]) -@post_change_hook("pop3.enabled") +@post_change_hook("pop3_enabled") def reconfigure_dovecot(setting_name, old_value, new_value): dovecot_package = "dovecot-pop3d" environment = os.environ.copy() environment.update({"DEBIAN_FRONTEND": "noninteractive"}) - if new_value == "True": + if new_value is True: command = [ "apt-get", "-y", diff --git a/src/ssh.py b/src/ssh.py index b89dc6c8e..8526e278f 100644 --- a/src/ssh.py +++ b/src/ssh.py @@ -1,4 +1,21 @@ -# encoding: utf-8 +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os @@ -64,7 +81,7 @@ def user_ssh_add_key(username, key, comment): parents=True, uid=user["uid"][0], ) - chmod(os.path.join(user["homeDirectory"][0], ".ssh"), 0o600) + chmod(os.path.join(user["homeDirectory"][0], ".ssh"), 0o700) # create empty file to set good permissions write_to_file(authorized_keys_file, "") @@ -158,15 +175,6 @@ def _get_user_for_ssh(username, attrs=None): "home_path": root_unix.pw_dir, } - if username == "admin": - admin_unix = pwd.getpwnam("admin") - return { - "username": "admin", - "fullname": "", - "mail": "", - "home_path": admin_unix.pw_dir, - } - # TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html from yunohost.utils.ldap import _get_ldap_interface diff --git a/src/tests/conftest.py b/src/tests/conftest.py index cd5cb307e..393c33564 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -51,7 +51,6 @@ old_translate = moulinette.core.Translator.translate def new_translate(self, key, *args, **kwargs): - if key not in self._translations[self.default_locale].keys(): raise KeyError("Unable to retrieve key %s for default locale !" % key) @@ -67,7 +66,6 @@ moulinette.core.Translator.translate = new_translate def pytest_cmdline_main(config): - import sys sys.path.insert(0, "/usr/lib/moulinette/") @@ -76,7 +74,6 @@ def pytest_cmdline_main(config): yunohost.init(debug=config.option.yunodebug) class DummyInterface: - type = "cli" def prompt(self, *args, **kwargs): diff --git a/src/tests/test_app_catalog.py b/src/tests/test_app_catalog.py index e9ecb1c12..f7363dabe 100644 --- a/src/tests/test_app_catalog.py +++ b/src/tests/test_app_catalog.py @@ -44,7 +44,6 @@ class AnyStringWith(str): def setup_function(function): - # Clear apps catalog cache shutil.rmtree(APPS_CATALOG_CACHE, ignore_errors=True) @@ -54,7 +53,6 @@ def setup_function(function): def teardown_function(function): - # Clear apps catalog cache # Otherwise when using apps stuff after running the test, # we'll still have the dummy unusable list @@ -67,7 +65,6 @@ def teardown_function(function): def test_apps_catalog_init(mocker): - # Cache is empty assert not glob.glob(APPS_CATALOG_CACHE + "/*") # Conf doesn't exist yet @@ -91,7 +88,6 @@ def test_apps_catalog_init(mocker): def test_apps_catalog_emptylist(): - # Initialize ... _initialize_apps_catalog_system() @@ -104,7 +100,6 @@ def test_apps_catalog_emptylist(): def test_apps_catalog_update_nominal(mocker): - # Initialize ... _initialize_apps_catalog_system() @@ -113,7 +108,6 @@ def test_apps_catalog_update_nominal(mocker): # Update with requests_mock.Mocker() as m: - _actual_apps_catalog_api_url, # Mock the server response with a dummy apps catalog m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) @@ -139,12 +133,10 @@ def test_apps_catalog_update_nominal(mocker): def test_apps_catalog_update_404(mocker): - # Initialize ... _initialize_apps_catalog_system() with requests_mock.Mocker() as m: - # 404 error m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, status_code=404) @@ -155,12 +147,10 @@ def test_apps_catalog_update_404(mocker): def test_apps_catalog_update_timeout(mocker): - # Initialize ... _initialize_apps_catalog_system() with requests_mock.Mocker() as m: - # Timeout m.register_uri( "GET", APPS_CATALOG_DEFAULT_URL_FULL, exc=requests.exceptions.ConnectTimeout @@ -173,12 +163,10 @@ def test_apps_catalog_update_timeout(mocker): def test_apps_catalog_update_sslerror(mocker): - # Initialize ... _initialize_apps_catalog_system() with requests_mock.Mocker() as m: - # SSL error m.register_uri( "GET", APPS_CATALOG_DEFAULT_URL_FULL, exc=requests.exceptions.SSLError @@ -191,12 +179,10 @@ def test_apps_catalog_update_sslerror(mocker): def test_apps_catalog_update_corrupted(mocker): - # Initialize ... _initialize_apps_catalog_system() with requests_mock.Mocker() as m: - # Corrupted json m.register_uri( "GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG[:-2] @@ -209,7 +195,6 @@ def test_apps_catalog_update_corrupted(mocker): def test_apps_catalog_load_with_empty_cache(mocker): - # Initialize ... _initialize_apps_catalog_system() @@ -218,7 +203,6 @@ def test_apps_catalog_load_with_empty_cache(mocker): # Update with requests_mock.Mocker() as m: - # Mock the server response with a dummy apps catalog m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) @@ -237,7 +221,6 @@ def test_apps_catalog_load_with_empty_cache(mocker): def test_apps_catalog_load_with_conflicts_between_lists(mocker): - # Initialize ... _initialize_apps_catalog_system() @@ -253,7 +236,6 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker): # Update with requests_mock.Mocker() as m: - # Mock the server response with a dummy apps catalog # + the same apps catalog for the second list m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) @@ -277,13 +259,11 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker): def test_apps_catalog_load_with_oudated_api_version(mocker): - # Initialize ... _initialize_apps_catalog_system() # Update with requests_mock.Mocker() as m: - mocker.spy(m18n, "n") m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) _update_apps_catalog() @@ -300,7 +280,6 @@ def test_apps_catalog_load_with_oudated_api_version(mocker): # Update with requests_mock.Mocker() as m: - # Mock the server response with a dummy apps catalog m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) diff --git a/src/tests/test_app_config.py b/src/tests/test_app_config.py index d6cf8045d..4a74cbc0d 100644 --- a/src/tests/test_app_config.py +++ b/src/tests/test_app_config.py @@ -25,17 +25,14 @@ from yunohost.utils.error import YunohostError, YunohostValidationError def setup_function(function): - clean() def teardown_function(function): - clean() def clean(): - # Make sure we have a ssowat os.system("mkdir -p /etc/ssowat/") app_ssowatconf() @@ -43,7 +40,6 @@ def clean(): test_apps = ["config_app", "legacy_app"] for test_app in test_apps: - if _is_installed(test_app): app_remove(test_app) @@ -66,7 +62,6 @@ def clean(): @pytest.fixture() def legacy_app(request): - main_domain = _get_maindomain() app_install( @@ -85,7 +80,6 @@ def legacy_app(request): @pytest.fixture() def config_app(request): - app_install( os.path.join(get_test_apps_dir(), "config_app_ynh"), args="", @@ -101,27 +95,24 @@ def config_app(request): def test_app_config_get(config_app): - - user_create("alice", "Alice", "White", _get_maindomain(), "test123Ynh") + user_create("alice", _get_maindomain(), "test123Ynh", fullname="Alice White") assert isinstance(app_config_get(config_app), dict) assert isinstance(app_config_get(config_app, full=True), dict) assert isinstance(app_config_get(config_app, export=True), dict) assert isinstance(app_config_get(config_app, "main"), dict) assert isinstance(app_config_get(config_app, "main.components"), dict) - assert app_config_get(config_app, "main.components.boolean") == "0" + assert app_config_get(config_app, "main.components.boolean") == 0 user_delete("alice") def test_app_config_nopanel(legacy_app): - with pytest.raises(YunohostValidationError): app_config_get(legacy_app) def test_app_config_get_nonexistentstuff(config_app): - with pytest.raises(YunohostValidationError): app_config_get("nonexistent") @@ -140,17 +131,16 @@ def test_app_config_get_nonexistentstuff(config_app): def test_app_config_regular_setting(config_app): - - assert app_config_get(config_app, "main.components.boolean") == "0" + assert app_config_get(config_app, "main.components.boolean") == 0 app_config_set(config_app, "main.components.boolean", "no") - assert app_config_get(config_app, "main.components.boolean") == "0" + assert app_config_get(config_app, "main.components.boolean") == 0 assert app_setting(config_app, "boolean") == "0" app_config_set(config_app, "main.components.boolean", "yes") - assert app_config_get(config_app, "main.components.boolean") == "1" + assert app_config_get(config_app, "main.components.boolean") == 1 assert app_setting(config_app, "boolean") == "1" with pytest.raises(YunohostValidationError), patch.object( @@ -160,7 +150,6 @@ def test_app_config_regular_setting(config_app): def test_app_config_bind_on_file(config_app): - # c.f. conf/test.php in the config app assert '$arg5= "Arg5 value";' in read_file("/var/www/config_app/test.php") assert app_config_get(config_app, "bind.variable.arg5") == "Arg5 value" @@ -173,18 +162,17 @@ def test_app_config_bind_on_file(config_app): assert app_setting(config_app, "arg5") == "Foo Bar" -def test_app_config_custom_get(config_app): - - assert app_setting(config_app, "arg9") is None - assert ( - "Files in /var/www" - in app_config_get(config_app, "bind.function.arg9")["ask"]["en"] - ) - assert app_setting(config_app, "arg9") is None +# def test_app_config_custom_get(config_app): +# +# assert app_setting(config_app, "arg9") is None +# assert ( +# "Files in /var/www" +# in app_config_get(config_app, "bind.function.arg9")["ask"]["en"] +# ) +# assert app_setting(config_app, "arg9") is None def test_app_config_custom_validator(config_app): - # c.f. the config script # arg8 is a password that must be at least 8 chars assert not os.path.exists("/var/www/config_app/password") @@ -198,7 +186,6 @@ def test_app_config_custom_validator(config_app): def test_app_config_custom_set(config_app): - assert not os.path.exists("/var/www/config_app/password") assert app_setting(config_app, "arg8") is None diff --git a/src/tests/test_app_resources.py b/src/tests/test_app_resources.py new file mode 100644 index 000000000..d2df647a3 --- /dev/null +++ b/src/tests/test_app_resources.py @@ -0,0 +1,391 @@ +import os +import pytest + +from moulinette.utils.process import check_output + +from yunohost.app import app_setting +from yunohost.domain import _get_maindomain +from yunohost.utils.resources import ( + AppResource, + AppResourceManager, + AppResourceClassesByType, +) +from yunohost.permission import user_permission_list, permission_delete +from yunohost.firewall import firewall_list + +dummyfile = "/tmp/dummyappresource-testapp" + + +class DummyAppResource(AppResource): + type = "dummy" + + default_properties = { + "file": "/tmp/dummyappresource-__APP__", + "content": "foo", + } + + def provision_or_update(self, context): + open(self.file, "w").write(self.content) + + if self.content == "forbiddenvalue": + raise Exception("Emeged you used the forbidden value!1!£&") + + def deprovision(self, context): + os.system(f"rm -f {self.file}") + + +AppResourceClassesByType["dummy"] = DummyAppResource + + +def setup_function(function): + clean() + + os.system("mkdir /etc/yunohost/apps/testapp") + os.system("echo 'id: testapp' > /etc/yunohost/apps/testapp/settings.yml") + os.system("echo 'packaging_format = 2' > /etc/yunohost/apps/testapp/manifest.toml") + os.system("echo 'id = \"testapp\"' >> /etc/yunohost/apps/testapp/manifest.toml") + + +def teardown_function(function): + clean() + + +def clean(): + os.system(f"rm -f {dummyfile}") + os.system("rm -rf /etc/yunohost/apps/testapp") + os.system("rm -rf /var/www/testapp") + os.system("rm -rf /home/yunohost.app/testapp") + os.system("apt remove lolcat sl nyancat yarn >/dev/null 2>/dev/null") + os.system("userdel testapp 2>/dev/null") + + for p in user_permission_list()["permissions"]: + if p.startswith("testapp."): + permission_delete(p, force=True, sync_perm=False) + + +def test_provision_dummy(): + current = {"resources": {}} + wanted = {"resources": {"dummy": {}}} + + assert not os.path.exists(dummyfile) + AppResourceManager("testapp", current=current, wanted=wanted).apply( + rollback_and_raise_exception_if_failure=False + ) + assert open(dummyfile).read().strip() == "foo" + + +def test_deprovision_dummy(): + current = {"resources": {"dummy": {}}} + wanted = {"resources": {}} + + open(dummyfile, "w").write("foo") + + assert open(dummyfile).read().strip() == "foo" + AppResourceManager("testapp", current=current, wanted=wanted).apply( + rollback_and_raise_exception_if_failure=False + ) + assert not os.path.exists(dummyfile) + + +def test_provision_dummy_nondefaultvalue(): + current = {"resources": {}} + wanted = {"resources": {"dummy": {"content": "bar"}}} + + assert not os.path.exists(dummyfile) + AppResourceManager("testapp", current=current, wanted=wanted).apply( + rollback_and_raise_exception_if_failure=False + ) + assert open(dummyfile).read().strip() == "bar" + + +def test_update_dummy(): + current = {"resources": {"dummy": {}}} + wanted = {"resources": {"dummy": {"content": "bar"}}} + + open(dummyfile, "w").write("foo") + + assert open(dummyfile).read().strip() == "foo" + AppResourceManager("testapp", current=current, wanted=wanted).apply( + rollback_and_raise_exception_if_failure=False + ) + assert open(dummyfile).read().strip() == "bar" + + +def test_update_dummy_failwithrollback(): + current = {"resources": {"dummy": {}}} + wanted = {"resources": {"dummy": {"content": "forbiddenvalue"}}} + + open(dummyfile, "w").write("foo") + + assert open(dummyfile).read().strip() == "foo" + with pytest.raises(Exception): + AppResourceManager("testapp", current=current, wanted=wanted).apply( + rollback_and_raise_exception_if_failure=True + ) + assert open(dummyfile).read().strip() == "foo" + + +def test_resource_system_user(): + r = AppResourceClassesByType["system_user"] + + conf = {} + + assert os.system("getent passwd testapp 2>/dev/null") != 0 + + r(conf, "testapp").provision_or_update() + + assert os.system("getent passwd testapp >/dev/null") == 0 + assert os.system("groups testapp | grep -q 'sftp.app'") != 0 + + conf["allow_sftp"] = True + r(conf, "testapp").provision_or_update() + + assert os.system("getent passwd testapp >/dev/null") == 0 + assert os.system("groups testapp | grep -q 'sftp.app'") == 0 + + r(conf, "testapp").deprovision() + + assert os.system("getent passwd testapp 2>/dev/null") != 0 + + +def test_resource_install_dir(): + r = AppResourceClassesByType["install_dir"] + conf = {"owner": "nobody:rx", "group": "nogroup:rx"} + + # FIXME: should also check settings ? + # FIXME: should also check automigrate from final_path + # FIXME: should also test changing the install folder location ? + + assert not os.path.exists("/var/www/testapp") + + r(conf, "testapp").provision_or_update() + + assert os.path.exists("/var/www/testapp") + unixperms = check_output("ls -ld /var/www/testapp").split() + assert unixperms[0] == "dr-xr-x---" + assert unixperms[2] == "nobody" + assert unixperms[3] == "nogroup" + + conf["owner"] = "nobody:rwx" + conf["group"] = "www-data:x" + + r(conf, "testapp").provision_or_update() + + assert os.path.exists("/var/www/testapp") + unixperms = check_output("ls -ld /var/www/testapp").split() + assert unixperms[0] == "drwx--x---" + assert unixperms[2] == "nobody" + assert unixperms[3] == "www-data" + + r(conf, "testapp").deprovision() + + assert not os.path.exists("/var/www/testapp") + + +def test_resource_data_dir(): + r = AppResourceClassesByType["data_dir"] + conf = {"owner": "nobody:rx", "group": "nogroup:rx"} + + assert not os.path.exists("/home/yunohost.app/testapp") + + r(conf, "testapp").provision_or_update() + + assert os.path.exists("/home/yunohost.app/testapp") + unixperms = check_output("ls -ld /home/yunohost.app/testapp").split() + assert unixperms[0] == "dr-xr-x---" + assert unixperms[2] == "nobody" + assert unixperms[3] == "nogroup" + + conf["owner"] = "nobody:rwx" + conf["group"] = "www-data:x" + + r(conf, "testapp").provision_or_update() + + assert os.path.exists("/home/yunohost.app/testapp") + unixperms = check_output("ls -ld /home/yunohost.app/testapp").split() + assert unixperms[0] == "drwx--x---" + assert unixperms[2] == "nobody" + assert unixperms[3] == "www-data" + + r(conf, "testapp").deprovision() + + # FIXME : implement and check purge option + # assert not os.path.exists("/home/yunohost.app/testapp") + + +def test_resource_ports(): + r = AppResourceClassesByType["ports"] + conf = {} + + assert not app_setting("testapp", "port") + + r(conf, "testapp").provision_or_update() + + assert app_setting("testapp", "port") + + r(conf, "testapp").deprovision() + + assert not app_setting("testapp", "port") + + +def test_resource_ports_several(): + r = AppResourceClassesByType["ports"] + conf = {"main": {"default": 12345}, "foobar": {"default": 23456}} + + assert not app_setting("testapp", "port") + assert not app_setting("testapp", "port_foobar") + + r(conf, "testapp").provision_or_update() + + assert app_setting("testapp", "port") + assert app_setting("testapp", "port_foobar") + + r(conf, "testapp").deprovision() + + assert not app_setting("testapp", "port") + assert not app_setting("testapp", "port_foobar") + + +def test_resource_ports_firewall(): + r = AppResourceClassesByType["ports"] + conf = {"main": {"default": 12345}} + + r(conf, "testapp").provision_or_update() + + assert 12345 not in firewall_list()["opened_ports"] + + conf = {"main": {"default": 12345, "exposed": "TCP"}} + + r(conf, "testapp").provision_or_update() + + assert 12345 in firewall_list()["opened_ports"] + + r(conf, "testapp").deprovision() + + assert 12345 not in firewall_list()["opened_ports"] + + +def test_resource_database(): + r = AppResourceClassesByType["database"] + conf = {"type": "mysql"} + + assert os.system("mysqlshow 'testapp' >/dev/null 2>/dev/null") != 0 + assert not app_setting("testapp", "db_name") + assert not app_setting("testapp", "db_user") + assert not app_setting("testapp", "db_pwd") + + r(conf, "testapp").provision_or_update() + + assert os.system("mysqlshow 'testapp' >/dev/null 2>/dev/null") == 0 + assert app_setting("testapp", "db_name") + assert app_setting("testapp", "db_user") + assert app_setting("testapp", "db_pwd") + + r(conf, "testapp").deprovision() + + assert os.system("mysqlshow 'testapp' >/dev/null 2>/dev/null") != 0 + assert not app_setting("testapp", "db_name") + assert not app_setting("testapp", "db_user") + assert not app_setting("testapp", "db_pwd") + + +def test_resource_apt(): + r = AppResourceClassesByType["apt"] + conf = { + "packages": "nyancat, sl", + "extras": { + "yarn": { + "repo": "deb https://dl.yarnpkg.com/debian/ stable main", + "key": "https://dl.yarnpkg.com/debian/pubkey.gpg", + "packages": "yarn", + } + }, + } + + assert os.system("dpkg --list | grep -q 'ii *nyancat '") != 0 + assert os.system("dpkg --list | grep -q 'ii *sl '") != 0 + assert os.system("dpkg --list | grep -q 'ii *yarn '") != 0 + assert os.system("dpkg --list | grep -q 'ii *lolcat '") != 0 + assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") != 0 + + r(conf, "testapp").provision_or_update() + + assert os.system("dpkg --list | grep -q 'ii *nyancat '") == 0 + assert os.system("dpkg --list | grep -q 'ii *sl '") == 0 + assert os.system("dpkg --list | grep -q 'ii *yarn '") == 0 + assert ( + os.system("dpkg --list | grep -q 'ii *lolcat '") != 0 + ) # Lolcat shouldnt be installed yet + assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") == 0 + + conf["packages"] += ", lolcat" + r(conf, "testapp").provision_or_update() + + assert os.system("dpkg --list | grep -q 'ii *nyancat '") == 0 + assert os.system("dpkg --list | grep -q 'ii *sl '") == 0 + assert os.system("dpkg --list | grep -q 'ii *yarn '") == 0 + assert os.system("dpkg --list | grep -q 'ii *lolcat '") == 0 + assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") == 0 + + r(conf, "testapp").deprovision() + + assert os.system("dpkg --list | grep -q 'ii *nyancat '") != 0 + assert os.system("dpkg --list | grep -q 'ii *sl '") != 0 + assert os.system("dpkg --list | grep -q 'ii *yarn '") != 0 + assert os.system("dpkg --list | grep -q 'ii *lolcat '") != 0 + assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") != 0 + + +def test_resource_permissions(): + maindomain = _get_maindomain() + os.system(f"echo 'domain: {maindomain}' >> /etc/yunohost/apps/testapp/settings.yml") + os.system("echo 'path: /testapp' >> /etc/yunohost/apps/testapp/settings.yml") + + # A manager object is required to set the label of the app... + manager = AppResourceManager("testapp", current={}, wanted={"name": "Test App"}) + r = AppResourceClassesByType["permissions"] + conf = { + "main": { + "url": "/", + "allowed": "visitors" + # TODO: test protected? + }, + } + + res = user_permission_list(full=True)["permissions"] + assert not any(key.startswith("testapp.") for key in res) + + r(conf, "testapp", manager).provision_or_update() + + res = user_permission_list(full=True)["permissions"] + assert "testapp.main" in res + assert "visitors" in res["testapp.main"]["allowed"] + assert res["testapp.main"]["url"] == "/" + assert "testapp.admin" not in res + + conf["admin"] = {"url": "/admin", "allowed": ""} + + r(conf, "testapp", manager).provision_or_update() + + res = user_permission_list(full=True)["permissions"] + + assert "testapp.main" in list(res.keys()) + assert "visitors" in res["testapp.main"]["allowed"] + assert res["testapp.main"]["url"] == "/" + + assert "testapp.admin" in res + assert not res["testapp.admin"]["allowed"] + assert res["testapp.admin"]["url"] == "/admin" + + conf["admin"]["url"] = "/adminpanel" + + r(conf, "testapp", manager).provision_or_update() + + res = user_permission_list(full=True)["permissions"] + + assert res["testapp.admin"]["url"] == "/adminpanel" + + r(conf, "testapp").deprovision() + + res = user_permission_list(full=True)["permissions"] + assert "testapp.main" not in res diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index 2a808b5bd..d7a591a36 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -15,9 +15,11 @@ from yunohost.app import ( _is_installed, app_upgrade, app_map, + app_manifest, + app_info, ) from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.tests.test_permission import ( check_LDAP_db_integrity, check_permission_for_apps, @@ -26,17 +28,14 @@ from yunohost.permission import user_permission_list, permission_delete def setup_function(function): - clean() def teardown_function(function): - clean() def clean(): - # Make sure we have a ssowat os.system("mkdir -p /etc/ssowat/") app_ssowatconf() @@ -45,12 +44,12 @@ def clean(): "break_yo_system", "legacy_app", "legacy_app__2", + "manifestv2_app", "full_domain_app", "my_webapp", ] for test_app in test_apps: - if _is_installed(test_app): app_remove(test_app) @@ -92,7 +91,6 @@ def check_permission_for_apps_call(): @pytest.fixture(scope="module") def secondary_domain(request): - if "example.test" not in domain_list()["domains"]: domain_add("example.test") @@ -110,32 +108,31 @@ def secondary_domain(request): def app_expected_files(domain, app): - yield "/etc/nginx/conf.d/{}.d/{}.conf".format(domain, app) if app.startswith("legacy_app"): yield "/var/www/%s/index.html" % app yield "/etc/yunohost/apps/%s/settings.yml" % app - yield "/etc/yunohost/apps/%s/manifest.json" % app + if "manifestv2" in app or "my_webapp" in app: + yield "/etc/yunohost/apps/%s/manifest.toml" % app + else: + yield "/etc/yunohost/apps/%s/manifest.json" % app yield "/etc/yunohost/apps/%s/scripts/install" % app yield "/etc/yunohost/apps/%s/scripts/remove" % app def app_is_installed(domain, app): - return _is_installed(app) and all( os.path.exists(f) for f in app_expected_files(domain, app) ) def app_is_not_installed(domain, app): - return not _is_installed(app) and not all( os.path.exists(f) for f in app_expected_files(domain, app) ) def app_is_exposed_on_http(domain, path, message_in_page): - try: r = requests.get( "https://127.0.0.1" + path + "/", @@ -149,7 +146,6 @@ def app_is_exposed_on_http(domain, path, message_in_page): def install_legacy_app(domain, path, public=True): - app_install( os.path.join(get_test_apps_dir(), "legacy_app_ynh"), args="domain={}&path={}&is_public={}".format(domain, path, 1 if public else 0), @@ -157,8 +153,17 @@ def install_legacy_app(domain, path, public=True): ) -def install_full_domain_app(domain): +def install_manifestv2_app(domain, path, public=True): + app_install( + os.path.join(get_test_apps_dir(), "manifestv2_app_ynh"), + args="domain={}&path={}&init_main_permission={}".format( + domain, path, "visitors" if public else "all_users" + ), + force=True, + ) + +def install_full_domain_app(domain): app_install( os.path.join(get_test_apps_dir(), "full_domain_app_ynh"), args="domain=%s" % domain, @@ -167,7 +172,6 @@ def install_full_domain_app(domain): def install_break_yo_system(domain, breakwhat): - app_install( os.path.join(get_test_apps_dir(), "break_yo_system_ynh"), args="domain={}&breakwhat={}".format(domain, breakwhat), @@ -176,7 +180,6 @@ def install_break_yo_system(domain, breakwhat): def test_legacy_app_install_main_domain(): - main_domain = _get_maindomain() install_legacy_app(main_domain, "/legacy") @@ -195,12 +198,139 @@ def test_legacy_app_install_main_domain(): assert app_is_not_installed(main_domain, "legacy_app") +def test_legacy_app_manifest_preinstall(): + m = app_manifest(os.path.join(get_test_apps_dir(), "legacy_app_ynh")) + # v1 manifesto are expected to have been autoconverted to v2 + + assert "id" in m + assert "description" in m + assert "integration" in m + assert "install" in m + assert m["doc"] == {} + assert m["notifications"] == { + "PRE_INSTALL": {}, + "PRE_UPGRADE": {}, + "POST_INSTALL": {}, + "POST_UPGRADE": {}, + } + + +def test_manifestv2_app_manifest_preinstall(): + m = app_manifest(os.path.join(get_test_apps_dir(), "manifestv2_app_ynh")) + + assert "id" in m + assert "install" in m + assert "description" in m + assert "doc" in m + assert ( + "This is a dummy description of this app features" + in m["doc"]["DESCRIPTION"]["en"] + ) + assert ( + "Ceci est une fausse description des fonctionalités de l'app" + in m["doc"]["DESCRIPTION"]["fr"] + ) + assert "notifications" in m + assert ( + "This is a dummy disclaimer to display prior to the install" + in m["notifications"]["PRE_INSTALL"]["main"]["en"] + ) + assert ( + "Ceci est un faux disclaimer à présenter avant l'installation" + in m["notifications"]["PRE_INSTALL"]["main"]["fr"] + ) + + +def test_manifestv2_app_install_main_domain(): + main_domain = _get_maindomain() + + install_manifestv2_app(main_domain, "/manifestv2") + + app_map_ = app_map(raw=True) + assert main_domain in app_map_ + assert "/manifestv2" in app_map_[main_domain] + assert "id" in app_map_[main_domain]["/manifestv2"] + assert app_map_[main_domain]["/manifestv2"]["id"] == "manifestv2_app" + + assert app_is_installed(main_domain, "manifestv2_app") + assert app_is_exposed_on_http(main_domain, "/manifestv2", "Hextris") + + app_remove("manifestv2_app") + + assert app_is_not_installed(main_domain, "manifestv2_app") + + +def test_manifestv2_app_info_postinstall(): + main_domain = _get_maindomain() + install_manifestv2_app(main_domain, "/manifestv2") + m = app_info("manifestv2_app", full=True)["manifest"] + + assert "id" in m + assert "install" in m + assert "description" in m + assert "doc" in m + assert "The app install dir is /var/www/manifestv2_app" in m["doc"]["ADMIN"]["en"] + assert ( + "Le dossier d'install de l'app est /var/www/manifestv2_app" + in m["doc"]["ADMIN"]["fr"] + ) + assert "notifications" in m + assert ( + "The app install dir is /var/www/manifestv2_app" + in m["notifications"]["POST_INSTALL"]["main"]["en"] + ) + assert ( + "The app id is manifestv2_app" + in m["notifications"]["POST_INSTALL"]["main"]["en"] + ) + assert ( + f"The app url is {main_domain}/manifestv2" + in m["notifications"]["POST_INSTALL"]["main"]["en"] + ) + + +def test_manifestv2_app_info_preupgrade(monkeypatch): + manifest = app_manifest(os.path.join(get_test_apps_dir(), "manifestv2_app_ynh")) + + from yunohost.app_catalog import _load_apps_catalog as original_load_apps_catalog + + def custom_load_apps_catalog(*args, **kwargs): + res = original_load_apps_catalog(*args, **kwargs) + res["apps"]["manifestv2_app"] = { + "id": "manifestv2_app", + "level": 10, + "lastUpdate": 999999999, + "maintained": True, + "manifest": manifest, + "state": "working", + } + res["apps"]["manifestv2_app"]["manifest"]["version"] = "99999~ynh1" + + return res + + monkeypatch.setattr("yunohost.app._load_apps_catalog", custom_load_apps_catalog) + + main_domain = _get_maindomain() + install_manifestv2_app(main_domain, "/manifestv2") + i = app_info("manifestv2_app", full=True) + + assert i["upgradable"] == "yes" + assert i["new_version"] == "99999~ynh1" + # FIXME : as I write this test, I realize that this implies the catalog API + # does provide the notifications, which means the list builder script + # should parse the files in the original app repo, possibly with proper i18n etc + assert ( + "This is a dummy disclaimer to display prior to any upgrade" + in i["from_catalog"]["manifest"]["notifications"]["PRE_UPGRADE"]["main"]["en"] + ) + + def test_app_from_catalog(): main_domain = _get_maindomain() app_install( "my_webapp", - args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0", + args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&init_main_permission=visitors&with_mysql=0&phpversion=none", ) app_map_ = app_map(raw=True) assert main_domain in app_map_ @@ -209,7 +339,9 @@ def test_app_from_catalog(): assert app_map_[main_domain]["/site"]["id"] == "my_webapp" assert app_is_installed(main_domain, "my_webapp") - assert app_is_exposed_on_http(main_domain, "/site", "Custom Web App") + assert app_is_exposed_on_http( + main_domain, "/site", "you have just installed My Webapp" + ) # Try upgrade, should do nothing app_upgrade("my_webapp") @@ -222,7 +354,6 @@ def test_app_from_catalog(): def test_legacy_app_install_secondary_domain(secondary_domain): - install_legacy_app(secondary_domain, "/legacy") assert app_is_installed(secondary_domain, "legacy_app") @@ -234,7 +365,6 @@ def test_legacy_app_install_secondary_domain(secondary_domain): def test_legacy_app_install_secondary_domain_on_root(secondary_domain): - install_legacy_app(secondary_domain, "/") app_map_ = app_map(raw=True) @@ -252,7 +382,6 @@ def test_legacy_app_install_secondary_domain_on_root(secondary_domain): def test_legacy_app_install_private(secondary_domain): - install_legacy_app(secondary_domain, "/legacy", public=False) assert app_is_installed(secondary_domain, "legacy_app") @@ -266,7 +395,6 @@ def test_legacy_app_install_private(secondary_domain): def test_legacy_app_install_unknown_domain(mocker): - with pytest.raises(YunohostError): with message(mocker, "app_argument_invalid"): install_legacy_app("whatever.nope", "/legacy") @@ -275,7 +403,6 @@ def test_legacy_app_install_unknown_domain(mocker): def test_legacy_app_install_multiple_instances(secondary_domain): - install_legacy_app(secondary_domain, "/foo") install_legacy_app(secondary_domain, "/bar") @@ -297,7 +424,6 @@ def test_legacy_app_install_multiple_instances(secondary_domain): def test_legacy_app_install_path_unavailable(mocker, secondary_domain): - # These will be removed in teardown install_legacy_app(secondary_domain, "/legacy") @@ -310,7 +436,6 @@ def test_legacy_app_install_path_unavailable(mocker, secondary_domain): def test_legacy_app_install_with_nginx_down(mocker, secondary_domain): - os.system("systemctl stop nginx") with raiseYunohostError( @@ -320,7 +445,6 @@ def test_legacy_app_install_with_nginx_down(mocker, secondary_domain): def test_legacy_app_failed_install(mocker, secondary_domain): - # This will conflict with the folder that the app # attempts to create, making the install fail mkdir("/var/www/legacy_app/", 0o750) @@ -333,7 +457,6 @@ def test_legacy_app_failed_install(mocker, secondary_domain): def test_legacy_app_failed_remove(mocker, secondary_domain): - install_legacy_app(secondary_domain, "/legacy") # The remove script runs with set -eu and attempt to remove this @@ -353,14 +476,12 @@ def test_legacy_app_failed_remove(mocker, secondary_domain): def test_full_domain_app(secondary_domain): - install_full_domain_app(secondary_domain) assert app_is_exposed_on_http(secondary_domain, "/", "This is a dummy app") def test_full_domain_app_with_conflicts(mocker, secondary_domain): - install_legacy_app(secondary_domain, "/legacy") with raiseYunohostError(mocker, "app_full_domain_unavailable"): @@ -368,7 +489,6 @@ def test_full_domain_app_with_conflicts(mocker, secondary_domain): def test_systemfuckedup_during_app_install(mocker, secondary_domain): - with pytest.raises(YunohostError): with message(mocker, "app_install_failed"): with message(mocker, "app_action_broke_system"): @@ -378,7 +498,6 @@ def test_systemfuckedup_during_app_install(mocker, secondary_domain): def test_systemfuckedup_during_app_remove(mocker, secondary_domain): - install_break_yo_system(secondary_domain, breakwhat="remove") with pytest.raises(YunohostError): @@ -390,7 +509,6 @@ def test_systemfuckedup_during_app_remove(mocker, secondary_domain): def test_systemfuckedup_during_app_install_and_remove(mocker, secondary_domain): - with pytest.raises(YunohostError): with message(mocker, "app_install_failed"): with message(mocker, "app_action_broke_system"): @@ -400,7 +518,6 @@ def test_systemfuckedup_during_app_install_and_remove(mocker, secondary_domain): def test_systemfuckedup_during_app_upgrade(mocker, secondary_domain): - install_break_yo_system(secondary_domain, breakwhat="upgrade") with pytest.raises(YunohostError): @@ -412,7 +529,6 @@ def test_systemfuckedup_during_app_upgrade(mocker, secondary_domain): def test_failed_multiple_app_upgrade(mocker, secondary_domain): - install_legacy_app(secondary_domain, "/legacy") install_break_yo_system(secondary_domain, breakwhat="upgrade") @@ -427,3 +543,176 @@ def test_failed_multiple_app_upgrade(mocker, secondary_domain): "legacy": os.path.join(get_test_apps_dir(), "legacy_app_ynh"), }, ) + + +class TestMockedAppUpgrade: + """ + This class is here to test the logical workflow of app_upgrade and thus + mock nearly all side effects + """ + + def setup_method(self, method): + self.apps_list = [] + self.upgradable_apps_list = [] + + def _mock_app_upgrade(self, mocker): + # app list + self._installed_apps = mocker.patch( + "yunohost.app._installed_apps", side_effect=lambda: self.apps_list + ) + + # just check if an app is really installed + mocker.patch( + "yunohost.app._is_installed", side_effect=lambda app: app in self.apps_list + ) + + # app_dict = + mocker.patch( + "yunohost.app.app_info", + side_effect=lambda app, full: { + "upgradable": "yes" if app in self.upgradable_apps_list else "no", + "manifest": {"id": app}, + "version": "?", + }, + ) + + def custom_extract_app(app): + return ( + { + "version": "?", + "packaging_format": 1, + "id": app, + "notifications": {"PRE_UPGRADE": None, "POST_UPGRADE": None}, + }, + "MOCKED_BY_TEST", + ) + + # return (manifest, extracted_app_folder) + mocker.patch("yunohost.app._extract_app", side_effect=custom_extract_app) + + # for [(name, passed, values, err), ...] in + mocker.patch( + "yunohost.app._check_manifest_requirements", + return_value=[(None, True, None, None)], + ) + + # raise on failure + mocker.patch("yunohost.app._assert_system_is_sane_for_app", return_value=True) + + from os.path import exists # import the unmocked function + + def custom_os_path_exists(path): + if path.endswith("manifest.toml"): + return True + return exists(path) + + mocker.patch("os.path.exists", side_effect=custom_os_path_exists) + + # manifest = + mocker.patch( + "yunohost.app.read_toml", return_value={"arguments": {"install": []}} + ) + + # install_failed, failure_message_with_debug_instructions = + self.hook_exec_with_script_debug_if_failure = mocker.patch( + "yunohost.hook.hook_exec_with_script_debug_if_failure", + return_value=(False, ""), + ) + # settings = + mocker.patch("yunohost.app._get_app_settings", return_value={}) + # return nothing + mocker.patch("yunohost.app._set_app_settings") + + from os import listdir # import the unmocked function + + def custom_os_listdir(path): + if path.endswith("MOCKED_BY_TEST"): + return [] + return listdir(path) + + mocker.patch("os.listdir", side_effect=custom_os_listdir) + mocker.patch("yunohost.app.rm") + mocker.patch("yunohost.app.cp") + mocker.patch("shutil.rmtree") + mocker.patch("yunohost.app.chmod") + mocker.patch("yunohost.app.chown") + mocker.patch("yunohost.app.app_ssowatconf") + + def test_app_upgrade_no_apps(self, mocker): + self._mock_app_upgrade(mocker) + + with pytest.raises(YunohostValidationError): + app_upgrade() + + def test_app_upgrade_app_not_install(self, mocker): + self._mock_app_upgrade(mocker) + + with pytest.raises(YunohostValidationError): + app_upgrade("some_app") + + def test_app_upgrade_one_app(self, mocker): + self._mock_app_upgrade(mocker) + self.apps_list = ["some_app"] + + # yunohost is happy, not apps to upgrade + app_upgrade() + + self.hook_exec_with_script_debug_if_failure.assert_not_called() + + self.upgradable_apps_list.append("some_app") + app_upgrade() + + self.hook_exec_with_script_debug_if_failure.assert_called_once() + assert ( + self.hook_exec_with_script_debug_if_failure.call_args.kwargs["env"][ + "YNH_APP_ID" + ] + == "some_app" + ) + + def test_app_upgrade_continue_on_failure(self, mocker): + self._mock_app_upgrade(mocker) + self.apps_list = ["a", "b", "c"] + self.upgradable_apps_list = self.apps_list + + def fails_on_b(self, *args, env, **kwargs): + if env["YNH_APP_ID"] == "b": + return True, "failed" + return False, "ok" + + self.hook_exec_with_script_debug_if_failure.side_effect = fails_on_b + + with pytest.raises(YunohostError): + app_upgrade() + + app_upgrade(continue_on_failure=True) + + def test_app_upgrade_continue_on_failure_broken_system(self, mocker): + """--continue-on-failure should stop on a broken system""" + + self._mock_app_upgrade(mocker) + self.apps_list = ["a", "broke_the_system", "c"] + self.upgradable_apps_list = self.apps_list + + def fails_on_b(self, *args, env, **kwargs): + if env["YNH_APP_ID"] == "broke_the_system": + return True, "failed" + return False, "ok" + + self.hook_exec_with_script_debug_if_failure.side_effect = fails_on_b + + def _assert_system_is_sane_for_app(manifest, state): + if state == "post" and manifest["id"] == "broke_the_system": + raise Exception() + return True + + mocker.patch( + "yunohost.app._assert_system_is_sane_for_app", + side_effect=_assert_system_is_sane_for_app, + ) + + with pytest.raises(YunohostError): + app_upgrade() + + with pytest.raises(YunohostError): + app_upgrade(continue_on_failure=True) diff --git a/src/tests/test_appurl.py b/src/tests/test_appurl.py index c036ae28a..d0c55f732 100644 --- a/src/tests/test_appurl.py +++ b/src/tests/test_appurl.py @@ -18,7 +18,6 @@ maindomain = _get_maindomain() def setup_function(function): - try: app_remove("register_url_app") except Exception: @@ -26,7 +25,6 @@ def setup_function(function): def teardown_function(function): - try: app_remove("register_url_app") except Exception: @@ -34,7 +32,6 @@ def teardown_function(function): def test_parse_app_instance_name(): - assert _parse_app_instance_name("yolo") == ("yolo", 1) assert _parse_app_instance_name("yolo1") == ("yolo1", 1) assert _parse_app_instance_name("yolo__0") == ("yolo__0", 1) @@ -72,8 +69,23 @@ def test_repo_url_definition(): assert _is_app_repo_url("git@github.com:YunoHost-Apps/foobar_ynh.git") assert _is_app_repo_url("https://git.super.host/~max/foobar_ynh") + ### Gitea + assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh") + assert _is_app_repo_url( + "https://gitea.instance.tld/user/repo_ynh/src/branch/branch_name" + ) + assert _is_app_repo_url("https://gitea.instance.tld/user/repo_ynh/src/tag/tag_name") + assert _is_app_repo_url( + "https://gitea.instance.tld/user/repo_ynh/src/commit/abcd1234" + ) + + ### Invalid patterns + + # no schema assert not _is_app_repo_url("github.com/YunoHost-Apps/foobar_ynh") + # http assert not _is_app_repo_url("http://github.com/YunoHost-Apps/foobar_ynh") + # does not end in `_ynh` assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_wat") assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh_wat") assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar/tree/testing") @@ -86,7 +98,6 @@ def test_repo_url_definition(): def test_urlavailable(): - # Except the maindomain/macnuggets to be available assert domain_url_available(maindomain, "/macnuggets") @@ -96,7 +107,6 @@ def test_urlavailable(): def test_registerurl(): - app_install( os.path.join(get_test_apps_dir(), "register_url_app_ynh"), args="domain={}&path={}".format(maindomain, "/urlregisterapp"), @@ -115,7 +125,6 @@ def test_registerurl(): def test_registerurl_baddomain(): - with pytest.raises(YunohostError): app_install( os.path.join(get_test_apps_dir(), "register_url_app_ynh"), diff --git a/src/tests/test_backuprestore.py b/src/tests/test_backuprestore.py index 03c3aa0c7..bca1b29a5 100644 --- a/src/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -6,6 +6,8 @@ from mock import patch from .conftest import message, raiseYunohostError, get_test_apps_dir +from moulinette.utils.text import random_ascii + from yunohost.app import app_install, app_remove, app_ssowatconf from yunohost.app import _is_installed from yunohost.backup import ( @@ -30,7 +32,6 @@ maindomain = "" def setup_function(function): - global maindomain maindomain = _get_maindomain() @@ -54,7 +55,7 @@ def setup_function(function): if "with_legacy_app_installed" in markers: assert not app_is_installed("legacy_app") - install_app("legacy_app_ynh", "/yolo") + install_app("legacy_app_ynh", "/yolo", "&is_public=true") assert app_is_installed("legacy_app") if "with_backup_recommended_app_installed" in markers: @@ -77,7 +78,7 @@ def setup_function(function): if "with_permission_app_installed" in markers: assert not app_is_installed("permissions_app") - user_create("alice", "Alice", "White", maindomain, "test123Ynh") + user_create("alice", maindomain, "test123Ynh", fullname="Alice White") with patch.object(os, "isatty", return_value=False): install_app("permissions_app_ynh", "/urlpermissionapp" "&admin=alice") assert app_is_installed("permissions_app") @@ -89,7 +90,6 @@ def setup_function(function): def teardown_function(function): - assert tmp_backup_directory_is_empty() reset_ssowat_conf() @@ -133,7 +133,6 @@ def check_permission_for_apps_call(): def app_is_installed(app): - if app == "permissions_app": return _is_installed(app) @@ -147,7 +146,6 @@ def app_is_installed(app): def backup_test_dependencies_are_met(): - # Dummy test apps (or backup archives) assert os.path.exists( os.path.join(get_test_apps_dir(), "backup_wordpress_from_4p2") @@ -161,7 +159,6 @@ def backup_test_dependencies_are_met(): def tmp_backup_directory_is_empty(): - if not os.path.exists("/home/yunohost.backup/tmp/"): return True else: @@ -169,7 +166,6 @@ def tmp_backup_directory_is_empty(): def clean_tmp_backup_directory(): - if tmp_backup_directory_is_empty(): return @@ -191,27 +187,23 @@ def clean_tmp_backup_directory(): def reset_ssowat_conf(): - # Make sure we have a ssowat os.system("mkdir -p /etc/ssowat/") app_ssowatconf() def delete_all_backups(): - for archive in backup_list()["archives"]: backup_delete(archive) def uninstall_test_apps_if_needed(): - for app in ["legacy_app", "backup_recommended_app", "wordpress", "permissions_app"]: if _is_installed(app): app_remove(app) def install_app(app, path, additionnal_args=""): - app_install( os.path.join(get_test_apps_dir(), app), args="domain={}&path={}{}".format(maindomain, path, additionnal_args), @@ -220,7 +212,6 @@ def install_app(app, path, additionnal_args=""): def add_archive_wordpress_from_4p2(): - os.system("mkdir -p /home/yunohost.backup/archives") os.system( @@ -231,7 +222,6 @@ def add_archive_wordpress_from_4p2(): def add_archive_system_from_4p2(): - os.system("mkdir -p /home/yunohost.backup/archives") os.system( @@ -247,10 +237,10 @@ def add_archive_system_from_4p2(): def test_backup_only_ldap(mocker): - # Create the backup - with message(mocker, "backup_created"): - backup_create(system=["conf_ldap"], apps=None) + name = random_ascii(8) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=["conf_ldap"], apps=None) archives = backup_list()["archives"] assert len(archives) == 1 @@ -262,7 +252,6 @@ def test_backup_only_ldap(mocker): def test_backup_system_part_that_does_not_exists(mocker): - # Create the backup with message(mocker, "backup_hook_unknown", hook="doesnt_exist"): with raiseYunohostError(mocker, "backup_nothings_done"): @@ -275,10 +264,10 @@ def test_backup_system_part_that_does_not_exists(mocker): def test_backup_and_restore_all_sys(mocker): - + name = random_ascii(8) # Create the backup - with message(mocker, "backup_created"): - backup_create(system=[], apps=None) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=[], apps=None) archives = backup_list()["archives"] assert len(archives) == 1 @@ -309,10 +298,10 @@ def test_backup_and_restore_all_sys(mocker): @pytest.mark.with_system_archive_from_4p2 def test_restore_system_from_Ynh4p2(monkeypatch, mocker): - + name = random_ascii(8) # Backup current system - with message(mocker, "backup_created"): - backup_create(system=[], apps=None) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=[], apps=None) archives = backup_list()["archives"] assert len(archives) == 2 @@ -337,7 +326,6 @@ def test_restore_system_from_Ynh4p2(monkeypatch, mocker): @pytest.mark.with_backup_recommended_app_installed def test_backup_script_failure_handling(monkeypatch, mocker): def custom_hook_exec(name, *args, **kwargs): - if os.path.basename(name).startswith("backup_"): raise Exception else: @@ -355,13 +343,15 @@ def test_backup_script_failure_handling(monkeypatch, mocker): @pytest.mark.with_backup_recommended_app_installed def test_backup_not_enough_free_space(monkeypatch, mocker): - def custom_disk_usage(path): + def custom_space_used_by_directory(path, *args, **kwargs): return 99999999999999999 def custom_free_space_in_directory(dirpath): return 0 - monkeypatch.setattr("yunohost.backup.disk_usage", custom_disk_usage) + monkeypatch.setattr( + "yunohost.backup.space_used_by_directory", custom_space_used_by_directory + ) monkeypatch.setattr( "yunohost.backup.free_space_in_directory", custom_free_space_in_directory ) @@ -371,7 +361,6 @@ def test_backup_not_enough_free_space(monkeypatch, mocker): def test_backup_app_not_installed(mocker): - assert not _is_installed("wordpress") with message(mocker, "unbackup_app", app="wordpress"): @@ -381,7 +370,6 @@ def test_backup_app_not_installed(mocker): @pytest.mark.with_backup_recommended_app_installed def test_backup_app_with_no_backup_script(mocker): - backup_script = "/etc/yunohost/apps/backup_recommended_app/scripts/backup" os.system("rm %s" % backup_script) assert not os.path.exists(backup_script) @@ -395,7 +383,6 @@ def test_backup_app_with_no_backup_script(mocker): @pytest.mark.with_backup_recommended_app_installed def test_backup_app_with_no_restore_script(mocker): - restore_script = "/etc/yunohost/apps/backup_recommended_app/scripts/restore" os.system("rm %s" % restore_script) assert not os.path.exists(restore_script) @@ -411,17 +398,17 @@ def test_backup_app_with_no_restore_script(mocker): @pytest.mark.clean_opt_dir def test_backup_with_different_output_directory(mocker): - + name = random_ascii(8) # Create the backup - with message(mocker, "backup_created"): + with message(mocker, "backup_created", name=name): backup_create( system=["conf_ynh_settings"], apps=None, output_directory="/opt/test_backup_output_directory", - name="backup", + name=name, ) - assert os.path.exists("/opt/test_backup_output_directory/backup.tar") + assert os.path.exists(f"/opt/test_backup_output_directory/{name}.tar") archives = backup_list()["archives"] assert len(archives) == 1 @@ -434,15 +421,15 @@ def test_backup_with_different_output_directory(mocker): @pytest.mark.clean_opt_dir def test_backup_using_copy_method(mocker): - # Create the backup - with message(mocker, "backup_created"): + name = random_ascii(8) + with message(mocker, "backup_created", name=name): backup_create( system=["conf_ynh_settings"], apps=None, output_directory="/opt/test_backup_output_directory", methods=["copy"], - name="backup", + name=name, ) assert os.path.exists("/opt/test_backup_output_directory/info.json") @@ -456,7 +443,6 @@ def test_backup_using_copy_method(mocker): @pytest.mark.with_wordpress_archive_from_4p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_wordpress_from_Ynh4p2(mocker): - with message(mocker, "restore_complete"): backup_restore( system=None, name=backup_list()["archives"][0], apps=["wordpress"] @@ -505,7 +491,6 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker): @pytest.mark.with_wordpress_archive_from_4p2 def test_restore_app_not_in_backup(mocker): - assert not _is_installed("wordpress") assert not _is_installed("yoloswag") @@ -522,7 +507,6 @@ def test_restore_app_not_in_backup(mocker): @pytest.mark.with_wordpress_archive_from_4p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_already_installed(mocker): - assert not _is_installed("wordpress") with message(mocker, "restore_complete"): @@ -542,25 +526,21 @@ def test_restore_app_already_installed(mocker): @pytest.mark.with_legacy_app_installed def test_backup_and_restore_legacy_app(mocker): - _test_backup_and_restore_app(mocker, "legacy_app") @pytest.mark.with_backup_recommended_app_installed def test_backup_and_restore_recommended_app(mocker): - _test_backup_and_restore_app(mocker, "backup_recommended_app") @pytest.mark.with_backup_recommended_app_installed_with_ynh_restore def test_backup_and_restore_with_ynh_restore(mocker): - _test_backup_and_restore_app(mocker, "backup_recommended_app") @pytest.mark.with_permission_app_installed def test_backup_and_restore_permission_app(mocker): - res = user_permission_list(full=True)["permissions"] assert "permissions_app.main" in res assert "permissions_app.admin" in res @@ -591,10 +571,10 @@ def test_backup_and_restore_permission_app(mocker): def _test_backup_and_restore_app(mocker, app): - # Create a backup of this app - with message(mocker, "backup_created"): - backup_create(system=None, apps=[app]) + name = random_ascii(8) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=None, apps=[app]) archives = backup_list()["archives"] assert len(archives) == 1 @@ -626,7 +606,6 @@ def _test_backup_and_restore_app(mocker, app): def test_restore_archive_with_no_json(mocker): - # Create a backup with no info.json associated os.system("touch /tmp/afile") os.system("tar -cvf /home/yunohost.backup/archives/badbackup.tar /tmp/afile") @@ -639,7 +618,6 @@ def test_restore_archive_with_no_json(mocker): @pytest.mark.with_wordpress_archive_from_4p2 def test_restore_archive_with_bad_archive(mocker): - # Break the archive os.system( "head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_4p2.tar > /home/yunohost.backup/archives/backup_wordpress_from_4p2_bad.tar" @@ -654,13 +632,13 @@ def test_restore_archive_with_bad_archive(mocker): def test_restore_archive_with_custom_hook(mocker): - custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, "restore") os.system("touch %s/99-yolo" % custom_restore_hook_folder) # Backup with custom hook system - with message(mocker, "backup_created"): - backup_create(system=[], apps=None) + name = random_ascii(8) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=[], apps=None) archives = backup_list()["archives"] assert len(archives) == 1 @@ -697,5 +675,6 @@ def test_backup_binds_are_readonly(mocker, monkeypatch): ) # Create the backup - with message(mocker, "backup_created"): - backup_create(system=[]) + name = random_ascii(8) + with message(mocker, "backup_created", name=name): + backup_create(name=name, system=[]) diff --git a/src/tests/test_dns.py b/src/tests/test_dns.py index a23ac7982..e896d9c9f 100644 --- a/src/tests/test_dns.py +++ b/src/tests/test_dns.py @@ -12,12 +12,10 @@ from yunohost.dns import ( def setup_function(function): - clean() def teardown_function(function): - clean() @@ -76,7 +74,6 @@ def example_domain(): def test_domain_dns_suggest(example_domain): - assert _build_dns_conf(example_domain) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index 95a33e0ba..b414c21d8 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -19,7 +19,6 @@ TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"] def setup_function(function): - # Save domain list in variable to avoid multiple calls to domain_list() domains = domain_list()["domains"] @@ -52,7 +51,6 @@ def setup_function(function): def teardown_function(function): - clean() @@ -102,7 +100,6 @@ def test_domain_config_get_default(): def test_domain_config_get_export(): - assert domain_config_get(TEST_DOMAINS[0], export=True)["xmpp"] == 1 assert domain_config_get(TEST_DOMAINS[1], export=True)["xmpp"] == 0 diff --git a/src/tests/test_ldapauth.py b/src/tests/test_ldapauth.py index a95dea443..9e3ae36cc 100644 --- a/src/tests/test_ldapauth.py +++ b/src/tests/test_ldapauth.py @@ -2,58 +2,79 @@ import pytest import os from yunohost.authenticators.ldap_admin import Authenticator as LDAPAuth -from yunohost.tools import tools_adminpw +from yunohost.user import user_create, user_list, user_update, user_delete +from yunohost.domain import _get_maindomain from moulinette import m18n from moulinette.core import MoulinetteError def setup_function(function): + for u in user_list()["users"]: + user_delete(u, purge=True) - if os.system("systemctl is-active slapd") != 0: + maindomain = _get_maindomain() + + if os.system("systemctl is-active slapd >/dev/null") != 0: os.system("systemctl start slapd && sleep 3") - tools_adminpw("yunohost", check_strength=False) + user_create("alice", maindomain, "Yunohost", admin=True, fullname="Alice White") + user_create("bob", maindomain, "test123Ynh", fullname="Bob Snow") + + +def teardown_function(): + os.system("systemctl is-active slapd >/dev/null || systemctl start slapd; sleep 5") + + for u in user_list()["users"]: + user_delete(u, purge=True) def test_authenticate(): - LDAPAuth().authenticate_credentials(credentials="yunohost") + LDAPAuth().authenticate_credentials(credentials="alice:Yunohost") + + +def test_authenticate_with_no_user(): + with pytest.raises(MoulinetteError): + LDAPAuth().authenticate_credentials(credentials="Yunohost") + + with pytest.raises(MoulinetteError): + LDAPAuth().authenticate_credentials(credentials=":Yunohost") + + +def test_authenticate_with_user_who_is_not_admin(): + with pytest.raises(MoulinetteError) as exception: + LDAPAuth().authenticate_credentials(credentials="bob:test123Ynh") + + translation = m18n.n("invalid_credentials") + expected_msg = translation.format() + assert expected_msg in str(exception) def test_authenticate_with_wrong_password(): with pytest.raises(MoulinetteError) as exception: - LDAPAuth().authenticate_credentials(credentials="bad_password_lul") + LDAPAuth().authenticate_credentials(credentials="alice:bad_password_lul") - translation = m18n.n("invalid_password") + translation = m18n.n("invalid_credentials") expected_msg = translation.format() assert expected_msg in str(exception) def test_authenticate_server_down(mocker): - os.system("systemctl stop slapd && sleep 3") + os.system("systemctl stop slapd && sleep 5") - # Now if slapd is down, moulinette tries to restart it - mocker.patch("os.system") - mocker.patch("time.sleep") - with pytest.raises(MoulinetteError) as exception: - LDAPAuth().authenticate_credentials(credentials="yunohost") - - translation = m18n.n("ldap_server_down") - expected_msg = translation.format() - assert expected_msg in str(exception) + LDAPAuth().authenticate_credentials(credentials="alice:Yunohost") def test_authenticate_change_password(): + LDAPAuth().authenticate_credentials(credentials="alice:Yunohost") - LDAPAuth().authenticate_credentials(credentials="yunohost") - - tools_adminpw("plopette", check_strength=False) + user_update("alice", change_password="plopette") with pytest.raises(MoulinetteError) as exception: - LDAPAuth().authenticate_credentials(credentials="yunohost") + LDAPAuth().authenticate_credentials(credentials="alice:Yunohost") - translation = m18n.n("invalid_password") + translation = m18n.n("invalid_credentials") expected_msg = translation.format() assert expected_msg in str(exception) - LDAPAuth().authenticate_credentials(credentials="plopette") + LDAPAuth().authenticate_credentials(credentials="alice:plopette") diff --git a/src/tests/test_permission.py b/src/tests/test_permission.py index 4e7f9f53d..10bd018d2 100644 --- a/src/tests/test_permission.py +++ b/src/tests/test_permission.py @@ -78,6 +78,7 @@ def _permission_create_with_dummy_app( "name": app, "id": app, "description": {"en": "Dummy app to test permissions"}, + "arguments": {"install": []}, }, f, ) @@ -108,7 +109,7 @@ def clean_user_groups_permission(): user_delete(u) for g in user_group_list()["groups"]: - if g not in ["all_users", "visitors"]: + if g not in ["all_users", "visitors", "admins"]: user_group_delete(g) for p in user_permission_list()["permissions"]: @@ -157,8 +158,8 @@ def setup_function(function): socket.getaddrinfo = new_getaddrinfo - user_create("alice", "Alice", "White", maindomain, dummy_password) - user_create("bob", "Bob", "Snow", maindomain, dummy_password) + user_create("alice", maindomain, dummy_password, fullname="Alice White") + user_create("bob", maindomain, dummy_password, fullname="Bob Snow") _permission_create_with_dummy_app( permission="wiki.main", url="/", @@ -257,7 +258,7 @@ def check_LDAP_db_integrity(): for user in user_search: user_dn = "uid=" + user["uid"][0] + ",ou=users,dc=yunohost,dc=org" - group_list = [_ldap_path_extract(m, "cn") for m in user["memberOf"]] + group_list = [_ldap_path_extract(m, "cn") for m in user.get("memberOf", [])] permission_list = [ _ldap_path_extract(m, "cn") for m in user.get("permission", []) ] @@ -353,7 +354,6 @@ def check_permission_for_apps(): def can_access_webpage(webpath, logged_as=None): - webpath = webpath.rstrip("/") sso_url = "https://" + maindomain + "/yunohost/sso/" @@ -1093,7 +1093,6 @@ def test_permission_protection_management_by_helper(): @pytest.mark.other_domains(number=1) def test_permission_app_propagation_on_ssowat(): - app_install( os.path.join(get_test_apps_dir(), "permissions_app_ynh"), args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s" @@ -1130,7 +1129,6 @@ def test_permission_app_propagation_on_ssowat(): @pytest.mark.other_domains(number=1) def test_permission_legacy_app_propagation_on_ssowat(): - app_install( os.path.join(get_test_apps_dir(), "legacy_app_ynh"), args="domain=%s&domain_2=%s&path=%s&is_public=1" diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 5917d32d4..e23be9925 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -1,20 +1,28 @@ +import inspect import sys import pytest import os +import tempfile +from contextlib import contextmanager from mock import patch from io import StringIO +from typing import Any, Literal, Sequence, TypedDict, Union + +from _pytest.mark.structures import ParameterSet + from moulinette import Moulinette - -from yunohost import domain, user -from yunohost.utils.config import ( +from yunohost import app, domain, user +from yunohost.utils.form import ( + OPTIONS, ask_questions_and_parse_answers, - PasswordQuestion, - DomainQuestion, - PathQuestion, - BooleanQuestion, - FileQuestion, + DisplayTextOption, + PasswordOption, + DomainOption, + WebPathOption, + BooleanOption, + FileOption, evaluate_simple_js_expression, ) from yunohost.utils.error import YunohostError, YunohostValidationError @@ -23,69 +31,1964 @@ from yunohost.utils.error import YunohostError, YunohostValidationError """ Argument default format: { - "name": "the_name", - "type": "one_of_the_available_type", // "sting" is not specified - "ask": { - "en": "the question in english", - "fr": "the question in french" - }, - "help": { - "en": "some help text in english", - "fr": "some help text in french" - }, - "example": "an example value", // optional - "default", "some stuff", // optional, not available for all types - "optional": true // optional, will skip if not answered + "the_name": { + "type": "one_of_the_available_type", // "sting" is not specified + "ask": { + "en": "the question in english", + "fr": "the question in french" + }, + "help": { + "en": "some help text in english", + "fr": "some help text in french" + }, + "example": "an example value", // optional + "default", "some stuff", // optional, not available for all types + "optional": true // optional, will skip if not answered + } } User answers: -{"name": "value", ...} +{"the_name": "value", ...} """ -def test_question_empty(): - ask_questions_and_parse_answers([], {}) == [] +# ╭───────────────────────────────────────────────────────╮ +# │ ┌─╮╭─┐╶┬╴╭─╴╷ ╷╶┬╴╭╮╷╭─╮ │ +# │ ├─╯├─┤ │ │ ├─┤ │ ││││╶╮ │ +# │ ╵ ╵ ╵ ╵ ╰─╴╵ ╵╶┴╴╵╰╯╰─╯ │ +# ╰───────────────────────────────────────────────────────╯ -def test_question_string(): - questions = [ - { - "name": "some_string", - "type": "string", - } +@contextmanager +def patch_isatty(isatty): + with patch.object(os, "isatty", return_value=isatty): + yield + + +@contextmanager +def patch_interface(interface: Literal["api", "cli"] = "api"): + with patch.object(Moulinette.interface, "type", interface), patch_isatty( + interface == "cli" + ): + yield + + +@contextmanager +def patch_prompt(return_value): + with patch_interface("cli"), patch.object( + Moulinette, "prompt", return_value=return_value + ) as prompt: + yield prompt + + +@pytest.fixture +def patch_no_tty(): + with patch_isatty(False): + yield + + +@pytest.fixture +def patch_with_tty(): + with patch_isatty(True): + yield + + +# ╭───────────────────────────────────────────────────────╮ +# │ ╭─╴╭─╴┌─╴╭╮╷╭─┐┌─╮╶┬╴╭─╮╭─╴ │ +# │ ╰─╮│ ├─╴│││├─┤├┬╯ │ │ │╰─╮ │ +# │ ╶─╯╰─╴╰─╴╵╰╯╵ ╵╵ ╰╶┴╴╰─╯╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + + +MinScenario = tuple[Any, Union[Literal["FAIL"], Any]] +PartialScenario = tuple[Any, Union[Literal["FAIL"], Any], dict[str, Any]] +FullScenario = tuple[Any, Union[Literal["FAIL"], Any], dict[str, Any], dict[str, Any]] + +Scenario = Union[ + MinScenario, + PartialScenario, + FullScenario, + "InnerScenario", +] + + +class InnerScenario(TypedDict, total=False): + scenarios: Sequence[Scenario] + raw_options: Sequence[dict[str, Any]] + data: Sequence[dict[str, Any]] + + +# ╭───────────────────────────────────────────────────────╮ +# │ Scenario generators/helpers │ +# ╰───────────────────────────────────────────────────────╯ + + +def get_hydrated_scenarios(raw_options, scenarios, data=[{}]): + """ + Normalize and hydrate a mixed list of scenarios to proper tuple/pytest.param flattened list values. + + Example:: + scenarios = [ + { + "raw_options": [{}, {"optional": True}], + "scenarios": [ + ("", "value", {"default": "value"}), + *unchanged("value", "other"), + ] + }, + *all_fails(-1, 0, 1, raw_options={"optional": True}), + *xfail(scenarios=[(True, "True"), (False, "False)], reason="..."), + ] + # Is exactly the same as + scenarios = [ + ("", "value", {"default": "value"}), + ("", "value", {"optional": True, "default": "value"}), + ("value", "value", {}), + ("value", "value", {"optional": True}), + ("other", "other", {}), + ("other", "other", {"optional": True}), + (-1, FAIL, {"optional": True}), + (0, FAIL, {"optional": True}), + (1, FAIL, {"optional": True}), + pytest.param(True, "True", {}, marks=pytest.mark.xfail(reason="...")), + pytest.param(False, "False", {}, marks=pytest.mark.xfail(reason="...")), + ] + """ + hydrated_scenarios = [] + for raw_option in raw_options: + for mocked_data in data: + for scenario in scenarios: + if isinstance(scenario, dict): + merged_raw_options = [ + {**raw_option, **raw_opt} + for raw_opt in scenario.get("raw_options", [{}]) + ] + hydrated_scenarios += get_hydrated_scenarios( + merged_raw_options, + scenario["scenarios"], + scenario.get("data", [mocked_data]), + ) + elif isinstance(scenario, ParameterSet): + intake, output, custom_raw_option = ( + scenario.values + if len(scenario.values) == 3 + else (*scenario.values, {}) + ) + merged_raw_option = {**raw_option, **custom_raw_option} + hydrated_scenarios.append( + pytest.param( + intake, + output, + merged_raw_option, + mocked_data, + marks=scenario.marks, + ) + ) + elif isinstance(scenario, tuple): + intake, output, custom_raw_option = ( + scenario if len(scenario) == 3 else (*scenario, {}) + ) + merged_raw_option = {**raw_option, **custom_raw_option} + hydrated_scenarios.append( + (intake, output, merged_raw_option, mocked_data) + ) + else: + raise Exception( + "Test scenario should be tuple(intake, output, raw_option), pytest.param(intake, output, raw_option) or dict(raw_options, scenarios, data)" + ) + + return hydrated_scenarios + + +def generate_test_name(intake, output, raw_option, data): + values_as_str = [] + for value in (intake, output): + if isinstance(value, str) and value != FAIL: + values_as_str.append(f"'{value}'") + elif inspect.isclass(value) and issubclass(value, Exception): + values_as_str.append(value.__name__) + else: + values_as_str.append(value) + name = f"{values_as_str[0]} -> {values_as_str[1]}" + + keys = [ + "=".join( + [ + key, + str(raw_option[key]) + if not isinstance(raw_option[key], str) + else f"'{raw_option[key]}'", + ] + ) + for key in raw_option.keys() + if key not in ("id", "type") ] - 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" + if keys: + name += " (" + ",".join(keys) + ")" + return name -def test_question_string_from_query_string(): +def pytest_generate_tests(metafunc): + """ + Pytest test factory that, for each `BaseTest` subclasses, parametrize its + methods if it requires it by checking the method's parameters. + For those and based on their `cls.scenarios`, a series of `pytest.param` are + automaticly injected as test values. + """ + if metafunc.cls and issubclass(metafunc.cls, BaseTest): + argnames = [] + argvalues = [] + ids = [] + fn_params = inspect.signature(metafunc.function).parameters - questions = [ - { - "name": "some_string", - "type": "string", - } + for params in [ + ["intake", "expected_output", "raw_option", "data"], + ["intake", "expected_normalized", "raw_option", "data"], + ["intake", "expected_humanized", "raw_option", "data"], + ]: + if all(param in fn_params for param in params): + argnames += params + if params[1] == "expected_output": + # Hydrate scenarios with generic raw_option data + argvalues += get_hydrated_scenarios( + [metafunc.cls.raw_option], metafunc.cls.scenarios + ) + ids += [ + generate_test_name(*args.values) + if isinstance(args, ParameterSet) + else generate_test_name(*args) + for args in argvalues + ] + elif params[1] == "expected_normalized": + argvalues += metafunc.cls.normalized + ids += [ + f"{metafunc.cls.raw_option['type']}-normalize-{scenario[0]}" + for scenario in metafunc.cls.normalized + ] + elif params[1] == "expected_humanized": + argvalues += metafunc.cls.humanized + ids += [ + f"{metafunc.cls.raw_option['type']}-normalize-{scenario[0]}" + for scenario in metafunc.cls.humanized + ] + + metafunc.parametrize(argnames, argvalues, ids=ids) + + +# ╭───────────────────────────────────────────────────────╮ +# │ Scenario helpers │ +# ╰───────────────────────────────────────────────────────╯ + +FAIL = YunohostValidationError + + +def nones( + *nones, output, raw_option: dict[str, Any] = {}, fail_if_required: bool = True +) -> list[PartialScenario]: + """ + Returns common scenarios for ~None values. + - required and required + as default -> `FAIL` + - optional and optional + as default -> `expected_output=None` + """ + return [ + (none, FAIL if fail_if_required else output, base_raw_option | raw_option) # type: ignore + for none in nones + for base_raw_option in ({}, {"default": none}) + ] + [ + (none, output, base_raw_option | raw_option) + for none in nones + for base_raw_option in ({"optional": True}, {"optional": True, "default": none}) ] - 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 unchanged(*args, raw_option: dict[str, Any] = {}) -> list[PartialScenario]: + """ + Returns a series of params for which output is expected to be the same as its intake + + Example:: + # expect `"value"` to output as `"value"`, etc. + unchanged("value", "yes", "none") + + """ + return [(arg, arg, raw_option.copy()) for arg in args] + + +def all_as(*args, output, raw_option: dict[str, Any] = {}) -> list[PartialScenario]: + """ + Returns a series of params for which output is expected to be the same single value + + Example:: + # expect all values to output as `True` + all_as("y", "yes", 1, True, output=True) + """ + return [(arg, output, raw_option.copy()) for arg in args] + + +def all_fails( + *args, raw_option: dict[str, Any] = {}, error=FAIL +) -> list[PartialScenario]: + """ + Returns a series of params for which output is expected to be failing with validation error + """ + return [(arg, error, raw_option.copy()) for arg in args] + + +def xpass(*, scenarios: list[Scenario], reason="unknown") -> list[Scenario]: + """ + Return a pytest param for which test should have fail but currently passes. + """ + return [ + pytest.param( + *scenario, + marks=pytest.mark.xfail( + reason=f"Currently valid but probably shouldn't. details: {reason}." + ), + ) + for scenario in scenarios + ] + + +def xfail(*, scenarios: list[Scenario], reason="unknown") -> list[Scenario]: + """ + Return a pytest param for which test should have passed but currently fails. + """ + return [ + pytest.param( + *scenario, + marks=pytest.mark.xfail( + reason=f"Currently invalid but should probably pass. details: {reason}." + ), + ) + for scenario in scenarios + ] + + +# ╭───────────────────────────────────────────────────────╮ +# │ ╶┬╴┌─╴╭─╴╶┬╴╭─╴ │ +# │ │ ├─╴╰─╮ │ ╰─╮ │ +# │ ╵ ╰─╴╶─╯ ╵ ╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + + +def _fill_or_prompt_one_option(raw_option, intake): + raw_option = raw_option.copy() + id_ = raw_option.pop("id") + options = {id_: raw_option} + answers = {id_: intake} if intake is not None else {} + + option = ask_questions_and_parse_answers(options, answers)[0] + + return (option, option.value) + + +def _test_value_is_expected_output(value, expected_output): + """ + Properly compares bools and None + """ + if isinstance(expected_output, bool) or expected_output is None: + assert value is expected_output + else: + assert value == expected_output + + +def _test_intake(raw_option, intake, expected_output): + option, value = _fill_or_prompt_one_option(raw_option, intake) + + _test_value_is_expected_output(value, expected_output) + + +def _test_intake_may_fail(raw_option, intake, expected_output): + if inspect.isclass(expected_output) and issubclass(expected_output, Exception): + with pytest.raises(expected_output): + _fill_or_prompt_one_option(raw_option, intake) + else: + _test_intake(raw_option, intake, expected_output) + + +class BaseTest: + raw_option: dict[str, Any] = {} + prefill: dict[Literal["raw_option", "prefill", "intake"], Any] + scenarios: list[Scenario] + + # fmt: off + # scenarios = [ + # *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + # *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + # *nones(None, "", output=""), + # ] + # fmt: on + # TODO + # - pattern (also on Date for example to see if it override the default pattern) + # - example + # - visible + # - redact + # - regex + # - hooks + + @classmethod + def get_raw_option(cls, raw_option={}, **kwargs): + base_raw_option = cls.raw_option.copy() + base_raw_option.update(**raw_option) + base_raw_option.update(**kwargs) + return base_raw_option + + @classmethod + def _test_basic_attrs(self): + raw_option = self.get_raw_option(optional=True) + id_ = raw_option["id"] + option, value = _fill_or_prompt_one_option(raw_option, None) + + is_special_readonly_option = isinstance(option, DisplayTextOption) + + assert isinstance(option, OPTIONS[raw_option["type"]]) + assert option.type == raw_option["type"] + assert option.name == id_ + assert option.ask == {"en": id_} + assert option.readonly is (True if is_special_readonly_option else False) + assert option.visible is None + # assert option.bind is None + + if is_special_readonly_option: + assert value is None + + return (raw_option, option, value) + + @pytest.mark.usefixtures("patch_no_tty") + def test_basic_attrs(self): + """ + Test basic options factories and BaseOption default attributes values. + """ + # Intermediate method since pytest doesn't like tests that returns something. + # This allow a test class to call `_test_basic_attrs` then do additional checks + self._test_basic_attrs() + + def test_options_prompted_with_ask_help(self, prefill_data=None): + """ + Test that assert that moulinette prompt is called with: + - `message` with translated string and possible choices list + - help` with translated string + - `prefill` is the expected string value from a custom default + - `is_password` is true for `password`s only + - `is_multiline` is true for `text`s only + - `autocomplete` is option choices + + Ran only once with `cls.prefill` data + """ + if prefill_data is None: + prefill_data = self.prefill + + base_raw_option = prefill_data["raw_option"] + prefill = prefill_data["prefill"] + + with patch_prompt("") as prompt: + raw_option = self.get_raw_option( + raw_option=base_raw_option, + ask={"en": "Can i haz question?"}, + help={"en": "Here's help!"}, + ) + option, value = _fill_or_prompt_one_option(raw_option, None) + + expected_message = option.ask["en"] + + if option.choices: + choices = ( + option.choices + if isinstance(option.choices, list) + else option.choices.keys() + ) + expected_message += f" [{' | '.join(choices)}]" + if option.type == "boolean": + expected_message += " [yes | no]" + + prompt.assert_called_with( + message=expected_message, + is_password=option.type == "password", + confirm=False, # FIXME no confirm? + prefill=prefill, + is_multiline=option.type == "text", + autocomplete=option.choices or [], + help=option.help["en"], + ) + + def test_scenarios(self, intake, expected_output, raw_option, data): + with patch_interface("api"): + _test_intake_may_fail( + raw_option, + intake, + expected_output, + ) + + +# ╭───────────────────────────────────────────────────────╮ +# │ DISPLAY_TEXT │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestDisplayText(BaseTest): + raw_option = {"type": "display_text", "id": "display_text_id"} + prefill = { + "raw_option": {}, + "prefill": " custom default", + } + # fmt: off + scenarios = [ + (None, None, {"ask": "Some text\na new line"}), + (None, None, {"ask": {"en": "Some text\na new line", "fr": "Un peu de texte\nune nouvelle ligne"}}), + ] + # fmt: on + + def test_options_prompted_with_ask_help(self, prefill_data=None): + pytest.skip(reason="no prompt for display types") + + def test_scenarios(self, intake, expected_output, raw_option, data): + _id = raw_option.pop("id") + answers = {_id: intake} if intake is not None else {} + options = None + with patch_interface("cli"): + if inspect.isclass(expected_output) and issubclass( + expected_output, Exception + ): + with pytest.raises(expected_output): + ask_questions_and_parse_answers({_id: raw_option}, answers) + else: + with patch.object(sys, "stdout", new_callable=StringIO) as stdout: + options = ask_questions_and_parse_answers( + {_id: raw_option}, answers + ) + assert stdout.getvalue() == f"{options[0].ask['en']}\n" + + +# ╭───────────────────────────────────────────────────────╮ +# │ MARKDOWN │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestMarkdown(TestDisplayText): + raw_option = {"type": "markdown", "id": "markdown_id"} + # in cli this option is exactly the same as "display_text", no markdown support for now + + +# ╭───────────────────────────────────────────────────────╮ +# │ ALERT │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestAlert(TestDisplayText): + raw_option = {"type": "alert", "id": "alert_id"} + prefill = { + "raw_option": {"ask": " Custom info message"}, + "prefill": " custom default", + } + # fmt: off + scenarios = [ + (None, None, {"ask": "Some text\na new line"}), + (None, None, {"ask": {"en": "Some text\na new line", "fr": "Un peu de texte\nune nouvelle ligne"}}), + *[(None, None, {"ask": "question", "style": style}) for style in ("success", "info", "warning", "danger")], + *xpass(scenarios=[ + (None, None, {"ask": "question", "style": "nimp"}), + ], reason="Should fail, wrong style"), + ] + # fmt: on + + def test_scenarios(self, intake, expected_output, raw_option, data): + style = raw_option.get("style", "info") + colors = {"danger": "31", "warning": "33", "info": "36", "success": "32"} + answers = {"alert_id": intake} if intake is not None else {} + + with patch_interface("cli"): + if inspect.isclass(expected_output) and issubclass( + expected_output, Exception + ): + with pytest.raises(expected_output): + ask_questions_and_parse_answers( + {"display_text_id": raw_option}, answers + ) + else: + with patch.object(sys, "stdout", new_callable=StringIO) as stdout: + options = ask_questions_and_parse_answers( + {"display_text_id": raw_option}, answers + ) + ask = options[0].ask["en"] + if style in colors: + color = colors[style] + title = style.title() + (":" if style != "success" else "!") + assert ( + stdout.getvalue() + == f"\x1b[{color}m\x1b[1m{title}\x1b[m {ask}\n" + ) + else: + # FIXME should fail + stdout.getvalue() == f"{ask}\n" + + +# ╭───────────────────────────────────────────────────────╮ +# │ BUTTON │ +# ╰───────────────────────────────────────────────────────╯ + + +# TODO + + +# ╭───────────────────────────────────────────────────────╮ +# │ STRING │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestString(BaseTest): + raw_option = {"type": "string", "id": "string_id"} + prefill = { + "raw_option": {"default": " custom default"}, + "prefill": " custom default", + } + # fmt: off + scenarios = [ + *nones(None, "", output=""), + # basic typed values + *unchanged(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), # FIXME should output as str? + *unchanged("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", raw_option={"optional": True}), + *xpass(scenarios=[ + ([], []), + ], reason="Should fail"), + # test strip + ("value", "value"), + ("value\n", "value"), + (" \n value\n", "value"), + (" \\n value\\n", "\\n value\\n"), + (" \tvalue\t", "value"), + (r" ##value \n \tvalue\n ", r"##value \n \tvalue\n"), + *xpass(scenarios=[ + ("value\nvalue", "value\nvalue"), + (" ##value \n \tvalue\n ", "##value \n \tvalue"), + ], reason=r"should fail or without `\n`?"), + # readonly + *xfail(scenarios=[ + ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ TEXT │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestText(BaseTest): + raw_option = {"type": "text", "id": "text_id"} + prefill = { + "raw_option": {"default": "some value\nanother line "}, + "prefill": "some value\nanother line ", + } + # fmt: off + scenarios = [ + *nones(None, "", output=""), + # basic typed values + *unchanged(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), # FIXME should fail or output as str? + *unchanged("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", raw_option={"optional": True}), + *xpass(scenarios=[ + ([], []) + ], reason="Should fail"), + ("value", "value"), + ("value\n value", "value\n value"), + # test no strip + *xpass(scenarios=[ + ("value\n", "value"), + (" \n value\n", "value"), + (" \\n value\\n", "\\n value\\n"), + (" \tvalue\t", "value"), + (" ##value \n \tvalue\n ", "##value \n \tvalue"), + (r" ##value \n \tvalue\n ", r"##value \n \tvalue\n"), + ], reason="Should not be stripped"), + # readonly + *xfail(scenarios=[ + ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ PASSWORD │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestPassword(BaseTest): + raw_option = {"type": "password", "id": "password_id"} + prefill = { + "raw_option": {"default": None, "optional": True}, + "prefill": "", + } + # fmt: off + scenarios = [ + *all_fails(False, True, 0, 1, -1, 1337, 13.37, raw_option={"optional": True}, error=TypeError), # FIXME those fails with TypeError + *all_fails([], ["one"], {}, raw_option={"optional": True}, error=AttributeError), # FIXME those fails with AttributeError + *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + *nones(None, "", output=""), + ("s3cr3t!!", FAIL, {"default": "SUPAs3cr3t!!"}), # default is forbidden + *xpass(scenarios=[ + ("s3cr3t!!", "s3cr3t!!", {"example": "SUPAs3cr3t!!"}), # example is forbidden + ], reason="Should fail; example is forbidden"), + *xpass(scenarios=[ + (" value \n moarc0mpl1cat3d\n ", "value \n moarc0mpl1cat3d"), + (" some_ value", "some_ value"), + ], reason="Should output exactly the same"), + ("s3cr3t!!", "s3cr3t!!"), + ("secret", FAIL), + *[("supersecret" + char, FAIL) for char in PasswordOption.forbidden_chars], # FIXME maybe add ` \n` to the list? + # readonly + *xpass(scenarios=[ + ("s3cr3t!!", "s3cr3t!!", {"readonly": True}), + ], reason="Should fail since readonly is forbidden"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ COLOR │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestColor(BaseTest): + raw_option = {"type": "color", "id": "color_id"} + prefill = { + "raw_option": {"default": "#ff0000"}, + "prefill": "#ff0000", + # "intake": "#ff00ff", + } + # fmt: off + scenarios = [ + *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + *nones(None, "", output=""), + # custom valid + ("#000000", "#000000"), + ("#000", "#000"), + ("#fe100", "#fe100"), + (" #fe100 ", "#fe100"), + ("#ABCDEF", "#ABCDEF"), + # custom fail + *xpass(scenarios=[ + ("#feaf", "#feaf"), + ], reason="Should fail; not a legal color value"), + ("000000", FAIL), + ("#12", FAIL), + ("#gggggg", FAIL), + ("#01010101af", FAIL), + *xfail(scenarios=[ + ("red", "#ff0000"), + ("yellow", "#ffff00"), + ], reason="Should work with pydantic"), + # readonly + *xfail(scenarios=[ + ("#ffff00", "#fe100", {"readonly": True, "default": "#fe100"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ NUMBER | RANGE │ +# ╰───────────────────────────────────────────────────────╯ +# Testing only number since "range" is only for webadmin (slider instead of classic intake). + + +class TestNumber(BaseTest): + raw_option = {"type": "number", "id": "number_id"} + prefill = { + "raw_option": {"default": 10}, + "prefill": "10", + } + # fmt: off + scenarios = [ + *all_fails([], ["one"], {}), + *all_fails("none", "_none", "False", "True", "[]", ",", "['one']", "one,two", r"{}", "value"), + + *nones(None, "", output=None), + *unchanged(0, 1, -1, 1337), + *xpass(scenarios=[(False, False)], reason="should fail or output as `0`"), + *xpass(scenarios=[(True, True)], reason="should fail or output as `1`"), + *all_as("0", 0, output=0), + *all_as("1", 1, output=1), + *all_as("1337", 1337, output=1337), + *xfail(scenarios=[ + ("-1", -1) + ], reason="should output as `-1` instead of failing"), + *all_fails(13.37, "13.37"), + + *unchanged(10, 5000, 10000, raw_option={"min": 10, "max": 10000}), + *all_fails(9, 10001, raw_option={"min": 10, "max": 10000}), + + *all_as(None, "", output=0, raw_option={"default": 0}), + *all_as(None, "", output=0, raw_option={"default": 0, "optional": True}), + (-10, -10, {"default": 10}), + (-10, -10, {"default": 10, "optional": True}), + # readonly + *xfail(scenarios=[ + (1337, 10000, {"readonly": True, "default": 10000}), + ], reason="Should not be overwritten"), + ] + # fmt: on + # FIXME should `step` be some kind of "multiple of"? + + +# ╭───────────────────────────────────────────────────────╮ +# │ BOOLEAN │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestBoolean(BaseTest): + raw_option = {"type": "boolean", "id": "boolean_id"} + prefill = { + "raw_option": {"default": True}, + "prefill": "yes", + } + # fmt: off + truthy_values = (True, 1, "1", "True", "true", "Yes", "yes", "y", "on") + falsy_values = (False, 0, "0", "False", "false", "No", "no", "n", "off") + scenarios = [ + *all_as(None, "", output=0), + *all_fails("none", "None"), # FIXME should output as `0` (default) like other none values when required? + *all_as(None, "", output=0, raw_option={"optional": True}), # FIXME should output as `None`? + *all_as("none", "None", output=None, raw_option={"optional": True}), + # FIXME even if default is explicity `None|""`, it ends up with class_default `0` + *all_as(None, "", output=0, raw_option={"default": None}), # FIXME this should fail, default is `None` + *all_as(None, "", output=0, raw_option={"optional": True, "default": None}), # FIXME even if default is explicity None, it ends up with class_default + *all_as(None, "", output=0, raw_option={"default": ""}), # FIXME this should fail, default is `""` + *all_as(None, "", output=0, raw_option={"optional": True, "default": ""}), # FIXME even if default is explicity None, it ends up with class_default + # With "none" behavior is ok + *all_fails(None, "", raw_option={"default": "none"}), + *all_as(None, "", output=None, raw_option={"optional": True, "default": "none"}), + # Unhandled types should fail + *all_fails(1337, "1337", "string", [], "[]", ",", "one,two"), + *all_fails(1337, "1337", "string", [], "[]", ",", "one,two", {"optional": True}), + # Required + *all_as(*truthy_values, output=1), + *all_as(*falsy_values, output=0), + # Optional + *all_as(*truthy_values, output=1, raw_option={"optional": True}), + *all_as(*falsy_values, output=0, raw_option={"optional": True}), + # test values as default, as required option without intake + *[(None, 1, {"default": true for true in truthy_values})], + *[(None, 0, {"default": false for false in falsy_values})], + # custom boolean output + ("", "disallow", {"yes": "allow", "no": "disallow"}), # required -> default to False -> `"disallow"` + ("n", "disallow", {"yes": "allow", "no": "disallow"}), + ("y", "allow", {"yes": "allow", "no": "disallow"}), + ("", False, {"yes": True, "no": False}), # required -> default to False -> `False` + ("n", False, {"yes": True, "no": False}), + ("y", True, {"yes": True, "no": False}), + ("", -1, {"yes": 1, "no": -1}), # required -> default to False -> `-1` + ("n", -1, {"yes": 1, "no": -1}), + ("y", 1, {"yes": 1, "no": -1}), + { + "raw_options": [ + {"yes": "no", "no": "yes", "optional": True}, + {"yes": False, "no": True, "optional": True}, + {"yes": "0", "no": "1", "optional": True}, + ], + # "no" for "yes" and "yes" for "no" should fail + "scenarios": all_fails("", "y", "n", error=AssertionError), + }, + # readonly + *xfail(scenarios=[ + (1, 0, {"readonly": True, "default": 0}), + ], reason="Should not be overwritten"), + ] + + +# ╭───────────────────────────────────────────────────────╮ +# │ DATE │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestDate(BaseTest): + raw_option = {"type": "date", "id": "date_id"} + prefill = { + "raw_option": {"default": "2024-12-29"}, + "prefill": "2024-12-29", + } + # fmt: off + scenarios = [ + *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + *nones(None, "", output=""), + # custom valid + ("2070-12-31", "2070-12-31"), + ("2024-02-29", "2024-02-29"), + *xfail(scenarios=[ + ("2025-06-15T13:45:30", "2025-06-15"), + ("2025-06-15 13:45:30", "2025-06-15") + ], reason="iso date repr should be valid and extra data striped"), + *xfail(scenarios=[ + (1749938400, "2025-06-15"), + (1749938400.0, "2025-06-15"), + ("1749938400", "2025-06-15"), + ("1749938400.0", "2025-06-15"), + ], reason="timestamp could be an accepted value"), + # custom invalid + ("29-12-2070", FAIL), + ("12-01-10", FAIL), + ("2022-02-29", FAIL), + # readonly + *xfail(scenarios=[ + ("2070-12-31", "2024-02-29", {"readonly": True, "default": "2024-02-29"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ TIME │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestTime(BaseTest): + raw_option = {"type": "time", "id": "time_id"} + prefill = { + "raw_option": {"default": "12:26"}, + "prefill": "12:26", + } + # fmt: off + scenarios = [ + *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + *nones(None, "", output=""), + # custom valid + *unchanged("00:00", "08:00", "12:19", "20:59", "23:59"), + ("3:00", "3:00"), # FIXME should fail or output as `"03:00"`? + *xfail(scenarios=[ + ("22:35:05", "22:35"), + ("22:35:03.514", "22:35"), + ], reason="time as iso format could be valid"), + # custom invalid + ("24:00", FAIL), + ("23:1", FAIL), + ("23:005", FAIL), + # readonly + *xfail(scenarios=[ + ("00:00", "08:00", {"readonly": True, "default": "08:00"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ EMAIL │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestEmail(BaseTest): + raw_option = {"type": "email", "id": "email_id"} + prefill = { + "raw_option": {"default": "Abc@example.tld"}, + "prefill": "Abc@example.tld", + } + # fmt: off + scenarios = [ + *all_fails(False, True, 0, 1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "0", "1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + + *nones(None, "", output=""), + ("\n Abc@example.tld ", "Abc@example.tld"), + # readonly + *xfail(scenarios=[ + ("Abc@example.tld", "admin@ynh.local", {"readonly": True, "default": "admin@ynh.local"}), + ], reason="Should not be overwritten"), + + # Next examples are from https://github.com/JoshData/python-email-validator/blob/main/tests/test_syntax.py + # valid email values + ("Abc@example.tld", "Abc@example.tld"), + ("Abc.123@test-example.com", "Abc.123@test-example.com"), + ("user+mailbox/department=shipping@example.tld", "user+mailbox/department=shipping@example.tld"), + ("伊昭傑@郵件.商務", "伊昭傑@郵件.商務"), + ("राम@मोहन.ईन्फो", "राम@मोहन.ईन्फो"), + ("юзер@екзампл.ком", "юзер@екзампл.ком"), + ("θσερ@εχαμπλε.ψομ", "θσερ@εχαμπλε.ψομ"), + ("葉士豪@臺網中心.tw", "葉士豪@臺網中心.tw"), + ("jeff@臺網中心.tw", "jeff@臺網中心.tw"), + ("葉士豪@臺網中心.台灣", "葉士豪@臺網中心.台灣"), + ("jeff葉@臺網中心.tw", "jeff葉@臺網中心.tw"), + ("ñoñó@example.tld", "ñoñó@example.tld"), + ("甲斐黒川日本@example.tld", "甲斐黒川日本@example.tld"), + ("чебурашкаящик-с-апельсинами.рф@example.tld", "чебурашкаящик-с-апельсинами.рф@example.tld"), + ("उदाहरण.परीक्ष@domain.with.idn.tld", "उदाहरण.परीक्ष@domain.with.idn.tld"), + ("ιωάννης@εεττ.gr", "ιωάννης@εεττ.gr"), + # invalid email (Hiding because our current regex is very permissive) + # ("my@localhost", FAIL), + # ("my@.leadingdot.com", FAIL), + # ("my@.leadingfwdot.com", FAIL), + # ("my@twodots..com", FAIL), + # ("my@twofwdots...com", FAIL), + # ("my@trailingdot.com.", FAIL), + # ("my@trailingfwdot.com.", FAIL), + # ("me@-leadingdash", FAIL), + # ("me@-leadingdashfw", FAIL), + # ("me@trailingdash-", FAIL), + # ("me@trailingdashfw-", FAIL), + # ("my@baddash.-.com", FAIL), + # ("my@baddash.-a.com", FAIL), + # ("my@baddash.b-.com", FAIL), + # ("my@baddashfw.-.com", FAIL), + # ("my@baddashfw.-a.com", FAIL), + # ("my@baddashfw.b-.com", FAIL), + # ("my@example.com\n", FAIL), + # ("my@example\n.com", FAIL), + # ("me@x!", FAIL), + # ("me@x ", FAIL), + # (".leadingdot@domain.com", FAIL), + # ("twodots..here@domain.com", FAIL), + # ("trailingdot.@domain.email", FAIL), + # ("me@⒈wouldbeinvalid.com", FAIL), + ("@example.com", FAIL), + # ("\nmy@example.com", FAIL), + ("m\ny@example.com", FAIL), + ("my\n@example.com", FAIL), + # ("11111111112222222222333333333344444444445555555555666666666677777@example.com", FAIL), + # ("111111111122222222223333333333444444444455555555556666666666777777@example.com", FAIL), + # ("me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444444444455555555556.com", FAIL), + # ("me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555566.com", FAIL), + # ("me@中1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555566.com", FAIL), + # ("my.long.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444.info", FAIL), + # ("my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333.info", FAIL), + # ("my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info", FAIL), + # ("my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444.info", FAIL), + # ("my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info", FAIL), + # ("me@bad-tld-1", FAIL), + # ("me@bad.tld-2", FAIL), + # ("me@xn--0.tld", FAIL), + # ("me@yy--0.tld", FAIL), + # ("me@yy--0.tld", FAIL), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ PATH │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestWebPath(BaseTest): + raw_option = {"type": "path", "id": "path_id"} + prefill = { + "raw_option": {"default": "some_path"}, + "prefill": "some_path", + } + # fmt: off + scenarios = [ + *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + + *nones(None, "", output=""), + # custom valid + ("/", "/"), + ("/one/two", "/one/two"), + *[ + (v, "/" + v) + for v in ("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value") + ], + ("value\n", "/value"), + ("//value", "/value"), + ("///value///", "/value"), + *xpass(scenarios=[ + ("value\nvalue", "/value\nvalue"), + ("value value", "/value value"), + ("value//value", "/value//value"), + ], reason="Should fail"), + *xpass(scenarios=[ + ("./here", "/./here"), + ("../here", "/../here"), + ("/somewhere/../here", "/somewhere/../here"), + ], reason="Should fail or flattened"), + + *xpass(scenarios=[ + ("/one?withquery=ah", "/one?withquery=ah"), + ], reason="Should fail or query string removed"), + *xpass(scenarios=[ + ("https://example.com/folder", "/https://example.com/folder") + ], reason="Should fail or scheme+domain removed"), + # readonly + *xfail(scenarios=[ + ("/overwrite", "/value", {"readonly": True, "default": "/value"}), + ], reason="Should not be overwritten"), + # FIXME should path have forbidden_chars? + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ URL │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestUrl(BaseTest): + raw_option = {"type": "url", "id": "url_id"} + prefill = { + "raw_option": {"default": "https://domain.tld"}, + "prefill": "https://domain.tld", + } + # fmt: off + scenarios = [ + *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + + *nones(None, "", output=""), + ("http://some.org/folder/file.txt", "http://some.org/folder/file.txt"), + # readonly + *xfail(scenarios=[ + ("https://overwrite.org", "https://example.org", {"readonly": True, "default": "https://example.org"}), + ], reason="Should not be overwritten"), + # rest is taken from https://github.com/pydantic/pydantic/blob/main/tests/test_networks.py + # valid + *unchanged( + # Those are valid but not sure how they will output with pydantic + 'http://example.org', + 'http://test', + 'http://localhost', + 'https://example.org/whatever/next/', + 'https://example.org', + 'http://localhost', + 'http://localhost/', + 'http://localhost:8000', + 'http://localhost:8000/', + 'https://foo_bar.example.com/', + 'http://example.co.jp', + 'http://www.example.com/a%C2%B1b', + 'http://www.example.com/~username/', + 'http://info.example.com?fred', + 'http://info.example.com/?fred', + 'http://xn--mgbh0fb.xn--kgbechtv/', + 'http://example.com/blue/red%3Fand+green', + 'http://www.example.com/?array%5Bkey%5D=value', + 'http://xn--rsum-bpad.example.org/', + 'http://123.45.67.8/', + 'http://123.45.67.8:8329/', + 'http://[2001:db8::ff00:42]:8329', + 'http://[2001::1]:8329', + 'http://[2001:db8::1]/', + 'http://www.example.com:8000/foo', + 'http://www.cwi.nl:80/%7Eguido/Python.html', + 'https://www.python.org/путь', + 'http://андрей@example.com', + 'https://exam_ple.com/', + 'http://twitter.com/@handle/', + 'http://11.11.11.11.example.com/action', + 'http://abc.11.11.11.11.example.com/action', + 'http://example#', + 'http://example/#', + 'http://example/#fragment', + 'http://example/?#', + 'http://example.org/path#', + 'http://example.org/path#fragment', + 'http://example.org/path?query#', + 'http://example.org/path?query#fragment', + ), + # Pydantic default parsing add a final `/` + ('https://foo_bar.example.com/', 'https://foo_bar.example.com/'), + ('https://exam_ple.com/', 'https://exam_ple.com/'), + *xfail(scenarios=[ + (' https://www.example.com \n', 'https://www.example.com/'), + ('HTTP://EXAMPLE.ORG', 'http://example.org/'), + ('https://example.org', 'https://example.org/'), + ('https://example.org?a=1&b=2', 'https://example.org/?a=1&b=2'), + ('https://example.org#a=3;b=3', 'https://example.org/#a=3;b=3'), + ('https://example.xn--p1ai', 'https://example.xn--p1ai/'), + ('https://example.xn--vermgensberatung-pwb', 'https://example.xn--vermgensberatung-pwb/'), + ('https://example.xn--zfr164b', 'https://example.xn--zfr164b/'), + ], reason="pydantic default behavior would append a final `/`"), + + # invalid + *all_fails( + 'ftp://example.com/', + "$https://example.org", + "../icons/logo.gif", + "abc", + "..", + "/", + "+http://example.com/", + "ht*tp://example.com/", + ), + *xpass(scenarios=[ + ("http:///", "http:///"), + ("http://??", "http://??"), + ("https://example.org more", "https://example.org more"), + ("http://2001:db8::ff00:42:8329", "http://2001:db8::ff00:42:8329"), + ("http://[192.168.1.1]:8329", "http://[192.168.1.1]:8329"), + ("http://example.com:99999", "http://example.com:99999"), + ], reason="Should fail"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ FILE │ +# ╰───────────────────────────────────────────────────────╯ + + +@pytest.fixture +def file_clean(): + FileOption.clean_upload_dirs() + yield + FileOption.clean_upload_dirs() + + +@contextmanager +def patch_file_cli(intake): + upload_dir = tempfile.mkdtemp(prefix="ynh_test_option_file") + _, filename = tempfile.mkstemp(dir=upload_dir) + with open(filename, "w") as f: + f.write(intake) + + yield filename + os.system(f"rm -f {filename}") + + +@contextmanager +def patch_file_api(intake): + from base64 import b64encode + + with patch_interface("api"): + yield b64encode(intake.encode()) + + +def _test_file_intake_may_fail(raw_option, intake, expected_output): + if inspect.isclass(expected_output) and issubclass(expected_output, Exception): + with pytest.raises(expected_output): + _fill_or_prompt_one_option(raw_option, intake) + + option, value = _fill_or_prompt_one_option(raw_option, intake) + + # The file is supposed to be copied somewhere else + assert value != intake + assert value.startswith("/tmp/ynh_filequestion_") + assert os.path.exists(value) + with open(value) as f: + assert f.read() == expected_output + + FileOption.clean_upload_dirs() + + assert not os.path.exists(value) + + +file_content1 = "helloworld" +file_content2 = """ +{ + "testy": true, + "test": ["one"] +} +""" + + +class TestFile(BaseTest): + raw_option = {"type": "file", "id": "file_id"} + # Prefill data is generated in `cls.test_options_prompted_with_ask_help` + # fmt: off + scenarios = [ + *nones(None, "", output=""), + *unchanged(file_content1, file_content2), + # other type checks are done in `test_wrong_intake` + ] + # fmt: on + # TODO test readonly + # TODO test accept + + @pytest.mark.usefixtures("patch_no_tty") + def test_basic_attrs(self): + raw_option, option, value = self._test_basic_attrs() + + accept = raw_option.get("accept", "") # accept default + assert option.accept == accept + + def test_options_prompted_with_ask_help(self): + with patch_file_cli(file_content1) as default_filename: + super().test_options_prompted_with_ask_help( + prefill_data={ + "raw_option": { + "default": default_filename, + }, + "prefill": default_filename, + } + ) + + @pytest.mark.usefixtures("file_clean") + def test_scenarios(self, intake, expected_output, raw_option, data): + if intake in (None, ""): + with patch_prompt(intake): + _test_intake_may_fail(raw_option, None, expected_output) + with patch_isatty(False): + _test_intake_may_fail(raw_option, intake, expected_output) + else: + with patch_file_cli(intake) as filename: + with patch_prompt(filename): + _test_file_intake_may_fail(raw_option, None, expected_output) + with patch_file_api(intake) as b64content: + with patch_isatty(False): + _test_file_intake_may_fail(raw_option, b64content, expected_output) + + @pytest.mark.parametrize( + "path", + [ + "/tmp/inexistant_file.txt", + "/tmp", + "/tmp/", + ], + ) + def test_wrong_cli_filename(self, path): + with patch_prompt(path): + with pytest.raises(YunohostValidationError): + _fill_or_prompt_one_option(self.raw_option, None) + + @pytest.mark.parametrize( + "intake", + [ + # fmt: off + False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, + "none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n" + # fmt: on + ], + ) + def test_wrong_intake(self, intake): + with pytest.raises(YunohostValidationError): + with patch_prompt(intake): + _fill_or_prompt_one_option(self.raw_option, None) + with patch_isatty(False): + _fill_or_prompt_one_option(self.raw_option, intake) + + +# ╭───────────────────────────────────────────────────────╮ +# │ SELECT │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestSelect(BaseTest): + raw_option = {"type": "select", "id": "select_id"} + prefill = { + "raw_option": {"default": "one", "choices": ["one", "two"]}, + "prefill": "one", + } + # fmt: off + scenarios = [ + { + # ["one", "two"] + "raw_options": [ + {"choices": ["one", "two"]}, + {"choices": {"one": "verbose one", "two": "verbose two"}}, + ], + "scenarios": [ + *nones(None, "", output=""), + *unchanged("one", "two"), + ("three", FAIL), + ] + }, + # custom bash style list as choices (only strings for now) + ("one", "one", {"choices": "one,two"}), + { + # [-1, 0, 1] + "raw_options": [ + {"choices": [-1, 0, 1, 10]}, + {"choices": {-1: "verbose -one", 0: "verbose zero", 1: "verbose one", 10: "verbose ten"}}, + ], + "scenarios": [ + *nones(None, "", output=""), + *unchanged(-1, 0, 1, 10), + *xfail(scenarios=[ + ("-1", -1), + ("0", 0), + ("1", 1), + ("10", 10), + ], reason="str -> int not handled"), + *all_fails("100", 100), + ] + }, + # [True, False, None] + *unchanged(True, False, raw_option={"choices": [True, False, None]}), # FIXME we should probably forbid None in choices + (None, FAIL, {"choices": [True, False, None]}), + { + # mixed types + "raw_options": [{"choices": ["one", 2, True]}], + "scenarios": [ + *xpass(scenarios=[ + ("one", "one"), + (2, 2), + (True, True), + ], reason="mixed choices, should fail"), + *all_fails("2", "True", "y"), + ] + }, + { + "raw_options": [{"choices": ""}, {"choices": []}], + "scenarios": [ + # FIXME those should fail at option level (wrong default, dev error) + *all_fails(None, ""), + *xpass(scenarios=[ + ("", "", {"optional": True}), + (None, "", {"optional": True}), + ], reason="empty choices, should fail at option instantiation"), + ] + }, + # readonly + *xfail(scenarios=[ + ("one", "two", {"readonly": True, "choices": ["one", "two"], "default": "two"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ TAGS │ +# ╰───────────────────────────────────────────────────────╯ + + +class TestTags(BaseTest): + raw_option = {"type": "tags", "id": "tags_id"} + prefill = { + "raw_option": {"default": ["one", "two"]}, + "prefill": "one,two", + } + # fmt: off + scenarios = [ + *nones(None, [], "", output=""), + # FIXME `","` could be considered a none value which kinda already is since it fail when required + (",", FAIL), + *xpass(scenarios=[ + (",", ",", {"optional": True}) + ], reason="Should output as `''`? ie: None"), + { + "raw_options": [ + {}, + {"choices": ["one", "two"]} + ], + "scenarios": [ + *unchanged("one", "one,two"), + (["one"], "one"), + (["one", "two"], "one,two"), + ] + }, + ("three", FAIL, {"choices": ["one", "two"]}), + *unchanged("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", "['one']", "one,two", r"{}", "value"), + (" value\n", "value"), + ([False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}], "False,True,-1,0,1,1337,13.37,[],['one'],{}"), + *(([t], str(t)) for t in (False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {})), + # basic types (not in a list) should fail + *all_fails(True, False, -1, 0, 1, 1337, 13.37, {}), + # Mixed choices should fail + ([False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}], FAIL, {"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), + ("False,True,-1,0,1,1337,13.37,[],['one'],{}", FAIL, {"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), + *all_fails(*([t] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), + *all_fails(*([str(t)] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), + # readonly + *xfail(scenarios=[ + ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "default": "one,two"}), + ], reason="Should not be overwritten"), + ] + # fmt: on + + +# ╭───────────────────────────────────────────────────────╮ +# │ DOMAIN │ +# ╰───────────────────────────────────────────────────────╯ + +main_domain = "ynh.local" +domains1 = ["ynh.local"] +domains2 = ["another.org", "ynh.local", "yet.another.org"] + + +@contextmanager +def patch_domains(*, domains, main_domain): + """ + Data mocking for DomainOption: + - yunohost.domain.domain_list + """ + with patch.object( + domain, + "domain_list", + return_value={"domains": domains, "main": main_domain}, + ), patch.object(domain, "_get_maindomain", return_value=main_domain): + yield + + +class TestDomain(BaseTest): + raw_option = {"type": "domain", "id": "domain_id"} + prefill = { + "raw_option": { + "default": None, + }, + "prefill": main_domain, + } + # fmt: off + scenarios = [ + # Probably not needed to test common types since those are not available as choices + # Also no scenarios with no domains since it should not be possible + { + "data": [{"main_domain": domains1[0], "domains": domains1}], + "scenarios": [ + *nones(None, "", output=domains1[0], fail_if_required=False), + (domains1[0], domains1[0], {}), + ("doesnt_exist.pouet", FAIL, {}), + ("fake.com", FAIL, {"choices": ["fake.com"]}), + # readonly + *xpass(scenarios=[ + (domains1[0], domains1[0], {"readonly": True}), + ], reason="Should fail since readonly is forbidden"), + ] + }, + { + "data": [{"main_domain": domains2[1], "domains": domains2}], + "scenarios": [ + *nones(None, "", output=domains2[1], fail_if_required=False), + (domains2[1], domains2[1], {}), + (domains2[0], domains2[0], {}), + ("doesnt_exist.pouet", FAIL, {}), + ("fake.com", FAIL, {"choices": ["fake.com"]}), + ] + }, + + ] + # fmt: on + + def test_options_prompted_with_ask_help(self, prefill_data=None): + with patch_domains(domains=[main_domain], main_domain=main_domain): + super().test_options_prompted_with_ask_help(prefill_data=prefill_data) + + def test_scenarios(self, intake, expected_output, raw_option, data): + with patch_domains(**data): + super().test_scenarios(intake, expected_output, raw_option, data) + + +# ╭───────────────────────────────────────────────────────╮ +# │ APP │ +# ╰───────────────────────────────────────────────────────╯ + +installed_webapp = { + "is_webapp": True, + "is_default": True, + "label": "My webapp", + "id": "my_webapp", + "domain_path": "/ynh-dev", +} +installed_non_webapp = { + "is_webapp": False, + "is_default": False, + "label": "My non webapp", + "id": "my_non_webapp", +} + + +@contextmanager +def patch_apps(*, apps): + """ + Data mocking for AppOption: + - yunohost.app.app_list + """ + with patch.object(app, "app_list", return_value={"apps": apps}): + yield + + +class TestApp(BaseTest): + raw_option = {"type": "app", "id": "app_id"} + # fmt: off + scenarios = [ + # Probably not needed to test common types since those are not available as choices + { + "data": [ + {"apps": []}, + {"apps": [installed_webapp]}, + {"apps": [installed_webapp, installed_non_webapp]}, + ], + "scenarios": [ + # FIXME there are currently 3 different nones (`None`, `""` and `_none`), choose one? + *nones(None, output=None), # FIXME Should return chosen none? + *nones("", output=""), # FIXME Should return chosen none? + *xpass(scenarios=[ + ("_none", "_none"), + ("_none", "_none", {"default": "_none"}), + ], reason="should fail; is required"), + *xpass(scenarios=[ + ("_none", "_none", {"optional": True}), + ("_none", "_none", {"optional": True, "default": "_none"}) + ], reason="Should output chosen none value"), + ("fake_app", FAIL), + ("fake_app", FAIL, {"choices": ["fake_app"]}), + ] + }, + { + "data": [ + {"apps": [installed_webapp]}, + {"apps": [installed_webapp, installed_non_webapp]}, + ], + "scenarios": [ + (installed_webapp["id"], installed_webapp["id"]), + (installed_webapp["id"], installed_webapp["id"], {"filter": "is_webapp"}), + (installed_webapp["id"], FAIL, {"filter": "is_webapp == false"}), + (installed_webapp["id"], FAIL, {"filter": "id != 'my_webapp'"}), + (None, None, {"filter": "id == 'fake_app'", "optional": True}), + ] + }, + { + "data": [{"apps": [installed_webapp, installed_non_webapp]}], + "scenarios": [ + (installed_non_webapp["id"], installed_non_webapp["id"]), + (installed_non_webapp["id"], FAIL, {"filter": "is_webapp"}), + # readonly + *xpass(scenarios=[ + (installed_non_webapp["id"], installed_non_webapp["id"], {"readonly": True}), + ], reason="Should fail since readonly is forbidden"), + ] + }, + ] + # fmt: on + + @pytest.mark.usefixtures("patch_no_tty") + def test_basic_attrs(self): + with patch_apps(apps=[]): + raw_option, option, value = self._test_basic_attrs() + + assert option.choices == {"_none": "---"} + assert option.filter is None + + with patch_apps(apps=[installed_webapp, installed_non_webapp]): + raw_option, option, value = self._test_basic_attrs() + + assert option.choices == { + "_none": "---", + "my_webapp": "My webapp (/ynh-dev)", + "my_non_webapp": "My non webapp (my_non_webapp)", + } + assert option.filter is None + + def test_options_prompted_with_ask_help(self, prefill_data=None): + with patch_apps(apps=[installed_webapp, installed_non_webapp]): + super().test_options_prompted_with_ask_help( + prefill_data={ + "raw_option": {"default": installed_webapp["id"]}, + "prefill": installed_webapp["id"], + } + ) + super().test_options_prompted_with_ask_help( + prefill_data={"raw_option": {"optional": True}, "prefill": ""} + ) + + def test_scenarios(self, intake, expected_output, raw_option, data): + with patch_apps(**data): + super().test_scenarios(intake, expected_output, raw_option, data) + + +# ╭───────────────────────────────────────────────────────╮ +# │ USER │ +# ╰───────────────────────────────────────────────────────╯ + +admin_username = "admin_user" +admin_user = { + "ssh_allowed": False, + "username": admin_username, + "mailbox-quota": "0", + "mail": "a@ynh.local", + "mail-aliases": [f"root@{main_domain}"], # Faking "admin" + "fullname": "john doe", + "group": [], +} +regular_username = "normal_user" +regular_user = { + "ssh_allowed": False, + "username": regular_username, + "mailbox-quota": "0", + "mail": "z@ynh.local", + "fullname": "john doe", + "group": [], +} + + +@contextmanager +def patch_users( + *, + users, + admin_username, + main_domain, +): + """ + Data mocking for UserOption: + - yunohost.user.user_list + - yunohost.user.user_info + - yunohost.domain._get_maindomain + """ + admin_info = next( + (user for user in users.values() if user["username"] == admin_username), + {"mail-aliases": []}, + ) + with patch.object(user, "user_list", return_value={"users": users}), patch.object( + user, + "user_info", + return_value=admin_info, # Faking admin user + ), patch.object(domain, "_get_maindomain", return_value=main_domain): + yield + + +class TestUser(BaseTest): + raw_option = {"type": "user", "id": "user_id"} + # fmt: off + scenarios = [ + # No tests for empty users since it should not happens + { + "data": [ + {"users": {admin_username: admin_user}, "admin_username": admin_username, "main_domain": main_domain}, + {"users": {admin_username: admin_user, regular_username: regular_user}, "admin_username": admin_username, "main_domain": main_domain}, + ], + "scenarios": [ + # FIXME User option is not really nullable, even if optional + *nones(None, "", output=admin_username, fail_if_required=False), + ("fake_user", FAIL), + ("fake_user", FAIL, {"choices": ["fake_user"]}), + ] + }, + { + "data": [ + {"users": {admin_username: admin_user, regular_username: regular_user}, "admin_username": admin_username, "main_domain": main_domain}, + ], + "scenarios": [ + *xpass(scenarios=[ + ("", regular_username, {"default": regular_username}) + ], reason="Should throw 'no default allowed'"), + # readonly + *xpass(scenarios=[ + (admin_username, admin_username, {"readonly": True}), + ], reason="Should fail since readonly is forbidden"), + ] + }, + ] + # fmt: on + + @pytest.mark.usefixtures("patch_no_tty") + def test_basic_attrs(self): + with patch_users( + users={admin_username: admin_user}, + admin_username=admin_username, + main_domain=main_domain, + ): + self._test_basic_attrs() + + def test_options_prompted_with_ask_help(self, prefill_data=None): + with patch_users( + users={admin_username: admin_user, regular_username: regular_user}, + admin_username=admin_username, + main_domain=main_domain, + ): + super().test_options_prompted_with_ask_help( + prefill_data={"raw_option": {}, "prefill": admin_username} + ) + # FIXME This should fail, not allowed to set a default + super().test_options_prompted_with_ask_help( + prefill_data={ + "raw_option": {"default": regular_username}, + "prefill": regular_username, + } + ) + + def test_scenarios(self, intake, expected_output, raw_option, data): + with patch_users(**data): + super().test_scenarios(intake, expected_output, raw_option, data) + + +# ╭───────────────────────────────────────────────────────╮ +# │ GROUP │ +# ╰───────────────────────────────────────────────────────╯ + +groups1 = ["all_users", "visitors", "admins"] +groups2 = ["all_users", "visitors", "admins", "custom_group"] + + +@contextmanager +def patch_groups(*, groups): + """ + Data mocking for GroupOption: + - yunohost.user.user_group_list + """ + with patch.object(user, "user_group_list", return_value={"groups": groups}): + yield + + +class TestGroup(BaseTest): + raw_option = {"type": "group", "id": "group_id"} + # fmt: off + scenarios = [ + # No tests for empty groups since it should not happens + { + "data": [ + {"groups": groups1}, + {"groups": groups2}, + ], + "scenarios": [ + # FIXME Group option is not really nullable, even if optional + *nones(None, "", output="all_users", fail_if_required=False), + ("admins", "admins"), + ("fake_group", FAIL), + ("fake_group", FAIL, {"choices": ["fake_group"]}), + ] + }, + { + "data": [ + {"groups": groups2}, + ], + "scenarios": [ + ("custom_group", "custom_group"), + *all_as("", None, output="visitors", raw_option={"default": "visitors"}), + *xpass(scenarios=[ + ("", "custom_group", {"default": "custom_group"}), + ], reason="Should throw 'default must be in (None, 'all_users', 'visitors', 'admins')"), + # readonly + *xpass(scenarios=[ + ("admins", "admins", {"readonly": True}), + ], reason="Should fail since readonly is forbidden"), + ] + }, + ] + # fmt: on + + def test_options_prompted_with_ask_help(self, prefill_data=None): + with patch_groups(groups=groups2): + super().test_options_prompted_with_ask_help( + prefill_data={"raw_option": {}, "prefill": "all_users"} + ) + super().test_options_prompted_with_ask_help( + prefill_data={ + "raw_option": {"default": "admins"}, + "prefill": "admins", + } + ) + # FIXME This should fail, not allowed to set a default which is not a default group + super().test_options_prompted_with_ask_help( + prefill_data={ + "raw_option": {"default": "custom_group"}, + "prefill": "custom_group", + } + ) + + def test_scenarios(self, intake, expected_output, raw_option, data): + with patch_groups(**data): + super().test_scenarios(intake, expected_output, raw_option, data) + + +# ╭───────────────────────────────────────────────────────╮ +# │ MULTIPLE │ +# ╰───────────────────────────────────────────────────────╯ + + +@pytest.fixture +def patch_entities(): + with patch_domains(domains=domains2, main_domain=main_domain), patch_apps( + apps=[installed_webapp, installed_non_webapp] + ), patch_users( + users={admin_username: admin_user, regular_username: regular_user}, + admin_username=admin_username, + main_domain=main_domain, + ), patch_groups( + groups=groups2 + ): + yield + + +def test_options_empty(): + ask_questions_and_parse_answers({}, {}) == [] + + +@pytest.mark.usefixtures("patch_entities", "file_clean") +def test_options_query_string(): + raw_options = { + "string_id": {"type": "string"}, + "text_id": {"type": "text"}, + "password_id": {"type": "password"}, + "color_id": {"type": "color"}, + "number_id": {"type": "number"}, + "boolean_id": {"type": "boolean"}, + "date_id": {"type": "date"}, + "time_id": {"type": "time"}, + "email_id": {"type": "email"}, + "path_id": {"type": "path"}, + "url_id": {"type": "url"}, + "file_id": {"type": "file"}, + "select_id": {"type": "select", "choices": ["one", "two"]}, + "tags_id": {"type": "tags", "choices": ["one", "two"]}, + "domain_id": {"type": "domain"}, + "app_id": {"type": "app"}, + "user_id": {"type": "user"}, + "group_id": {"type": "group"}, + } + + results = { + "string_id": "string", + "text_id": "text\ntext", + "password_id": "sUpRSCRT", + "color_id": "#ffff00", + "number_id": 10, + "boolean_id": 1, + "date_id": "2030-03-06", + "time_id": "20:55", + "email_id": "coucou@ynh.local", + "path_id": "/ynh-dev", + "url_id": "https://yunohost.org", + "file_id": file_content1, + "select_id": "one", + "tags_id": "one,two", + "domain_id": main_domain, + "app_id": installed_webapp["id"], + "user_id": regular_username, + "group_id": "admins", + } + + @contextmanager + def patch_query_string(file_repr): + yield ( + "string_id= string" + "&text_id=text\ntext" + "&password_id=sUpRSCRT" + "&color_id=#ffff00" + "&number_id=10" + "&boolean_id=y" + "&date_id=2030-03-06" + "&time_id=20:55" + "&email_id=coucou@ynh.local" + "&path_id=ynh-dev/" + "&url_id=https://yunohost.org" + f"&file_id={file_repr}" + "&select_id=one" + "&tags_id=one,two" + # FIXME We can't test with parse.qs for now, next syntax is available only with config panels + # "&tags_id=one" + # "&tags_id=two" + f"&domain_id={main_domain}" + f"&app_id={installed_webapp['id']}" + f"&user_id={regular_username}" + "&group_id=admins" + # not defined extra values are silently ignored + "&fake_id=fake_value" + ) + + def _assert_correct_values(options, raw_options): + form = {option.name: option.value for option in options} + + for k, v in results.items(): + if k == "file_id": + assert os.path.exists(form["file_id"]) and os.path.isfile( + form["file_id"] + ) + with open(form["file_id"], "r") as f: + assert f.read() == file_content1 + else: + assert form[k] == results[k] + + assert len(options) == len(raw_options.keys()) + assert "fake_id" not in form + + with patch_interface("api"), patch_file_api(file_content1) as b64content: + with patch_query_string(b64content.decode("utf-8")) as query_string: + options = ask_questions_and_parse_answers(raw_options, query_string) + _assert_correct_values(options, raw_options) + + with patch_interface("cli"), patch_file_cli(file_content1) as filepath: + with patch_query_string(filepath) as query_string: + options = ask_questions_and_parse_answers(raw_options, query_string) + _assert_correct_values(options, raw_options) def test_question_string_default_type(): - questions = [ - { - "name": "some_string", - } - ] + questions = {"some_string": {}} answers = {"some_string": "some_value"} out = ask_questions_and_parse_answers(questions, answers)[0] @@ -95,210 +1998,16 @@ def test_question_string_default_type(): 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", + questions = { + "some_string": { "ask": ask_text, "example": example_text, } - ] + } answers = {} with patch.object( @@ -309,29 +2018,8 @@ def test_question_string_input_test_ask_with_example(): assert example_text in prompt.call_args[1]["message"] -@pytest.mark.skip # we should do something with this help -def test_question_string_input_test_ask_with_help(): - ask_text = "some question" - help_text = "some_help" - questions = [ - { - "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"]}] + questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} answers = {"some_string": "fr"} out = ask_questions_and_parse_answers(questions, answers)[0] @@ -341,7 +2029,7 @@ def test_question_string_with_choice(): def test_question_string_with_choice_prompt(): - questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] + questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} answers = {"some_string": "fr"} with patch.object(Moulinette, "prompt", return_value="fr"), patch.object( os, "isatty", return_value=True @@ -354,7 +2042,7 @@ def test_question_string_with_choice_prompt(): def test_question_string_with_choice_bad(): - questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] + questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} answers = {"some_string": "bad"} with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): @@ -364,13 +2052,12 @@ def test_question_string_with_choice_bad(): def test_question_string_with_choice_ask(): ask_text = "some question" choices = ["fr", "en", "es", "it", "ru"] - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "ask": ask_text, "choices": choices, } - ] + } answers = {} with patch.object(Moulinette, "prompt", return_value="ru") as prompt, patch.object( @@ -384,14 +2071,13 @@ def test_question_string_with_choice_ask(): def test_question_string_with_choice_default(): - questions = [ - { - "name": "some_string", + questions = { + "some_string": { "type": "string", "choices": ["fr", "en"], "default": "en", } - ] + } answers = {} with patch.object(os, "isatty", return_value=False): out = ask_questions_and_parse_answers(questions, answers)[0] @@ -401,235 +2087,17 @@ def test_question_string_with_choice_default(): 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", + questions = { + "some_password": { "type": "password", "ask": ask_text, "example": example_text, } - ] + } answers = {} with patch.object( @@ -640,311 +2108,17 @@ def test_question_password_input_test_ask_with_example(): assert example_text in prompt.call_args[1]["message"] -@pytest.mark.skip # we should do something with this help -def test_question_password_input_test_ask_with_help(): - ask_text = "some question" - help_text = "some_help" - questions = [ - { - "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", + questions = { + "some_path": { "type": "path", "ask": ask_text, "example": example_text, } - ] + } answers = {} with patch.object( @@ -955,947 +2129,17 @@ def test_question_path_input_test_ask_with_example(): assert example_text in prompt.call_args[1]["message"] -@pytest.mark.skip # we should do something with this help -def test_question_path_input_test_ask_with_help(): - ask_text = "some question" - help_text = "some_help" - questions = [ - { - "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", + questions = { + "some_number": { "type": "number", "ask": ask_text, "example": example_value, } - ] + } answers = {} with patch.object( @@ -1906,194 +2150,89 @@ def test_question_number_input_test_ask_with_example(): assert example_value in prompt.call_args[1]["message"] -@pytest.mark.skip # we should do something with this help -def test_question_number_input_test_ask_with_help(): - ask_text = "some question" - help_value = 1337 - questions = [ - { - "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 BooleanOption.normalize("yes") == 1 + assert BooleanOption.normalize("Yes") == 1 + assert BooleanOption.normalize(" yes ") == 1 + assert BooleanOption.normalize("y") == 1 + assert BooleanOption.normalize("true") == 1 + assert BooleanOption.normalize("True") == 1 + assert BooleanOption.normalize("on") == 1 + assert BooleanOption.normalize("1") == 1 + assert BooleanOption.normalize(1) == 1 - 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 BooleanOption.normalize("no") == 0 + assert BooleanOption.normalize("No") == 0 + assert BooleanOption.normalize(" no ") == 0 + assert BooleanOption.normalize("n") == 0 + assert BooleanOption.normalize("false") == 0 + assert BooleanOption.normalize("False") == 0 + assert BooleanOption.normalize("off") == 0 + assert BooleanOption.normalize("0") == 0 + assert BooleanOption.normalize(0) == 0 - 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 + assert BooleanOption.normalize("") is None + assert BooleanOption.normalize(" ") is None + assert BooleanOption.normalize(" none ") is None + assert BooleanOption.normalize("None") is None + assert BooleanOption.normalize("noNe") is None + assert BooleanOption.normalize(None) is None def test_normalize_boolean_humanize(): + assert BooleanOption.humanize("yes") == "yes" + assert BooleanOption.humanize("true") == "yes" + assert BooleanOption.humanize("on") == "yes" - 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" + assert BooleanOption.humanize("no") == "no" + assert BooleanOption.humanize("false") == "no" + assert BooleanOption.humanize("off") == "no" def test_normalize_boolean_invalid(): - with pytest.raises(YunohostValidationError): - BooleanQuestion.normalize("yesno") + BooleanOption.normalize("yesno") with pytest.raises(YunohostValidationError): - BooleanQuestion.normalize("foobar") + BooleanOption.normalize("foobar") with pytest.raises(YunohostValidationError): - BooleanQuestion.normalize("enabled") + BooleanOption.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 BooleanOption.normalize("yes", customyesno) == "enabled" + assert BooleanOption.normalize("true", customyesno) == "enabled" + assert BooleanOption.normalize("enabled", customyesno) == "enabled" + assert BooleanOption.humanize("yes", customyesno) == "yes" + assert BooleanOption.humanize("true", customyesno) == "yes" + assert BooleanOption.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" + assert BooleanOption.normalize("no", customyesno) == "disabled" + assert BooleanOption.normalize("false", customyesno) == "disabled" + assert BooleanOption.normalize("disabled", customyesno) == "disabled" + assert BooleanOption.humanize("no", customyesno) == "no" + assert BooleanOption.humanize("false", customyesno) == "no" + assert BooleanOption.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" + assert DomainOption.normalize("https://yolo.swag/") == "yolo.swag" + assert DomainOption.normalize("http://yolo.swag") == "yolo.swag" + assert DomainOption.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" + assert WebPathOption.normalize("") == "/" + assert WebPathOption.normalize("") == "/" + assert WebPathOption.normalize("macnuggets") == "/macnuggets" + assert WebPathOption.normalize("/macnuggets") == "/macnuggets" + assert WebPathOption.normalize(" /macnuggets ") == "/macnuggets" + assert WebPathOption.normalize("/macnuggets") == "/macnuggets" + assert WebPathOption.normalize("mac/nuggets") == "/mac/nuggets" + assert WebPathOption.normalize("/macnuggets/") == "/macnuggets" + assert WebPathOption.normalize("macnuggets/") == "/macnuggets" + assert WebPathOption.normalize("////macnuggets///") == "/macnuggets" def test_simple_evaluate(): diff --git a/src/tests/test_regenconf.py b/src/tests/test_regenconf.py index f454f33e3..8dda1a7f2 100644 --- a/src/tests/test_regenconf.py +++ b/src/tests/test_regenconf.py @@ -16,19 +16,16 @@ SSHD_CONFIG = "/etc/ssh/sshd_config" def setup_function(function): - _force_clear_hashes([TEST_DOMAIN_NGINX_CONFIG]) clean() def teardown_function(function): - clean() _force_clear_hashes([TEST_DOMAIN_NGINX_CONFIG]) def clean(): - assert os.system("pgrep slapd >/dev/null") == 0 assert os.system("pgrep nginx >/dev/null") == 0 @@ -48,7 +45,6 @@ def clean(): def test_add_domain(): - domain_add(TEST_DOMAIN) assert TEST_DOMAIN in domain_list()["domains"] @@ -60,7 +56,6 @@ def test_add_domain(): def test_add_and_edit_domain_conf(): - domain_add(TEST_DOMAIN) assert os.path.exists(TEST_DOMAIN_NGINX_CONFIG) @@ -73,7 +68,6 @@ def test_add_and_edit_domain_conf(): def test_add_domain_conf_already_exists(): - os.system("echo ' ' >> %s" % TEST_DOMAIN_NGINX_CONFIG) domain_add(TEST_DOMAIN) @@ -84,7 +78,6 @@ def test_add_domain_conf_already_exists(): def test_ssh_conf_unmanaged(): - _force_clear_hashes([SSHD_CONFIG]) assert SSHD_CONFIG not in _get_conf_hashes("ssh") @@ -95,7 +88,6 @@ def test_ssh_conf_unmanaged(): def test_ssh_conf_unmanaged_and_manually_modified(mocker): - _force_clear_hashes([SSHD_CONFIG]) os.system("echo ' ' >> %s" % SSHD_CONFIG) diff --git a/src/tests/test_service.py b/src/tests/test_service.py index 88013a3fe..84573fd89 100644 --- a/src/tests/test_service.py +++ b/src/tests/test_service.py @@ -14,17 +14,14 @@ from yunohost.service import ( def setup_function(function): - clean() def teardown_function(function): - clean() def clean(): - # To run these tests, we assume ssh(d) service exists and is running assert os.system("pgrep sshd >/dev/null") == 0 @@ -45,46 +42,39 @@ def clean(): def test_service_status_all(): - status = service_status() assert "ssh" in status.keys() assert status["ssh"]["status"] == "running" def test_service_status_single(): - status = service_status("ssh") assert "status" in status.keys() assert status["status"] == "running" def test_service_log(): - logs = service_log("ssh") assert "journalctl" in logs.keys() assert "/var/log/auth.log" in logs.keys() def test_service_status_unknown_service(mocker): - with raiseYunohostError(mocker, "service_unknown"): service_status(["ssh", "doesnotexists"]) def test_service_add(): - service_add("dummyservice", description="A dummy service to run tests") assert "dummyservice" in service_status().keys() def test_service_add_real_service(): - service_add("networking") assert "networking" in service_status().keys() def test_service_remove(): - service_add("dummyservice", description="A dummy service to run tests") assert "dummyservice" in service_status().keys() service_remove("dummyservice") @@ -92,7 +82,6 @@ def test_service_remove(): def test_service_remove_service_that_doesnt_exists(mocker): - assert "dummyservice" not in service_status().keys() with raiseYunohostError(mocker, "service_unknown"): @@ -102,7 +91,6 @@ def test_service_remove_service_that_doesnt_exists(mocker): def test_service_update_to_add_properties(): - service_add("dummyservice", description="dummy") assert not _get_services()["dummyservice"].get("test_status") service_add("dummyservice", description="dummy", test_status="true") @@ -110,7 +98,6 @@ def test_service_update_to_add_properties(): def test_service_update_to_change_properties(): - service_add("dummyservice", description="dummy", test_status="false") assert _get_services()["dummyservice"].get("test_status") == "false" service_add("dummyservice", description="dummy", test_status="true") @@ -118,7 +105,6 @@ def test_service_update_to_change_properties(): def test_service_update_to_remove_properties(): - service_add("dummyservice", description="dummy", test_status="false") assert _get_services()["dummyservice"].get("test_status") == "false" service_add("dummyservice", description="dummy", test_status="") @@ -126,7 +112,6 @@ def test_service_update_to_remove_properties(): def test_service_conf_broken(): - os.system("echo pwet > /etc/nginx/conf.d/broken.conf") status = service_status("nginx") diff --git a/src/tests/test_settings.py b/src/tests/test_settings.py index 1a9063e56..20f959a80 100644 --- a/src/tests/test_settings.py +++ b/src/tests/test_settings.py @@ -1,179 +1,229 @@ import os -import json -import glob import pytest +import yaml +from mock import patch -from yunohost.utils.error import YunohostError - -import yunohost.settings as settings +import moulinette +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.settings import ( settings_get, settings_list, - _get_settings, settings_set, settings_reset, settings_reset_all, - SETTINGS_PATH_OTHER_LOCATION, SETTINGS_PATH, - DEFAULTS, ) -DEFAULTS["example.bool"] = {"type": "bool", "default": True} -DEFAULTS["example.int"] = {"type": "int", "default": 42} -DEFAULTS["example.string"] = {"type": "string", "default": "yolo swag"} -DEFAULTS["example.enum"] = {"type": "enum", "default": "a", "choices": ["a", "b", "c"]} +EXAMPLE_SETTINGS = """ +[example] + [example.example] + [example.example.boolean] + type = "boolean" + yes = "True" + no = "False" + default = "True" + + [example.example.number] + type = "number" + default = 42 + + [example.example.string] + type = "string" + default = "yolo swag" + + [example.example.select] + type = "select" + choices = ["a", "b", "c"] + default = "a" +""" def setup_function(function): - os.system("mv /etc/yunohost/settings.json /etc/yunohost/settings.json.saved") + # Backup settings + if os.path.exists(SETTINGS_PATH): + os.system(f"mv {SETTINGS_PATH} {SETTINGS_PATH}.saved") + # Add example settings to config panel + os.system( + "cp /usr/share/yunohost/config_global.toml /usr/share/yunohost/config_global.toml.saved" + ) + with open("/usr/share/yunohost/config_global.toml", "a") as file: + file.write(EXAMPLE_SETTINGS) def teardown_function(function): - os.system("mv /etc/yunohost/settings.json.saved /etc/yunohost/settings.json") - for filename in glob.glob("/etc/yunohost/settings-*.json"): - os.remove(filename) + if os.path.exists("/etc/yunohost/settings.yml.saved"): + os.system(f"mv {SETTINGS_PATH}.saved {SETTINGS_PATH}") + elif os.path.exists(SETTINGS_PATH): + os.remove(SETTINGS_PATH) + os.system( + "mv /usr/share/yunohost/config_global.toml.saved /usr/share/yunohost/config_global.toml" + ) -def monkey_get_setting_description(key): - return "Dummy %s setting" % key.split(".")[-1] +old_translate = moulinette.core.Translator.translate -settings._get_setting_description = monkey_get_setting_description +def _monkeypatch_translator(self, key, *args, **kwargs): + if key.startswith("global_settings_setting_"): + return f"Dummy translation for {key}" + + return old_translate(self, key, *args, **kwargs) + + +moulinette.core.Translator.translate = _monkeypatch_translator + + +def _get_settings(): + return yaml.load(open(SETTINGS_PATH, "r")) def test_settings_get_bool(): - assert settings_get("example.bool") + assert settings_get("example.example.boolean") -def test_settings_get_full_bool(): - assert settings_get("example.bool", True) == { - "type": "bool", - "value": True, - "default": True, - "description": "Dummy bool setting", - } +# FIXME : Testing this doesn't make sense ? This should be tested in test_config.py ? +# def test_settings_get_full_bool(): +# assert settings_get("example.example.boolean", True) == {'version': '1.0', +# 'i18n': 'global_settings_setting', +# 'panels': [{'services': [], +# 'actions': {'apply': {'en': 'Apply'}}, +# 'sections': [{'name': '', +# 'services': [], +# 'optional': True, +# 'options': [{'type': 'boolean', +# 'yes': 'True', +# 'no': 'False', +# 'default': 'True', +# 'id': 'boolean', +# 'name': 'boolean', +# 'optional': True, +# 'current_value': 'True', +# 'ask': 'global_settings_setting_boolean', +# 'choices': []}], +# 'id': 'example'}], +# 'id': 'example', +# 'name': {'en': 'Example'}}]} def test_settings_get_int(): - assert settings_get("example.int") == 42 + assert settings_get("example.example.number") == 42 -def test_settings_get_full_int(): - assert settings_get("example.int", True) == { - "type": "int", - "value": 42, - "default": 42, - "description": "Dummy int setting", - } +# def test_settings_get_full_int(): +# assert settings_get("example.int", True) == { +# "type": "int", +# "value": 42, +# "default": 42, +# "description": "Dummy int setting", +# } def test_settings_get_string(): - assert settings_get("example.string") == "yolo swag" + assert settings_get("example.example.string") == "yolo swag" -def test_settings_get_full_string(): - assert settings_get("example.string", True) == { - "type": "string", - "value": "yolo swag", - "default": "yolo swag", - "description": "Dummy string setting", - } +# def test_settings_get_full_string(): +# assert settings_get("example.example.string", True) == { +# "type": "string", +# "value": "yolo swag", +# "default": "yolo swag", +# "description": "Dummy string setting", +# } -def test_settings_get_enum(): - assert settings_get("example.enum") == "a" +def test_settings_get_select(): + assert settings_get("example.example.select") == "a" -def test_settings_get_full_enum(): - assert settings_get("example.enum", True) == { - "type": "enum", - "value": "a", - "default": "a", - "description": "Dummy enum setting", - "choices": ["a", "b", "c"], - } +# def test_settings_get_full_select(): +# option = settings_get("example.example.select", full=True).get('panels')[0].get('sections')[0].get('options')[0] +# assert option.get('choices') == ["a", "b", "c"] def test_settings_get_doesnt_exists(): - with pytest.raises(YunohostError): + with pytest.raises(YunohostValidationError): settings_get("doesnt.exists") -def test_settings_list(): - assert settings_list() == _get_settings() +# def test_settings_list(): +# assert settings_list() == _get_settings() def test_settings_set(): - settings_set("example.bool", False) - assert settings_get("example.bool") is False + settings_set("example.example.boolean", False) + assert settings_get("example.example.boolean") == 0 - settings_set("example.bool", "on") - assert settings_get("example.bool") is True + settings_set("example.example.boolean", "on") + assert settings_get("example.example.boolean") == 1 def test_settings_set_int(): - settings_set("example.int", 21) - assert settings_get("example.int") == 21 + settings_set("example.example.number", 21) + assert settings_get("example.example.number") == 21 -def test_settings_set_enum(): - settings_set("example.enum", "c") - assert settings_get("example.enum") == "c" +def test_settings_set_select(): + settings_set("example.example.select", "c") + assert settings_get("example.example.select") == "c" def test_settings_set_doesexit(): - with pytest.raises(YunohostError): + with pytest.raises(YunohostValidationError): settings_set("doesnt.exist", True) def test_settings_set_bad_type_bool(): - with pytest.raises(YunohostError): - settings_set("example.bool", 42) - with pytest.raises(YunohostError): - settings_set("example.bool", "pouet") + with patch.object(os, "isatty", return_value=False): + with pytest.raises(YunohostError): + settings_set("example.example.boolean", 42) + with pytest.raises(YunohostError): + settings_set("example.example.boolean", "pouet") def test_settings_set_bad_type_int(): - with pytest.raises(YunohostError): - settings_set("example.int", True) - with pytest.raises(YunohostError): - settings_set("example.int", "pouet") + # with pytest.raises(YunohostError): + # settings_set("example.example.number", True) + with patch.object(os, "isatty", return_value=False): + with pytest.raises(YunohostError): + settings_set("example.example.number", "pouet") -def test_settings_set_bad_type_string(): - with pytest.raises(YunohostError): - settings_set("example.string", True) - with pytest.raises(YunohostError): - settings_set("example.string", 42) +# def test_settings_set_bad_type_string(): +# with pytest.raises(YunohostError): +# settings_set(eexample.example.string", True) +# with pytest.raises(YunohostError): +# settings_set("example.example.string", 42) -def test_settings_set_bad_value_enum(): - with pytest.raises(YunohostError): - settings_set("example.enum", True) - with pytest.raises(YunohostError): - settings_set("example.enum", "e") - with pytest.raises(YunohostError): - settings_set("example.enum", 42) - with pytest.raises(YunohostError): - settings_set("example.enum", "pouet") +def test_settings_set_bad_value_select(): + with patch.object(os, "isatty", return_value=False): + with pytest.raises(YunohostError): + settings_set("example.example.select", True) + with pytest.raises(YunohostError): + settings_set("example.example.select", "e") + with pytest.raises(YunohostError): + settings_set("example.example.select", 42) + with pytest.raises(YunohostError): + settings_set("example.example.select", "pouet") def test_settings_list_modified(): - settings_set("example.int", 21) - assert settings_list()["example.int"] == { - "default": 42, - "description": "Dummy int setting", - "type": "int", - "value": 21, - } + settings_set("example.example.number", 21) + assert int(settings_list()["example.example.number"]["value"]) == 21 def test_reset(): - settings_set("example.int", 21) - assert settings_get("example.int") == 21 - settings_reset("example.int") - assert settings_get("example.int") == settings_get("example.int", True)["default"] + option = ( + settings_get("example.example.number", full=True) + .get("panels")[0] + .get("sections")[0] + .get("options")[0] + ) + settings_set("example.example.number", 21) + assert settings_get("example.example.number") == 21 + settings_reset("example.example.number") + assert settings_get("example.example.number") == option["default"] def test_settings_reset_doesexit(): @@ -183,10 +233,10 @@ def test_settings_reset_doesexit(): def test_reset_all(): settings_before = settings_list() - settings_set("example.bool", False) - settings_set("example.int", 21) - settings_set("example.string", "pif paf pouf") - settings_set("example.enum", "c") + settings_set("example.example.boolean", False) + settings_set("example.example.number", 21) + settings_set("example.example.string", "pif paf pouf") + settings_set("example.example.select", "c") assert settings_before != settings_list() settings_reset_all() if settings_before != settings_list(): @@ -194,30 +244,30 @@ def test_reset_all(): assert settings_before[i] == settings_list()[i] -def test_reset_all_backup(): - settings_before = settings_list() - settings_set("example.bool", False) - settings_set("example.int", 21) - settings_set("example.string", "pif paf pouf") - settings_set("example.enum", "c") - settings_after_modification = settings_list() - assert settings_before != settings_after_modification - old_settings_backup_path = settings_reset_all()["old_settings_backup_path"] - - for i in settings_after_modification: - del settings_after_modification[i]["description"] - - assert settings_after_modification == json.load(open(old_settings_backup_path, "r")) +# def test_reset_all_backup(): +# settings_before = settings_list() +# settings_set("example.bool", False) +# settings_set("example.int", 21) +# settings_set("example.string", "pif paf pouf") +# settings_set("example.select", "c") +# settings_after_modification = settings_list() +# assert settings_before != settings_after_modification +# old_settings_backup_path = settings_reset_all()["old_settings_backup_path"] +# +# for i in settings_after_modification: +# del settings_after_modification[i]["description"] +# +# assert settings_after_modification == json.load(open(old_settings_backup_path, "r")) -def test_unknown_keys(): - unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown" - unknown_setting = { - "unkown_key": {"value": 42, "default": 31, "type": "int"}, - } - open(SETTINGS_PATH, "w").write(json.dumps(unknown_setting)) - - # stimulate a write - settings_reset_all() - - assert unknown_setting == json.load(open(unknown_settings_path, "r")) +# def test_unknown_keys(): +# unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown" +# unknown_setting = { +# "unkown_key": {"value": 42, "default": 31, "type": "int"}, +# } +# open(SETTINGS_PATH, "w").write(json.dumps(unknown_setting)) +# +# # stimulate a write +# settings_reset_all() +# +# assert unknown_setting == json.load(open(unknown_settings_path, "r")) diff --git a/src/tests/test_user-group.py b/src/tests/test_user-group.py index e561118e0..eececb827 100644 --- a/src/tests/test_user-group.py +++ b/src/tests/test_user-group.py @@ -11,7 +11,6 @@ from yunohost.user import ( user_import, user_export, FIELDS_FOR_IMPORT, - FIRST_ALIASES, user_group_list, user_group_create, user_group_delete, @@ -29,7 +28,7 @@ def clean_user_groups(): user_delete(u, purge=True) for g in user_group_list()["groups"]: - if g not in ["all_users", "visitors"]: + if g not in ["all_users", "visitors", "admins"]: user_group_delete(g) @@ -39,9 +38,9 @@ def setup_function(function): global maindomain maindomain = _get_maindomain() - user_create("alice", "Alice", "White", maindomain, "test123Ynh") - user_create("bob", "Bob", "Snow", maindomain, "test123Ynh") - user_create("jack", "Jack", "Black", maindomain, "test123Ynh") + user_create("alice", maindomain, "test123Ynh", admin=True, fullname="Alice White") + user_create("bob", maindomain, "test123Ynh", fullname="Bob Snow") + user_create("jack", maindomain, "test123Ynh", fullname="Jack Black") user_group_create("dev") user_group_create("apps") @@ -80,6 +79,7 @@ def test_list_groups(): assert "alice" in res assert "bob" in res assert "jack" in res + assert "alice" in res["admins"]["members"] for u in ["alice", "bob", "jack"]: assert u in res assert u in res[u]["members"] @@ -92,9 +92,8 @@ def test_list_groups(): def test_create_user(mocker): - with message(mocker, "user_created"): - user_create("albert", "Albert", "Good", maindomain, "test123Ynh") + user_create("albert", maindomain, "test123Ynh", fullname="Albert Good") group_res = user_group_list()["groups"] assert "albert" in user_list()["users"] @@ -104,7 +103,6 @@ def test_create_user(mocker): def test_del_user(mocker): - with message(mocker, "user_deleted"): user_delete("alice") @@ -175,10 +173,9 @@ def test_import_user(mocker): def test_export_user(mocker): result = user_export() - aliases = ",".join([alias + maindomain for alias in FIRST_ALIASES]) should_be = ( "username;firstname;lastname;password;mail;mail-alias;mail-forward;mailbox-quota;groups\r\n" - f"alice;Alice;White;;alice@{maindomain};{aliases};;0;dev\r\n" + f"alice;Alice;White;;alice@{maindomain};;;0;admins,dev\r\n" f"bob;Bob;Snow;;bob@{maindomain};;;0;apps\r\n" f"jack;Jack;Black;;jack@{maindomain};;;0;" ) @@ -186,7 +183,6 @@ def test_export_user(mocker): def test_create_group(mocker): - with message(mocker, "group_created", group="adminsys"): user_group_create("adminsys") @@ -197,7 +193,6 @@ def test_create_group(mocker): def test_del_group(mocker): - with message(mocker, "group_deleted", group="dev"): user_group_delete("dev") @@ -212,17 +207,17 @@ def test_del_group(mocker): def test_create_user_with_password_too_simple(mocker): with raiseYunohostError(mocker, "password_listed"): - user_create("other", "Alice", "White", maindomain, "12") + user_create("other", maindomain, "12", fullname="Alice White") def test_create_user_already_exists(mocker): with raiseYunohostError(mocker, "user_already_exists"): - user_create("alice", "Alice", "White", maindomain, "test123Ynh") + user_create("alice", maindomain, "test123Ynh", fullname="Alice White") def test_create_user_with_domain_that_doesnt_exists(mocker): with raiseYunohostError(mocker, "domain_unknown"): - user_create("alice", "Alice", "White", "doesnt.exists", "test123Ynh") + user_create("alice", "doesnt.exists", "test123Ynh", fullname="Alice White") def test_update_user_with_mail_address_already_taken(mocker): @@ -272,8 +267,13 @@ def test_update_user(mocker): user_update("alice", firstname="NewName", lastname="NewLast") info = user_info("alice") - assert info["firstname"] == "NewName" - assert info["lastname"] == "NewLast" + assert info["fullname"] == "NewName NewLast" + + with message(mocker, "user_updated"): + user_update("alice", fullname="New2Name New2Last") + + info = user_info("alice") + assert info["fullname"] == "New2Name New2Last" def test_update_group_add_user(mocker): diff --git a/src/tools.py b/src/tools.py index abf224c1c..488ed516b 100644 --- a/src/tools.py +++ b/src/tools.py @@ -1,28 +1,22 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_tools.py - - Specific tools -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import pwd import re import os import subprocess @@ -34,9 +28,13 @@ from typing import List from moulinette import Moulinette, m18n from moulinette.utils.log import getActionLogger from moulinette.utils.process import call_async_output -from moulinette.utils.filesystem import read_yaml, write_to_yaml, cp, mkdir, rm +from moulinette.utils.filesystem import read_yaml, write_to_yaml, cp, mkdir, rm, chown -from yunohost.app import app_upgrade, app_list +from yunohost.app import ( + app_upgrade, + app_list, + _list_upgradable_apps, +) from yunohost.app_catalog import ( _initialize_apps_catalog_system, _update_apps_catalog, @@ -45,10 +43,12 @@ from yunohost.domain import domain_add from yunohost.firewall import firewall_upnp from yunohost.service import service_start, service_enable from yunohost.regenconf import regen_conf -from yunohost.utils.packages import ( +from yunohost.utils.system import ( _dump_sources_list, _list_upgradable_apt_packages, ynh_packages_version, + dpkg_is_broken, + dpkg_lock_available, ) from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation, OperationLogger @@ -62,14 +62,7 @@ def tools_versions(): return ynh_packages_version() -def tools_adminpw(new_password, check_strength=True): - """ - Change admin password - - Keyword argument: - new_password - - """ +def tools_rootpw(new_password, check_strength=True): from yunohost.user import _hash_user_password from yunohost.utils.password import ( assert_password_is_strong_enough, @@ -77,48 +70,33 @@ def tools_adminpw(new_password, check_strength=True): ) import spwd + assert_password_is_compatible(new_password) if check_strength: assert_password_is_strong_enough("admin", new_password) - assert_password_is_compatible(new_password) - new_hash = _hash_user_password(new_password) - from yunohost.utils.ldap import _get_ldap_interface - - ldap = _get_ldap_interface() - + # Write as root password try: - ldap.update( - "cn=admin", - {"userPassword": [new_hash]}, - ) - except Exception as e: - logger.error(f"unable to change admin password : {e}") - raise YunohostError("admin_password_change_failed") - else: - # Write as root password - try: - hash_root = spwd.getspnam("root").sp_pwd + hash_root = spwd.getspnam("root").sp_pwd - with open("/etc/shadow", "r") as before_file: - before = before_file.read() + with open("/etc/shadow", "r") as before_file: + before = before_file.read() - with open("/etc/shadow", "w") as after_file: - after_file.write( - before.replace( - "root:" + hash_root, "root:" + new_hash.replace("{CRYPT}", "") - ) + with open("/etc/shadow", "w") as after_file: + after_file.write( + before.replace( + "root:" + hash_root, "root:" + new_hash.replace("{CRYPT}", "") ) - # An IOError may be thrown if for some reason we can't read/write /etc/passwd - # A KeyError could also be thrown if 'root' is not in /etc/passwd in the first place (for example because no password defined ?) - # (c.f. the line about getspnam) - except (IOError, KeyError): - logger.warning(m18n.n("root_password_desynchronized")) - return - - logger.info(m18n.n("root_password_replaced_by_admin_password")) - logger.success(m18n.n("admin_password_changed")) + ) + # An IOError may be thrown if for some reason we can't read/write /etc/passwd + # A KeyError could also be thrown if 'root' is not in /etc/passwd in the first place (for example because no password defined ?) + # (c.f. the line about getspnam) + except (IOError, KeyError): + logger.warning(m18n.n("root_password_desynchronized")) + return + else: + logger.info(m18n.n("root_password_changed")) def tools_maindomain(new_main_domain=None): @@ -166,39 +144,17 @@ def _set_hostname(hostname, pretty_hostname=None): logger.debug(out) -def _detect_virt(): - """ - Returns the output of systemd-detect-virt (so e.g. 'none' or 'lxc' or ...) - You can check the man of the command to have a list of possible outputs... - """ - - p = subprocess.Popen( - "systemd-detect-virt".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT - ) - - out, _ = p.communicate() - return out.split()[0] - - @is_unit_operation() def tools_postinstall( operation_logger, domain, + username, + fullname, password, ignore_dyndns=False, - force_password=False, force_diskspace=False, + overwrite_root_password=True, ): - """ - YunoHost post-install - - Keyword argument: - domain -- YunoHost main domain - ignore_dyndns -- Do not subscribe domain to a DynDNS service (only - needed for nohost.me, noho.st domains) - password -- YunoHost admin password - - """ from yunohost.dyndns import _dyndns_available from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.utils.password import ( @@ -206,6 +162,7 @@ def tools_postinstall( assert_password_is_compatible, ) from yunohost.domain import domain_main_domain + from yunohost.user import user_create, ADMIN_ALIASES import psutil # Do some checks at first @@ -218,8 +175,21 @@ def tools_postinstall( raw_msg=True, ) + # Crash early if the username is already a system user, which is + # a common confusion. We don't want to crash later and end up in an half-configured state. + all_existing_usernames = {x.pw_name for x in pwd.getpwall()} + if username in all_existing_usernames: + raise YunohostValidationError("system_username_exists") + + if username in ADMIN_ALIASES: + raise YunohostValidationError( + f"Unfortunately, {username} cannot be used as a username", raw_msg=True + ) + # Check there's at least 10 GB on the rootfs... - disk_partitions = sorted(psutil.disk_partitions(), key=lambda k: k.mountpoint) + disk_partitions = sorted( + psutil.disk_partitions(all=True), key=lambda k: k.mountpoint + ) main_disk_partitions = [d for d in disk_partitions if d.mountpoint in ["/", "/var"]] main_space = sum( psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions @@ -230,12 +200,12 @@ def tools_postinstall( # Check password assert_password_is_compatible(password) - - if not force_password: - assert_password_is_strong_enough("admin", password) + assert_password_is_strong_enough("admin", password) # If this is a nohost.me/noho.st, actually check for availability if not ignore_dyndns and is_yunohost_dyndns_domain(domain): + available = None + # Check if the domain is available... try: available = _dyndns_available(domain) @@ -268,8 +238,11 @@ def tools_postinstall( domain_add(domain, dyndns) domain_main_domain(domain) - # Update LDAP admin and create home dir - tools_adminpw(password, check_strength=not force_password) + # First user + user_create(username, domain, password, admin=True, fullname=fullname) + + if overwrite_root_password: + tools_rootpw(password) # Enable UPnP silently and reload firewall firewall_upnp("enable", no_refresh=True) @@ -319,6 +292,17 @@ def tools_postinstall( def tools_regen_conf( names=[], with_diff=False, force=False, dry_run=False, list_pending=False ): + # Make sure the settings are migrated before running the migration, + # which may otherwise fuck things up such as the ssh config ... + # We do this here because the regen-conf is called before the migration in debian/postinst + if os.path.exists("/etc/yunohost/settings.json") and not os.path.exists( + "/etc/yunohost/settings.yml" + ): + try: + tools_migrations_run(["0025_global_settings_to_configpanel"]) + except Exception as e: + logger.error(e) + return regen_conf(names, with_diff, force, dry_run, list_pending) @@ -338,10 +322,11 @@ def tools_update(target=None): upgradable_system_packages = [] if target in ["system", "all"]: - # Update APT cache # LC_ALL=C is here to make sure the results are in english - command = "LC_ALL=C apt-get update -o Acquire::Retries=3" + command = ( + "LC_ALL=C apt-get update -o Acquire::Retries=3 --allow-releaseinfo-change" + ) # Filter boring message about "apt not having a stable CLI interface" # Also keep track of wether or not we encountered a warning... @@ -391,12 +376,34 @@ def tools_update(target=None): except YunohostError as e: logger.error(str(e)) - upgradable_apps = list(app_list(upgradable=True)["apps"]) + upgradable_apps = _list_upgradable_apps() if len(upgradable_apps) == 0 and len(upgradable_system_packages) == 0: logger.info(m18n.n("already_up_to_date")) - return {"system": upgradable_system_packages, "apps": upgradable_apps} + important_yunohost_upgrade = False + if upgradable_system_packages and any( + p["name"] == "yunohost" for p in upgradable_system_packages + ): + yunohost = [p for p in upgradable_system_packages if p["name"] == "yunohost"][0] + current_version = yunohost["current_version"].split(".")[:2] + new_version = yunohost["new_version"].split(".")[:2] + important_yunohost_upgrade = current_version != new_version + + # Wrapping this in a try/except just in case for some reason we can't load + # the migrations, which would result in the update/upgrade process being blocked... + try: + pending_migrations = tools_migrations_list(pending=True)["migrations"] + except Exception as e: + logger.error(e) + pending_migrations = [] + + return { + "system": upgradable_system_packages, + "apps": upgradable_apps, + "important_yunohost_upgrade": important_yunohost_upgrade, + "pending_migrations": pending_migrations, + } @is_unit_operation() @@ -408,18 +415,18 @@ def tools_upgrade(operation_logger, target=None): apps -- List of apps to upgrade (or [] to update all apps) system -- True to upgrade system """ - from yunohost.utils import packages - if packages.dpkg_is_broken(): + if dpkg_is_broken(): raise YunohostValidationError("dpkg_is_broken") # Check for obvious conflict with other dpkg/apt commands already running in parallel - if not packages.dpkg_lock_available(): + if not dpkg_lock_available(): raise YunohostValidationError("dpkg_lock_not_available") if target not in ["apps", "system"]: raise YunohostValidationError( - "Uhoh ?! tools_upgrade should have 'apps' or 'system' value for argument target" + "Uhoh ?! tools_upgrade should have 'apps' or 'system' value for argument target", + raw_msg=True, ) # @@ -428,7 +435,6 @@ def tools_upgrade(operation_logger, target=None): # if target == "apps": - # Make sure there's actually something to upgrade upgradable_apps = [app["id"] for app in app_list(upgradable=True)["apps"]] @@ -452,7 +458,6 @@ def tools_upgrade(operation_logger, target=None): # if target == "system": - # Check that there's indeed some packages to upgrade upgradables = list(_list_upgradable_apt_packages()) if not upgradables: @@ -478,13 +483,6 @@ def tools_upgrade(operation_logger, target=None): logger.debug("Running apt command :\n{}".format(dist_upgrade)) - def is_relevant(line): - irrelevants = [ - "service sudo-ldap already provided", - "Reading database ...", - ] - return all(i not in line.rstrip() for i in irrelevants) - callbacks = ( lambda l: logger.info("+ " + l.rstrip() + "\r") if _apt_log_line_is_relevant(l) @@ -496,8 +494,10 @@ def tools_upgrade(operation_logger, target=None): returncode = call_async_output(dist_upgrade, callbacks, shell=True) # If yunohost is being upgraded from the webadmin - if "yunohost" in upgradables and Moulinette.interface.type == "api": - + if ( + any(p["name"] == "yunohost" for p in upgradables) + and Moulinette.interface.type == "api" + ): # Restart the API after 10 sec (at now doesn't support sub-minute times...) # We do this so that the API / webadmin still gets the proper HTTP response # It's then up to the webadmin to implement a proper UX process to wait 10 sec and then auto-fresh the webadmin @@ -510,7 +510,7 @@ def tools_upgrade(operation_logger, target=None): logger.warning( m18n.n( "tools_upgrade_failed", - packages_list=", ".join(upgradables), + packages_list=", ".join([p["name"] for p in upgradables]), ) ) @@ -538,6 +538,8 @@ def _apt_log_line_is_relevant(line): "==> Keeping old config file as default.", "is a disabled or a static unit", " update-rc.d: warning: start and stop actions are no longer supported; falling back to defaults", + "insserv: warning: current stop runlevel", + "insserv: warning: current start runlevel", ] return line.rstrip() and all(i not in line.rstrip() for i in irrelevants) @@ -719,7 +721,6 @@ def tools_migrations_run( # Actually run selected migrations for migration in targets: - # If we are migrating in "automatic mode" (i.e. from debian configure # during an upgrade of the package) but we are asked for running # migrations to be ran manually by the user, stop there and ask the @@ -775,7 +776,6 @@ def tools_migrations_run( _write_migration_state(migration.id, "skipped") operation_logger.success() else: - try: migration.operation_logger = operation_logger logger.info(m18n.n("migrations_running_forward", id=migration.id)) @@ -807,14 +807,12 @@ def tools_migrations_state(): def _write_migration_state(migration_id, state): - current_states = tools_migrations_state() current_states["migrations"][migration_id] = state write_to_yaml(MIGRATIONS_STATE_PATH, current_states) def _get_migrations_list(): - # states is a datastructure that represents the last run migration # it has this form: # { @@ -865,7 +863,6 @@ def _get_migration_by_name(migration_name): def _load_migration(migration_file): - migration_id = migration_file[: -len(".py")] logger.debug(m18n.n("migrations_loading_migration", id=migration_id)) @@ -900,7 +897,6 @@ def _skip_all_migrations(): def _tools_migrations_run_after_system_restore(backup_version): - all_migrations = _get_migrations_list() current_version = version.parse(ynh_packages_version()["yunohost"]["version"]) @@ -927,7 +923,6 @@ def _tools_migrations_run_after_system_restore(backup_version): def _tools_migrations_run_before_app_restore(backup_version, app_id): - all_migrations = _get_migrations_list() current_version = version.parse(ynh_packages_version()["yunohost"]["version"]) @@ -954,7 +949,6 @@ def _tools_migrations_run_before_app_restore(backup_version, app_id): class Migration: - # Those are to be implemented by daughter classes mode = "auto" @@ -980,9 +974,8 @@ class Migration: def description(self): return m18n.n(f"migration_description_{self.id}") - def ldap_migration(self, run): + def ldap_migration(run): def func(self): - # Backup LDAP before the migration logger.info(m18n.n("migration_ldap_backup_before_migration")) try: @@ -1008,22 +1001,28 @@ class Migration: try: run(self, backup_folder) except Exception: - logger.warning( - m18n.n("migration_ldap_migration_failed_trying_to_rollback") - ) - os.system("systemctl stop slapd") - # To be sure that we don't keep some part of the old config - rm("/etc/ldap/slapd.d", force=True, recursive=True) - cp(f"{backup_folder}/ldap_config", "/etc/ldap", recursive=True) - cp(f"{backup_folder}/ldap_db", "/var/lib/ldap", recursive=True) - cp( - f"{backup_folder}/apps_settings", - "/etc/yunohost/apps", - recursive=True, - ) - os.system("systemctl start slapd") - rm(backup_folder, force=True, recursive=True) - logger.info(m18n.n("migration_ldap_rollback_success")) + if self.ldap_migration_started: + logger.warning( + m18n.n("migration_ldap_migration_failed_trying_to_rollback") + ) + os.system("systemctl stop slapd") + # To be sure that we don't keep some part of the old config + rm("/etc/ldap", force=True, recursive=True) + cp(f"{backup_folder}/ldap_config", "/etc/ldap", recursive=True) + chown("/etc/ldap/schema/", "openldap", "openldap", recursive=True) + chown("/etc/ldap/slapd.d/", "openldap", "openldap", recursive=True) + rm("/var/lib/ldap", force=True, recursive=True) + cp(f"{backup_folder}/ldap_db", "/var/lib/ldap", recursive=True) + rm("/etc/yunohost/apps", force=True, recursive=True) + chown("/var/lib/ldap/", "openldap", recursive=True) + cp( + f"{backup_folder}/apps_settings", + "/etc/yunohost/apps", + recursive=True, + ) + os.system("systemctl start slapd") + rm(backup_folder, force=True, recursive=True) + logger.info(m18n.n("migration_ldap_rollback_success")) raise else: rm(backup_folder, force=True, recursive=True) diff --git a/src/user.py b/src/user.py index 0c5a577d7..00876854e 100644 --- a/src/user.py +++ b/src/user.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2014 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_user.py - - Manage users -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import pwd @@ -40,6 +33,7 @@ from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.service import service_status from yunohost.log import is_unit_operation +from yunohost.utils.system import binary_to_human logger = getActionLogger("yunohost.user") @@ -55,11 +49,10 @@ FIELDS_FOR_IMPORT = { "groups": r"^|([a-z0-9_]+(,?[a-z0-9_]+)*)$", } -FIRST_ALIASES = ["root@", "admin@", "webmaster@", "postmaster@", "abuse@"] +ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] def user_list(fields=None): - from yunohost.utils.ldap import _get_ldap_interface ldap_attrs = { @@ -129,17 +122,52 @@ def user_list(fields=None): return {"users": users} +def list_shells(): + with open("/etc/shells", "r") as f: + content = f.readlines() + + return [line.strip() for line in content if line.startswith("/")] + + +def shellexists(shell): + """Check if the provided shell exists and is executable.""" + return os.path.isfile(shell) and os.access(shell, os.X_OK) + + @is_unit_operation([("username", "user")]) def user_create( operation_logger, username, - firstname, - lastname, domain, password, + fullname=None, + firstname=None, + lastname=None, mailbox_quota="0", + admin=False, from_import=False, + loginShell=None, ): + if firstname or lastname: + logger.warning( + "Options --firstname / --lastname of 'yunohost user create' are deprecated. We recommend using --fullname instead." + ) + + if not fullname or not fullname.strip(): + if not firstname.strip(): + raise YunohostValidationError( + "You should specify the fullname of the user using option -F" + ) + lastname = ( + lastname or " " + ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... + fullname = f"{firstname} {lastname}".strip() + else: + fullname = fullname.strip() + firstname = fullname.split()[0] + lastname = ( + " ".join(fullname.split()[1:]) or " " + ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... from yunohost.domain import domain_list, _get_maindomain, _assert_domain_exists from yunohost.hook import hook_callback @@ -151,7 +179,7 @@ def user_create( # Ensure compatibility and sufficiently complex password assert_password_is_compatible(password) - assert_password_is_strong_enough("user", password) + assert_password_is_strong_enough("admin" if admin else "user", password) # Validate domain used for email address/xmpp account if domain is None: @@ -192,10 +220,7 @@ def user_create( if username in all_existing_usernames: raise YunohostValidationError("system_username_exists") - main_domain = _get_maindomain() - aliases = [alias + main_domain for alias in FIRST_ALIASES] - - if mail in aliases: + if mail.split("@")[0] in ADMIN_ALIASES: raise YunohostValidationError("mail_unavailable") if not from_import: @@ -205,14 +230,22 @@ def user_create( all_uid = {str(x.pw_uid) for x in pwd.getpwall()} all_gid = {str(x.gr_gid) for x in grp.getgrall()} + # Prevent users from obtaining uid 1007 which is the uid of the legacy admin, + # and there could be a edge case where a new user becomes owner of an old, removed admin user + all_uid.add("1007") + all_gid.add("1007") + uid_guid_found = False while not uid_guid_found: # LXC uid number is limited to 65536 by default uid = str(random.randint(1001, 65000)) uid_guid_found = uid not in all_uid and uid not in all_gid - # Adapt values for LDAP - fullname = f"{firstname} {lastname}" + if not loginShell: + loginShell = "/bin/bash" + else: + if not shellexists(loginShell) or loginShell not in list_shells(): + raise YunohostValidationError("invalid_shell", shell=loginShell) attr_dict = { "objectClass": [ @@ -233,13 +266,9 @@ def user_create( "gidNumber": [uid], "uidNumber": [uid], "homeDirectory": ["/home/" + username], - "loginShell": ["/bin/bash"], + "loginShell": [loginShell], } - # If it is the first user, add some aliases - if not ldap.search(base="ou=users", filter="uid=*"): - attr_dict["mail"] = [attr_dict["mail"]] + aliases - try: ldap.add(f"uid={username},ou=users", attr_dict) except Exception as e: @@ -265,6 +294,8 @@ def user_create( # Create group for user and add to group 'all_users' user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) user_group_update(groupname="all_users", add=username, force=True, sync_perm=True) + if admin: + user_group_update(groupname="admins", add=username, sync_perm=True) # Trigger post_user_create hooks env_dict = { @@ -286,14 +317,6 @@ def user_create( @is_unit_operation([("username", "user")]) def user_delete(operation_logger, username, purge=False, from_import=False): - """ - Delete user - - Keyword argument: - username -- Username to delete - purge - - """ from yunohost.hook import hook_callback from yunohost.utils.ldap import _get_ldap_interface @@ -351,23 +374,22 @@ def user_update( remove_mailalias=None, mailbox_quota=None, from_import=False, + fullname=None, + loginShell=None, ): - """ - Update user informations + if firstname or lastname: + logger.warning( + "Options --firstname / --lastname of 'yunohost user create' are deprecated. We recommend using --fullname instead." + ) - Keyword argument: - lastname - mail - firstname - add_mailalias -- Mail aliases to add - remove_mailforward -- Mailforward addresses to remove - username -- Username of user to update - add_mailforward -- Mailforward addresses to add - change_password -- New password to set - remove_mailalias -- Mail aliases to remove + if fullname and fullname.strip(): + fullname = fullname.strip() + firstname = fullname.split()[0] + lastname = ( + " ".join(fullname.split()[1:]) or " " + ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... - """ - from yunohost.domain import domain_list, _get_maindomain + from yunohost.domain import domain_list from yunohost.app import app_ssowatconf from yunohost.utils.password import ( assert_password_is_strong_enough, @@ -380,7 +402,7 @@ def user_update( # Populate user informations ldap = _get_ldap_interface() - attrs_to_fetch = ["givenName", "sn", "mail", "maildrop"] + attrs_to_fetch = ["givenName", "sn", "mail", "maildrop", "memberOf"] result = ldap.search( base="ou=users", filter="uid=" + username, @@ -396,20 +418,20 @@ def user_update( if firstname: new_attr_dict["givenName"] = [firstname] # TODO: Validate new_attr_dict["cn"] = new_attr_dict["displayName"] = [ - firstname + " " + user["sn"][0] + (firstname + " " + user["sn"][0]).strip() ] env_dict["YNH_USER_FIRSTNAME"] = firstname if lastname: new_attr_dict["sn"] = [lastname] # TODO: Validate new_attr_dict["cn"] = new_attr_dict["displayName"] = [ - user["givenName"][0] + " " + lastname + (user["givenName"][0] + " " + lastname).strip() ] env_dict["YNH_USER_LASTNAME"] = lastname if lastname and firstname: new_attr_dict["cn"] = new_attr_dict["displayName"] = [ - firstname + " " + lastname + (firstname + " " + lastname).strip() ] # change_password is None if user_update is not called to change the password @@ -421,17 +443,18 @@ def user_update( change_password = Moulinette.prompt( m18n.n("ask_password"), is_password=True, confirm=True ) + # Ensure compatibility and sufficiently complex password assert_password_is_compatible(change_password) - assert_password_is_strong_enough("user", change_password) + is_admin = "cn=admins,ou=groups,dc=yunohost,dc=org" in user["memberOf"] + assert_password_is_strong_enough( + "admin" if is_admin else "user", change_password + ) new_attr_dict["userPassword"] = [_hash_user_password(change_password)] env_dict["YNH_USER_PASSWORD"] = change_password if mail: - main_domain = _get_maindomain() - aliases = [alias + main_domain for alias in FIRST_ALIASES] - # If the requested mail address is already as main address or as an alias by this user if mail in user["mail"]: user["mail"].remove(mail) @@ -445,7 +468,8 @@ def user_update( raise YunohostError( "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] ) - if mail in aliases: + + if mail.split("@")[0] in ADMIN_ALIASES: raise YunohostValidationError("mail_unavailable") new_attr_dict["mail"] = [mail] + user["mail"][1:] @@ -454,6 +478,9 @@ def user_update( if not isinstance(add_mailalias, list): add_mailalias = [add_mailalias] for mail in add_mailalias: + if mail.split("@")[0] in ADMIN_ALIASES: + raise YunohostValidationError("mail_unavailable") + # (c.f. similar stuff as before) if mail in user["mail"]: user["mail"].remove(mail) @@ -508,6 +535,12 @@ def user_update( new_attr_dict["mailuserquota"] = [mailbox_quota] env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota + if loginShell is not None: + if not shellexists(loginShell) or loginShell not in list_shells(): + raise YunohostValidationError("invalid_shell", shell=loginShell) + new_attr_dict["loginShell"] = [loginShell] + env_dict["YNH_USER_LOGINSHELL"] = loginShell + if not from_import: operation_logger.start() @@ -516,6 +549,10 @@ def user_update( except Exception as e: raise YunohostError("user_update_failed", user=username, error=e) + # Invalidate passwd and group to update the loginShell + subprocess.call(["nscd", "-i", "passwd"]) + subprocess.call(["nscd", "-i", "group"]) + # Trigger post_user_update hooks hook_callback("post_user_update", env=env_dict) @@ -537,7 +574,7 @@ def user_info(username): ldap = _get_ldap_interface() - user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"] + user_attrs = ["cn", "mail", "uid", "maildrop", "mailuserquota", "loginShell"] if len(username.split("@")) == 2: filter = "mail=" + username @@ -554,9 +591,8 @@ def user_info(username): result_dict = { "username": user["uid"][0], "fullname": user["cn"][0], - "firstname": user["givenName"][0], - "lastname": user["sn"][0], "mail": user["mail"][0], + "loginShell": user["loginShell"][0], "mail-aliases": [], "mail-forward": [], } @@ -595,8 +631,8 @@ def user_info(username): has_value = re.search(r"Value=(\d+)", cmd_result) if has_value: - storage_use = int(has_value.group(1)) - storage_use = _convertSize(storage_use) + storage_use = int(has_value.group(1)) * 1000 + storage_use = binary_to_human(storage_use) if is_limited: has_percent = re.search(r"%=(\d+)", cmd_result) @@ -695,7 +731,6 @@ def user_import(operation_logger, csvfile, update=False, delete=False): ) for user in reader: - # Validate column values against regexes format_errors = [ f"{key}: '{user[key]}' doesn't match the expected format" @@ -849,10 +884,9 @@ def user_import(operation_logger, csvfile, update=False, delete=False): user_update( new_infos["username"], - new_infos["firstname"], - new_infos["lastname"], - new_infos["mail"], - new_infos["password"], + firstname=new_infos["firstname"], + lastname=new_infos["lastname"], + change_password=new_infos["password"], mailbox_quota=new_infos["mailbox-quota"], mail=new_infos["mail"], add_mailalias=new_infos["mail-alias"], @@ -892,12 +926,12 @@ def user_import(operation_logger, csvfile, update=False, delete=False): try: user_create( user["username"], - user["firstname"], - user["lastname"], user["domain"], user["password"], user["mailbox-quota"], from_import=True, + firstname=user["firstname"], + lastname=user["lastname"], ) update(user) result["created"] += 1 @@ -952,7 +986,6 @@ def user_group_list(short=False, full=False, include_primary_groups=True): users = user_list()["users"] groups = {} for infos in groups_infos: - name = infos["cn"][0] if not include_primary_groups and name in users: @@ -1070,7 +1103,7 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): # # We also can't delete "all_users" because that's a special group... existing_users = list(user_list()["users"].keys()) - undeletable_groups = existing_users + ["all_users", "visitors"] + undeletable_groups = existing_users + ["all_users", "visitors", "admins"] if groupname in undeletable_groups and not force: raise YunohostValidationError("group_cannot_be_deleted", group=groupname) @@ -1096,22 +1129,14 @@ def user_group_update( groupname, add=None, remove=None, + add_mailalias=None, + remove_mailalias=None, force=False, sync_perm=True, from_import=False, ): - """ - Update user informations - - Keyword argument: - groupname -- Groupname to update - add -- User(s) to add in group - remove -- User(s) to remove in group - - """ - from yunohost.permission import permission_sync_to_user - from yunohost.utils.ldap import _get_ldap_interface + from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract existing_users = list(user_list()["users"].keys()) @@ -1128,9 +1153,28 @@ def user_group_update( "group_cannot_edit_primary_group", group=groupname ) + ldap = _get_ldap_interface() + + # Fetch info for this group + result = ldap.search( + "ou=groups", + "cn=" + groupname, + ["cn", "member", "permission", "mail", "objectClass"], + ) + + if not result: + raise YunohostValidationError("group_unknown", group=groupname) + + group = result[0] + # We extract the uid for each member of the group to keep a simple flat list of members - current_group = user_group_info(groupname)["members"] - new_group = copy.copy(current_group) + current_group_mail = group.get("mail", []) + new_group_mail = copy.copy(current_group_mail) + current_group_members = [ + _ldap_path_extract(p, "uid") for p in group.get("member", []) + ] + new_group_members = copy.copy(current_group_members) + new_attr_dict = {} if add: users_to_add = [add] if not isinstance(add, list) else add @@ -1139,43 +1183,106 @@ def user_group_update( if user not in existing_users: raise YunohostValidationError("user_unknown", user=user) - if user in current_group: + if user in current_group_members: logger.warning( m18n.n("group_user_already_in_group", user=user, group=groupname) ) else: operation_logger.related_to.append(("user", user)) + logger.info(m18n.n("group_user_add", group=groupname, user=user)) - new_group += users_to_add + new_group_members += users_to_add if remove: users_to_remove = [remove] if not isinstance(remove, list) else remove for user in users_to_remove: - if user not in current_group: + if user not in current_group_members: logger.warning( m18n.n("group_user_not_in_group", user=user, group=groupname) ) else: operation_logger.related_to.append(("user", user)) + logger.info(m18n.n("group_user_remove", group=groupname, user=user)) - # Remove users_to_remove from new_group - # Kinda like a new_group -= users_to_remove - new_group = [u for u in new_group if u not in users_to_remove] + # Remove users_to_remove from new_group_members + # Kinda like a new_group_members -= users_to_remove + new_group_members = [u for u in new_group_members if u not in users_to_remove] - new_group_dns = [ - "uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group - ] + # If something changed, we add this to the stuff to commit later in the code + if set(new_group_members) != set(current_group_members): + new_group_members_dns = [ + "uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group_members + ] + new_attr_dict["member"] = set(new_group_members_dns) + new_attr_dict["memberUid"] = set(new_group_members) - if set(new_group) != set(current_group): + # Check the whole alias situation + if add_mailalias: + from yunohost.domain import domain_list + + domains = domain_list()["domains"] + + if not isinstance(add_mailalias, list): + add_mailalias = [add_mailalias] + for mail in add_mailalias: + if mail.split("@")[0] in ADMIN_ALIASES and groupname != "admins": + raise YunohostValidationError("mail_unavailable") + if mail in current_group_mail: + continue + try: + ldap.validate_uniqueness({"mail": mail}) + except Exception as e: + raise YunohostError("group_update_failed", group=groupname, error=e) + if mail[mail.find("@") + 1 :] not in domains: + raise YunohostError( + "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] + ) + new_group_mail.append(mail) + logger.info(m18n.n("group_mailalias_add", group=groupname, mail=mail)) + + if remove_mailalias: + from yunohost.domain import _get_maindomain + + if not isinstance(remove_mailalias, list): + remove_mailalias = [remove_mailalias] + for mail in remove_mailalias: + if ( + "@" in mail + and mail.split("@")[0] in ADMIN_ALIASES + and groupname == "admins" + and mail.split("@")[1] == _get_maindomain() + ): + raise YunohostValidationError( + f"The alias {mail} can not be removed from the 'admins' group", + raw_msg=True, + ) + if mail in new_group_mail: + new_group_mail.remove(mail) + logger.info( + m18n.n("group_mailalias_remove", group=groupname, mail=mail) + ) + else: + raise YunohostValidationError("mail_alias_remove_failed", mail=mail) + + if set(new_group_mail) != set(current_group_mail): + logger.info(m18n.n("group_update_aliases", group=groupname)) + new_attr_dict["mail"] = set(new_group_mail) + + if new_attr_dict["mail"] and "mailGroup" not in group["objectClass"]: + new_attr_dict["objectClass"] = group["objectClass"] + ["mailGroup"] + if not new_attr_dict["mail"] and "mailGroup" in group["objectClass"]: + new_attr_dict["objectClass"] = [ + c + for c in group["objectClass"] + if c != "mailGroup" and c != "mailAccount" + ] + + if new_attr_dict: if not from_import: operation_logger.start() - ldap = _get_ldap_interface() try: - ldap.update( - f"cn={groupname},ou=groups", - {"member": set(new_group_dns), "memberUid": set(new_group)}, - ) + ldap.update(f"cn={groupname},ou=groups", new_attr_dict) except Exception as e: raise YunohostError("group_update_failed", group=groupname, error=e) @@ -1184,7 +1291,10 @@ def user_group_update( if not from_import: if groupname != "all_users": - logger.success(m18n.n("group_updated", group=groupname)) + if not new_attr_dict: + logger.info(m18n.n("group_no_change", group=groupname)) + else: + logger.success(m18n.n("group_updated", group=groupname)) else: logger.debug(m18n.n("group_updated", group=groupname)) @@ -1208,7 +1318,7 @@ def user_group_info(groupname): result = ldap.search( "ou=groups", "cn=" + groupname, - ["cn", "member", "permission"], + ["cn", "member", "permission", "mail"], ) if not result: @@ -1223,6 +1333,7 @@ def user_group_info(groupname): "permissions": [ _ldap_path_extract(p, "cn") for p in infos.get("permission", []) ], + "mail-aliases": [m for m in infos.get("mail", [])], } @@ -1252,6 +1363,14 @@ def user_group_remove(groupname, usernames, force=False, sync_perm=True): ) +def user_group_add_mailalias(groupname, aliases): + return user_group_update(groupname, add_mailalias=aliases, sync_perm=False) + + +def user_group_remove_mailalias(groupname, aliases): + return user_group_update(groupname, remove_mailalias=aliases, sync_perm=False) + + # # Permission subcategory # @@ -1324,14 +1443,6 @@ def user_ssh_remove_key(username, key): # -def _convertSize(num, suffix=""): - for unit in ["K", "M", "G", "T", "P", "E", "Z"]: - if abs(num) < 1024.0: - return "{:3.1f}{}{}".format(num, unit, suffix) - num /= 1024.0 - return "{:.1f}{}{}".format(num, "Yi", suffix) - - def _hash_user_password(password): """ This function computes and return a salted hash for the password in input. @@ -1359,3 +1470,20 @@ def _hash_user_password(password): salt = "$6$" + salt + "$" return "{CRYPT}" + crypt.crypt(str(password), salt) + + +def _update_admins_group_aliases(old_main_domain, new_main_domain): + current_admin_aliases = user_group_info("admins")["mail-aliases"] + + aliases_to_remove = [ + a + for a in current_admin_aliases + if "@" in a + and a.split("@")[1] == old_main_domain + and a.split("@")[0] in ADMIN_ALIASES + ] + aliases_to_add = [f"{a}@{new_main_domain}" for a in ADMIN_ALIASES] + + user_group_update( + "admins", add_mailalias=aliases_to_add, remove_mailalias=aliases_to_remove + ) diff --git a/src/utils/__init__.py b/src/utils/__init__.py index e69de29bb..7c1e7b0cd 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -0,0 +1,18 @@ +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py new file mode 100644 index 000000000..2c56eb754 --- /dev/null +++ b/src/utils/configpanel.py @@ -0,0 +1,686 @@ +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import glob +import os +import re +import urllib.parse +from collections import OrderedDict +from typing import Union + +from moulinette import Moulinette, m18n +from moulinette.interfaces.cli import colorize +from moulinette.utils.filesystem import mkdir, read_toml, read_yaml, write_to_yaml +from moulinette.utils.log import getActionLogger +from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.form import ( + OPTIONS, + BaseOption, + FileOption, + ask_questions_and_parse_answers, + evaluate_simple_js_expression, +) +from yunohost.utils.i18n import _value_for_locale + +logger = getActionLogger("yunohost.configpanel") +CONFIG_PANEL_VERSION_SUPPORTED = 1.0 + + +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._get_raw_settings() + self._hydrate() + + # In 'classic' mode, we display the current value if key refer to an option + if self.filter_key.count(".") == 2 and mode == "classic": + option = self.filter_key.split(".")[-1] + value = self.values.get(option, None) + + option_type = None + for _, _, option_ in self._iterate(): + if option_["id"] == option: + option_type = OPTIONS[option_["type"]] + break + + return option_type.normalize(value) if option_type else value + + # Format result in 'classic' or 'export' mode + logger.debug(f"Formating result in '{mode}' mode") + result = {} + for panel, section, option in self._iterate(): + if section["is_action_section"] and mode != "full": + continue + + key = f"{panel['id']}.{section['id']}.{option['id']}" + if mode == "export": + result[option["id"]] = option.get("current_value") + continue + + ask = None + if "ask" in option: + ask = _value_for_locale(option["ask"]) + elif "i18n" in self.config: + ask = m18n.n(self.config["i18n"] + "_" + option["id"]) + + if mode == "full": + option["ask"] = ask + question_class = OPTIONS[option.get("type", "string")] + # FIXME : maybe other properties should be taken from the question, not just choices ?. + option["choices"] = question_class(option).choices + option["default"] = question_class(option).default + option["pattern"] = question_class(option).pattern + else: + result[key] = {"ask": ask} + if "current_value" in option: + question_class = OPTIONS[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._get_raw_settings() + self._hydrate() + BaseOption.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...) + FileOption.clean_upload_dirs() + + self._reload_services() + + logger.success("Config updated as expected") + operation_logger.success() + + def list_actions(self): + actions = {} + + # FIXME : meh, loading the entire config panel is again going to cause + # stupid issues for domain (e.g loading registrar stuff when willing to just list available actions ...) + self.filter_key = "" + self._get_config_panel() + for panel, section, option in self._iterate(): + if option["type"] == "button": + key = f"{panel['id']}.{section['id']}.{option['id']}" + actions[key] = _value_for_locale(option["ask"]) + + return actions + + def run_action(self, action=None, args=None, args_file=None, operation_logger=None): + # + # FIXME : this stuff looks a lot like set() ... + # + + self.filter_key = ".".join(action.split(".")[:2]) + action_id = action.split(".")[2] + + # Read config panel toml + self._get_config_panel() + + # FIXME: should also check that there's indeed a key called action + if not self.config: + raise YunohostValidationError(f"No action named {action}", raw_msg=True) + + # Import and parse pre-answered options + logger.debug("Import and parse pre-answered options") + self._parse_pre_answered(args, None, args_file) + + # Read or get values and hydrate the config + self._get_raw_settings() + self._hydrate() + BaseOption.operation_logger = operation_logger + self._ask(action=action_id) + + # FIXME: here, we could want to check constrains on + # the action's visibility / requirements wrt to the answer to questions ... + + if operation_logger: + operation_logger.start() + + try: + self._run_action(action_id) + except YunohostError: + raise + # Script got manually interrupted ... + # N.B. : KeyboardInterrupt does not inherit from Exception + except (KeyboardInterrupt, EOFError): + error = m18n.n("operation_interrupted") + logger.error(m18n.n("config_action_failed", action=action, error=error)) + raise + # Something wrong happened in Yunohost's code (most probably hook_exec) + except Exception: + import traceback + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error(m18n.n("config_action_failed", action=action, error=error)) + raise + finally: + # Delete files uploaded from API + # FIXME : this is currently done in the context of config panels, + # but could also happen in the context of app install ... (or anywhere else + # where we may parse args etc...) + FileOption.clean_upload_dirs() + + # FIXME: i18n + logger.success(f"Action {action_id} successful") + operation_logger.success() + + def _get_raw_config(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_raw_config() + + # Check TOML config panel is in a supported version + if float(toml_config_panel["version"]) < CONFIG_PANEL_VERSION_SUPPORTED: + logger.error( + f"Config panels version {toml_config_panel['version']} are not supported" + ) + return None + + # Transform toml format into internal format + format_description = { + "root": { + "properties": ["version", "i18n"], + "defaults": {"version": 1.0}, + }, + "panels": { + "properties": ["name", "services", "actions", "help"], + "defaults": { + "services": [], + "actions": {"apply": {"en": "Apply"}}, + }, + }, + "sections": { + "properties": ["name", "services", "optional", "help", "visible"], + "defaults": { + "name": "", + "services": [], + "optional": True, + "is_action_section": False, + }, + }, + "options": { + "properties": [ + "ask", + "type", + "bind", + "help", + "example", + "default", + "style", + "icon", + "placeholder", + "visible", + "optional", + "choices", + "yes", + "no", + "pattern", + "limit", + "min", + "max", + "step", + "accept", + "redact", + "filter", + "readonly", + "enabled", + # "confirm", # TODO: to ask confirmation before running an action + ], + "defaults": {}, + }, + } + + def _build_internal_config_panel(raw_infos, level): + """Convert TOML in internal format ('full' mode used by webadmin) + Here are some properties of 1.0 config panel in toml: + - node properties and node children are mixed, + - text are in english only + - some properties have default values + This function detects all children nodes and put them in a list + """ + + defaults = format_description[level]["defaults"] + properties = format_description[level]["properties"] + + # Start building the ouput (merging the raw infos + defaults) + out = {key: raw_infos.get(key, value) for key, value in defaults.items()} + + # Now fill the sublevels (+ apply filter_key) + i = list(format_description).index(level) + sublevel = list(format_description)[i + 1] if level != "options" else None + search_key = filter_key[i] if len(filter_key) > i else False + + for key, value in raw_infos.items(): + # Key/value are a child node + if ( + isinstance(value, OrderedDict) + and key not in properties + and sublevel + ): + # We exclude all nodes not referenced by the filter_key + if search_key and key != search_key: + continue + subnode = _build_internal_config_panel(value, sublevel) + subnode["id"] = key + if level == "root": + subnode.setdefault("name", {"en": key.capitalize()}) + elif level == "sections": + subnode["name"] = key # legacy + subnode.setdefault("optional", raw_infos.get("optional", True)) + # If this section contains at least one button, it becomes an "action" section + if subnode.get("type") == "button": + out["is_action_section"] = True + out.setdefault(sublevel, []).append(subnode) + # Key/value are a property + else: + if key not in properties: + logger.warning(f"Unknown key '{key}' found in config panel") + # Todo search all i18n keys + out[key] = ( + value + if key not in ["ask", "help", "name"] or isinstance(value, dict) + else {"en": value} + ) + return out + + self.config = _build_internal_config_panel(toml_config_panel, "root") + + try: + self.config["panels"][0]["sections"][0]["options"][0] + except (KeyError, IndexError): + raise YunohostValidationError( + "config_unknown_filter_key", filter_key=self.filter_key + ) + + # List forbidden keywords from helpers and sections toml (to avoid conflict) + forbidden_keywords = [ + "old", + "app", + "changed", + "file_hash", + "binds", + "types", + "formats", + "getter", + "setter", + "short_setting", + "type", + "bind", + "nothing_changed", + "changes_validated", + "result", + "max_progression", + ] + forbidden_keywords += format_description["sections"] + forbidden_readonly_types = ["password", "app", "domain", "user", "file"] + + for _, _, option in self._iterate(): + if option["id"] in forbidden_keywords: + raise YunohostError("config_forbidden_keyword", keyword=option["id"]) + if ( + option.get("readonly", False) + and option.get("type", "string") in forbidden_readonly_types + ): + raise YunohostError( + "config_forbidden_readonly_type", + type=option["type"], + id=option["id"], + ) + + return self.config + + def _get_default_values(self): + return { + option["id"]: option["default"] + for _, _, option in self._iterate() + if "default" in option + } + + def _get_raw_settings(self): + """ + Retrieve entries in YAML file + And set default values if needed + """ + + # Inject defaults if needed (using the magic .update() ;)) + self.values = self._get_default_values() + + # Retrieve entries in the YAML + if os.path.exists(self.save_path) and os.path.isfile(self.save_path): + self.values.update(read_yaml(self.save_path) or {}) + + def _hydrate(self): + # Hydrating config panel with current value + for _, section, option in self._iterate(): + if option["id"] not in self.values: + allowed_empty_types = [ + "alert", + "display_text", + "markdown", + "file", + "button", + ] + + if section["is_action_section"] and option.get("default") is not None: + self.values[option["id"]] = option["default"] + elif ( + option["type"] in allowed_empty_types + or option.get("bind") == "null" + ): + continue + else: + raise YunohostError( + f"Config panel question '{option['id']}' should be initialized with a value during install or upgrade.", + raw_msg=True, + ) + value = self.values[option["name"]] + + # Allow to use value instead of current_value in app config script. + # e.g. apps may write `echo 'value: "foobar"'` in the config file (which is more intuitive that `echo 'current_value: "foobar"'` + # For example hotspot used it... + # See https://github.com/YunoHost/yunohost/pull/1546 + if ( + isinstance(value, dict) + and "value" in value + and "current_value" not in value + ): + value["current_value"] = value["value"] + + # In general, the value is just a simple value. + # Sometimes it could be a dict used to overwrite the option itself + value = value if isinstance(value, dict) else {"current_value": value} + option.update(value) + + self.values[option["id"]] = value.get("current_value") + + return self.values + + def _ask(self, action=None): + logger.debug("Ask unanswered question and prevalidate data") + + if "i18n" in self.config: + for panel, section, option in self._iterate(): + if "ask" not in option: + option["ask"] = m18n.n(self.config["i18n"] + "_" + option["id"]) + # auto add i18n help text if present in locales + if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"): + option["help"] = m18n.n( + self.config["i18n"] + "_" + option["id"] + "_help" + ) + + def display_header(message): + """CLI panel/section header display""" + if Moulinette.interface.type == "cli" and self.filter_key.count(".") < 2: + Moulinette.display(colorize(message, "purple")) + + for panel, section, obj in self._iterate(["panel", "section"]): + if ( + section + and section.get("visible") + and not evaluate_simple_js_expression( + section["visible"], context=self.future_values + ) + ): + continue + + # Ugly hack to skip action section ... except when when explicitly running actions + if not action: + if section and section["is_action_section"]: + continue + + if panel == obj: + name = _value_for_locale(panel["name"]) + display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}") + else: + name = _value_for_locale(section["name"]) + if name: + display_header(f"\n# {name}") + elif section: + # filter action section options in case of multiple buttons + section["options"] = [ + option + for option in section["options"] + if option.get("type", "string") != "button" + or option["id"] == action + ] + + if panel == obj: + continue + + # Check and ask unanswered questions + prefilled_answers = self.args.copy() + prefilled_answers.update(self.new_values) + + questions = ask_questions_and_parse_answers( + {question["name"]: question for question in section["options"]}, + prefilled_answers=prefilled_answers, + current_values=self.values, + hooks=self.hooks, + ) + self.new_values.update( + { + question.name: question.value + for question in questions + if question.value is not None + } + ) + + @property + def future_values(self): + return {**self.values, **self.new_values} + + def __getattr__(self, name): + if "new_values" in self.__dict__ and name in self.new_values: + return self.new_values[name] + + if "values" in self.__dict__ and name in self.values: + return self.values[name] + + return self.__dict__[name] + + def _parse_pre_answered(self, args, value, args_file): + args = urllib.parse.parse_qs(args or "", keep_blank_values=True) + self.args = {key: ",".join(value_) for key, value_ in args.items()} + + 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) diff --git a/src/utils/dns.py b/src/utils/dns.py index ccb6c5406..b3ca4b564 100644 --- a/src/utils/dns.py +++ b/src/utils/dns.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import dns.resolver from typing import List @@ -33,19 +31,16 @@ external_resolvers_: List[str] = [] def is_yunohost_dyndns_domain(domain): - return any( domain.endswith(f".{dyndns_domain}") for dyndns_domain in YNH_DYNDNS_DOMAINS ) def is_special_use_tld(domain): - return any(domain.endswith(f".{tld}") for tld in SPECIAL_USE_TLDS) def external_resolvers(): - global external_resolvers_ if not external_resolvers_: diff --git a/src/utils/error.py b/src/utils/error.py index a92f3bd5a..9be48c5df 100644 --- a/src/utils/error.py +++ b/src/utils/error.py @@ -1,30 +1,26 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# from moulinette.core import MoulinetteError, MoulinetteAuthenticationError from moulinette import m18n class YunohostError(MoulinetteError): - http_code = 500 """ @@ -46,7 +42,6 @@ class YunohostError(MoulinetteError): super(YunohostError, self).__init__(msg, raw_msg=True) def content(self): - if not self.log_ref: return super().content() else: @@ -54,14 +49,11 @@ class YunohostError(MoulinetteError): class YunohostValidationError(YunohostError): - http_code = 400 def content(self): - return {"error": self.strerror, "error_key": self.key, **self.kwargs} class YunohostAuthenticationError(MoulinetteAuthenticationError): - pass diff --git a/src/utils/filesystem.py b/src/utils/filesystem.py deleted file mode 100644 index 04d7d3906..000000000 --- a/src/utils/filesystem.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" -import os - - -def free_space_in_directory(dirpath): - stat = os.statvfs(dirpath) - return stat.f_frsize * stat.f_bavail - - -def space_used_by_directory(dirpath): - stat = os.statvfs(dirpath) - return stat.f_frsize * stat.f_blocks diff --git a/src/utils/config.py b/src/utils/form.py similarity index 53% rename from src/utils/config.py rename to src/utils/form.py index ec7faa719..12c3249c3 100644 --- a/src/utils/config.py +++ b/src/utils/form.py @@ -1,53 +1,47 @@ -# -*- 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 +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import ast import operator as op -from collections import OrderedDict -from typing import Optional, Dict, List, Union, Any, Mapping, Callable +import os +import re +import shutil +import tempfile +import urllib.parse +from typing import Any, Callable, Dict, List, Mapping, Optional, Union -from moulinette.interfaces.cli import colorize from moulinette import Moulinette, m18n +from moulinette.interfaces.cli import colorize +from moulinette.utils.filesystem import read_file, write_to_file from moulinette.utils.log import getActionLogger -from 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 +from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.i18n import _value_for_locale + +logger = getActionLogger("yunohost.form") + + +# ╭───────────────────────────────────────────────────────╮ +# │ ┌─╴╷ ╷╭─┐╷ │ +# │ ├─╴│╭╯├─┤│ │ +# │ ╰─╴╰╯ ╵ ╵╰─╴ │ +# ╰───────────────────────────────────────────────────────╯ -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 @@ -192,500 +186,14 @@ def evaluate_simple_js_expression(expr, context={}): return evaluate_simple_ast(node, context) -class ConfigPanel: - entity_type = "config" - save_path_tpl: Union[str, None] = None - config_path_tpl = "/usr/share/yunohost/config_{entity_type}.toml" - save_mode = "full" +# ╭───────────────────────────────────────────────────────╮ +# │ ╭─╮┌─╮╶┬╴╶┬╴╭─╮╭╮╷╭─╴ │ +# │ │ │├─╯ │ │ │ ││││╰─╮ │ +# │ ╰─╯╵ ╵ ╶┴╴╰─╯╵╰╯╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ - @classmethod - def list(cls): - """ - List available config panel - """ - try: - entities = [ - re.match( - "^" + cls.save_path_tpl.format(entity="(?p)") + "$", f - ).group("entity") - for f in glob.glob(cls.save_path_tpl.format(entity="*")) - if os.path.isfile(f) - ] - except FileNotFoundError: - entities = [] - return entities - def __init__(self, entity, config_path=None, save_path=None, creation=False): - self.entity = entity - self.config_path = config_path - if not config_path: - self.config_path = self.config_path_tpl.format( - entity=entity, entity_type=self.entity_type - ) - self.save_path = save_path - if not save_path and self.save_path_tpl: - self.save_path = self.save_path_tpl.format(entity=entity) - self.config = {} - self.values = {} - self.new_values = {} - - if ( - self.save_path - and self.save_mode != "diff" - and not creation - and not os.path.exists(self.save_path) - ): - raise YunohostValidationError( - f"{self.entity_type}_unknown", **{self.entity_type: entity} - ) - if self.save_path and creation and os.path.exists(self.save_path): - raise YunohostValidationError( - f"{self.entity_type}_exists", **{self.entity_type: entity} - ) - - # Search for hooks in the config panel - self.hooks = { - func: getattr(self, func) - for func in dir(self) - if callable(getattr(self, func)) - and re.match("^(validate|post_ask)__", func) - } - - def get(self, key="", mode="classic"): - self.filter_key = key or "" - - # Read config panel toml - self._get_config_panel() - - if not self.config: - raise YunohostValidationError("config_no_panel") - - # Read or get values and hydrate the config - self._load_current_values() - self._hydrate() - - # In 'classic' mode, we display the current value if key refer to an option - if self.filter_key.count(".") == 2 and mode == "classic": - option = self.filter_key.split(".")[-1] - 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 - 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: +class BaseOption: hide_user_input_in_prompt = False pattern: Optional[Dict] = None @@ -702,10 +210,13 @@ class Question: self.default = question.get("default", None) self.optional = question.get("optional", False) self.visible = question.get("visible", None) + self.readonly = question.get("readonly", False) # Don't restrict choices if there's none specified self.choices = question.get("choices", None) self.pattern = question.get("pattern", self.pattern) - self.ask = question.get("ask", {"en": self.name}) + self.ask = question.get("ask", self.name) + if not isinstance(self.ask, dict): + self.ask = {"en": self.ask} self.help = question.get("help") self.redact = question.get("redact", False) self.filter = question.get("filter", None) @@ -730,6 +241,60 @@ class Question: value = value.strip() return value + def ask_if_needed(self): + if self.visible and not evaluate_simple_js_expression( + self.visible, context=self.context + ): + # FIXME There could be several use case if the question is not displayed: + # - we doesn't want to give a specific value + # - we want to keep the previous value + # - we want the default value + self.value = self.values[self.name] = None + return self.values + + for i in range(5): + # Display question if no value filled or if it's a readonly message + if Moulinette.interface.type == "cli" and os.isatty(1): + text_for_user_input_in_cli = self._format_text_for_user_input_in_cli() + if self.readonly: + Moulinette.display(text_for_user_input_in_cli) + self.value = self.values[self.name] = self.current_value + return self.values + elif self.value is None: + self._prompt(text_for_user_input_in_cli) + + # Apply default value + class_default = getattr(self, "default_value", None) + if self.value in [None, ""] and ( + self.default is not None or class_default is not None + ): + self.value = class_default if self.default is None else self.default + + try: + # Normalize and validate + self.value = self.normalize(self.value, self) + self._value_pre_validator() + except YunohostValidationError as e: + # If in interactive cli, re-ask the current question + if i < 4 and Moulinette.interface.type == "cli" and os.isatty(1): + logger.error(str(e)) + self.value = None + continue + + # Otherwise raise the ValidationError + raise + + break + + self.value = self.values[self.name] = self._value_post_validator() + + # Search for post actions in hooks + post_hook = f"post_ask__{self.name}" + if post_hook in self.hooks: + self.values.update(self.hooks[post_hook](self)) + + return self.values + def _prompt(self, text): prefill = "" if self.current_value is not None: @@ -746,85 +311,17 @@ class Question: 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: - + if self.readonly: + text_for_user_input_in_cli = colorize(text_for_user_input_in_cli, "purple") + if self.choices: + return ( + text_for_user_input_in_cli + f" {self.choices[self.current_value]}" + ) + return text_for_user_input_in_cli + f" {self.humanize(self.current_value)}" + elif self.choices: # Prevent displaying a shitload of choices # (e.g. 100+ available users when choosing an app admin...) choices = ( @@ -846,7 +343,27 @@ class Question: return text_for_user_input_in_cli - def _post_parse_value(self): + def _value_pre_validator(self): + if self.value in [None, ""] and not self.optional: + raise YunohostValidationError("app_argument_required", name=self.name) + + # we have an answer, do some post checks + if self.value not in [None, ""]: + if self.choices and self.value not in self.choices: + raise YunohostValidationError( + "app_argument_choice_invalid", + name=self.name, + value=self.value, + choices=", ".join(str(choice) for choice in self.choices), + ) + if self.pattern and not re.match(self.pattern["regexp"], str(self.value)): + raise YunohostValidationError( + self.pattern["error"], + name=self.name, + value=self.value, + ) + + def _value_post_validator(self): if not self.redact: return self.value @@ -870,92 +387,66 @@ class Question: return self.value -class StringQuestion(Question): +# ╭───────────────────────────────────────────────────────╮ +# │ DISPLAY OPTIONS │ +# ╰───────────────────────────────────────────────────────╯ + + +class DisplayTextOption(BaseOption): + argument_type = "display_text" + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + + self.optional = True + self.readonly = True + self.style = question.get( + "style", "info" if question["type"] == "alert" else "" + ) + + def _format_text_for_user_input_in_cli(self): + text = _value_for_locale(self.ask) + + if self.style in ["success", "info", "warning", "danger"]: + color = { + "success": "green", + "info": "cyan", + "warning": "yellow", + "danger": "red", + } + prompt = m18n.g(self.style) if self.style != "danger" else m18n.n("danger") + return colorize(prompt, color[self.style]) + f" {text}" + else: + return text + + +class ButtonOption(BaseOption): + argument_type = "button" + enabled = None + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + self.enabled = question.get("enabled", None) + + +# ╭───────────────────────────────────────────────────────╮ +# │ INPUT OPTIONS │ +# ╰───────────────────────────────────────────────────────╯ + + +# ─ STRINGS ─────────────────────────────────────────────── + + +class StringOption(BaseOption): 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): +class PasswordOption(BaseOption): hide_user_input_in_prompt = True argument_type = "password" default_value = "" @@ -971,8 +462,8 @@ class PasswordQuestion(Question): "app_argument_password_no_default", name=self.name ) - def _prevalidate(self): - super()._prevalidate() + def _value_pre_validator(self): + super()._value_pre_validator() if self.value not in [None, ""]: if any(char in self.value for char in self.forbidden_chars): @@ -986,46 +477,95 @@ class PasswordQuestion(Question): assert_password_is_strong_enough("user", self.value) -class PathQuestion(Question): - argument_type = "path" - default_value = "" +class ColorOption(StringOption): + pattern = { + "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", + "error": "config_validate_color", # i18n: config_validate_color + } + + +# ─ NUMERIC ─────────────────────────────────────────────── + + +class NumberOption(BaseOption): + argument_type = "number" + default_value = None + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + 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 - option = option.__dict__ if isinstance(option, Question) else option + if isinstance(value, str): + value = value.strip() - 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", - ) + if isinstance(value, str) and value.isdigit(): + return int(value) - return "/" + value.strip().strip(" /") + if value in [None, ""]: + return None + + option = option.__dict__ if isinstance(option, BaseOption) else option + raise YunohostValidationError( + "app_argument_invalid", + name=option.get("name"), + error=m18n.n("invalid_number"), + ) + + def _value_pre_validator(self): + super()._value_pre_validator() + 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 BooleanQuestion(Question): +# ─ BOOLEAN ─────────────────────────────────────────────── + + +class BooleanOption(BaseOption): argument_type = "boolean" default_value = 0 yes_answers = ["1", "yes", "y", "true", "t", "on"] no_answers = ["0", "no", "n", "false", "f", "off"] + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + self.yes = question.get("yes", 1) + self.no = question.get("no", 0) + if self.default is None: + self.default = self.no + @staticmethod def humanize(value, option={}): - - option = option.__dict__ if isinstance(option, Question) else option + option = option.__dict__ if isinstance(option, BaseOption) else option yes = option.get("yes", 1) no = option.get("no", 0) - value = BooleanQuestion.normalize(value, option) + value = BooleanOption.normalize(value, option) if value == yes: return "yes" @@ -1043,8 +583,7 @@ class BooleanQuestion(Question): @staticmethod def normalize(value, option={}): - - option = option.__dict__ if isinstance(option, Question) else option + option = option.__dict__ if isinstance(option, BaseOption) else option if isinstance(value, str): value = value.strip() @@ -1052,8 +591,8 @@ class BooleanQuestion(Question): technical_yes = option.get("yes", 1) technical_no = option.get("no", 0) - no_answers = BooleanQuestion.no_answers - yes_answers = BooleanQuestion.yes_answers + no_answers = BooleanOption.no_answers + yes_answers = BooleanOption.yes_answers assert ( str(technical_yes).lower() not in no_answers @@ -1082,27 +621,216 @@ class BooleanQuestion(Question): 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 get(self, key, default=None): + return getattr(self, key, default) def _format_text_for_user_input_in_cli(self): text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() - text_for_user_input_in_cli += " [yes | no]" + if not self.readonly: + text_for_user_input_in_cli += " [yes | no]" return text_for_user_input_in_cli - def get(self, key, default=None): - return getattr(self, key, default) + +# ─ TIME ────────────────────────────────────────────────── -class DomainQuestion(Question): +class DateOption(StringOption): + pattern = { + "regexp": r"^\d{4}-\d\d-\d\d$", + "error": "config_validate_date", # i18n: config_validate_date + } + + def _value_pre_validator(self): + from datetime import datetime + + super()._value_pre_validator() + + if self.value not in [None, ""]: + try: + datetime.strptime(self.value, "%Y-%m-%d") + except ValueError: + raise YunohostValidationError("config_validate_date") + + +class TimeOption(StringOption): + pattern = { + "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", + "error": "config_validate_time", # i18n: config_validate_time + } + + +# ─ LOCATIONS ───────────────────────────────────────────── + + +class EmailOption(StringOption): + pattern = { + "regexp": r"^.+@.+", + "error": "config_validate_email", # i18n: config_validate_email + } + + +class WebPathOption(BaseOption): + argument_type = "path" + default_value = "" + + @staticmethod + def normalize(value, option={}): + option = option.__dict__ if isinstance(option, BaseOption) else option + + if not isinstance(value, str): + raise YunohostValidationError( + "app_argument_invalid", + name=option.get("name"), + error="Argument for path should be a string.", + ) + + if not value.strip(): + if option.get("optional"): + return "" + # Hmpf here we could just have a "else" case + # but we also want WebPathOption.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="Option is mandatory", + ) + + return "/" + value.strip().strip(" /") + + +class URLOption(StringOption): + pattern = { + "regexp": r"^https?://.*$", + "error": "config_validate_url", # i18n: config_validate_url + } + + +# ─ FILE ────────────────────────────────────────────────── + + +class FileOption(BaseOption): + argument_type = "file" + upload_dirs: List[str] = [] + + def __init__( + self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} + ): + super().__init__(question, context, hooks) + self.accept = question.get("accept", "") + + @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 _value_pre_validator(self): + if self.value is None: + self.value = self.current_value + + super()._value_pre_validator() + + # Validation should have already failed if required + if self.value in [None, ""]: + return self.value + + if Moulinette.interface.type != "api": + if not os.path.exists(str(self.value)) or not os.path.isfile( + str(self.value) + ): + raise YunohostValidationError( + "app_argument_invalid", + name=self.name, + error=m18n.n("file_does_not_exist", path=str(self.value)), + ) + + def _value_post_validator(self): + from base64 import b64decode + + if not self.value: + return "" + + upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_") + _, file_path = tempfile.mkstemp(dir=upload_dir) + + FileOption.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 + + +# ─ CHOICES ─────────────────────────────────────────────── + + +class TagsOption(BaseOption): + argument_type = "tags" + default_value = "" + + @staticmethod + def humanize(value, option={}): + if isinstance(value, list): + return ",".join(str(v) for v in value) + return value + + @staticmethod + def normalize(value, option={}): + if isinstance(value, list): + return ",".join(str(v) for v in value) + if isinstance(value, str): + value = value.strip() + return value + + def _value_pre_validator(self): + values = self.value + if isinstance(values, str): + values = values.split(",") + elif values is None: + values = [] + + if not isinstance(values, list): + if self.choices: + raise YunohostValidationError( + "app_argument_choice_invalid", + name=self.name, + value=self.value, + choices=", ".join(str(choice) for choice in self.choices), + ) + raise YunohostValidationError( + "app_argument_invalid", + name=self.name, + error=f"'{str(self.value)}' is not a list", + ) + + for value in values: + self.value = value + super()._value_pre_validator() + self.value = values + + def _value_post_validator(self): + if isinstance(self.value, list): + self.value = ",".join(self.value) + return super()._value_post_validator() + + +class DomainOption(BaseOption): argument_type = "domain" def __init__( @@ -1133,7 +861,7 @@ class DomainQuestion(Question): return value -class AppQuestion(Question): +class AppOption(BaseOption): argument_type = "app" def __init__( @@ -1160,7 +888,7 @@ class AppQuestion(Question): self.choices.update({app["id"]: _app_display(app) for app in apps}) -class UserQuestion(Question): +class UserOption(BaseOption): argument_type = "user" def __init__( @@ -1184,6 +912,8 @@ class UserQuestion(Question): ) if self.default is None: + # FIXME: this code is obsolete with the new admins group + # Should be replaced by something like "any first user we find in the admin group" root_mail = "root@%s" % _get_maindomain() for user in self.choices.keys(): if root_mail in user_info(user).get("mail-aliases", []): @@ -1191,180 +921,72 @@ class UserQuestion(Question): break -class NumberQuestion(Question): - argument_type = "number" - default_value = None +class GroupOption(BaseOption): + argument_type = "group" 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) + from yunohost.user import user_group_list - @staticmethod - def normalize(value, option={}): + super().__init__(question, context) - 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"), + self.choices = list( + user_group_list(short=True, include_primary_groups=False)["groups"] ) - def _prevalidate(self): - super()._prevalidate() - if self.value in [None, ""]: - return + def _human_readable_group(g): + # i18n: visitors + # i18n: all_users + # i18n: admins + return m18n.n(g) if g in ["visitors", "all_users", "admins"] else g - 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), - ) + self.choices = {g: _human_readable_group(g) for g in self.choices} - 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), - ) + if self.default is None: + self.default = "all_users" -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, +OPTIONS = { + "display_text": DisplayTextOption, + "markdown": DisplayTextOption, + "alert": DisplayTextOption, + "button": ButtonOption, + "string": StringOption, + "text": StringOption, + "password": PasswordOption, + "color": ColorOption, + "number": NumberOption, + "range": NumberOption, + "boolean": BooleanOption, + "date": DateOption, + "time": TimeOption, + "email": EmailOption, + "path": WebPathOption, + "url": URLOption, + "file": FileOption, + "select": StringOption, + "tags": TagsOption, + "domain": DomainOption, + "app": AppOption, + "user": UserOption, + "group": GroupOption, } +# ╭───────────────────────────────────────────────────────╮ +# │ ╷ ╷╶┬╴╶┬╴╷ ╭─╴ │ +# │ │ │ │ │ │ ╰─╮ │ +# │ ╰─╯ ╵ ╶┴╴╰─╴╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + + 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]: +) -> List[BaseOption]: """Parse arguments store in either manifest.json or actions.json or from a config panel against the user answers when they are present. @@ -1393,10 +1015,23 @@ def ask_questions_and_parse_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"]) + for name, raw_question in raw_questions.items(): + raw_question["name"] = name + question_class = OPTIONS[raw_question.get("type", "string")] + raw_question["value"] = answers.get(name) question = question_class(raw_question, context=context, hooks=hooks) + if question.type == "button": + if question.enabled is None or evaluate_simple_js_expression( # type: ignore + question.enabled, context=context # type: ignore + ): # type: ignore + continue + else: + raise YunohostValidationError( + "config_action_disabled", + action=question.name, + help=_value_for_locale(question.help), + ) + new_values = question.ask_if_needed() answers.update(new_values) context.update(new_values) @@ -1409,9 +1044,7 @@ 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 - ) + question = OPTIONS[raw_question.get("type", "string")](raw_question) if question.choices: raw_question["choices"] = question.choices raw_question["default"] = question.default diff --git a/src/utils/i18n.py b/src/utils/i18n.py index a0daf8181..2aafafbdd 100644 --- a/src/utils/i18n.py +++ b/src/utils/i18n.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# from moulinette import m18n diff --git a/src/utils/ldap.py b/src/utils/ldap.py index b204460fe..11141dcb0 100644 --- a/src/utils/ldap.py +++ b/src/utils/ldap.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- -""" License - - Copyright (C) 2019 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import atexit import logging @@ -38,7 +36,6 @@ _ldap_interface = None def _get_ldap_interface(): - global _ldap_interface if _ldap_interface is None: @@ -96,7 +93,7 @@ class LDAPInterface: def _reconnect(): con = ldap.ldapobject.ReconnectLDAPObject( - URI, retry_max=10, retry_delay=0.5 + URI, retry_max=10, retry_delay=2 ) self._connect(con) return con @@ -157,6 +154,8 @@ class LDAPInterface: try: result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs) + except ldap.SERVER_DOWN as e: + raise e except Exception as e: raise MoulinetteError( "error during LDAP search operation with: base='%s', " diff --git a/src/utils/legacy.py b/src/utils/legacy.py index 5e5d15fe8..82507d64d 100644 --- a/src/utils/legacy.py +++ b/src/utils/legacy.py @@ -1,3 +1,21 @@ +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import glob @@ -62,6 +80,32 @@ LEGACY_PERMISSION_LABEL = { ): "api", # $excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-receive%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-upload%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/info/refs } +LEGACY_SETTINGS = { + "security.password.admin.strength": "security.password.admin_strength", + "security.password.user.strength": "security.password.user_strength", + "security.ssh.compatibility": "security.ssh.ssh_compatibility", + "security.ssh.port": "security.ssh.ssh_port", + "security.ssh.password_authentication": "security.ssh.ssh_password_authentication", + "security.nginx.redirect_to_https": "security.nginx.nginx_redirect_to_https", + "security.nginx.compatibility": "security.nginx.nginx_compatibility", + "security.postfix.compatibility": "security.postfix.postfix_compatibility", + "pop3.enabled": "email.pop3.pop3_enabled", + "smtp.allow_ipv6": "email.smtp.smtp_allow_ipv6", + "smtp.relay.host": "email.smtp.smtp_relay_host", + "smtp.relay.port": "email.smtp.smtp_relay_port", + "smtp.relay.user": "email.smtp.smtp_relay_user", + "smtp.relay.password": "email.smtp.smtp_relay_password", + "backup.compress_tar_archives": "misc.backup.backup_compress_tar_archives", + "ssowat.panel_overlay.enabled": "misc.portal.ssowat_panel_overlay_enabled", + "security.webadmin.allowlist.enabled": "security.webadmin.webadmin_allowlist_enabled", + "security.webadmin.allowlist": "security.webadmin.webadmin_allowlist", + "security.experimental.enabled": "security.experimental.security_experimental_enabled", +} + + +def translate_legacy_settings_to_configpanel_settings(settings): + return LEGACY_SETTINGS.get(settings, settings) + def legacy_permission_label(app, permission_type): return LEGACY_PERMISSION_LABEL.get( @@ -149,7 +193,6 @@ LEGACY_PHP_VERSION_REPLACEMENTS = [ def _patch_legacy_php_versions(app_folder): - files_to_patch = [] files_to_patch.extend(glob.glob("%s/conf/*" % app_folder)) files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder)) @@ -159,7 +202,6 @@ def _patch_legacy_php_versions(app_folder): files_to_patch.append("%s/manifest.toml" % app_folder) for filename in files_to_patch: - # Ignore non-regular files if not os.path.isfile(filename): continue @@ -173,7 +215,6 @@ def _patch_legacy_php_versions(app_folder): def _patch_legacy_php_versions_in_settings(app_folder): - settings = read_yaml(os.path.join(app_folder, "settings.yml")) if settings.get("fpm_config_dir") in ["/etc/php/7.0/fpm", "/etc/php/7.3/fpm"]: @@ -199,7 +240,6 @@ def _patch_legacy_php_versions_in_settings(app_folder): def _patch_legacy_helpers(app_folder): - files_to_patch = [] files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder)) files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder)) @@ -209,6 +249,11 @@ def _patch_legacy_helpers(app_folder): "yunohost app checkport": {"important": True}, "yunohost tools port-available": {"important": True}, "yunohost app checkurl": {"important": True}, + "yunohost user create": { + "pattern": r"yunohost user create (\S+) (-f|--firstname) (\S+) (-l|--lastname) \S+ (.*)", + "replace": r"yunohost user create \1 --fullname \3 \5", + "important": False, + }, # Remove # Automatic diagnosis data from YunoHost # __PRE_TAG1__$(yunohost tools diagnosis | ...)__PRE_TAG2__" @@ -242,7 +287,6 @@ def _patch_legacy_helpers(app_folder): infos["replace"] = infos.get("replace") for filename in files_to_patch: - # Ignore non-regular files if not os.path.isfile(filename): continue @@ -256,7 +300,6 @@ def _patch_legacy_helpers(app_folder): show_warning = False for helper, infos in stuff_to_replace.items(): - # Ignore if not relevant for this file if infos.get("only_for") and not any( filename.endswith(f) for f in infos["only_for"] @@ -280,7 +323,6 @@ def _patch_legacy_helpers(app_folder): ) if replaced_stuff: - # Check the app do load the helper # If it doesn't, add the instruction ourselve (making sure it's after the #!/bin/bash if it's there... if filename.split("/")[-1] in [ diff --git a/src/utils/network.py b/src/utils/network.py index 28dcb204c..2a13f966e 100644 --- a/src/utils/network.py +++ b/src/utils/network.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2017 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import re import logging @@ -31,7 +29,6 @@ logger = logging.getLogger("yunohost.utils.network") def get_public_ip(protocol=4): - assert protocol in [4, 6], ( "Invalid protocol version for get_public_ip: %s, expected 4 or 6" % protocol ) @@ -92,7 +89,6 @@ def get_public_ip_from_remote_server(protocol=4): def get_network_interfaces(): - # Get network devices and their addresses (raw infos from 'ip addr') devices_raw = {} output = check_output("ip addr show") @@ -113,7 +109,6 @@ def get_network_interfaces(): def get_gateway(): - output = check_output("ip route show") m = re.search(r"default via (.*) dev ([a-z]+[0-9]?)", output) if not m: diff --git a/src/utils/password.py b/src/utils/password.py index a38bc4e23..833933d33 100644 --- a/src/utils/password.py +++ b/src/utils/password.py @@ -1,29 +1,26 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YunoHost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import sys import os -import json import string import subprocess +import yaml SMALL_PWD_LIST = [ "yunohost", @@ -36,7 +33,14 @@ SMALL_PWD_LIST = [ "rpi", ] -MOST_USED_PASSWORDS = "/usr/share/yunohost/100000-most-used-passwords.txt" +# +# 100k firsts "most used password" with length 8+ +# +# List obtained with: +# curl -L https://github.com/danielmiessler/SecLists/raw/master/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt \ +# | grep -v -E "^[a-zA-Z0-9]{1,7}$" | head -n 100000 | gzip > 100000-most-used-passwords-length8plus.txt.gz +# +MOST_USED_PASSWORDS = "/usr/share/yunohost/100000-most-used-passwords-length8plus.txt" # Length, digits, lowers, uppers, others STRENGTH_LEVELS = [ @@ -54,18 +58,16 @@ def assert_password_is_compatible(password): """ if len(password) >= 127: - # Note that those imports are made here and can't be put # on top (at least not the moulinette ones) # because the moulinette needs to be correctly initialized # as well as modules available in python's path. from yunohost.utils.error import YunohostValidationError - raise YunohostValidationError("admin_password_too_long") + raise YunohostValidationError("password_too_long") def assert_password_is_strong_enough(profile, password): - PasswordValidator(profile).validate(password) @@ -76,7 +78,7 @@ class PasswordValidator: The profile shall be either "user" or "admin" and will correspond to a validation strength - defined via the setting "security.password..strength" + defined via the setting "security.password._strength" """ self.profile = profile @@ -85,9 +87,9 @@ class PasswordValidator: # from settings.py because this file is also meant to be # use as a script by ssowat. # (or at least that's my understanding -- Alex) - settings = json.load(open("/etc/yunohost/settings.json", "r")) - setting_key = "security.password." + profile + ".strength" - self.validation_strength = int(settings[setting_key]["value"]) + settings = yaml.safe_load(open("/etc/yunohost/settings.yml", "r")) + setting_key = profile + "_strength" + self.validation_strength = int(settings[setting_key]) except Exception: # Fallback to default value if we can't fetch settings for some reason self.validation_strength = 1 @@ -193,7 +195,6 @@ class PasswordValidator: return strength_level def is_in_most_used_list(self, password): - # Decompress file if compressed if os.path.exists("%s.gz" % MOST_USED_PASSWORDS): os.system("gzip -fd %s.gz" % MOST_USED_PASSWORDS) diff --git a/src/utils/resources.py b/src/utils/resources.py new file mode 100644 index 000000000..8d33c3bac --- /dev/null +++ b/src/utils/resources.py @@ -0,0 +1,1353 @@ +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import os +import copy +import shutil +import random +import tempfile +import subprocess +from typing import Dict, Any, List, Union + +from moulinette import m18n +from moulinette.utils.process import check_output +from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import mkdir, chown, chmod, write_to_file +from moulinette.utils.filesystem import ( + rm, +) +from yunohost.utils.system import system_arch +from yunohost.utils.error import YunohostError, YunohostValidationError + +logger = getActionLogger("yunohost.app_resources") + + +class AppResourceManager: + def __init__(self, app: str, current: Dict, wanted: Dict): + self.app = app + self.current = current + self.wanted = wanted + + if "resources" not in self.current: + self.current["resources"] = {} + if "resources" not in self.wanted: + self.wanted["resources"] = {} + + def apply( + self, rollback_and_raise_exception_if_failure, operation_logger=None, **context + ): + todos = list(self.compute_todos()) + completed = [] + rollback = False + exception = None + + for todo, name, old, new in todos: + try: + if todo == "deprovision": + # FIXME : i18n, better info strings + logger.info(f"Deprovisionning {name}...") + old.deprovision(context=context) + elif todo == "provision": + logger.info(f"Provisionning {name}...") + new.provision_or_update(context=context) + elif todo == "update": + logger.info(f"Updating {name}...") + new.provision_or_update(context=context) + except (KeyboardInterrupt, Exception) as e: + exception = e + if isinstance(e, KeyboardInterrupt): + logger.error(m18n.n("operation_interrupted")) + else: + logger.warning(f"Failed to {todo} {name} : {e}") + if rollback_and_raise_exception_if_failure: + rollback = True + completed.append((todo, name, old, new)) + break + else: + pass + else: + completed.append((todo, name, old, new)) + + if rollback: + for todo, name, old, new in completed: + try: + # (NB. here we want to undo the todo) + if todo == "deprovision": + # FIXME : i18n, better info strings + logger.info(f"Reprovisionning {name}...") + old.provision_or_update(context=context) + elif todo == "provision": + logger.info(f"Deprovisionning {name}...") + new.deprovision(context=context) + elif todo == "update": + logger.info(f"Reverting {name}...") + old.provision_or_update(context=context) + except (KeyboardInterrupt, Exception) as e: + if isinstance(e, KeyboardInterrupt): + logger.error(m18n.n("operation_interrupted")) + else: + logger.error(f"Failed to rollback {name} : {e}") + + if exception: + if rollback_and_raise_exception_if_failure: + logger.error( + m18n.n("app_resource_failed", app=self.app, error=exception) + ) + if operation_logger: + failure_message_with_debug_instructions = operation_logger.error( + str(exception) + ) + raise YunohostError( + failure_message_with_debug_instructions, raw_msg=True + ) + else: + raise YunohostError(str(exception), raw_msg=True) + else: + logger.error(exception) + + def compute_todos(self): + for name, infos in reversed(self.current["resources"].items()): + if name not in self.wanted["resources"].keys(): + resource = AppResourceClassesByType[name](infos, self.app, self) + yield ("deprovision", name, resource, None) + + for name, infos in self.wanted["resources"].items(): + wanted_resource = AppResourceClassesByType[name](infos, self.app, self) + if name not in self.current["resources"].keys(): + yield ("provision", name, None, wanted_resource) + else: + infos_ = self.current["resources"][name] + current_resource = AppResourceClassesByType[name]( + infos_, self.app, self + ) + yield ("update", name, current_resource, wanted_resource) + + +class AppResource: + type: str = "" + default_properties: Dict[str, Any] = {} + + def __init__(self, properties: Dict[str, Any], app: str, manager=None): + self.app = app + self.manager = manager + + for key, value in self.default_properties.items(): + if isinstance(value, str): + value = value.replace("__APP__", self.app) + setattr(self, key, value) + + for key, value in properties.items(): + if isinstance(value, str): + value = value.replace("__APP__", self.app) + setattr(self, key, value) + + def get_setting(self, key): + from yunohost.app import app_setting + + return app_setting(self.app, key) + + def set_setting(self, key, value): + from yunohost.app import app_setting + + app_setting(self.app, key, value=value) + + def delete_setting(self, key): + from yunohost.app import app_setting + + app_setting(self.app, key, delete=True) + + def check_output_bash_snippet(self, snippet, env={}): + from yunohost.app import ( + _make_environment_for_app_script, + ) + + env_ = _make_environment_for_app_script( + self.app, + force_include_app_settings=True, + ) + env_.update(env) + + with tempfile.NamedTemporaryFile(prefix="ynh_") as fp: + fp.write(snippet.encode()) + fp.seek(0) + with tempfile.TemporaryFile() as stderr: + out = check_output(f"bash {fp.name}", env=env_, stderr=stderr) + + stderr.seek(0) + err = stderr.read().decode() + + return out, err + + def _run_script(self, action, script, env={}): + from yunohost.app import ( + _make_tmp_workdir_for_app, + _make_environment_for_app_script, + ) + from yunohost.hook import hook_exec_with_script_debug_if_failure + + tmpdir = _make_tmp_workdir_for_app(app=self.app) + + env_ = _make_environment_for_app_script( + self.app, + workdir=tmpdir, + action=f"{action}_{self.type}", + force_include_app_settings=True, + ) + env_.update(env) + + script_path = f"{tmpdir}/{action}_{self.type}" + script = f""" +source /usr/share/yunohost/helpers +ynh_abort_if_errors + +{script} +""" + + write_to_file(script_path, script) + + from yunohost.log import OperationLogger + + # FIXME ? : this is an ugly hack :( + active_operation_loggers = [ + o for o in OperationLogger._instances if o.ended_at is None + ] + if active_operation_loggers: + operation_logger = active_operation_loggers[-1] + else: + operation_logger = OperationLogger( + "resource_snippet", [("app", self.app)], env=env_ + ) + operation_logger.start() + + try: + ( + call_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( + script_path, + env=env_, + operation_logger=operation_logger, + error_message_if_script_failed="An error occured inside the script snippet", + error_message_if_failed=lambda e: f"{action} failed for {self.type} : {e}", + ) + finally: + if call_failed: + raise YunohostError( + failure_message_with_debug_instructions, raw_msg=True + ) + else: + # FIXME: currently in app install code, we have + # more sophisticated code checking if this broke something on the system etc. + # dunno if we want to do this here or manage it elsewhere + pass + + # print(ret) + + +class SourcesResource(AppResource): + """ + Declare what are the sources / assets used by this app. Typically, this corresponds to some tarball published by the upstream project, that needs to be downloaded and extracted in the install dir using the ynh_setup_source helper. + + This resource is intended both to declare the assets, which will be parsed by ynh_setup_source during the app script runtime, AND to prefetch and validate the sha256sum of those asset before actually running the script, to be able to report an error early when the asset turns out to not be available for some reason. + + Various options are available to accomodate the behavior according to the asset structure + + ##### Example + + ```toml + [resources.sources] + + [resources.sources.main] + url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz" + sha256 = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + + autoupdate.strategy = "latest_github_tag" + ``` + + Or more complex examples with several element, including one with asset that depends on the arch + + ```toml + [resources.sources] + + [resources.sources.main] + in_subdir = false + amd64.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.amd64.tar.gz" + amd64.sha256 = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + i386.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.386.tar.gz" + i386.sha256 = "53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3" + armhf.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.arm.tar.gz" + armhf.sha256 = "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865" + + autoupdate.strategy = "latest_github_release" + autoupdate.asset.amd64 = ".*\\.amd64.tar.gz" + autoupdate.asset.i386 = ".*\\.386.tar.gz" + autoupdate.asset.armhf = ".*\\.arm.tar.gz" + + [resources.sources.zblerg] + url = "https://zblerg.com/download/zblerg" + sha256 = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2" + format = "script" + rename = "zblerg.sh" + + ``` + + ##### Properties (for each source) + + - `prefetch` : `true` (default) or `false`, wether or not to pre-fetch this asset during the provisioning phase of the resource. If several arch-dependent url are provided, YunoHost will only prefetch the one for the current system architecture. + - `url` : the asset's URL + - If the asset's URL depend on the architecture, you may instead provide `amd64.url`, `i386.url`, `armhf.url` and `arm64.url` (depending on what architectures are supported), using the same `dpkg --print-architecture` nomenclature as for the supported architecture key in the manifest + - `sha256` : the asset's sha256sum. This is used both as an integrity check, and as a layer of security to protect against malicious actors which could have injected malicious code inside the asset... + - Same as `url` : if the asset's URL depend on the architecture, you may instead provide `amd64.sha256`, `i386.sha256`, ... + - `format` : The "format" of the asset. It is typically automatically guessed from the extension of the URL (or the mention of "tarball", "zipball" in the URL), but can be set explicitly: + - `tar.gz`, `tar.xz`, `tar.bz2` : will use `tar` to extract the archive + - `zip` : will use `unzip` to extract the archive + - `docker` : useful to extract files from an already-built docker image (instead of rebuilding them locally). Will use `docker-image-extract` + - `whatever`: whatever arbitrary value, not really meaningful except to imply that the file won't be extracted (eg because it's a .deb to be manually installed with dpkg/apt, or a script, or ...) + - `in_subdir`: `true` (default) or `false`, depending on if there's an intermediate subdir in the archive before accessing the actual files. Can also be `N` (an integer) to handle special cases where there's `N` level of subdir to get rid of to actually access the files + - `extract` : `true` or `false`. Defaults to `true` for archives such as `zip`, `tar.gz`, `tar.bz2`, ... Or defaults to `false` when `format` is not something that should be extracted. When `extract = false`, the file will only be `mv`ed to the location, possibly renamed using the `rename` value + - `rename`: some string like `whatever_your_want`, to be used for convenience when `extract` is `false` and the default name of the file is not practical + - `platform`: for example `linux/amd64` (defaults to `linux/$YNH_ARCH`) to be used in conjonction with `format = "docker"` to specify which architecture to extract for + + ###### Regarding `autoupdate` + + Strictly speaking, this has nothing to do with the actual app install. `autoupdate` is expected to contain metadata for automatic maintenance / update of the app sources info in the manifest. It is meant to be a simpler replacement for "autoupdate" Github workflow mechanism. + + The infos are used by this script : https://github.com/YunoHost/apps/blob/master/tools/autoupdate_app_sources/autoupdate_app_sources.py which is ran by the YunoHost infrastructure periodically and will create the corresponding pull request automatically. + + The script will rely on the code repo specified in the upstream section of the manifest. + + `autoupdate.strategy` is expected to be one of : + - `latest_github_tag` : look for the latest tag (by sorting tags and finding the "largest" version). Then using the corresponding tar.gz url. Tags containing `rc`, `beta`, `alpha`, `start` are ignored, and actually any tag which doesn't look like `x.y.z` or `vx.y.z` + - `latest_github_release` : similar to `latest_github_tags`, but starting from the list of releases. Pre- or draft releases are ignored. Releases may have assets attached to them, in which case you can define: + - `autoupdate.asset = "some regex"` (when there's only one asset to use). The regex is used to find the appropriate asset among the list of all assets + - or several `autoupdate.asset.$arch = "some_regex"` (when the asset is arch-specific). The regex is used to find the appropriate asset for the specific arch among the list of assets + - `latest_github_commit` : will use the latest commit on github, and the corresponding tarball. If this is used for the 'main' source, it will also assume that the version is YYYY.MM.DD corresponding to the date of the commit. + + It is also possible to define `autoupdate.upstream` to use a different Git(hub) repository instead of the code repository from the upstream section of the manifest. This can be useful when, for example, the app uses other assets such as plugin from a different repository. + + ##### Provision/Update + - For elements with `prefetch = true`, will download the asset (for the appropriate architecture) and store them in `/var/cache/yunohost/download/$app/$source_id`, to be later picked up by `ynh_setup_source`. (NB: this only happens during install and upgrade, not restore) + + ##### Deprovision + - Nothing (just cleanup the cache) + """ + + type = "sources" + priority = 10 + + default_sources_properties: Dict[str, Any] = { + "prefetch": True, + "url": None, + "sha256": None, + } + + sources: Dict[str, Dict[str, Any]] = {} + + def __init__(self, properties: Dict[str, Any], *args, **kwargs): + for source_id, infos in properties.items(): + properties[source_id] = copy.copy(self.default_sources_properties) + properties[source_id].update(infos) + + super().__init__({"sources": properties}, *args, **kwargs) + + def deprovision(self, context: Dict = {}): + if os.path.isdir(f"/var/cache/yunohost/download/{self.app}/"): + rm(f"/var/cache/yunohost/download/{self.app}/", recursive=True) + + def provision_or_update(self, context: Dict = {}): + # Don't prefetch stuff during restore + if context.get("action") == "restore": + return + + for source_id, infos in self.sources.items(): + if not infos["prefetch"]: + continue + + if infos["url"] is None: + arch = system_arch() + if ( + arch in infos + and isinstance(infos[arch], dict) + and isinstance(infos[arch].get("url"), str) + and isinstance(infos[arch].get("sha256"), str) + ): + self.prefetch(source_id, infos[arch]["url"], infos[arch]["sha256"]) + else: + raise YunohostError( + f"In resources.sources: it looks like you forgot to define url/sha256 or {arch}.url/{arch}.sha256", + raw_msg=True, + ) + else: + if infos["sha256"] is None: + raise YunohostError( + f"In resources.sources: it looks like the sha256 is missing for {source_id}", + raw_msg=True, + ) + self.prefetch(source_id, infos["url"], infos["sha256"]) + + def prefetch(self, source_id, url, expected_sha256): + logger.debug(f"Prefetching asset {source_id}: {url} ...") + + if not os.path.isdir(f"/var/cache/yunohost/download/{self.app}/"): + mkdir(f"/var/cache/yunohost/download/{self.app}/", parents=True) + filename = f"/var/cache/yunohost/download/{self.app}/{source_id}" + + # NB: we use wget and not requests.get() because we want to output to a file (ie avoid ending up with the full archive in RAM) + # AND the nice --tries, --no-dns-cache, --timeout options ... + p = subprocess.Popen( + [ + "/usr/bin/wget", + "--tries=3", + "--no-dns-cache", + "--timeout=900", + "--no-verbose", + "--output-document=" + filename, + url, + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + out, _ = p.communicate() + returncode = p.returncode + if returncode != 0: + if os.path.exists(filename): + rm(filename) + raise YunohostError( + "app_failed_to_download_asset", + source_id=source_id, + url=url, + app=self.app, + out=out.decode(), + ) + + assert os.path.exists( + filename + ), f"For some reason, wget worked but {filename} doesnt exists?" + + computed_sha256 = check_output(f"sha256sum {filename}").split()[0] + if computed_sha256 != expected_sha256: + size = check_output(f"du -hs {filename}").split()[0] + rm(filename) + raise YunohostError( + "app_corrupt_source", + source_id=source_id, + url=url, + app=self.app, + expected_sha256=expected_sha256, + computed_sha256=computed_sha256, + size=size, + ) + + +class PermissionsResource(AppResource): + """ + Configure the SSO permissions/tiles. Typically, webapps are expected to have a 'main' permission mapped to '/', meaning that a tile pointing to the `$domain/$path` will be available in the SSO for users allowed to access that app. + + Additional permissions can be created, typically to have a specific tile and/or access rules for the admin part of a webapp. + + The list of allowed user/groups may be initialized using the content of the `init_{perm}_permission` question from the manifest, hence `init_main_permission` replaces the `is_public` question and shall contain a group name (typically, `all_users` or `visitors`). + + ##### Example + ```toml + [resources.permissions] + main.url = "/" + # (these two previous lines should be enough in the majority of cases) + + admin.url = "/admin" + admin.show_tile = false + admin.allowed = "admins" # Assuming the "admins" group exists (cf future developments ;)) + ``` + + ##### Properties (for each perm name) + - `url`: The relative URI corresponding to this permission. Typically `/` or `/something`. This property may be omitted for non-web permissions. + - `show_tile`: (default: `true` if `url` is defined) Wether or not a tile should be displayed for that permission in the user portal + - `allowed`: (default: nobody) The group initially allowed to access this perm, if `init_{perm}_permission` is not defined in the manifest questions. Note that the admin may tweak who is allowed/unallowed on that permission later on, this is only meant to **initialize** the permission. + - `auth_header`: (default: `true`) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true + - `protected`: (default: `false`) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission. Defaults to 'false'. + - `additional_urls`: (default: none) List of additional URL for which access will be allowed/forbidden + + ##### Provision/Update + - Delete any permissions that may exist and be related to this app yet is not declared anymore + - Loop over the declared permissions and create them if needed or update them with the new values + + ##### Deprovision + - Delete all permission related to this app + + ##### Legacy management + - Legacy `is_public` setting will be deleted if it exists + """ + + # Notes for future ? + # deep_clean -> delete permissions for any __APP__.foobar where app not in app list... + # backup -> handled elsewhere by the core, should be integrated in there (dump .ldif/yml?) + # restore -> handled by the core, should be integrated in there (restore .ldif/yml?) + + type = "permissions" + priority = 80 + + default_properties: Dict[str, Any] = {} + + default_perm_properties: Dict[str, Any] = { + "url": None, + "additional_urls": [], + "auth_header": True, + "allowed": None, + "show_tile": None, # To be automagically set to True by default if an url is defined and show_tile not provided + "protected": False, + } + + permissions: Dict[str, Dict[str, Any]] = {} + + def __init__(self, properties: Dict[str, Any], *args, **kwargs): + # FIXME : if url != None, we should check that there's indeed a domain/path defined ? ie that app is a webapp + + # Validate packager-provided infos + for perm, infos in properties.items(): + if "auth_header" in infos and not isinstance( + infos.get("auth_header"), bool + ): + raise YunohostError( + f"In manifest, for permission '{perm}', 'auth_header' should be a boolean", + raw_msg=True, + ) + if "show_tile" in infos and not isinstance(infos.get("show_tile"), bool): + raise YunohostError( + f"In manifest, for permission '{perm}', 'show_tile' should be a boolean", + raw_msg=True, + ) + if "protected" in infos and not isinstance(infos.get("protected"), bool): + raise YunohostError( + f"In manifest, for permission '{perm}', 'protected' should be a boolean", + raw_msg=True, + ) + if "additional_urls" in infos and not isinstance( + infos.get("additional_urls"), list + ): + raise YunohostError( + f"In manifest, for permission '{perm}', 'additional_urls' should be a list", + raw_msg=True, + ) + + if "main" not in properties: + properties["main"] = copy.copy(self.default_perm_properties) + + for perm, infos in properties.items(): + properties[perm] = copy.copy(self.default_perm_properties) + properties[perm].update(infos) + if properties[perm]["show_tile"] is None: + properties[perm]["show_tile"] = bool(properties[perm]["url"]) + + if properties["main"]["url"] is not None and ( + not isinstance(properties["main"].get("url"), str) + or properties["main"]["url"] != "/" + ): + raise YunohostError( + "URL for the 'main' permission should be '/' for webapps (or left undefined for non-webapps). Note that / refers to the install url of the app, i.e $domain.tld/$path/", + raw_msg=True, + ) + + super().__init__({"permissions": properties}, *args, **kwargs) + + from yunohost.app import _get_app_settings, _hydrate_app_template + + settings = _get_app_settings(self.app) + for perm, infos in self.permissions.items(): + if infos.get("url") and "__" in infos.get("url"): + infos["url"] = _hydrate_app_template(infos["url"], settings) + + if infos.get("additional_urls"): + infos["additional_urls"] = [ + _hydrate_app_template(url, settings) + for url in infos["additional_urls"] + ] + + def provision_or_update(self, context: Dict = {}): + from yunohost.permission import ( + permission_create, + permission_url, + permission_delete, + user_permission_list, + user_permission_update, + permission_sync_to_user, + ) + + # Delete legacy is_public setting if not already done + self.delete_setting("is_public") + + # Detect that we're using a full-domain app, + # in which case we probably need to automagically + # define the "path" setting with "/" + if ( + isinstance(self.permissions["main"]["url"], str) + and self.get_setting("domain") + and not self.get_setting("path") + ): + self.set_setting("path", "/") + + existing_perms = user_permission_list(short=True, apps=[self.app])[ + "permissions" + ] + for perm in existing_perms: + if perm.split(".")[1] not in self.permissions.keys(): + permission_delete(perm, force=True, sync_perm=False) + + for perm, infos in self.permissions.items(): + perm_id = f"{self.app}.{perm}" + if perm_id not in existing_perms: + # Use the 'allowed' key from the manifest, + # or use the 'init_{perm}_permission' from the install questions + # which is temporarily saved as a setting as an ugly hack to pass the info to this piece of code... + init_allowed = ( + infos["allowed"] + or self.get_setting(f"init_{perm}_permission") + or [] + ) + + # If we're choosing 'visitors' from the init_{perm}_permission question, add all_users too + if not infos["allowed"] and init_allowed == "visitors": + init_allowed = ["visitors", "all_users"] + + permission_create( + perm_id, + allowed=init_allowed, + # This is why the ugly hack with self.manager exists >_> + label=self.manager.wanted["name"] if perm == "main" else perm, + url=infos["url"], + additional_urls=infos["additional_urls"], + auth_header=infos["auth_header"], + sync_perm=False, + ) + self.delete_setting(f"init_{perm}_permission") + + user_permission_update( + perm_id, + show_tile=infos["show_tile"], + protected=infos["protected"], + sync_perm=False, + ) + permission_url( + perm_id, + url=infos["url"], + set_url=infos["additional_urls"], + auth_header=infos["auth_header"], + sync_perm=False, + ) + + permission_sync_to_user() + + def deprovision(self, context: Dict = {}): + from yunohost.permission import ( + permission_delete, + user_permission_list, + permission_sync_to_user, + ) + + existing_perms = user_permission_list(short=True, apps=[self.app])[ + "permissions" + ] + for perm in existing_perms: + permission_delete(perm, force=True, sync_perm=False) + + permission_sync_to_user() + + +class SystemuserAppResource(AppResource): + """ + Provision a system user to be used by the app. The username is exactly equal to the app id + + ##### Example + ```toml + [resources.system_user] + # (empty - defaults are usually okay) + ``` + + ##### Properties + - `allow_ssh`: (default: False) Adds the user to the ssh.app group, allowing SSH connection via this user + - `allow_sftp`: (default: False) Adds the user to the sftp.app group, allowing SFTP connection via this user + - `home`: (default: `/var/www/__APP__`) Defines the home property for this user. NB: unfortunately you can't simply use `__INSTALL_DIR__` or `__DATA_DIR__` for now + + ##### Provision/Update + - will create the system user if it doesn't exists yet + - will add/remove the ssh/sftp.app groups + + ##### Deprovision + - deletes the user and group + """ + + # Notes for future? + # + # deep_clean -> uuuuh ? delete any user that could correspond to an app x_x ? + # + # backup -> nothing + # restore -> provision + + type = "system_user" + priority = 20 + + default_properties: Dict[str, Any] = { + "allow_ssh": False, + "allow_sftp": False, + "home": "/var/www/__APP__", + } + + # FIXME : wat do regarding ssl-cert, multimedia, and other groups + + allow_ssh: bool = False + allow_sftp: bool = False + home: str = "" + + def provision_or_update(self, context: Dict = {}): + # FIXME : validate that no yunohost user exists with that name? + # and/or that no system user exists during install ? + + if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") != 0: + # FIXME: improve logging ? os.system wont log stdout / stderr + cmd = f"useradd --system --user-group {self.app} --home-dir {self.home} --no-create-home" + ret = os.system(cmd) + assert ret == 0, f"useradd command failed with exit code {ret}" + + if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") != 0: + raise YunohostError( + f"Failed to create system user for {self.app}", raw_msg=True + ) + + # Update groups + groups = set(check_output(f"groups {self.app}").strip().split()[2:]) + + if self.allow_ssh: + groups.add("ssh.app") + elif "ssh.app" in groups: + groups.remove("ssh.app") + + if self.allow_sftp: + groups.add("sftp.app") + elif "sftp.app" in groups: + groups.remove("sftp.app") + + os.system(f"usermod -G {','.join(groups)} {self.app}") + + # Update home dir + raw_user_line_in_etc_passwd = check_output(f"getent passwd {self.app}").strip() + user_infos = raw_user_line_in_etc_passwd.split(":") + current_home = user_infos[5] + if current_home != self.home: + ret = os.system(f"usermod --home {self.home} {self.app} 2>/dev/null") + # Most of the time this won't work because apparently we can't change the home dir while being logged-in -_- + # So we gotta brute force by replacing the line in /etc/passwd T_T + if ret != 0: + user_infos[5] = self.home + new_raw_user_line_in_etc_passwd = ":".join(user_infos) + os.system( + f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd" + ) + + def deprovision(self, context: Dict = {}): + if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: + os.system(f"deluser {self.app} >/dev/null") + if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: + raise YunohostError( + f"Failed to delete system user for {self.app}", raw_msg=True + ) + + if os.system(f"getent group {self.app} >/dev/null 2>/dev/null") == 0: + os.system(f"delgroup {self.app} >/dev/null") + if os.system(f"getent group {self.app} >/dev/null 2>/dev/null") == 0: + raise YunohostError( + f"Failed to delete system user for {self.app}", raw_msg=True + ) + + # FIXME : better logging and error handling, add stdout/stderr from the deluser/delgroup commands... + + +class InstalldirAppResource(AppResource): + """ + Creates a directory to be used by the app as the installation directory, typically where the app sources and assets are located. The corresponding path is stored in the settings as `install_dir` + + ##### Example + ```toml + [resources.install_dir] + # (empty - defaults are usually okay) + ``` + + ##### Properties + - `dir`: (default: `/var/www/__APP__`) The full path of the install dir + - `owner`: (default: `__APP__:rwx`) The owner (and owner permissions) for the install dir + - `group`: (default: `__APP__:rx`) The group (and group permissions) for the install dir + + ##### Provision/Update + - during install, the folder will be deleted if it already exists (FIXME: is this what we want?) + - if the dir path changed and a folder exists at the old location, the folder will be `mv`'ed to the new location + - otherwise, creates the directory if it doesn't exists yet + - (re-)apply permissions (only on the folder itself, not recursively) + - save the value of `dir` as `install_dir` in the app's settings, which can be then used by the app scripts (`$install_dir`) and conf templates (`__INSTALL_DIR__`) + + ##### Deprovision + - recursively deletes the directory if it exists + + ##### Legacy management + - In the past, the setting was called `final_path`. The code will automatically rename it as `install_dir`. + - As explained in the 'Provision/Update' section, the folder will also be moved if the location changed + + """ + + # Notes for future? + # deep_clean -> uuuuh ? delete any dir in /var/www/ that would not correspond to an app x_x ? + # backup -> cp install dir + # restore -> cp install dir + + type = "install_dir" + priority = 30 + + default_properties: Dict[str, Any] = { + "dir": "/var/www/__APP__", + "owner": "__APP__:rwx", + "group": "__APP__:rx", + } + + dir: str = "" + owner: str = "" + group: str = "" + + # FIXME: change default dir to /opt/stuff if app ain't a webapp... + + def provision_or_update(self, context: Dict = {}): + assert self.dir.strip() # Be paranoid about self.dir being empty... + assert self.owner.strip() + assert self.group.strip() + + current_install_dir = self.get_setting("install_dir") or self.get_setting( + "final_path" + ) + + # If during install, /var/www/$app already exists, assume that it's okay to remove and recreate it + # FIXME : is this the right thing to do ? + if not current_install_dir and os.path.isdir(self.dir): + rm(self.dir, recursive=True) + + # isdir will be True if the path is a symlink pointing to a dir + # This should cover cases where people moved the data dir to another place via a symlink (ie we dont enter the if) + if not os.path.isdir(self.dir): + # Handle case where install location changed, in which case we shall move the existing install dir + # FIXME: confirm that's what we wanna do + # Maybe a middle ground could be to compute the size, check that it's not too crazy (eg > 1G idk), + # and check for available space on the destination + if current_install_dir and os.path.isdir(current_install_dir): + logger.warning( + f"Moving {current_install_dir} to {self.dir}... (this may take a while)" + ) + shutil.move(current_install_dir, self.dir) + else: + mkdir(self.dir, parents=True) + + owner, owner_perm = self.owner.split(":") + group, group_perm = self.group.split(":") + owner_perm_octal = ( + (4 if "r" in owner_perm else 0) + + (2 if "w" in owner_perm else 0) + + (1 if "x" in owner_perm else 0) + ) + group_perm_octal = ( + (4 if "r" in group_perm else 0) + + (2 if "w" in group_perm else 0) + + (1 if "x" in group_perm else 0) + ) + + perm_octal = 0o100 * owner_perm_octal + 0o010 * group_perm_octal + + # NB: we use realpath here to cover cases where self.dir could actually be a symlink + # in which case we want to apply the perm to the pointed dir, not to the symlink + chmod(os.path.realpath(self.dir), perm_octal) + chown(os.path.realpath(self.dir), owner, group) + # FIXME: shall we apply permissions recursively ? + + self.set_setting("install_dir", self.dir) + self.delete_setting("final_path") # Legacy + + def deprovision(self, context: Dict = {}): + assert self.dir.strip() # Be paranoid about self.dir being empty... + assert self.owner.strip() + assert self.group.strip() + + # FIXME : check that self.dir has a sensible value to prevent catastrophes + if os.path.isdir(self.dir): + rm(self.dir, recursive=True) + # FIXME : in fact we should delete settings to be consistent + + +class DatadirAppResource(AppResource): + """ + Creates a directory to be used by the app as the data store directory, typically where the app multimedia or large assets added by users are located. The corresponding path is stored in the settings as `data_dir`. This resource behaves very similarly to install_dir. + + ##### Example + ```toml + [resources.data_dir] + # (empty - defaults are usually okay) + ``` + + ##### Properties + - `dir`: (default: `/home/yunohost.app/__APP__`) The full path of the data dir + - `subdirs`: (default: empty list) A list of subdirs to initialize inside the data dir. For example, `['foo', 'bar']` + - `owner`: (default: `__APP__:rwx`) The owner (and owner permissions) for the data dir + - `group`: (default: `__APP__:rx`) The group (and group permissions) for the data dir + + ##### Provision/Update + - if the dir path changed and a folder exists at the old location, the folder will be `mv`'ed to the new location + - otherwise, creates the directory if it doesn't exists yet + - create each subdir declared and which do not exist already + - (re-)apply permissions (only on the folder itself and declared subdirs, not recursively) + - save the value of `dir` as `data_dir` in the app's settings, which can be then used by the app scripts (`$data_dir`) and conf templates (`__DATA_DIR__`) + + ##### Deprovision + - (only if the purge option is chosen by the user) recursively deletes the directory if it exists + - also delete the corresponding setting + + ##### Legacy management + - In the past, the setting may have been called `datadir`. The code will automatically rename it as `data_dir`. + - As explained in the 'Provision/Update' section, the folder will also be moved if the location changed + + """ + + # notes for future ? + # deep_clean -> zblerg idk nothing + # backup -> cp data dir ? (if not backup_core_only) + # restore -> cp data dir ? (if in backup) + + type = "data_dir" + priority = 40 + + default_properties: Dict[str, Any] = { + "dir": "/home/yunohost.app/__APP__", + "subdirs": [], + "owner": "__APP__:rwx", + "group": "__APP__:rx", + } + + dir: str = "" + subdirs: list = [] + owner: str = "" + group: str = "" + + def provision_or_update(self, context: Dict = {}): + assert self.dir.strip() # Be paranoid about self.dir being empty... + assert self.owner.strip() + assert self.group.strip() + + current_data_dir = self.get_setting("data_dir") or self.get_setting("datadir") + + # isdir will be True if the path is a symlink pointing to a dir + # This should cover cases where people moved the data dir to another place via a symlink (ie we dont enter the if) + if not os.path.isdir(self.dir): + # Handle case where install location changed, in which case we shall move the existing install dir + # FIXME: same as install_dir, is this what we want ? + if current_data_dir and os.path.isdir(current_data_dir): + logger.warning( + f"Moving {current_data_dir} to {self.dir}... (this may take a while)" + ) + shutil.move(current_data_dir, self.dir) + else: + mkdir(self.dir, parents=True) + + for subdir in self.subdirs: + full_path = os.path.join(self.dir, subdir) + if not os.path.isdir(full_path): + mkdir(full_path, parents=True) + + owner, owner_perm = self.owner.split(":") + group, group_perm = self.group.split(":") + owner_perm_octal = ( + (4 if "r" in owner_perm else 0) + + (2 if "w" in owner_perm else 0) + + (1 if "x" in owner_perm else 0) + ) + group_perm_octal = ( + (4 if "r" in group_perm else 0) + + (2 if "w" in group_perm else 0) + + (1 if "x" in group_perm else 0) + ) + perm_octal = 0o100 * owner_perm_octal + 0o010 * group_perm_octal + + # NB: we use realpath here to cover cases where self.dir could actually be a symlink + # in which case we want to apply the perm to the pointed dir, not to the symlink + chmod(os.path.realpath(self.dir), perm_octal) + chown(os.path.realpath(self.dir), owner, group) + for subdir in self.subdirs: + full_path = os.path.join(self.dir, subdir) + chmod(os.path.realpath(full_path), perm_octal) + chown(os.path.realpath(full_path), owner, group) + + self.set_setting("data_dir", self.dir) + self.delete_setting("datadir") # Legacy + + def deprovision(self, context: Dict = {}): + assert self.dir.strip() # Be paranoid about self.dir being empty... + assert self.owner.strip() + assert self.group.strip() + + if context.get("purge_data_dir", False) and os.path.isdir(self.dir): + rm(self.dir, recursive=True) + + self.delete_setting("data_dir") + + +class AptDependenciesAppResource(AppResource): + """ + Create a virtual package in apt, depending on the list of specified packages that the app needs. The virtual packages is called `$app-ynh-deps` (with `_` being replaced by `-` in the app name, see `ynh_install_app_dependencies`) + + ##### Example + ```toml + [resources.apt] + packages = ["nyancat", "lolcat", "sl"] + + # (this part is optional and corresponds to the legacy ynh_install_extra_app_dependencies helper) + extras.yarn.repo = "deb https://dl.yarnpkg.com/debian/ stable main" + extras.yarn.key = "https://dl.yarnpkg.com/debian/pubkey.gpg" + extras.yarn.packages = ["yarn"] + ``` + + ##### Properties + - `packages`: List of packages to be installed via `apt` + - `packages_from_raw_bash`: A multi-line bash snippet (using triple quotes as open/close) which should echo additional packages to be installed. Meant to be used for packages to be conditionally installed depending on architecture, debian version, install questions, or other logic. + - `extras`: A dict of (repo, key, packages) corresponding to "extra" repositories to fetch dependencies from + + ##### Provision/Update + - The code literally calls the bash helpers `ynh_install_app_dependencies` and `ynh_install_extra_app_dependencies`, similar to what happens in v1. + - Note that when `packages` contains some phpX.Y-foobar dependencies, this will automagically define a `phpversion` setting equal to `X.Y` which can therefore be used in app scripts ($phpversion) or templates (`__PHPVERSION__`) + + ##### Deprovision + - The code literally calls the bash helper `ynh_remove_app_dependencies` + """ + + # Notes for future? + # deep_clean -> remove any __APP__-ynh-deps for app not in app list + # backup -> nothing + # restore = provision + + type = "apt" + priority = 50 + + default_properties: Dict[str, Any] = {"packages": [], "extras": {}} + + packages: List = [] + packages_from_raw_bash: str = "" + extras: Dict[str, Dict[str, Union[str, List]]] = {} + + def __init__(self, properties: Dict[str, Any], *args, **kwargs): + super().__init__(properties, *args, **kwargs) + + if isinstance(self.packages, str): + self.packages = [value.strip() for value in self.packages.split(",")] + + if self.packages_from_raw_bash: + out, err = self.check_output_bash_snippet(self.packages_from_raw_bash) + if err: + logger.error( + "Error while running apt resource packages_from_raw_bash snippet:" + ) + logger.error(err) + self.packages += out.split("\n") + + for key, values in self.extras.items(): + if isinstance(values.get("packages"), str): + values["packages"] = [value.strip() for value in values["packages"].split(",")] # type: ignore + + if ( + not isinstance(values.get("repo"), str) + or not isinstance(values.get("key"), str) + or not isinstance(values.get("packages"), list) + ): + raise YunohostError( + "In apt resource in the manifest: 'extras' repo should have the keys 'repo', 'key' defined as strings and 'packages' defined as list", + raw_msg=True, + ) + + def provision_or_update(self, context: Dict = {}): + script = " ".join(["ynh_install_app_dependencies", *self.packages]) + for repo, values in self.extras.items(): + script += "\n" + " ".join( + [ + "ynh_install_extra_app_dependencies", + f"--repo='{values['repo']}'", + f"--key='{values['key']}'", + f"--package='{' '.join(values['packages'])}'", + ] + ) + # FIXME : we're feeding the raw value of values['packages'] to the helper .. if we want to be consistent, may they should be comma-separated, though in the majority of cases, only a single package is installed from an extra repo.. + + self._run_script("provision_or_update", script) + + def deprovision(self, context: Dict = {}): + self._run_script("deprovision", "ynh_remove_app_dependencies") + + +class PortsResource(AppResource): + """ + Book port(s) to be used by the app, typically to be used to the internal reverse-proxy between nginx and the app process. + + Note that because multiple ports can be booked, each properties is prefixed by the name of the port. `main` is a special name and will correspond to the setting `$port`, whereas for example `xmpp_client` will correspond to the setting `$port_xmpp_client`. + + ##### Example + ```toml + [resources.ports] + # (empty should be fine for most apps... though you can customize stuff if absolutely needed) + + + main.default = 12345 # if you really want to specify a prefered value .. but shouldnt matter in the majority of cases + + xmpp_client.default = 5222 # if you need another port, pick a name for it (here, "xmpp_client") + xmpp_client.exposed = "TCP" # here, we're telling that the port needs to be publicly exposed on TCP on the firewall + ``` + + ##### Properties (for every port name) + - `default`: The prefered value for the port. If this port is already being used by another process right now, or is booked in another app's setting, the code will increment the value until it finds a free port and store that value as the setting. If no value is specified, a random value between 10000 and 60000 is used. + - `exposed`: (default: `false`) Wether this port should be opened on the firewall and be publicly reachable. This should be kept to `false` for the majority of apps than only need a port for internal reverse-proxying! Possible values: `false`, `true`(=`Both`), `Both`, `TCP`, `UDP`. This will result in the port being opened on the firewall, and the diagnosis checking that a program answers on that port. + - `fixed`: (default: `false`) Tells that the app absolutely needs the specific value provided in `default`, typically because it's needed for a specific protocol + + ##### Provision/Update (for every port name) + - If not already booked, look for a free port, starting with the `default` value (or a random value between 10000 and 60000 if no `default` set) + - If `exposed` is not `false`, open the port in the firewall accordingly - otherwise make sure it's closed. + - The value of the port is stored in the `$port` setting for the `main` port, or `$port_NAME` for other `NAME`s + + ##### Deprovision + - Close the ports on the firewall if relevant + - Deletes all the port settings + + ##### Legacy management + - In the past, some settings may have been named `NAME_port` instead of `port_NAME`, in which case the code will automatically rename the old setting. + """ + + # Notes for future? + # deep_clean -> ? + # backup -> nothing (backup port setting) + # restore -> nothing (restore port setting) + + type = "ports" + priority = 70 + + default_properties: Dict[str, Any] = {} + + default_port_properties = { + "default": None, + "exposed": False, # or True(="Both"), "TCP", "UDP" + "fixed": False, + } + + ports: Dict[str, Dict[str, Any]] + + def __init__(self, properties: Dict[str, Any], *args, **kwargs): + if "main" not in properties: + properties["main"] = {} + + for port, infos in properties.items(): + properties[port] = copy.copy(self.default_port_properties) + properties[port].update(infos) + + if properties[port]["default"] is None: + properties[port]["default"] = random.randint(10000, 60000) + + super().__init__({"ports": properties}, *args, **kwargs) + + def _port_is_used(self, port): + # FIXME : this could be less brutal than two os.system... + cmd1 = ( + "ss --numeric --listening --tcp --udp | awk '{print$5}' | grep --quiet --extended-regexp ':%s$'" + % port + ) + # This second command is mean to cover (most) case where an app is using a port yet ain't currently using it for some reason (typically service ain't up) + cmd2 = f"grep --quiet --extended-regexp \"port: '?{port}'?\" /etc/yunohost/apps/*/settings.yml" + return os.system(cmd1) == 0 or os.system(cmd2) == 0 + + def provision_or_update(self, context: Dict = {}): + from yunohost.firewall import firewall_allow, firewall_disallow + + for name, infos in self.ports.items(): + setting_name = f"port_{name}" if name != "main" else "port" + port_value = self.get_setting(setting_name) + if not port_value and name != "main": + # Automigrate from legacy setting foobar_port (instead of port_foobar) + legacy_setting_name = f"{name}_port" + port_value = self.get_setting(legacy_setting_name) + if port_value: + self.set_setting(setting_name, port_value) + self.delete_setting(legacy_setting_name) + continue + + if not port_value: + port_value = infos["default"] + + if infos["fixed"]: + if self._port_is_used(port_value): + raise YunohostValidationError( + f"Port {port_value} is already used by another process or app.", + raw_msg=True, + ) + else: + while self._port_is_used(port_value): + port_value += 1 + + self.set_setting(setting_name, port_value) + + if infos["exposed"]: + firewall_allow(infos["exposed"], port_value, reload_only_if_change=True) + else: + firewall_disallow( + infos["exposed"], port_value, reload_only_if_change=True + ) + + def deprovision(self, context: Dict = {}): + from yunohost.firewall import firewall_disallow + + for name, infos in self.ports.items(): + setting_name = f"port_{name}" if name != "main" else "port" + value = self.get_setting(setting_name) + self.delete_setting(setting_name) + if value and str(value).strip(): + firewall_disallow( + infos["exposed"], int(value), reload_only_if_change=True + ) + + +class DatabaseAppResource(AppResource): + """ + Initialize a database, either using MySQL or Postgresql. Relevant DB infos are stored in settings `$db_name`, `$db_user` and `$db_pwd`. + + NB: only one DB can be handled in such a way (is there really an app that would need two completely different DB ?...) + + NB2: no automagic migration will happen in an suddenly change `type` from `mysql` to `postgresql` or viceversa in its life + + ##### Example + ```toml + [resources.database] + type = "mysql" # or : "postgresql". Only these two values are supported + ``` + + ##### Properties + - `type`: The database type, either `mysql` or `postgresql` + + ##### Provision/Update + - (Re)set the `$db_name` and `$db_user` settings with the sanitized app name (replacing `-` and `.` with `_`) + - If `$db_pwd` doesn't already exists, pick a random database password and store it in that setting + - If the database doesn't exists yet, create the SQL user and DB using `ynh_mysql_create_db` or `ynh_psql_create_db`. + + ##### Deprovision + - Drop the DB using `ynh_mysql_remove_db` or `ynh_psql_remove_db` + - Deletes the `db_name`, `db_user` and `db_pwd` settings + + ##### Legacy management + - In the past, the sql passwords may have been named `mysqlpwd` or `psqlpwd`, in which case it will automatically be renamed as `db_pwd` + """ + + # Notes for future? + # deep_clean -> ... idk look into any db name that would not be related to any app... + # backup -> dump db + # restore -> setup + inject db dump + + type = "database" + priority = 90 + dbtype: str = "" + + default_properties: Dict[str, Any] = { + "dbtype": None, + } + + def __init__(self, properties: Dict[str, Any], *args, **kwargs): + if "type" not in properties or properties["type"] not in [ + "mysql", + "postgresql", + ]: + raise YunohostError( + "Specifying the type of db ('mysql' or 'postgresql') is mandatory for db resources", + raw_msg=True, + ) + + # Hack so that people can write type = "mysql/postgresql" in toml but it's loaded as dbtype + # to avoid conflicting with the generic self.type of the resource object... + # dunno if that's really a good idea :| + properties = {"dbtype": properties["type"]} + + super().__init__(properties, *args, **kwargs) + + def db_exists(self, db_name): + if self.dbtype == "mysql": + return os.system(f"mysqlshow | grep -q -w '{db_name}' 2>/dev/null") == 0 + elif self.dbtype == "postgresql": + return ( + os.system( + f"sudo --login --user=postgres psql '{db_name}' -c ';' >/dev/null 2>/dev/null" + ) + == 0 + ) + else: + return False + + def provision_or_update(self, context: Dict = {}): + # This is equivalent to ynh_sanitize_dbid + db_name = self.app.replace("-", "_").replace(".", "_") + db_user = db_name + self.set_setting("db_name", db_name) + self.set_setting("db_user", db_user) + + db_pwd = None + if self.get_setting("db_pwd"): + db_pwd = self.get_setting("db_pwd") + else: + # Legacy setting migration + legacypasswordsetting = ( + "psqlpwd" if self.dbtype == "postgresql" else "mysqlpwd" + ) + if self.get_setting(legacypasswordsetting): + db_pwd = self.get_setting(legacypasswordsetting) + self.delete_setting(legacypasswordsetting) + self.set_setting("db_pwd", db_pwd) + + if not db_pwd: + from moulinette.utils.text import random_ascii + + db_pwd = random_ascii(24) + self.set_setting("db_pwd", db_pwd) + + if not self.db_exists(db_name): + if self.dbtype == "mysql": + self._run_script( + "provision", + f"ynh_mysql_create_db '{db_name}' '{db_user}' '{db_pwd}'", + ) + elif self.dbtype == "postgresql": + self._run_script( + "provision", + f"ynh_psql_create_user '{db_user}' '{db_pwd}'; ynh_psql_create_db '{db_name}' '{db_user}'", + ) + + def deprovision(self, context: Dict = {}): + db_name = self.app.replace("-", "_").replace(".", "_") + db_user = db_name + + if self.dbtype == "mysql": + self._run_script( + "deprovision", f"ynh_mysql_remove_db '{db_name}' '{db_user}'" + ) + elif self.dbtype == "postgresql": + self._run_script( + "deprovision", f"ynh_psql_remove_db '{db_name}' '{db_user}'" + ) + + self.delete_setting("db_name") + self.delete_setting("db_user") + self.delete_setting("db_pwd") + + +AppResourceClassesByType = {c.type: c for c in AppResource.__subclasses__()} diff --git a/src/utils/packages.py b/src/utils/system.py similarity index 51% rename from src/utils/packages.py rename to src/utils/system.py index 3105bc4c7..a169bd62c 100644 --- a/src/utils/packages.py +++ b/src/utils/system.py @@ -1,37 +1,112 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2015 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import re import os import logging from moulinette.utils.process import check_output -from packaging import version +from yunohost.utils.error import YunohostError logger = logging.getLogger("yunohost.utils.packages") YUNOHOST_PACKAGES = ["yunohost", "yunohost-admin", "moulinette", "ssowat"] -def get_ynh_package_version(package): +def debian_version(): + return check_output('grep "^VERSION_CODENAME=" /etc/os-release | cut -d= -f2') + +def system_arch(): + return check_output("dpkg --print-architecture") + + +def system_virt(): + """ + Returns the output of systemd-detect-virt (so e.g. 'none' or 'lxc' or ...) + You can check the man of the command to have a list of possible outputs... + """ + # Detect virt technology (if not bare metal) and arch + # Gotta have this "|| true" because it systemd-detect-virt return 'none' + # with an error code on bare metal ~.~ + return check_output("systemd-detect-virt || true") + + +def free_space_in_directory(dirpath): + stat = os.statvfs(dirpath) + return stat.f_frsize * stat.f_bavail + + +def space_used_by_directory(dirpath, follow_symlinks=True): + if not follow_symlinks: + du_output = check_output(["du", "-sb", dirpath], shell=False) + return int(du_output.split()[0]) + + stat = os.statvfs(dirpath) + return ( + stat.f_frsize * stat.f_blocks + ) # FIXME : this doesnt do what the function name suggest this does ... + + +def human_to_binary(size: str) -> int: + symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") + factor = {} + for i, s in enumerate(symbols): + factor[s] = 1 << (i + 1) * 10 + + suffix = size[-1] + size = size[:-1] + + if suffix not in symbols: + raise YunohostError( + f"Invalid size suffix '{suffix}', expected one of {symbols}" + ) + + try: + size_ = float(size) + except Exception: + raise YunohostError(f"Failed to convert size {size} to float") + + return int(size_ * factor[suffix]) + + +def binary_to_human(n: int) -> str: + """ + Convert bytes or bits into human readable format with binary prefix + """ + symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") + prefix = {} + for i, s in enumerate(symbols): + prefix[s] = 1 << (i + 1) * 10 + for s in reversed(symbols): + if n >= prefix[s]: + value = float(n) / prefix[s] + return "%.1f%s" % (value, s) + return "%s" % n + + +def ram_available(): + import psutil + + return (psutil.virtual_memory().available, psutil.swap_memory().free) + + +def get_ynh_package_version(package): # Returns the installed version and release version ('stable' or 'testing' # or 'unstable') @@ -40,7 +115,7 @@ def get_ynh_package_version(package): # may handle changelog differently ! changelog = "/usr/share/doc/%s/changelog.gz" % package - cmd = "gzip -cd %s 2>/dev/null | head -n1" % changelog + cmd = "gzip -cd %s 2>/dev/null | grep -v 'BASH_XTRACEFD' | head -n1" % changelog if not os.path.exists(changelog): return {"version": "?", "repo": "?"} out = check_output(cmd).split() @@ -48,43 +123,6 @@ def get_ynh_package_version(package): return {"version": out[1].strip("()"), "repo": out[2].strip(";")} -def meets_version_specifier(pkg_name, specifier): - """ - Check if a package installed version meets specifier - - specifier is something like ">> 1.2.3" - """ - - # In practice, this function is only used to check the yunohost version - # installed. - # We'll trim any ~foobar in the current installed version because it's not - # handled correctly by version.parse, but we don't care so much in that - # context - assert pkg_name in YUNOHOST_PACKAGES - pkg_version = get_ynh_package_version(pkg_name)["version"] - pkg_version = re.split(r"\~|\+|\-", pkg_version)[0] - pkg_version = version.parse(pkg_version) - - # Extract operator and version specifier - op, req_version = re.search(r"(<<|<=|=|>=|>>) *([\d\.]+)", specifier).groups() - req_version = version.parse(req_version) - - # Python2 had a builtin that returns (-1, 0, 1) depending on comparison - # c.f. https://stackoverflow.com/a/22490617 - def cmp(a, b): - return (a > b) - (a < b) - - deb_operators = { - "<<": lambda v1, v2: cmp(v1, v2) in [-1], - "<=": lambda v1, v2: cmp(v1, v2) in [-1, 0], - "=": lambda v1, v2: cmp(v1, v2) in [0], - ">=": lambda v1, v2: cmp(v1, v2) in [0, 1], - ">>": lambda v1, v2: cmp(v1, v2) in [1], - } - - return deb_operators[op](pkg_version, req_version) - - def ynh_packages_version(*args, **kwargs): # from cli the received arguments are: # (Namespace(_callbacks=deque([]), _tid='_global', _to_return={}), []) {} @@ -114,7 +152,6 @@ def dpkg_lock_available(): def _list_upgradable_apt_packages(): - # List upgradable packages # LC_ALL=C is here to make sure the results are in english upgradable_raw = check_output("LC_ALL=C apt list --upgradable") @@ -124,7 +161,6 @@ def _list_upgradable_apt_packages(): line.strip() for line in upgradable_raw.split("\n") if line.strip() ] for line in upgradable_raw: - # Remove stupid warning and verbose messages >.> if "apt does not have a stable CLI interface" in line or "Listing..." in line: continue @@ -144,7 +180,6 @@ def _list_upgradable_apt_packages(): def _dump_sources_list(): - from glob import glob filenames = glob("/etc/apt/sources.list") + glob("/etc/apt/sources.list.d/*") diff --git a/src/utils/yunopaste.py b/src/utils/yunopaste.py index 35e829991..46131846d 100644 --- a/src/utils/yunopaste.py +++ b/src/utils/yunopaste.py @@ -1,5 +1,21 @@ -# -*- coding: utf-8 -*- - +# +# Copyright (c) 2023 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import requests import json import logging @@ -12,7 +28,6 @@ logger = logging.getLogger("yunohost.utils.yunopaste") def yunopaste(data): - paste_server = "https://paste.yunohost.org" try: diff --git a/tests/test_helpers.d/ynhtest_setup_source.sh b/tests/test_helpers.d/ynhtest_setup_source.sh index fe61e7401..6a74a587c 100644 --- a/tests/test_helpers.d/ynhtest_setup_source.sh +++ b/tests/test_helpers.d/ynhtest_setup_source.sh @@ -18,51 +18,51 @@ _make_dummy_src() { } ynhtest_setup_source_nominal() { - final_path="$(mktemp -d -p $VAR_WWW)" + install_dir="$(mktemp -d -p $VAR_WWW)" _make_dummy_src > ../conf/dummy.src - ynh_setup_source --dest_dir="$final_path" --source_id="dummy" + ynh_setup_source --dest_dir="$install_dir" --source_id="dummy" - test -e "$final_path" - test -e "$final_path/index.html" + test -e "$install_dir" + test -e "$install_dir/index.html" } ynhtest_setup_source_nominal_upgrade() { - final_path="$(mktemp -d -p $VAR_WWW)" + install_dir="$(mktemp -d -p $VAR_WWW)" _make_dummy_src > ../conf/dummy.src - ynh_setup_source --dest_dir="$final_path" --source_id="dummy" + ynh_setup_source --dest_dir="$install_dir" --source_id="dummy" - test "$(cat $final_path/index.html)" == "Lorem Ipsum" + test "$(cat $install_dir/index.html)" == "Lorem Ipsum" # Except index.html to get overwritten during next ynh_setup_source - echo "IEditedYou!" > $final_path/index.html - test "$(cat $final_path/index.html)" == "IEditedYou!" + echo "IEditedYou!" > $install_dir/index.html + test "$(cat $install_dir/index.html)" == "IEditedYou!" - ynh_setup_source --dest_dir="$final_path" --source_id="dummy" + ynh_setup_source --dest_dir="$install_dir" --source_id="dummy" - test "$(cat $final_path/index.html)" == "Lorem Ipsum" + test "$(cat $install_dir/index.html)" == "Lorem Ipsum" } ynhtest_setup_source_with_keep() { - final_path="$(mktemp -d -p $VAR_WWW)" + install_dir="$(mktemp -d -p $VAR_WWW)" _make_dummy_src > ../conf/dummy.src - echo "IEditedYou!" > $final_path/index.html - echo "IEditedYou!" > $final_path/test.txt + echo "IEditedYou!" > $install_dir/index.html + echo "IEditedYou!" > $install_dir/test.txt - ynh_setup_source --dest_dir="$final_path" --source_id="dummy" --keep="index.html test.txt" + ynh_setup_source --dest_dir="$install_dir" --source_id="dummy" --keep="index.html test.txt" - test -e "$final_path" - test -e "$final_path/index.html" - test -e "$final_path/test.txt" - test "$(cat $final_path/index.html)" == "IEditedYou!" - test "$(cat $final_path/test.txt)" == "IEditedYou!" + test -e "$install_dir" + test -e "$install_dir/index.html" + test -e "$install_dir/test.txt" + test "$(cat $install_dir/index.html)" == "IEditedYou!" + test "$(cat $install_dir/test.txt)" == "IEditedYou!" } ynhtest_setup_source_with_patch() { - final_path="$(mktemp -d -p $VAR_WWW)" + install_dir="$(mktemp -d -p $VAR_WWW)" _make_dummy_src > ../conf/dummy.src mkdir -p ../sources/patches @@ -74,7 +74,7 @@ ynhtest_setup_source_with_patch() { +Lorem Ipsum dolor sit amet EOF - ynh_setup_source --dest_dir="$final_path" --source_id="dummy" + ynh_setup_source --dest_dir="$install_dir" --source_id="dummy" - test "$(cat $final_path/index.html)" == "Lorem Ipsum dolor sit amet" + test "$(cat $install_dir/index.html)" == "Lorem Ipsum dolor sit amet" } diff --git a/tox.ini b/tox.ini index dc2c52074..c38df434b 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ deps = py39-black-{run,check}: black py39-mypy: mypy >= 0.900 commands = - py39-lint: flake8 src doc maintenance tests --ignore E402,E501,E203,W503 --exclude src/vendor + py39-lint: flake8 src doc maintenance tests --ignore E402,E501,E203,W503,E741 --exclude src/tests,src/vendor py39-invalidcode: flake8 src bin maintenance --exclude src/tests,src/vendor --select F,E722,W605 py39-black-check: black --check --diff bin src doc maintenance tests py39-black-run: black bin src doc maintenance tests